|
|
@@ -16,7 +16,7 @@
|
|
|
>
|
|
|
<q-card
|
|
|
flat
|
|
|
- class="stat-card cursor-pointer bg-white"
|
|
|
+ class="stat-card cursor-pointer bg-white full-height"
|
|
|
:class="{ 'stat-card--active': activeCard === card.key }"
|
|
|
@click="onCardClick(card)"
|
|
|
>
|
|
|
@@ -25,7 +25,7 @@
|
|
|
<div class="text-h5 text-weight-bold q-mt-xs text-dark">
|
|
|
{{ stats[card.key] ?? '—' }}
|
|
|
</div>
|
|
|
- <div class="text-caption text-grey-7 ellipsis">
|
|
|
+ <div class="text-grey-7 stat-card__label">
|
|
|
{{ $t(card.labelKey) }}
|
|
|
</div>
|
|
|
</q-card-section>
|
|
|
@@ -33,18 +33,26 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <q-card v-if="activeCard" flat class="bg-white" style="border-radius: 12px">
|
|
|
+ <q-card v-if="activeCardConfig" flat class="bg-white" style="border-radius: 12px">
|
|
|
<DefaultTableServerSide
|
|
|
ref="tableRef"
|
|
|
:key="activeCard"
|
|
|
- :columns="tableColumns"
|
|
|
+ :columns="activeCardConfig.columns"
|
|
|
:api-call="activeApiCall"
|
|
|
+ :extra-filters="extraFilters"
|
|
|
+ :search-placeholder="$t(activeCardConfig.searchPlaceholderKey)"
|
|
|
:add-item="false"
|
|
|
- sem-table-top
|
|
|
- :open-item="['dependentes_pendentes', 'associados_pendentes'].includes(activeCard)"
|
|
|
- :show-search-field="!['ultimos_acessos', 'associados_com_acesso_app'].includes(activeCard)"
|
|
|
+ :open-item="!!activeCardConfig.approval"
|
|
|
@on-row-click="onRowClick"
|
|
|
>
|
|
|
+ <template #filters>
|
|
|
+ <DashboardFilters
|
|
|
+ v-model="filters"
|
|
|
+ :card="activeCardConfig"
|
|
|
+ :status-options="statusOptions"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+
|
|
|
<template #body-cell-status="{ row }">
|
|
|
<q-td>
|
|
|
<q-badge
|
|
|
@@ -56,6 +64,16 @@
|
|
|
</q-td>
|
|
|
</template>
|
|
|
|
|
|
+ <template #body-cell-type="{ row }">
|
|
|
+ <q-td class="text-center">
|
|
|
+ <q-badge
|
|
|
+ outline
|
|
|
+ :color="row.type === 'agreement' ? 'teal' : 'violet-normal'"
|
|
|
+ :label="row.type === 'agreement' ? $t('parceiro.type.agreement') : $t('parceiro.type.partner')"
|
|
|
+ />
|
|
|
+ </q-td>
|
|
|
+ </template>
|
|
|
+
|
|
|
<template #body-cell-user_type="{ row }">
|
|
|
<q-td class="text-center">
|
|
|
<q-badge
|
|
|
@@ -85,14 +103,12 @@ import { useQuasar } from "quasar";
|
|
|
import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
|
import DefaultTableServerSide from "src/components/defaults/DefaultTableServerSide.vue";
|
|
|
import { getDashboardStats } from "src/api/dashboard";
|
|
|
-import { getUsersPaginated } from "src/api/user";
|
|
|
-import { getDependentsPaginated } from "src/api/profile";
|
|
|
-import { getPartnerAgreementsPaginated, getExpiringPartnerAgreementsPaginated } from "src/api/partnerAgreement";
|
|
|
-import { getAccessLogsPaginated } from "src/api/userAccessLog";
|
|
|
-import { formatDateYMDtoDMY, getStatusColor, getStatusI18nKey } from "src/helpers/utils";
|
|
|
+import { getStatusColor, getStatusI18nKey } from "src/helpers/utils";
|
|
|
import { permissionStore } from "src/stores/permission";
|
|
|
+import DashboardFilters from "./components/DashboardFilters.vue";
|
|
|
import DependentApprovalDialog from "./components/DependentApprovalDialog.vue";
|
|
|
import AssociadoApprovalDialog from "./components/AssociadoApprovalDialog.vue";
|
|
|
+import { buildDashboardCards, formatDateTime } from "./dashboardCards";
|
|
|
|
|
|
const { t } = useI18n();
|
|
|
const $q = useQuasar();
|
|
|
@@ -103,110 +119,67 @@ const statsLoading = ref(true);
|
|
|
const stats = ref({});
|
|
|
const activeCard = ref(null);
|
|
|
|
|
|
-const statCards = [
|
|
|
- { key: "total_associados", icon: "mdi-account-group", labelKey: "dashboard.stats.total_associados" },
|
|
|
- { key: "associados_ativos", icon: "mdi-trending-up", labelKey: "dashboard.stats.associados_ativos" },
|
|
|
- { key: "parceiros", icon: "mdi-handshake", labelKey: "dashboard.stats.parceiros" },
|
|
|
- { key: "contratos_a_vencer", icon: "mdi-file-clock", labelKey: "dashboard.stats.contratos_a_vencer" },
|
|
|
- { key: "novos_mes", icon: "mdi-account-plus", labelKey: "dashboard.stats.novos_mes" },
|
|
|
- { key: "associados_pendentes", icon: "mdi-account-alert", labelKey: "dashboard.stats.associados_pendentes" },
|
|
|
- { key: "dependentes_pendentes", icon: "mdi-account-child-circle", labelKey: "dashboard.stats.dependentes_pendentes" },
|
|
|
- { key: "ultimos_acessos", icon: "mdi-login-variant", labelKey: "dashboard.stats.ultimos_acessos" },
|
|
|
- { key: "associados_com_acesso_app", icon: "mdi-cellphone-check", labelKey: "dashboard.stats.total_acessos_app" },
|
|
|
-];
|
|
|
-
|
|
|
-// Colunas por tipo de card
|
|
|
-const columnsAssociados = computed(() => [
|
|
|
- { name: "name", label: t("common.terms.name"), field: "name", align: "left", sortable: true },
|
|
|
- { name: "email", label: t("common.terms.email"), field: "email", align: "left", sortable: true },
|
|
|
- { name: "created_at", label: t("dashboard.stats.association_date"), field: (row) => formatDateYMDtoDMY(row.created_at), align: "left", sortable: true },
|
|
|
- { name: "status", label: t("common.terms.status"), field: "status", align: "left" },
|
|
|
-]);
|
|
|
-
|
|
|
-const columnsParceiros = computed(() => [
|
|
|
- { name: "company_name", label: t("common.terms.name"), field: "company_name", align: "left", sortable: true },
|
|
|
- { name: "responsible", label: t("parceiro.responsible"), field: "responsible", align: "left", sortable: true },
|
|
|
- { name: "created_at", label: t("dashboard.stats.registration_date"), field: (row) => formatDateYMDtoDMY(row.created_at), align: "left", sortable: true },
|
|
|
- { name: "status", label: t("common.terms.status"), field: "status", align: "left" },
|
|
|
-]);
|
|
|
-
|
|
|
-const columnsContratosAVencer = computed(() => [
|
|
|
- { name: "company_name", label: t("common.terms.name"), field: "company_name", align: "left", sortable: true },
|
|
|
- { name: "responsible", label: t("parceiro.responsible"), field: "responsible", align: "left", sortable: true },
|
|
|
- { name: "contract_end", label: t("parceiro.contract_end"), field: (row) => formatDateYMDtoDMY(row.contract_end), align: "left", sortable: true },
|
|
|
- { name: "status", label: t("common.terms.status"), field: "status", align: "left" },
|
|
|
-]);
|
|
|
-
|
|
|
-const columnsDependentes = computed(() => [
|
|
|
- { name: "responsible_user_name", label: t("associado.associado"), field: (row) => row.responsible_user?.name ?? "—", align: "left", sortable: false },
|
|
|
- { name: "name", label: t("associado.dependent"), field: "name", align: "left", sortable: false },
|
|
|
- { name: "created_at", label: t("associado.dependent_registered_at"), field: (row) => formatDateTime(row.created_at), align: "left", sortable: false },
|
|
|
-]);
|
|
|
-
|
|
|
-const columnsAccessLogs = computed(() => [
|
|
|
- { name: "user_name", label: t("access_log.user"), field: "user_name", align: "left", sortable: false },
|
|
|
- { name: "user_type", label: t("access_log.user_type"), field: "user_type", align: "center", sortable: false },
|
|
|
- { name: "accessed_at", label: t("access_log.accessed_at"), field: "accessed_at", align: "left", sortable: false },
|
|
|
-]);
|
|
|
-
|
|
|
-const tableColumns = computed(() => {
|
|
|
- if (!activeCard.value) return [];
|
|
|
- if (activeCard.value === "contratos_a_vencer") return columnsContratosAVencer.value;
|
|
|
- if (activeCard.value === "parceiros" || activeCard.value === "novos_mes") return columnsParceiros.value;
|
|
|
- if (activeCard.value === "ultimos_acessos" || activeCard.value === "associados_com_acesso_app") return columnsAccessLogs.value;
|
|
|
- if (activeCard.value === "dependentes_pendentes") return columnsDependentes.value;
|
|
|
- return columnsAssociados.value;
|
|
|
-});
|
|
|
+// Filtros do bloco aberto e status disponíveis (mapeados pelo backend)
|
|
|
+const filters = ref({});
|
|
|
+const statusOptions = ref([]);
|
|
|
+
|
|
|
+const statCards = computed(() => buildDashboardCards(t));
|
|
|
+
|
|
|
+const activeCardConfig = computed(
|
|
|
+ () => statCards.value.find((card) => card.key === activeCard.value) ?? null,
|
|
|
+);
|
|
|
|
|
|
-// Função de API correspondente a cada card — nova referência a cada computed
|
|
|
+const extraFilters = computed(() => ({ ...filters.value }));
|
|
|
+
|
|
|
+// Os status possíveis são pedidos apenas na primeira busca de cada bloco
|
|
|
const activeApiCall = computed(() => {
|
|
|
- switch (activeCard.value) {
|
|
|
- case "total_associados":
|
|
|
- return (p) => getUsersPaginated({ ...p, type: "associado" });
|
|
|
- case "associados_ativos":
|
|
|
- return (p) => getUsersPaginated({ ...p, type: "associado", status: "active" });
|
|
|
- case "associados_pendentes":
|
|
|
- return (p) => getUsersPaginated({ ...p, type: "associado", status: "pending" });
|
|
|
- case "dependentes_pendentes":
|
|
|
- return (p) => getDependentsPaginated({ ...p, status: "pending" });
|
|
|
- case "parceiros":
|
|
|
- return (p) => getPartnerAgreementsPaginated({ ...p });
|
|
|
- case "contratos_a_vencer":
|
|
|
- return (p) => getExpiringPartnerAgreementsPaginated({ ...p });
|
|
|
- case "novos_mes":
|
|
|
- return (p) => getPartnerAgreementsPaginated({ ...p, createdMonth: "current" });
|
|
|
- case "ultimos_acessos":
|
|
|
- return (p) => getAccessLogsPaginated({ ...p, last_per_user: true });
|
|
|
- case "associados_com_acesso_app":
|
|
|
- return (p) => getAccessLogsPaginated({ ...p, type: "associado", last_per_user: true });
|
|
|
- default:
|
|
|
- return null;
|
|
|
- }
|
|
|
-});
|
|
|
+ const card = activeCardConfig.value;
|
|
|
+ if (!card) return null;
|
|
|
|
|
|
-const formatDateTime = (isoString) => {
|
|
|
- if (!isoString) return "—";
|
|
|
- const d = new Date(isoString.includes("T") ? isoString : isoString.replace(" ", "T"));
|
|
|
- if (Number.isNaN(d.getTime())) return isoString;
|
|
|
- return d.toLocaleString("pt-BR", {
|
|
|
- day: "2-digit", month: "2-digit", year: "numeric",
|
|
|
- hour: "2-digit", minute: "2-digit", second: "2-digit",
|
|
|
- });
|
|
|
-};
|
|
|
+ let needsStatusOptions = card.filters.includes("status");
|
|
|
+
|
|
|
+ return async (params) => {
|
|
|
+ const response = await card.request({
|
|
|
+ ...params,
|
|
|
+ withStatusOptions: needsStatusOptions || undefined,
|
|
|
+ });
|
|
|
+
|
|
|
+ const statuses = response.data.result.statuses;
|
|
|
+ if (statuses) {
|
|
|
+ statusOptions.value = statuses;
|
|
|
+ needsStatusOptions = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return response;
|
|
|
+ };
|
|
|
+});
|
|
|
|
|
|
const onCardClick = (card) => {
|
|
|
- activeCard.value = activeCard.value === card.key ? null : card.key;
|
|
|
+ if (activeCard.value === card.key) {
|
|
|
+ activeCard.value = null;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ activeCard.value = card.key;
|
|
|
+ filters.value = { ...card.defaultFilters };
|
|
|
+ statusOptions.value = [];
|
|
|
};
|
|
|
|
|
|
+const rowStatus = (row) => (typeof row.status === "object" ? row.status?.value : row.status);
|
|
|
+
|
|
|
const onRowClick = ({ row }) => {
|
|
|
- if (activeCard.value === "dependentes_pendentes") {
|
|
|
+ const approval = activeCardConfig.value?.approval;
|
|
|
+
|
|
|
+ if (approval === "dependente") {
|
|
|
onDependentRowClick(row);
|
|
|
- } else if (activeCard.value === "associados_pendentes") {
|
|
|
+ } else if (approval === "associado") {
|
|
|
onAssociadoRowClick(row);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
const onDependentRowClick = (row) => {
|
|
|
+ if (rowStatus(row) !== "pending") return;
|
|
|
+
|
|
|
if (!permission_store.getAccess("associado.dependente_aprovacao", "edit")) {
|
|
|
$q.notify({ type: "negative", message: t("validation.permissions.edit") });
|
|
|
return;
|
|
|
@@ -221,6 +194,8 @@ const onDependentRowClick = (row) => {
|
|
|
};
|
|
|
|
|
|
const onAssociadoRowClick = (row) => {
|
|
|
+ if (rowStatus(row) !== "pending") return;
|
|
|
+
|
|
|
if (!permission_store.getAccess("associado", "edit")) {
|
|
|
$q.notify({ type: "negative", message: t("validation.permissions.edit") });
|
|
|
return;
|
|
|
@@ -250,6 +225,13 @@ onMounted(async () => {
|
|
|
transition: box-shadow 0.2s, border-color 0.2s, background-color 0.2s;
|
|
|
}
|
|
|
|
|
|
+.stat-card__label {
|
|
|
+ font-size: 0.7rem;
|
|
|
+ line-height: 1.25;
|
|
|
+ white-space: normal;
|
|
|
+ word-break: break-word;
|
|
|
+}
|
|
|
+
|
|
|
.stat-card:hover {
|
|
|
box-shadow: 0 2px 8px rgba(102, 29, 117, 0.15);
|
|
|
}
|