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

feat: :sparkles: feat (afastamentos) criada funcao de importar afastamentos

foi criada a funcao de importar excel para afastar associados, com base no exato mesmo excel da importacao de associados

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

+ 9 - 0
src/api/user.js

@@ -104,3 +104,12 @@ export const importAssociados = async (file) => {
   });
   return data.payload;
 };
+
+export const importAfastamentos = async (file) => {
+  const formData = new FormData();
+  formData.append("file", file);
+  const { data } = await api.post("/user/import/afastamentos", formData, {
+    headers: { "Content-Type": "multipart/form-data" },
+  });
+  return data.payload;
+};

+ 20 - 8
src/components/ImportHistoryDialog.vue

@@ -30,6 +30,7 @@
 </template>
 
 <script setup>
+import { computed } from "vue";
 import { useDialogPluginComponent } from "quasar";
 import { useI18n } from "vue-i18n";
 import DefaultDialogHeader from "src/components/defaults/DefaultDialogHeader.vue";
@@ -48,14 +49,25 @@ defineEmits([...useDialogPluginComponent.emits]);
 const { t } = useI18n();
 const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
 
-const columns = [
-  { name: "imported_at", label: t("import_log.date_time"),   field: "imported_at", align: "left",   sortable: false },
-  { name: "user",        label: t("import_log.imported_by"), field: "user",        align: "left",   sortable: false },
-  { name: "total",       label: t("import_log.total"),       field: "total",       align: "center", sortable: false },
-  { name: "created",     label: t("import_log.created"),     field: "created",     align: "center", sortable: false },
-  { name: "updated",     label: t("import_log.updated"),     field: "updated",     align: "center", sortable: false },
-  { name: "inactivated", label: t("import_log.inactivated"), field: "inactivated", align: "center", sortable: false },
-];
+const columns = computed(() => {
+  const base = [
+    { name: "imported_at", label: t("import_log.date_time"),   field: "imported_at", align: "left",   sortable: false },
+    { name: "user",        label: t("import_log.imported_by"), field: "user",        align: "left",   sortable: false },
+    { name: "total",       label: t("import_log.total"),       field: "total",       align: "center", sortable: false },
+    { name: "created",     label: t("import_log.created"),     field: "created",     align: "center", sortable: false },
+  ];
+
+  if (props.type === "afastamento") {
+    base.push({ name: "on_leave", label: t("import_log.on_leave"), field: "on_leave", align: "center", sortable: false });
+  } else {
+    base.push(
+      { name: "updated",     label: t("import_log.updated"),     field: "updated",     align: "center", sortable: false },
+      { name: "inactivated", label: t("import_log.inactivated"), field: "inactivated", align: "center", sortable: false },
+    );
+  }
+
+  return base;
+});
 
 const apiCall = (params) => getImportLogsPaginated({ ...params, type: props.type });
 

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

@@ -499,6 +499,7 @@
     "pending_refuse": "Refuse",
     "import_associados": "Associates",
     "import_sync_result": "{created} created · {updated} updated · {inactivated} inactivated",
+    "import_afastamentos_result": "{created} created · {on_leave} on leave",
     "import_processing": "Import is still processing. Please refresh the page in a moment.",
     "search_placeholder": "Search Members",
     "filter_by_position": "Filter by position",
@@ -800,6 +801,7 @@
     "total": "Total",
     "created": "New",
     "updated": "Updated",
+    "on_leave": "On leave",
     "inactivated": "Inactivated",
     "no_history": "No imports recorded"
   }

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

@@ -500,6 +500,7 @@
     "pending_refuse": "Rechazar",
     "import_associados": "Asociados",
     "import_sync_result": "{created} creados · {updated} actualizados · {inactivated} inactivados",
+    "import_afastamentos_result": "{created} nuevos · {on_leave} en licencia",
     "import_processing": "La importación está en progreso. Actualice la página en unos instantes.",
     "search_placeholder": "Buscar por asociados",
     "filter_by_position": "Filtrar por cargo",
@@ -800,6 +801,7 @@
     "total": "Total",
     "created": "Nuevos",
     "updated": "Actualizados",
+    "on_leave": "En licencia",
     "inactivated": "Desactivados",
     "no_history": "Ninguna importación registrada"
   }

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

@@ -500,6 +500,7 @@
     "pending_refuse": "Recusar",
     "import_associados": "Associados",
     "import_sync_result": "{created} criados · {updated} atualizados · {inactivated} inativados",
+    "import_afastamentos_result": "{created} novos · {on_leave} afastados",
     "import_processing": "Importação em andamento. Atualize a página em alguns instantes.",
     "search_placeholder": "Buscar por associados",
     "filter_by_position": "Filtrar por cargo",
@@ -801,6 +802,7 @@
     "total": "Total",
     "created": "Novos",
     "updated": "Atualizados",
+    "on_leave": "Afastados",
     "inactivated": "Desativados",
     "no_history": "Nenhuma importação registrada"
   }

+ 63 - 5
src/pages/gestao-associados/GestaoAssociadosPage.vue

@@ -7,6 +7,13 @@
       class="hidden"
       @change="onFileSelected"
     />
+    <input
+      ref="importAfastamentosFileInput"
+      type="file"
+      accept=".xlsx"
+      class="hidden"
+      @change="onAfastamentosFileSelected"
+    />
 
     <DefaultHeaderPage>
       <template #after>
@@ -19,6 +26,8 @@
             :label="$t('associado.import_afastamentos')"
             icon="mdi-upload"
             padding="6px 10px"
+            :loading="importingAfastamentos"
+            @click="onImportAfastamentosClick"
           />
           <q-btn
             unelevated
@@ -38,8 +47,18 @@
             icon="mdi-clock-outline"
             padding="6px 10px"
             :title="$t('import_log.history_title')"
-            @click="onOpenImportHistory"
-          />
+          >
+            <q-menu>
+              <q-list dense>
+                <q-item v-close-popup clickable @click="onOpenImportHistory('associado')">
+                  <q-item-section>{{ $t('associado.import_associados') }}</q-item-section>
+                </q-item>
+                <q-item v-close-popup clickable @click="onOpenImportHistory('afastamento')">
+                  <q-item-section>{{ $t('associado.import_afastamentos') }}</q-item-section>
+                </q-item>
+              </q-list>
+            </q-menu>
+          </q-btn>
           <q-btn
             unelevated
             dense
@@ -143,7 +162,7 @@ import { ref, computed, defineAsyncComponent, useTemplateRef } from "vue";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { permissionStore } from "src/stores/permission";
-import { getAssociadosPaginated, importAssociados, setUserOnLeave, approveUser } from "src/api/user";
+import { getAssociadosPaginated, importAssociados, importAfastamentos, setUserOnLeave, approveUser } 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";
@@ -169,6 +188,9 @@ const tableRef = ref(null);
 const importFileInput = useTemplateRef("importFileInput");
 const { polling: importing, start: startPolling } = useImportPoller();
 
+const importAfastamentosFileInput = useTemplateRef("importAfastamentosFileInput");
+const { polling: importingAfastamentos, start: startAfastamentosPolling } = useImportPoller();
+
 const selectedPosition = ref(null);
 const selectedSector = ref(null);
 const extraFilters = computed(() => ({
@@ -322,8 +344,8 @@ const onOpenAppointments = (row) => {
   });
 };
 
-const onOpenImportHistory = () => {
-  $q.dialog({ component: ImportHistoryDialog, componentProps: { type: 'associado' } });
+const onOpenImportHistory = (type) => {
+  $q.dialog({ component: ImportHistoryDialog, componentProps: { type } });
 };
 
 const onImportClick = () => {
@@ -362,6 +384,42 @@ const onFileSelected = async (event) => {
     $q.notify({ type: "negative", message: t("http.errors.failed") });
   }
 };
+
+const onImportAfastamentosClick = () => {
+  if (!permission_store.getAccess("associado", "add")) {
+    $q.notify({ type: "negative", message: t("validation.permissions.add") });
+    return;
+  }
+  importAfastamentosFileInput.value.value = null;
+  importAfastamentosFileInput.value.click();
+};
+
+const onAfastamentosFileSelected = async (event) => {
+  const file = event.target.files?.[0];
+  if (!file) return;
+
+  try {
+    const { import_id } = await importAfastamentos(file);
+
+    startAfastamentosPolling(import_id, {
+      onComplete: (stats) => {
+        $q.notify({
+          type: "positive",
+          message: t("associado.import_afastamentos_result", {
+            created:  stats.created,
+            on_leave: stats.on_leave,
+          }),
+          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") }),
+    });
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  }
+};
 </script>
 
 <style scoped>