Преглед изворни кода

feat: :sparkles: feat (queue-jobs import) adicionado queue e jobs nos imports

foram implementados queue e jobs nos imports para conseguir importar grandes quantidades de dados

fase:dev | origin:escopo
Gustavo Zanatta пре 1 недеља
родитељ
комит
a09c904649

+ 6 - 0
src/api/importStatus.js

@@ -0,0 +1,6 @@
+import api from "src/api";
+
+export const getImportStatus = async (importId) => {
+  const { data } = await api.get(`/import-status/${importId}`);
+  return data.payload;
+};

+ 56 - 0
src/composables/useImportPoller.js

@@ -0,0 +1,56 @@
+import { ref, onUnmounted } from "vue";
+import { getImportStatus } from "src/api/importStatus";
+
+const POLL_INTERVAL_MS = 30000;
+const MAX_ATTEMPTS     = 10; // 5 minutes
+
+export function useImportPoller() {
+  const polling   = ref(false);
+  let   timerId   = null;
+  let   attempts  = 0;
+
+  const stop = () => {
+    if (timerId !== null) {
+      clearInterval(timerId);
+      timerId  = null;
+      attempts = 0;
+    }
+    polling.value = false;
+  };
+
+  onUnmounted(stop);
+
+  const start = (importId, { onComplete, onError, onTimeout }) => {
+    stop();
+    polling.value = true;
+    attempts      = 0;
+
+    timerId = setInterval(async () => {
+      attempts++;
+
+      if (attempts > MAX_ATTEMPTS) {
+        stop();
+        onTimeout?.();
+        return;
+      }
+
+      try {
+        const result = await getImportStatus(importId);
+
+        if (result.status === "completed") {
+          stop();
+          onComplete?.(result.stats);
+        } else if (result.status === "failed") {
+          stop();
+          onError?.(result.message ?? "unknown error");
+        }
+        // 'pending' | 'processing' → keep polling
+      } catch {
+        stop();
+        onError?.("request_failed");
+      }
+    }, POLL_INTERVAL_MS);
+  };
+
+  return { polling, start, stop };
+}

+ 2 - 0
src/i18n/locales/en.json

@@ -423,6 +423,7 @@
     "import_afastamentos": "Absences",
     "import_associados": "Import Associates",
     "import_sync_result": "{created} created · {updated} updated · {inactivated} inactivated",
+    "import_processing": "Import is still processing. Please refresh the page in a moment.",
     "search_placeholder": "Search Members",
     "no_dependents": "No dependents registered",
     "remove_photo": "Do you want to remove your profile photo?",
@@ -611,6 +612,7 @@
     "import_convenios":        "Import Medical Agreements",
     "import_parceiros_result": "{created} created · {updated} updated · {inactivated} inactivated",
     "import_convenios_result": "{partners_created} clinics created · {services_created} services created · {partners_inactivated} inactivated",
+    "import_processing":       "Import is still processing. Please refresh the page in a moment.",
     "cadastro_title":          "Partner Registration",
     "cadastro_parceiro":       "Partner Registration",
     "dados_parceiro":          "Partner Data",

+ 2 - 0
src/i18n/locales/es.json

@@ -423,6 +423,7 @@
     "import_afastamentos": "Ausencias",
     "import_associados": "Importar Asociados",
     "import_sync_result": "{created} creados · {updated} actualizados · {inactivated} inactivados",
+    "import_processing": "La importación está en progreso. Actualice la página en unos instantes.",
     "search_placeholder": "Buscar Asociados",
     "no_dependents": "Ningún dependiente registrado",
     "remove_photo": "¿Deseas eliminar tu foto de perfil?",
@@ -610,6 +611,7 @@
     "import_convenios":        "Importar Convenios Médicos",
     "import_parceiros_result": "{created} creados · {updated} actualizados · {inactivated} inactivados",
     "import_convenios_result": "{partners_created} clínicas creadas · {services_created} servicios creados · {partners_inactivated} inactivados",
+    "import_processing":       "La importación está en progreso. Actualice la página en unos instantes.",
     "cadastro_title":          "Registro de Socio",
     "cadastro_parceiro":       "Registro de Socio",
     "dados_parceiro":          "Datos del Socio",

+ 2 - 0
src/i18n/locales/pt.json

@@ -423,6 +423,7 @@
     "import_afastamentos": "Afastamentos",
     "import_associados": "Importar Associados",
     "import_sync_result": "{created} criados · {updated} atualizados · {inactivated} inativados",
+    "import_processing": "Importação em andamento. Atualize a página em alguns instantes.",
     "search_placeholder": "Buscar por Associados",
     "no_dependents": "Nenhum dependente cadastrado",
     "remove_photo": "Deseja remover sua foto de perfil?",
@@ -611,6 +612,7 @@
     "import_convenios":        "Importar Convênios Médicos",
     "import_parceiros_result": "{created} criados · {updated} atualizados · {inactivated} inativados",
     "import_convenios_result": "{partners_created} clínicas criadas · {services_created} serviços criados · {partners_inactivated} inativados",
+    "import_processing":       "Importação em andamento. Atualize a página em alguns instantes.",
     "cadastro_title":          "Cadastro de Parceiro",
     "cadastro_parceiro":       "Cadastro de Parceiro",
     "dados_parceiro":          "Dados de Parceiro",

+ 19 - 14
src/pages/gestao-associados/GestaoAssociadosPage.vue

@@ -80,6 +80,7 @@ import { useI18n } from "vue-i18n";
 import { permissionStore } from "src/stores/permission";
 import { getAssociadosPaginated, importAssociados } from "src/api/user";
 import { getStatusColor, getStatusI18nKey } from "src/helpers/utils";
+import { useImportPoller } from "src/composables/useImportPoller";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultTableServerSide from "src/components/defaults/DefaultTableServerSide.vue";
 import AccessLogsTable from "./components/AccessLogsTable.vue";
@@ -93,7 +94,7 @@ const $q = useQuasar();
 const { t } = useI18n();
 const tableRef = ref(null);
 const importFileInput = useTemplateRef("importFileInput");
-const importing = ref(false);
+const { polling: importing, start: startPolling } = useImportPoller();
 
 const columns = computed(() => [
   {
@@ -192,23 +193,27 @@ const onFileSelected = async (event) => {
   const file = event.target.files?.[0];
   if (!file) return;
 
-  importing.value = true;
   try {
-    const stats = await importAssociados(file);
-    $q.notify({
-      type: "positive",
-      message: t("associado.import_sync_result", {
-        created:     stats.created,
-        updated:     stats.updated,
-        inactivated: stats.inactivated,
-      }),
-      timeout: 6000,
+    const { import_id } = await importAssociados(file);
+
+    startPolling(import_id, {
+      onComplete: (stats) => {
+        $q.notify({
+          type: "positive",
+          message: t("associado.import_sync_result", {
+            created:     stats.created,
+            updated:     stats.updated,
+            inactivated: stats.inactivated,
+          }),
+          timeout: 6000,
+        });
+        tableRef.value?.refresh();
+      },
+      onError: () => $q.notify({ type: "negative", message: t("http.errors.failed") }),
+      onTimeout: () => $q.notify({ type: "warning", message: t("associado.import_processing") }),
     });
-    tableRef.value?.refresh();
   } catch {
     $q.notify({ type: "negative", message: t("http.errors.failed") });
-  } finally {
-    importing.value = false;
   }
 };
 </script>

+ 38 - 29
src/pages/parceiros-convenios/ParceirosConveniosPage.vue

@@ -116,6 +116,7 @@ import { userStore } from "src/stores/user";
 import { getPartnerAgreements, importParceiros, importConveniosMedicos } from "src/api/partnerAgreement";
 import { getCategories } from "src/api/category";
 import { normalizeString } from "src/helpers/utils";
+import { useImportPoller } from "src/composables/useImportPoller";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import PartnerAgreementCard from "./components/PartnerAgreementCard.vue";
 
@@ -130,10 +131,10 @@ const allPartners = ref([]);
 const categories = ref([]);
 const activeCategory = ref("all");
 const searchQuery = ref("");
-const importingParceiros = ref(false);
-const importingConvenios = ref(false);
-const importParceiroInput = useTemplateRef("importParceiroInput");
+const importParceiroInput  = useTemplateRef("importParceiroInput");
 const importConveniosInput = useTemplateRef("importConveniosInput");
+const { polling: importingParceiros, start: startParceiroPolling }   = useImportPoller();
+const { polling: importingConvenios, start: startConveniosPolling }  = useImportPoller();
 
 const filteredPartners = computed(() => {
   let list = allPartners.value;
@@ -200,23 +201,27 @@ const onParceiroFileSelected = async (event) => {
   const file = event.target.files?.[0];
   if (!file) return;
 
-  importingParceiros.value = true;
   try {
-    const stats = await importParceiros(file);
-    $q.notify({
-      type: "positive",
-      message: t("parceiro.import_parceiros_result", {
-        created:     stats.created,
-        updated:     stats.updated,
-        inactivated: stats.inactivated,
-      }),
-      timeout: 7000,
+    const { import_id } = await importParceiros(file);
+
+    startParceiroPolling(import_id, {
+      onComplete: async (stats) => {
+        $q.notify({
+          type: "positive",
+          message: t("parceiro.import_parceiros_result", {
+            created:     stats.created,
+            updated:     stats.updated,
+            inactivated: stats.inactivated,
+          }),
+          timeout: 7000,
+        });
+        await loadPartners();
+      },
+      onError:   () => $q.notify({ type: "negative", message: t("http.errors.failed") }),
+      onTimeout: () => $q.notify({ type: "warning",  message: t("parceiro.import_processing") }),
     });
-    await loadPartners();
   } catch {
     $q.notify({ type: "negative", message: t("http.errors.failed") });
-  } finally {
-    importingParceiros.value = false;
   }
 };
 
@@ -224,23 +229,27 @@ const onConveniosFileSelected = async (event) => {
   const file = event.target.files?.[0];
   if (!file) return;
 
-  importingConvenios.value = true;
   try {
-    const stats = await importConveniosMedicos(file);
-    $q.notify({
-      type: "positive",
-      message: t("parceiro.import_convenios_result", {
-        partners_created:    stats.partners_created,
-        services_created:    stats.services_created,
-        partners_inactivated: stats.partners_inactivated,
-      }),
-      timeout: 7000,
+    const { import_id } = await importConveniosMedicos(file);
+
+    startConveniosPolling(import_id, {
+      onComplete: async (stats) => {
+        $q.notify({
+          type: "positive",
+          message: t("parceiro.import_convenios_result", {
+            partners_created:     stats.partners_created,
+            services_created:     stats.services_created,
+            partners_inactivated: stats.partners_inactivated,
+          }),
+          timeout: 7000,
+        });
+        await loadPartners();
+      },
+      onError:   () => $q.notify({ type: "negative", message: t("http.errors.failed") }),
+      onTimeout: () => $q.notify({ type: "warning",  message: t("parceiro.import_processing") }),
     });
-    await loadPartners();
   } catch {
     $q.notify({ type: "negative", message: t("http.errors.failed") });
-  } finally {
-    importingConvenios.value = false;
   }
 };