Sfoglia il codice sorgente

Merge branch 'feature/SERPRATI-GUS-plataforma-v1' of gogs.softpar.inf.br:Softpar/sfp_front_vue_serprati_digital into feature/SERPRATI-GUS-plataforma-v1

kayo henrique 3 settimane fa
parent
commit
745eb659ba

+ 54 - 0
src/api/relatorio.js

@@ -0,0 +1,54 @@
+import api from "src/api";
+
+export const getRelatorioCounters = async () => {
+  const { data } = await api.get("/relatorio/counters");
+  return data.payload;
+};
+
+export const getNovoAssociadosPaginated = async ({ page = 1, perPage = 10, filter } = {}) => {
+  const params = { page, per_page: perPage };
+  if (filter) params.search = filter;
+  const { data } = await api.get("/relatorio/novos-associados", { params });
+  return { data: { result: data.payload } };
+};
+
+export const getContatosAssociadosPaginated = async ({ page = 1, perPage = 10, filter } = {}) => {
+  const params = { page, per_page: perPage };
+  if (filter) params.search = filter;
+  const { data } = await api.get("/relatorio/contatos-associados", { params });
+  return { data: { result: data.payload } };
+};
+
+export const getExclusoesMesPaginated = async ({ page = 1, perPage = 10, filter } = {}) => {
+  const params = { page, per_page: perPage };
+  if (filter) params.search = filter;
+  const { data } = await api.get("/relatorio/exclusoes-mes", { params });
+  return { data: { result: data.payload } };
+};
+
+export const exportRelatorio = async (tipo) => {
+  const nomes = {
+    "novos-associados": "novos_associados",
+    "contatos-associados": "contatos_associados",
+    "exclusoes-mes": "exclusoes_mes",
+  };
+
+  const hoje = new Date();
+  const dd = String(hoje.getDate()).padStart(2, "0");
+  const mm = String(hoje.getMonth() + 1).padStart(2, "0");
+  const yyyy = hoje.getFullYear();
+  const filename = `${nomes[tipo]}_${dd}-${mm}-${yyyy}.xlsx`;
+
+  const response = await api.get(`/relatorio/${tipo}/export`, {
+    responseType: "blob",
+  });
+
+  const url = window.URL.createObjectURL(new Blob([response.data]));
+  const link = document.createElement("a");
+  link.href = url;
+  link.setAttribute("download", filename);
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+  window.URL.revokeObjectURL(url);
+};

+ 14 - 12
src/components/defaults/DefaultTableServerSide.vue

@@ -334,19 +334,21 @@ watch(
 );
 
 watch(
-  pagination,
-  async (newVal, oldVal) => {
-    if (!oldVal || loading.value) return;
-
-    if (
-      newVal.rowsPerPage !== oldVal.rowsPerPage ||
-      newVal.filter !== oldVal.filter ||
-      newVal.page !== oldVal.page
-    ) {
-      await onRequest();
-    }
+  () => pagination.value.filter,
+  (newFilter, oldFilter) => {
+    if (newFilter === oldFilter || loading.value) return;
+    pagination.value.page = 1;
+    onRequest();
+  },
+);
+
+watch(
+  () => pagination.value.rowsPerPage,
+  (newVal, oldVal) => {
+    if (newVal === oldVal || loading.value) return;
+    pagination.value.page = 1;
+    onRequest();
   },
-  { deep: true },
 );
 
 onMounted(async () => {

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

@@ -52,6 +52,9 @@
     },
     "notificacoes": {
       "description": "History"
+    },
+    "relatorios": {
+      "description": "View management reports"
     }
   },
   "common": {
@@ -484,7 +487,10 @@
       "pt": "Português",
       "en": "English",
       "es": "Español"
-    }
+    },
+    "cpf": "CPF",
+    "phone": "Phone",
+    "email": "E-mail"
   },
   "charts": {
     "nps": {
@@ -629,5 +635,16 @@
     "empty": "No notifications found",
     "recipient_associado": "Associate",
     "recipient_parceiro": "Partner/Agreements"
+  },
+  "relatorio": {
+    "novos_associados": "New Members",
+    "contatos": "Contacts",
+    "exclusoes_mes": "Monthly Exclusions",
+    "exportar_csv": "Export Excel",
+    "col": {
+      "cadastro": "Registration Date",
+      "exclusao": "Exclusion Date"
+    },
+    "exportar_excel": "Export Excel"
   }
 }

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

@@ -52,6 +52,9 @@
     },
     "notificacoes": {
       "description": "Historial"
+    },
+    "relatorios": {
+      "description": "Visualice reportes gerenciales del sistema"
     }
   },
   "common": {
@@ -484,7 +487,10 @@
       "pt": "Português",
       "en": "English",
       "es": "Español"
-    }
+    },
+    "cpf": "CPF",
+    "phone": "Teléfono",
+    "email": "E-mail"
   },
   "charts": {
     "nps": {
@@ -628,5 +634,16 @@
     "empty": "No se encontraron notificaciones",
     "recipient_associado": "Asociado",
     "recipient_parceiro": "Socio/Convenios"
+  },
+  "relatorio": {
+    "novos_associados": "Nuevos Asociados",
+    "contatos": "Contactos",
+    "exclusoes_mes": "Exclusiones del mes",
+    "exportar_csv": "Exportar Excel",
+    "col": {
+      "cadastro": "Registro",
+      "exclusao": "Exclusión"
+    },
+    "exportar_excel": "Exportar Excel"
   }
 }

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

@@ -52,6 +52,9 @@
     },
     "notificacoes": {
       "description": "Histórico"
+    },
+    "relatorios": {
+      "description": "Visualize relatórios gerenciais do sistema"
     }
   },
   "common": {
@@ -484,7 +487,10 @@
       "pt": "Português",
       "en": "English",
       "es": "Español"
-    }
+    },
+    "cpf": "CPF",
+    "phone": "Telefone",
+    "email": "E-mail"
   },
   "charts": {
     "nps": {
@@ -629,5 +635,16 @@
     "empty": "Nenhuma notificação encontrada",
     "recipient_associado": "Associado",
     "recipient_parceiro": "Parceiro/Convênios"
+  },
+  "relatorio": {
+    "novos_associados": "Novos Associados",
+    "contatos": "Contatos",
+    "exclusoes_mes": "Exclusões no mês",
+    "exportar_csv": "Exportar Excel",
+    "col": {
+      "cadastro": "Cadastro",
+      "exclusao": "Exclusão"
+    },
+    "exportar_excel": "Exportar Excel"
   }
 }

+ 272 - 0
src/pages/relatorios/RelatoriosPage.vue

@@ -0,0 +1,272 @@
+<template>
+  <div>
+    <DefaultHeaderPage>
+      <template #after>
+        <q-btn
+          unelevated
+          class="btn-export"
+          :label="$t('relatorio.exportar_excel')"
+          icon="mdi-download"
+          padding="8px 16px"
+          :loading="exportLoading"
+          @click="onExport"
+        />
+      </template>
+    </DefaultHeaderPage>
+
+    <div class="relatorio-tabs-row q-mb-md">
+      <div
+        v-for="tab in tabs"
+        :key="tab.name"
+        class="relatorio-tab-card"
+        :class="{ active: activeTab === tab.name }"
+        @click="selectTab(tab.name)"
+      >
+        <q-icon :name="tab.icon" size="22px" color="violet-normal" />
+        <div class="tab-body">
+          <span class="tab-count">
+            {{ counters[tab.counterKey] !== undefined ? counters[tab.counterKey] : '—' }}
+          </span>
+          <span class="tab-label">{{ tab.label }}</span>
+        </div>
+      </div>
+    </div>
+
+    <div v-if="activeTab === 'novos-associados'">
+      <DefaultTableServerSide
+        :key="tableKey"
+        :columns="columnsNovosAssociados"
+        :api-call="getNovoAssociadosPaginated"
+        :add-item="false"
+        :show-search-field="true"
+      />
+    </div>
+
+    <div v-else-if="activeTab === 'contatos-associados'">
+      <DefaultTableServerSide
+        :key="tableKey"
+        :columns="columnsContatos"
+        :api-call="getContatosAssociadosPaginated"
+        :add-item="false"
+        :show-search-field="true"
+      />
+    </div>
+
+    <div v-else-if="activeTab === 'exclusoes-mes'">
+      <DefaultTableServerSide
+        :key="tableKey"
+        :columns="columnsExclusoes"
+        :api-call="getExclusoesMesPaginated"
+        :add-item="false"
+        :show-search-field="true"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from "vue";
+import { useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
+import DefaultTableServerSide from "src/components/defaults/DefaultTableServerSide.vue";
+import {
+  getRelatorioCounters,
+  getNovoAssociadosPaginated,
+  getContatosAssociadosPaginated,
+  getExclusoesMesPaginated,
+  exportRelatorio,
+} from "src/api/relatorio.js";
+
+const $q = useQuasar();
+const { t } = useI18n();
+
+const activeTab = ref("novos-associados");
+const tableKey = ref(0);
+const exportLoading = ref(false);
+const counters = ref({
+  novos_associados: undefined,
+  contatos: undefined,
+  exclusoes_mes: undefined,
+});
+
+const tabs = [
+  {
+    name: "novos-associados",
+    label: t("relatorio.novos_associados"),
+    icon: "mdi-account-multiple-outline",
+    counterKey: "novos_associados",
+  },
+  {
+    name: "contatos-associados",
+    label: t("relatorio.contatos"),
+    icon: "mdi-phone-outline",
+    counterKey: "contatos",
+  },
+  {
+    name: "exclusoes-mes",
+    label: t("relatorio.exclusoes_mes"),
+    icon: "mdi-account-remove-outline",
+    counterKey: "exclusoes_mes",
+  },
+];
+
+const columnsNovosAssociados = [
+  {
+    name: "name",
+    label: t("common.terms.name"),
+    field: "name",
+    align: "left",
+    sortable: true,
+  },
+  {
+    name: "cpf",
+    label: t("associado.cpf"),
+    field: "cpf",
+    align: "left",
+    sortable: false,
+  },
+  {
+    name: "created_at",
+    label: t("relatorio.col.cadastro"),
+    field: "created_at",
+    align: "left",
+    sortable: false,
+  },
+];
+
+const columnsContatos = [
+  {
+    name: "name",
+    label: t("common.terms.name"),
+    field: "name",
+    align: "left",
+    sortable: true,
+  },
+  {
+    name: "phone",
+    label: t("associado.phone"),
+    field: "phone",
+    align: "left",
+    sortable: false,
+  },
+  {
+    name: "email",
+    label: t("associado.email"),
+    field: "email",
+    align: "left",
+    sortable: false,
+  },
+];
+
+const columnsExclusoes = [
+  {
+    name: "name",
+    label: t("common.terms.name"),
+    field: "name",
+    align: "left",
+    sortable: true,
+  },
+  {
+    name: "cpf",
+    label: t("associado.cpf"),
+    field: "cpf",
+    align: "left",
+    sortable: false,
+  },
+  {
+    name: "excluded_at",
+    label: t("relatorio.col.exclusao"),
+    field: "excluded_at",
+    align: "left",
+    sortable: false,
+  },
+];
+
+const selectTab = (name) => {
+  activeTab.value = name;
+  tableKey.value++;
+};
+
+const onExport = async () => {
+  exportLoading.value = true;
+  try {
+    await exportRelatorio(activeTab.value);
+  } catch {
+    $q.notify({ type: "negative", message: t("http.errors.failed") });
+  } finally {
+    exportLoading.value = false;
+  }
+};
+
+onMounted(async () => {
+  try {
+    const data = await getRelatorioCounters();
+    counters.value = data;
+  } catch {
+    // counters permanecem como undefined, exibindo '—'
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+@use "src/css/quasar.variables.scss" as vars;
+
+.relatorio-tabs-row {
+  display: flex;
+  gap: 12px;
+}
+
+.relatorio-tab-card {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 14px 20px;
+  border-radius: 8px;
+  border: 1.5px solid vars.$color-border;
+  background: vars.$surface;
+  cursor: pointer;
+  transition: border-color 0.15s, background 0.15s;
+  user-select: none;
+
+  &:hover:not(.active) {
+    border-color: vars.$violet-light-active;
+    background: vars.$violet-light;
+  }
+
+  &.active {
+    border-color: vars.$violet-normal;
+    background: vars.$violet-light;
+  }
+}
+
+.tab-body {
+  display: flex;
+  align-items: baseline;
+  gap: 8px;
+}
+
+.tab-count {
+  font-size: 1.375rem;
+  font-weight: 700;
+  color: vars.$violet-normal;
+  line-height: 1;
+}
+
+.tab-label {
+  font-size: 0.875rem;
+  color: vars.$color-text-2;
+  font-weight: 400;
+}
+
+.btn-export {
+  background: linear-gradient(90deg, #4d1658 0%, #8b30a5 100%) !important;
+  color: white !important;
+  border-radius: 8px !important;
+}
+
+.btn-export :deep(.q-icon) {
+  color: white !important;
+}
+</style>

+ 20 - 0
src/router/routes/relatorio.route.js

@@ -0,0 +1,20 @@
+export default [
+  {
+    path: "/relatorios",
+    name: "RelatoriosPage",
+    component: () => import("pages/relatorios/RelatoriosPage.vue"),
+    meta: {
+      title: { value: "ui.navigation.relatorios", translate: true },
+      description: { value: "page.relatorios.description", translate: true },
+      requireAuth: true,
+      requiredPermission: "relatorio",
+      breadcrumbs: [
+        {
+          name: "RelatoriosPage",
+          title: "ui.navigation.relatorios",
+          translate: true,
+        },
+      ],
+    },
+  },
+];

+ 1 - 1
src/stores/navigation.js

@@ -74,7 +74,7 @@ export const navigationStore = defineStore("navigation", () => {
     {
       type: "single",
       title: "ui.navigation.relatorios",
-      name: "relatorios",
+      name: "RelatoriosPage",
       icon: "mdi-chart-bar",
       permission: false,
       permissionScope: "relatorio",