浏览代码

feat: :sparkles: feat (importacoes) criada importacoes do sistema

foram criadas as importacoes de associados, parceiros e convenios medicos

fase:dev | origin:escopo
Gustavo Zanatta 1 周之前
父节点
当前提交
4b12748fd3

+ 18 - 0
src/api/partnerAgreement.js

@@ -130,3 +130,21 @@ export const uploadMyPartnerMedia = async (file) => {
 export const deleteMyPartnerMedia = async (mediaId) => {
   await api.delete(`/partner-agreement/my/media/${mediaId}`);
 };
+
+export const importParceiros = async (file) => {
+  const form = new FormData();
+  form.append("file", file);
+  const { data } = await api.post("/partner-agreement/import/parceiros", form, {
+    headers: { "Content-Type": "multipart/form-data" },
+  });
+  return data.payload;
+};
+
+export const importConveniosMedicos = async (file) => {
+  const form = new FormData();
+  form.append("file", file);
+  const { data } = await api.post("/partner-agreement/import/convenios-medicos", form, {
+    headers: { "Content-Type": "multipart/form-data" },
+  });
+  return data.payload;
+};

+ 9 - 0
src/api/user.js

@@ -65,3 +65,12 @@ export const getUsersPaginated = async ({ page = 1, perPage = 10, filter, type,
 
 export const getAssociadosPaginated = async (params = {}) =>
   getUsersPaginated({ ...params, type: "associado" });
+
+export const importAssociados = async (file) => {
+  const formData = new FormData();
+  formData.append("file", file);
+  const { data } = await api.post("/user/import/associados", formData, {
+    headers: { "Content-Type": "multipart/form-data" },
+  });
+  return data.payload;
+};

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

@@ -421,6 +421,8 @@
     "dependent": "Dependent",
     "dependents": "Dependents",
     "import_afastamentos": "Absences",
+    "import_associados": "Import Associates",
+    "import_sync_result": "{created} created · {updated} updated · {inactivated} inactivated",
     "search_placeholder": "Search Members",
     "no_dependents": "No dependents registered",
     "remove_photo": "Do you want to remove your profile photo?",
@@ -605,6 +607,10 @@
     "supplier_price":          "Supplier Price",
     "requires_scheduling":     "Requires Scheduling",
     "search_placeholder":      "Search for service",
+    "import_parceiros":        "Import Partners",
+    "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",
     "cadastro_title":          "Partner Registration",
     "cadastro_parceiro":       "Partner Registration",
     "dados_parceiro":          "Partner Data",

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

@@ -421,6 +421,8 @@
     "dependent": "Dependiente",
     "dependents": "Dependientes",
     "import_afastamentos": "Ausencias",
+    "import_associados": "Importar Asociados",
+    "import_sync_result": "{created} creados · {updated} actualizados · {inactivated} inactivados",
     "search_placeholder": "Buscar Asociados",
     "no_dependents": "Ningún dependiente registrado",
     "remove_photo": "¿Deseas eliminar tu foto de perfil?",
@@ -604,6 +606,10 @@
     "supplier_price":          "Precio Proveedor",
     "requires_scheduling":     "Requiere Programación",
     "search_placeholder":      "Buscar por servicio",
+    "import_parceiros":        "Importar Socios",
+    "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",
     "cadastro_title":          "Registro de Socio",
     "cadastro_parceiro":       "Registro de Socio",
     "dados_parceiro":          "Datos del Socio",

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

@@ -421,6 +421,8 @@
     "dependent": "Dependente",
     "dependents": "Dependentes",
     "import_afastamentos": "Afastamentos",
+    "import_associados": "Importar Associados",
+    "import_sync_result": "{created} criados · {updated} atualizados · {inactivated} inativados",
     "search_placeholder": "Buscar por Associados",
     "no_dependents": "Nenhum dependente cadastrado",
     "remove_photo": "Deseja remover sua foto de perfil?",
@@ -605,6 +607,10 @@
     "supplier_price":          "Preço Fornecedor",
     "requires_scheduling":     "Requer Agendamento",
     "search_placeholder":      "Pesquisar por serviço",
+    "import_parceiros":        "Importar Parceiros",
+    "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",
     "cadastro_title":          "Cadastro de Parceiro",
     "cadastro_parceiro":       "Cadastro de Parceiro",
     "dados_parceiro":          "Dados de Parceiro",

+ 48 - 3
src/pages/gestao-associados/GestaoAssociadosPage.vue

@@ -1,5 +1,13 @@
 <template>
   <div>
+    <input
+      ref="importFileInput"
+      type="file"
+      accept=".xlsx"
+      class="hidden"
+      @change="onFileSelected"
+    />
+
     <DefaultHeaderPage>
       <template #after>
         <div class="flex gap-sm q-mt-md">
@@ -13,9 +21,11 @@
           <q-btn
             unelevated
             class="btn-gradient"
-            :label="$t('common.actions.import')"
+            :label="$t('associado.import_associados')"
             icon="mdi-upload"
             padding="8px 12px"
+            :loading="importing"
+            @click="onImportClick"
           />
           <q-btn
             unelevated
@@ -64,11 +74,11 @@
 </template>
 
 <script setup>
-import { ref, computed, defineAsyncComponent } from "vue";
+import { ref, computed, defineAsyncComponent, useTemplateRef } from "vue";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { permissionStore } from "src/stores/permission";
-import { getAssociadosPaginated } from "src/api/user";
+import { getAssociadosPaginated, importAssociados } from "src/api/user";
 import { getStatusColor, getStatusI18nKey } from "src/helpers/utils";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 import DefaultTableServerSide from "src/components/defaults/DefaultTableServerSide.vue";
@@ -82,6 +92,8 @@ const permission_store = permissionStore();
 const $q = useQuasar();
 const { t } = useI18n();
 const tableRef = ref(null);
+const importFileInput = useTemplateRef("importFileInput");
+const importing = ref(false);
 
 const columns = computed(() => [
   {
@@ -166,6 +178,39 @@ const onRowClick = async ({ row }) => {
     tableRef.value?.refresh();
   });
 };
+
+const onImportClick = () => {
+  if (!permission_store.getAccess("associado", "add")) {
+    $q.notify({ type: "negative", message: t("validation.permissions.add") });
+    return;
+  }
+  importFileInput.value.value = null;
+  importFileInput.value.click();
+};
+
+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,
+    });
+    tableRef.value?.refresh();
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  } finally {
+    importing.value = false;
+  }
+};
 </script>
 
 <style scoped>

+ 115 - 8
src/pages/parceiros-convenios/ParceirosConveniosPage.vue

@@ -1,8 +1,41 @@
 <template>
   <div class="parceiros-page">
+    <input
+      ref="importParceiroInput"
+      type="file"
+      accept=".xlsx"
+      class="hidden"
+      @change="onParceiroFileSelected"
+    />
+    <input
+      ref="importConveniosInput"
+      type="file"
+      accept=".xlsx"
+      class="hidden"
+      @change="onConveniosFileSelected"
+    />
+
     <DefaultHeaderPage>
       <template #after>
         <div v-if="isAdministrador" class="flex gap-sm q-mt-md">
+          <q-btn
+            unelevated
+            class="btn-gradient"
+            :label="$t('parceiro.import_parceiros')"
+            icon="mdi-upload"
+            padding="8px 12px"
+            :loading="importingParceiros"
+            @click="onImportParceirosClick"
+          />
+          <q-btn
+            unelevated
+            class="btn-gradient"
+            :label="$t('parceiro.import_convenios')"
+            icon="mdi-hospital-box"
+            padding="8px 12px"
+            :loading="importingConvenios"
+            @click="onImportConveniosClick"
+          />
           <q-btn
             unelevated
             class="btn-gradient"
@@ -74,13 +107,13 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from "vue";
+import { ref, computed, onMounted, useTemplateRef } from "vue";
 import { useRouter } from "vue-router";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { permissionStore } from "src/stores/permission";
 import { userStore } from "src/stores/user";
-import { getPartnerAgreements } from "src/api/partnerAgreement";
+import { getPartnerAgreements, importParceiros, importConveniosMedicos } from "src/api/partnerAgreement";
 import { getCategories } from "src/api/category";
 import { normalizeString } from "src/helpers/utils";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
@@ -97,6 +130,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 importConveniosInput = useTemplateRef("importConveniosInput");
 
 const filteredPartners = computed(() => {
   let list = allPartners.value;
@@ -132,14 +169,84 @@ const onEditItem = (partner) => {
   router.push({ name: "ParceiroCadastroPage", params: { id: partner.id } });
 };
 
+const loadPartners = async () => {
+  const [partners, cats] = await Promise.all([
+    getPartnerAgreements(),
+    getCategories("partner"),
+  ]);
+  allPartners.value = partners;
+  categories.value = cats;
+};
+
+const onImportParceirosClick = () => {
+  if (!permission_store.getAccess("parceiro.convenio", "add")) {
+    $q.notify({ type: "negative", message: t("validation.permissions.add") });
+    return;
+  }
+  importParceiroInput.value.value = null;
+  importParceiroInput.value.click();
+};
+
+const onImportConveniosClick = () => {
+  if (!permission_store.getAccess("parceiro.convenio", "add")) {
+    $q.notify({ type: "negative", message: t("validation.permissions.add") });
+    return;
+  }
+  importConveniosInput.value.value = null;
+  importConveniosInput.value.click();
+};
+
+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,
+    });
+    await loadPartners();
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  } finally {
+    importingParceiros.value = false;
+  }
+};
+
+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,
+    });
+    await loadPartners();
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  } finally {
+    importingConvenios.value = false;
+  }
+};
+
 onMounted(async () => {
   try {
-    const [partners, cats] = await Promise.all([
-      getPartnerAgreements(),
-      getCategories("partner"),
-    ]);
-    allPartners.value = partners;
-    categories.value = cats;
+    await loadPartners();
   } finally {
     loading.value = false;
   }