Переглянути джерело

Merge branch 'feature-serprati-kay-importação-exames' of Softpar/sfp_front_vue_serprati_digital into development

zntt 1 тиждень тому
батько
коміт
58878139e5

+ 17 - 0
src/api/partnerAgreementService.js

@@ -50,3 +50,20 @@ export const uploadServiceMedia = async (id, file) => {
 export const deleteServiceMedia = async (id, mediaId) => {
   await api.delete(`/partner-agreement-service/${id}/media/${mediaId}`);
 };
+
+export const importServices = async (partnerAgreementId, file) => {
+  const form = new FormData();
+
+  form.append("file", file);
+
+  const { data } = await api.post(`/partner-agreement-service/partner/${partnerAgreementId}/import`,
+    form,
+    {
+      headers: {
+        "Content-Type": "multipart/form-data",
+      },
+    },
+  );
+
+  return data.payload;
+};

+ 15 - 2
src/components/ApproveAppointmentDialog.vue

@@ -10,7 +10,9 @@
           <DefaultInputDatePicker
             v-model:untreated-date="date"
             :label="$t('common.terms.date')"
-            :rules="[inputRules.required]"
+            :rules="[inputRules.required, inputRules.exactAdvanceDays(ADVANCE_DAYS)]"
+            :hint="$t('agendamento.data_disponivel', { date: allowedDateLabel })"
+            :date-options="allowedDateOptions"
             placeholder="dd/mm/aaaa"
             lazy-rules
           />
@@ -33,9 +35,10 @@
 </template>
 
 <script setup>
-import { ref, useTemplateRef } from "vue";
+import { computed, ref, useTemplateRef } from "vue";
 import { useDialogPluginComponent } from "quasar";
 import { useInputRules } from "src/composables/useInputRules";
+import { addDaysToToday, formatDateBR } from "src/helpers/utils";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
 
@@ -47,6 +50,16 @@ const { inputRules } = useInputRules();
 const formRef = useTemplateRef("formRef");
 const date = ref("");
 const time = ref("");
+const ADVANCE_DAYS = 2;
+
+const allowedDateLabel = computed(() => formatDateBR(addDaysToToday(ADVANCE_DAYS)));
+const allowedDateOptions = computed(() => {
+  const allowedDate = addDaysToToday(ADVANCE_DAYS);
+  const month = String(allowedDate.getMonth() + 1).padStart(2, "0");
+  const day = String(allowedDate.getDate()).padStart(2, "0");
+
+  return [`${allowedDate.getFullYear()}/${month}/${day}`];
+});
 
 const onConfirm = async () => {
   const valid = await formRef.value?.validate();

+ 7 - 1
src/i18n/locales/en.json

@@ -994,5 +994,11 @@
     "on_leave": "On leave",
     "inactivated": "Inactivated",
     "no_history": "No imports recorded"
-  }
+  },
+  "import_partner": {
+  "import_exams": "Import Exams",
+  "import_exams_result": "Import completed. Exams created: {created}. Exams updated: {updated}.",
+  "import_exams_processing": "The exam import is being processed.",
+  "import_exams_error": "Unable to import the exams."
+}
 }

+ 7 - 1
src/i18n/locales/es.json

@@ -994,5 +994,11 @@
     "on_leave": "En licencia",
     "inactivated": "Desactivados",
     "no_history": "Ninguna importación registrada"
+  },
+  "import_partner": {
+    "import_exams": "Importar Exámenes",
+    "import_exams_result": "Importación completada. Exámenes creados: {created}. Exámenes actualizados: {updated}.",
+    "import_exams_processing": "La importación de los exámenes está siendo procesada.",
+    "import_exams_error": "No fue posible importar los exámenes."
   }
-}
+}

+ 7 - 1
src/i18n/locales/pt.json

@@ -995,5 +995,11 @@
     "on_leave": "Afastados",
     "inactivated": "Desativados",
     "no_history": "Nenhuma importação registrada"
+  },
+  "import_parceiro": {
+    "import_exames": "Importar Exames",
+    "import_exames_result": "Importação concluída. Exames criados: {created}. Exames atualizados: {updated}.",
+    "import_exames_processing": "A importação dos exames está sendo processada.",
+    "import_exames_error": "Não foi possível importar os exames."
   }
-}
+}

+ 2 - 16
src/pages/agendamentos/AppointmentsAdminPage.vue

@@ -81,9 +81,7 @@
                 <DefaultInputDatePicker
                   v-model:untreated-date="form.date"
                   :label="$t('common.terms.date')"
-                  :rules="[inputRules.required, inputRules.exactAdvanceDays(ADVANCE_DAYS)]"
-                  :hint="$t('agendamento.data_disponivel', { date: allowedDateLabel })"
-                  :date-options="allowedDateOptions"
+                  :rules="[inputRules.required]"
                   placeholder="dd/mm/aaaa"
                   lazy-rules
                   class="col-12 col-md-6"
@@ -110,7 +108,6 @@
                 <DefaultInputDatePicker
                   v-model:untreated-date="form.date"
                   :label="$t('agendamento.data_opcional')"
-                  :date-options="allowedDateOptions"
                   placeholder="dd/mm/aaaa"
                   class="col-12 col-md-6"
                 />
@@ -334,7 +331,7 @@ import { ref, computed, onMounted, useTemplateRef, nextTick, watch } from "vue";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { useInputRules } from "src/composables/useInputRules";
-import { excerpt, addDaysToToday, formatDateBR } from "src/helpers/utils";
+import { excerpt } from "src/helpers/utils";
 
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultTableServerSide from "src/components/defaults/DefaultTableServerSide.vue";
@@ -375,8 +372,6 @@ const counters = ref({
   recusados: undefined,
 });
 
-const ADVANCE_DAYS = 2;
-
 const form = ref({
   type: "consulta",
   associado: null,
@@ -392,15 +387,6 @@ const form = ref({
 
 const isConsulta = computed(() => form.value.type === "consulta");
 
-const allowedDateLabel = computed(() => formatDateBR(addDaysToToday(ADVANCE_DAYS)));
-
-const allowedDateOptions = computed(() => {
-  const date = addDaysToToday(ADVANCE_DAYS);
-  const month = String(date.getMonth() + 1).padStart(2, "0");
-  const day = String(date.getDate()).padStart(2, "0");
-  return [`${date.getFullYear()}/${month}/${day}`];
-});
-
 watch(
   () => form.value.forDependent,
   (forDependent) => {

+ 111 - 1
src/pages/parceiros-convenios/components/CadastroFormPanel.vue

@@ -319,6 +319,27 @@
         <template v-if="!serviceFormVisible">
           <div class="bg-violet-light q-pb-md">
             <div class="row justify-end q-mb-sm q-gutter-sm">
+
+                <input
+                    ref="importExamesInput"
+                    type="file"
+                    accept=".xlsx,.csv"
+                    class="hidden"
+                    @change="onExamesFileSelected"
+                  />
+
+               <q-btn
+                  v-if="serviceTab.serviceType === 'exame'"
+                  unelevated
+                  icon="mdi-upload"
+                  :label="$t('import_parceiro.import_exames')"
+                  padding="6px 12px"
+                  :disable="!entityId"
+                  :loading="importingExames"
+                  class="btn-gradient"
+                  @click="onImportExamesClick"
+                />
+
               <q-btn
                 unelevated
                 icon="mdi-plus"
@@ -395,7 +416,8 @@ import {
   deletePartnerMedia,
 } from "src/api/partnerAgreement";
 import { permissionStore } from "src/stores/permission";
-import { getServicesByPartner } from "src/api/partnerAgreementService";
+import { getServicesByPartner, importServices } from "src/api/partnerAgreementService";
+import { useImportPoller } from "src/composables/useImportPoller";
 import { formatToBRLCurrencyOrDash } from "src/helpers/utils";
 import axios from "axios";
 
@@ -695,6 +717,87 @@ const loadServices = async () => {
   }
 };
 
+const onImportExamesClick = () => {
+  if (!permission_store.getAccess("parceiro.servico", "add")) {
+    $q.notify({
+      type: "negative",
+      message: t("validation.permissions.add"),
+    });
+    return;
+  }
+
+  if (!entityId.value) {
+    return;
+  }
+
+  const input = importExamesInput.value?.[0];
+
+  if (!input) {
+    console.error("Input de importação de exames não encontrado.");
+    return;
+  }
+
+  input.value = null;
+  input.click();
+};
+
+const onExamesFileSelected = async (event) => {
+  const file = event.target.files?.[0];
+
+  if (!file) {
+    return;
+  }
+
+  if (!entityId.value) {
+    return;
+  }
+
+  try {
+    const { import_id } = await importServices(
+      entityId.value,
+      file,
+    );
+
+    startExamesPolling(import_id, {
+      onComplete: async (stats) => {
+        $q.notify({
+          type: "positive",
+          message: t("import_parceiro.import_exames_result", {
+            created: stats?.created ?? 0,
+            updated: stats?.updated ?? 0,
+          }),
+          timeout: 7000,
+        });
+
+        await loadServices();
+      },
+
+      onError: () => {
+        $q.notify({
+          type: "negative",
+          message: t("http.errors.failed"),
+        });
+      },
+
+      onTimeout: () => {
+        $q.notify({
+          type: "warning",
+          message: t("import_parceiro.import_exames_processing"),
+        });
+      },
+    });
+  } catch (error) {
+    console.error(error);
+
+    $q.notify({
+      type: "negative",
+      message: t("http.errors.failed"),
+    });
+  } finally {
+    event.target.value = null;
+  }
+};
+
 const onAddService = () => {
   editingServiceId.value   = null;
   serviceFormVisible.value = true;
@@ -746,6 +849,13 @@ const populateForms = (p) => {
   contractMedia.value = p.media ?? [];
 };
 
+//import de exames/serviços
+const importExamesInput = useTemplateRef("importExamesInput");
+const {
+  polling: importingExames,
+  start: startExamesPolling,
+} = useImportPoller();
+
 onMounted(async () => {
   if (entityId.value) {
     try {

+ 7 - 1
src/pages/parceiros-convenios/components/NovaGuiaExameForm.vue

@@ -55,6 +55,8 @@
       <DefaultInputDatePicker
         v-model:untreated-date="form.date"
         :label="$t('agendamento.data_opcional')"
+        :rules="[optionalExactAdvanceDaysRule]"
+        :hint="$t('agendamento.data_disponivel', { date: allowedDateLabel })"
         :date-options="allowedDateOptions"
         placeholder="dd/mm/aaaa"
         class="col-12 col-md-6"
@@ -99,7 +101,7 @@
 import { ref, computed, onMounted, nextTick } from "vue";
 import { createPartnerExam } from "src/api/appointment";
 import { getMyPartnerAgreement } from "src/api/partnerAgreement";
-import { formatToBRLCurrency, addDaysToToday } from "src/helpers/utils";
+import { formatToBRLCurrency, addDaysToToday, formatDateBR } from "src/helpers/utils";
 import { useInputRules } from "src/composables/useInputRules";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
@@ -117,6 +119,7 @@ const partnerAgreementId = ref(null);
 
 const ADVANCE_DAYS = 2;
 
+const allowedDateLabel = computed(() => formatDateBR(addDaysToToday(ADVANCE_DAYS)));
 const allowedDateOptions = computed(() => {
   const date = addDaysToToday(ADVANCE_DAYS);
   const month = String(date.getMonth() + 1).padStart(2, "0");
@@ -124,6 +127,9 @@ const allowedDateOptions = computed(() => {
   return [`${date.getFullYear()}/${month}/${day}`];
 });
 
+const optionalExactAdvanceDaysRule = (value) =>
+  !value || inputRules.exactAdvanceDays(ADVANCE_DAYS)(value);
+
 const emptyForm = () => ({
   associado: null,
   forDependent: false,

+ 9 - 0
src/pages/parceiros-convenios/components/ServicoFormPanel.vue

@@ -16,6 +16,13 @@
                 readonly
                 class="col-3 input-violet"
               />
+
+              <DefaultInput
+                  v-model="form.code_exams"
+                  label="Código do exame"
+                  class="col-2 input-violet"
+                />
+
               <DefaultInput
                 v-model="form.name"
                 v-model:error="validationErrors.name"
@@ -201,6 +208,7 @@ const categoryOptions   = ref([]);
 
 const form = ref({
   partner_agreement_id: props.partnerId,
+  code_exams:           "",
   name:                 "",
   description:          "",
   service_number:       "",
@@ -240,6 +248,7 @@ const loadService = async () => {
   try {
     const svc = await getPartnerAgreementService(props.serviceId);
     form.value.name                = svc.name                ?? "";
+    form.value.code_exams = svc.code_exams ?? "";
     form.value.description         = svc.description         ?? "";
     form.value.service_number      = svc.service_number      ?? "";
     form.value.category_id         = svc.category_id         ?? null;