소스 검색

feat: :sparkles: feat(importação exames ) Foi realizado o ajuste final referente a importação do exame e tambem feito a mudança da regra referente ao agendamento

Foi realizado o ajuste final referente a importação do exame e tambem feito a mudança da regra referente ao agendamento, agora o administrador consegue cadastrar fora da regra dos dois  dias de antecedencia porem o associado e o parceiro ainda mantem a regra

fase:dev | origin:escopo
kayo henrique 1 주 전
부모
커밋
0a3f9c343e
2개의 변경된 파일389개의 추가작업 그리고 58개의 파일을 삭제
  1. 8 2
      app/Http/Requests/AppointmentRequest.php
  2. 381 56
      app/Services/ExamesImportService.php

+ 8 - 2
app/Http/Requests/AppointmentRequest.php

@@ -6,6 +6,7 @@ use App\Enums\AppointmentStatusEnum;
 use App\Enums\PartnerAgreementServiceTypeEnum;
 use App\Enums\PartnerAgreementTypeEnum;
 use App\Enums\UserDependentStatusEnum;
+use App\Enums\UserTypeEnum;
 use App\Models\Appointment;
 use App\Rules\ExactAdvanceDays;
 use Illuminate\Foundation\Http\FormRequest;
@@ -27,7 +28,7 @@ class AppointmentRequest extends FormRequest
             ->whereNull('deleted_at')
             ->when(
                 $this->filled('partner_agreement_id'),
-                fn ($rule) => $rule->where('partner_agreement_id', $this->input('partner_agreement_id')),
+                fn($rule) => $rule->where('partner_agreement_id', $this->input('partner_agreement_id')),
             );
 
         $dependentExists = Rule::exists('user_dependents', 'id')
@@ -51,7 +52,12 @@ class AppointmentRequest extends FormRequest
             $rules['partner_agreement_id']         = ['required', 'integer', $partnerExists];
             $rules['partner_agreement_service_id'] = ['required', 'integer', $serviceExists];
 
-            $rules['date'] = ['required', 'date', new ExactAdvanceDays(self::CONSULTA_ADVANCE_DAYS)];
+            $rules['date'] = ['required', 'date'];
+
+            if (in_array(Auth::user()?->type, [UserTypeEnum::ASSOCIADO, UserTypeEnum::PARCEIRO,], true)) {
+                $rules['date'][] = new ExactAdvanceDays(self::CONSULTA_ADVANCE_DAYS);
+            }
+
             $rules['time'] = 'required|date_format:H:i';
         }
 

+ 381 - 56
app/Services/ExamesImportService.php

@@ -5,6 +5,7 @@ namespace App\Services;
 use App\Enums\PartnerAgreementServiceStatusEnum;
 use App\Enums\PartnerAgreementServiceTypeEnum;
 use App\Imports\ExamesImport;
+use App\Models\Category;
 use App\Models\PartnerAgreementService;
 use Maatwebsite\Excel\Facades\Excel;
 
@@ -23,27 +24,63 @@ class ExamesImportService
         $created = 0;
         $updated = 0;
 
+        /*
+         * Guarda o último bloco encontrado.
+         *
+         * Exemplo:
+         *
+         * BLOCO:
+         * "EXAMES DE IMAGEM"
+         *
+         * Os próximos exames receberão essa informação
+         * na descrição até que outro bloco seja encontrado.
+         */
+        $currentBlockDescription = null;
+
+        /*
+         * Localiza a categoria EXAME.
+         */
+        $examCategory = Category::whereRaw(
+            'LOWER(TRIM(name)) = ?',
+            ['exame']
+        )->first();
+
+        /*
+         * A categoria é obrigatória para a importação.
+         */
+        if (!$examCategory) {
+            throw new \RuntimeException(
+                'A categoria "exame" não foi encontrada.'
+            );
+        }
+
         foreach ($rows as $row) {
             /*
              * Normaliza as células da linha.
              */
             $cells = collect($row)
-                ->map(fn ($value) => trim((string) $value))
+                ->map(function ($value) {
+                    return trim((string) $value);
+                })
                 ->values();
 
             /*
              * Ignora linha completamente vazia.
              */
-            if ($cells->filter(fn ($value) => $value !== '')->isEmpty()) {
+            if (
+                $cells
+                    ->filter(fn ($value) => $value !== '')
+                    ->isEmpty()
+            ) {
                 continue;
             }
 
             /*
-             * Estrutura esperada do Excel:
+             * Estrutura esperada:
              *
              * Coluna 0 = Código do exame
              * Coluna 1 = Nome do exame
-             * Coluna 2 = Preço
+             * Coluna 2 = Valor / informação adicional
              */
             $codeExams = $cells->get(0, '');
             $name = $cells->get(1, '');
@@ -57,22 +94,27 @@ class ExamesImportService
             }
 
             /*
-             * Trata cabeçalhos do Excel.
+             * Normalização para identificação de cabeçalhos.
              */
             $codeHeader = mb_strtoupper($codeExams);
             $nameHeader = mb_strtoupper($name);
             $priceHeader = mb_strtoupper($price);
 
+            /*
+             * Identifica cabeçalhos.
+             */
             $isHeader =
                 str_contains($codeHeader, 'CÓDIGO') ||
                 str_contains($codeHeader, 'CODIGO') ||
                 $nameHeader === 'EXAME' ||
                 $nameHeader === 'NOME' ||
-                str_contains($nameHeader, 'EXAME') &&
                 (
-                    str_contains($priceHeader, 'VALOR') ||
-                    str_contains($priceHeader, 'PREÇO') ||
-                    str_contains($priceHeader, 'PRECO')
+                    str_contains($nameHeader, 'EXAME') &&
+                    (
+                        str_contains($priceHeader, 'VALOR') ||
+                        str_contains($priceHeader, 'PREÇO') ||
+                        str_contains($priceHeader, 'PRECO')
+                    )
                 );
 
             if ($isHeader) {
@@ -80,32 +122,129 @@ class ExamesImportService
             }
 
             /*
-             * Converte o preço para o formato numérico.
+             * Processa a terceira coluna.
+             *
+             * Retorna:
+             *
+             * [
+             *     'price' => valor numérico,
+             *     'description' => texto adicional
+             * ]
+             */
+            $priceData = $this->parsePrice($price);
+
+            $associatePrice = $priceData['price'];
+            $priceDescription = $priceData['description'];
+
+            /*
+             * Define se realmente existe um preço.
+             *
+             * Importante:
+             * "0" também é considerado um valor válido.
              */
-            $associatePrice = $this->parsePrice($price);
+            $isPrice = $associatePrice !== null;
 
             /*
-             * Procura um exame já cadastrado para esse parceiro.
+             * REGRA DOS BLOCOS
+             *
+             * Se:
+             *
+             * - não existe código
+             * - e a terceira coluna NÃO possui valor
+             *
+             * então a linha é um bloco.
              *
-             * Primeiro tenta localizar pelo código quando
-             * o código foi informado.
+             * Exemplo:
+             *
+             * Código: vazio
+             * Nome: "EXAMES LABORATORIAIS"
+             * Valor: vazio
+             *
+             * Essa linha NÃO será cadastrada.
+             *
+             * Ela será utilizada como descrição dos exames
+             * seguintes até que outro bloco seja encontrado.
+             */
+            if ($codeExams === '' && !$isPrice) {
+                $currentBlockDescription = $name;
+
+                continue;
+            }
+
+            /*
+             * Monta a descrição final do exame.
+             *
+             * Primeiro adiciona o bloco.
+             *
+             * Depois adiciona qualquer texto encontrado junto
+             * ao valor.
+             *
+             * Exemplo:
+             *
+             * Bloco:
+             * "PACOTE CONTRASTE"
+             *
+             * Terceira coluna:
+             * "150,00 + contraste"
+             *
+             * Resultado:
+             *
+             * descrição:
+             * "PACOTE CONTRASTE + contraste"
+             *
+             * preço:
+             * 150.00
+             */
+            $descriptionParts = [];
+
+            if (
+                $currentBlockDescription !== null &&
+                trim($currentBlockDescription) !== ''
+            ) {
+                $descriptionParts[] = trim($currentBlockDescription);
+            }
+
+            if (
+                $priceDescription !== null &&
+                trim($priceDescription) !== ''
+            ) {
+                $descriptionParts[] = trim($priceDescription);
+            }
+
+            $description = !empty($descriptionParts)
+                ? implode(' ', $descriptionParts)
+                : null;
+
+            /*
+             * Procura exame existente.
+             *
+             * Primeiro pelo código.
              */
             $service = null;
 
             if ($codeExams !== '') {
                 $service = PartnerAgreementService::withTrashed()
-                    ->where('partner_agreement_id', $partnerAgreementId)
-                    ->where('code_exams', $codeExams)
+                    ->where(
+                        'partner_agreement_id',
+                        $partnerAgreementId
+                    )
+                    ->where(
+                        'code_exams',
+                        $codeExams
+                    )
                     ->first();
             }
 
             /*
-             * Caso não exista código ou não tenha encontrado pelo código,
+             * Caso não tenha encontrado pelo código,
              * procura pelo nome.
              */
             if (!$service) {
                 $service = PartnerAgreementService::withTrashed()
-                    ->where('partner_agreement_id', $partnerAgreementId)
+                    ->where(
+                        'partner_agreement_id',
+                        $partnerAgreementId
+                    )
                     ->whereRaw(
                         'LOWER(TRIM(name)) = LOWER(TRIM(?))',
                         [$name]
@@ -114,7 +253,9 @@ class ExamesImportService
             }
 
             /*
+             * ==========================================================
              * EXAME JÁ EXISTE
+             * ==========================================================
              */
             if ($service) {
                 /*
@@ -127,50 +268,92 @@ class ExamesImportService
                 $changed = false;
 
                 /*
-                 * Atualiza o código quando informado.
+                 * Atualiza código.
                  */
                 if (
                     $codeExams !== '' &&
                     $service->code_exams !== $codeExams
                 ) {
                     $service->code_exams = $codeExams;
+
                     $changed = true;
                 }
 
                 /*
-                 * Atualiza o preço somente quando
-                 * o Excel trouxe um preço válido.
+                 * Atualiza preço somente quando
+                 * existe valor válido na planilha.
                  */
                 if (
                     $associatePrice !== null &&
-                    (float) $service->associate_price !== (float) $associatePrice
+                    (
+                        (float) $service->associate_price !==
+                        (float) $associatePrice
+                    )
                 ) {
                     $service->associate_price = $associatePrice;
+
                     $changed = true;
                 }
 
                 /*
-                 * Garante que o serviço seja do tipo EXAME.
+                 * Garante categoria EXAME.
                  */
                 if (
-                    $service->type !== PartnerAgreementServiceTypeEnum::EXAME
+                    (int) $service->category_id !==
+                    (int) $examCategory->id
                 ) {
-                    $service->type = PartnerAgreementServiceTypeEnum::EXAME;
+                    $service->category_id = $examCategory->id;
+
                     $changed = true;
                 }
 
                 /*
-                 * Garante que o exame fique ativo.
+                 * Garante tipo EXAME.
                  */
                 if (
-                    $service->status !== PartnerAgreementServiceStatusEnum::ACTIVE
+                    $service->type !==
+                    PartnerAgreementServiceTypeEnum::EXAME
                 ) {
-                    $service->status = PartnerAgreementServiceStatusEnum::ACTIVE;
+                    $service->type =
+                        PartnerAgreementServiceTypeEnum::EXAME;
+
                     $changed = true;
                 }
 
+                /*
+                 * Garante status ACTIVE.
+                 */
+                if (
+                    $service->status !==
+                    PartnerAgreementServiceStatusEnum::ACTIVE
+                ) {
+                    $service->status =
+                        PartnerAgreementServiceStatusEnum::ACTIVE;
+
+                    $changed = true;
+                }
+
+                /*
+                 * Atualiza descrição.
+                 *
+                 * Aqui usamos a descrição já processada,
+                 * incluindo:
+                 *
+                 * - bloco
+                 * - texto adicional do preço
+                 */
+                if ($service->description !== $description) {
+                    $service->description = $description;
+
+                    $changed = true;
+                }
+
+                /*
+                 * Salva somente se houve alteração.
+                 */
                 if ($changed) {
                     $service->save();
+
                     $updated++;
                 }
 
@@ -178,71 +361,213 @@ class ExamesImportService
             }
 
             /*
+             * ==========================================================
              * EXAME NÃO EXISTE
+             * ==========================================================
+             *
+             * Cria um novo exame.
+             *
+             * Mesmo sem código, se existir preço na terceira coluna,
+             * a linha será cadastrada.
+             *
+             * Isso resolve o caso do:
              *
-             * Cria um novo serviço vinculado
-             * ao parceiro informado.
+             * PACOTE CONTRASTE
+             *
+             * quando ele possui valor.
              */
             PartnerAgreementService::create([
                 'partner_agreement_id' => $partnerAgreementId,
-                'code_exams'           => $codeExams !== '' ? $codeExams : null,
-                'name'                 => $name,
-                'associate_price'      => $associatePrice,
-                'type'                 => PartnerAgreementServiceTypeEnum::EXAME,
-                'status'               => PartnerAgreementServiceStatusEnum::ACTIVE,
+
+                'code_exams' => $codeExams !== ''
+                    ? $codeExams
+                    : null,
+
+                'name' => $name,
+
+                'description' => $description,
+
+                'category_id' => $examCategory->id,
+
+                'associate_price' => $associatePrice,
+
+                'type' =>
+                    PartnerAgreementServiceTypeEnum::EXAME,
+
+                'status' =>
+                    PartnerAgreementServiceStatusEnum::ACTIVE,
             ]);
 
             $created++;
         }
 
         return [
-            'total'   => $rows->count(),
+            'total' => $rows->count(),
             'created' => $created,
             'updated' => $updated,
         ];
     }
 
     /**
-     * Converte diferentes formatos de preço
-     * para float.
+     * Processa a terceira coluna.
+     *
+     * Aceita exemplos como:
+     *
+     * 50
+     * 50,00
+     * 50.00
+     * 1.250,50
+     * R$ 50,00
+     * 50,00 + contraste
+     * R$ 50,00 + contraste
+     *
+     * Retorna:
+     *
+     * [
+     *     'price' => ?float,
+     *     'description' => ?string
+     * ]
      */
-    private function parsePrice(mixed $value): ?float
+    private function parsePrice(mixed $value): array
     {
-        if ($value === null || $value === '') {
-            return null;
+        /*
+         * Campo vazio.
+         */
+        if ($value === null || trim((string) $value) === '') {
+            return [
+                'price' => null,
+                'description' => null,
+            ];
         }
 
         $value = trim((string) $value);
 
         /*
-         * Remove moeda e espaços.
-         *
-         * Exemplos:
-         * R$ 50,00 -> 50,00
-         * R$50,00  -> 50,00
+         * Remove R$.
+         */
+        $value = str_replace(
+            ['R$', 'r$'],
+            '',
+            $value
+        );
+
+        /*
+         * Normaliza espaços.
          */
-        $value = str_replace(['R$', ' '], '', $value);
+        $value = trim($value);
 
         /*
-         * Formato brasileiro:
+         * Procura o primeiro valor numérico.
          *
+         * Exemplos encontrados:
+         *
+         * 50
          * 50,00
+         * 50.00
+         * 1.250,50
+         *
+         * O modificador u permite trabalhar corretamente
+         * com caracteres UTF-8.
+         */
+        $pattern = '/\d+(?:\.\d{3})*(?:,\d{1,2})?|\d+(?:\.\d{1,2})?/u';
+
+        if (!preg_match($pattern, $value, $matches)) {
+            /*
+             * Não existe valor.
+             *
+             * Nesse caso todo o conteúdo é considerado texto.
+             */
+            return [
+                'price' => null,
+                'description' => $value,
+            ];
+        }
+
+        $numericValue = $matches[0];
+
+        /*
+         * Converte formato brasileiro.
+         *
          * 1.250,50
+         * =>
+         * 1250.50
          */
-        if (str_contains($value, ',')) {
-            $value = str_replace('.', '', $value);
-            $value = str_replace(',', '.', $value);
+        if (str_contains($numericValue, ',')) {
+            $numericValue = str_replace(
+                '.',
+                '',
+                $numericValue
+            );
+
+            $numericValue = str_replace(
+                ',',
+                '.',
+                $numericValue
+            );
         }
 
         /*
-         * Formato decimal:
+         * Caso seja algo como:
          *
-         * 50
          * 50.00
-         * 1250.50
+         *
+         * mantém como decimal.
          */
-        return is_numeric($value)
-            ? (float) $value
+        $price = is_numeric($numericValue)
+            ? (float) $numericValue
             : null;
+
+        /*
+         * Remove o valor encontrado do texto original.
+         *
+         * Exemplo:
+         *
+         * "50,00 + contraste"
+         *
+         * vira:
+         *
+         * "+ contraste"
+         */
+        $description = preg_replace(
+            '/' . preg_quote($matches[0], '/') . '/u',
+            '',
+            $value,
+            1
+        );
+
+        /*
+         * Remove espaços extras.
+         */
+        $description = trim(
+            preg_replace(
+                '/\s+/u',
+                ' ',
+                (string) $description
+            )
+        );
+
+        /*
+         * Remove caracteres separadores sobrando
+         * no começo/fim.
+         *
+         * Exemplo:
+         *
+         * "+ contraste"
+         *
+         * vira:
+         *
+         * "contraste"
+         */
+        $description = trim(
+            $description,
+            " \t\n\r\0\x0B+-–—"
+        );
+
+        return [
+            'price' => $price,
+            'description' => $description !== ''
+                ? $description
+                : null,
+        ];
     }
 }