|
@@ -1,36 +1,217 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div>
|
|
<div>
|
|
|
- <DefaultHeaderPage>
|
|
|
|
|
- </DefaultHeaderPage>
|
|
|
|
|
|
|
+ <DefaultHeaderPage />
|
|
|
|
|
|
|
|
- <div v-if="!isLoading" class="column gap q-pa-sm">
|
|
|
|
|
- <div>Administrador: {{ store.userTipo }}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div class="q-pa-sm">
|
|
|
|
|
+ <div v-if="statsLoading" class="flex flex-center q-pa-xl">
|
|
|
|
|
+ <q-spinner color="violet-normal" size="50px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <div class="row q-col-gutter-md q-mb-md">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="card in statCards"
|
|
|
|
|
+ :key="card.key"
|
|
|
|
|
+ class="col-12 col-sm-6 col-md-4 col-lg-2"
|
|
|
|
|
+ >
|
|
|
|
|
+ <q-card
|
|
|
|
|
+ flat
|
|
|
|
|
+ class="stat-card cursor-pointer bg-white"
|
|
|
|
|
+ :class="{ 'stat-card--active': activeCard === card.key }"
|
|
|
|
|
+ @click="onCardClick(card)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <q-card-section class="q-pa-md">
|
|
|
|
|
+ <q-icon :name="card.icon" size="28px" color="violet-normal" />
|
|
|
|
|
+ <div class="text-h5 text-weight-bold q-mt-xs text-dark">
|
|
|
|
|
+ {{ stats[card.key] ?? '—' }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="text-caption text-grey-7 ellipsis">
|
|
|
|
|
+ {{ $t(card.labelKey) }}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </q-card-section>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="activeCard">
|
|
|
|
|
+ <q-card flat class="bg-white q-pa-md" style="border-radius: 12px">
|
|
|
|
|
+ <div v-if="tableLoading" class="flex flex-center q-pa-xl">
|
|
|
|
|
+ <q-spinner color="violet-normal" size="40px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div v-else class="flex flex-center full-width q-pa-xl">
|
|
|
|
|
- <q-spinner color="primary" size="50px" />
|
|
|
|
|
|
|
+ <DefaultTable
|
|
|
|
|
+ v-else
|
|
|
|
|
+ v-model:rows="tableRows"
|
|
|
|
|
+ :columns="tableColumns"
|
|
|
|
|
+ no-api-call
|
|
|
|
|
+ :rows-per-page="10"
|
|
|
|
|
+ :show-search-field="false"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template #body-cell-status="{ row }">
|
|
|
|
|
+ <q-td>
|
|
|
|
|
+ <q-badge
|
|
|
|
|
+ :color="getStatusColor(row.status)"
|
|
|
|
|
+ :label="$t(getStatusI18nKey(row.status))"
|
|
|
|
|
+ class="text-capitalize"
|
|
|
|
|
+ />
|
|
|
|
|
+ </q-td>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </DefaultTable>
|
|
|
|
|
+ </q-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { onMounted, ref } from "vue";
|
|
|
|
|
|
|
+import { ref, computed, onMounted } from "vue";
|
|
|
|
|
+import { useI18n } from "vue-i18n";
|
|
|
import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
|
-import { userStore } from "src/stores/user";
|
|
|
|
|
|
|
+import DefaultTable from "src/components/defaults/DefaultTable.vue";
|
|
|
|
|
+import { getDashboardStats } from "src/api/dashboard";
|
|
|
|
|
+import { getUsers } from "src/api/user";
|
|
|
|
|
+import { getPartnerAgreements } from "src/api/partnerAgreement";
|
|
|
|
|
+import { formatDateYMDtoDMY, getStatusColor, getStatusI18nKey } from "src/helpers/utils";
|
|
|
|
|
+
|
|
|
|
|
+const { t } = useI18n();
|
|
|
|
|
+
|
|
|
|
|
+const statsLoading = ref(true);
|
|
|
|
|
+const tableLoading = ref(false);
|
|
|
|
|
+const stats = ref({});
|
|
|
|
|
+const activeCard = ref(null);
|
|
|
|
|
+const tableRows = ref([]);
|
|
|
|
|
+
|
|
|
|
|
+const usersCache = ref(null);
|
|
|
|
|
+const partnersCache = 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" },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+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 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;
|
|
|
|
|
+ }
|
|
|
|
|
+ return columnsAssociados.value;
|
|
|
|
|
+});
|
|
|
|
|
|
|
|
-const store = userStore();
|
|
|
|
|
|
|
|
|
|
-const isLoading = ref(true);
|
|
|
|
|
|
|
+
|
|
|
|
|
+const loadUsers = async () => {
|
|
|
|
|
+ if (usersCache.value) return usersCache.value;
|
|
|
|
|
+ const users = await getUsers();
|
|
|
|
|
+ usersCache.value = users;
|
|
|
|
|
+ return users;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const loadPartners = async () => {
|
|
|
|
|
+ if (partnersCache.value) return partnersCache.value;
|
|
|
|
|
+ const partners = await getPartnerAgreements();
|
|
|
|
|
+ partnersCache.value = partners;
|
|
|
|
|
+ return partners;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const isCurrentMonth = (dateStr) => {
|
|
|
|
|
+ if (!dateStr) return false;
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const [datePart] = dateStr.split(" ");
|
|
|
|
|
+ const [year, month] = datePart.split("-");
|
|
|
|
|
+ return parseInt(year) === now.getFullYear() && parseInt(month) === now.getMonth() + 1;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const getStatusValue = (status) => typeof status === "object" ? status?.value : status;
|
|
|
|
|
+
|
|
|
|
|
+const onCardClick = async (card) => {
|
|
|
|
|
+ if (activeCard.value === card.key) return;
|
|
|
|
|
+
|
|
|
|
|
+ activeCard.value = card.key;
|
|
|
|
|
+ tableLoading.value = true;
|
|
|
|
|
+ tableRows.value = [];
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (card.key === "contratos_a_vencer") {
|
|
|
|
|
+ const partners = await loadPartners();
|
|
|
|
|
+ const today = new Date();
|
|
|
|
|
+ today.setHours(0, 0, 0, 0);
|
|
|
|
|
+ const in30Days = new Date(today);
|
|
|
|
|
+ in30Days.setDate(in30Days.getDate() + 30);
|
|
|
|
|
+ tableRows.value = partners.filter((p) => {
|
|
|
|
|
+ if (!p.contract_end) return false;
|
|
|
|
|
+ const end = new Date(p.contract_end + "T00:00:00");
|
|
|
|
|
+ return end >= today && end <= in30Days;
|
|
|
|
|
+ });
|
|
|
|
|
+ } else if (card.key === "parceiros") {
|
|
|
|
|
+ const partners = await loadPartners();
|
|
|
|
|
+ tableRows.value = partners;
|
|
|
|
|
+ } else if (card.key === "novos_mes") {
|
|
|
|
|
+ const partners = await loadPartners();
|
|
|
|
|
+ tableRows.value = partners.filter((p) => isCurrentMonth(p.created_at));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const users = await loadUsers();
|
|
|
|
|
+ const associados = users.filter((u) => getStatusValue(u.type) === "associado");
|
|
|
|
|
+
|
|
|
|
|
+ if (card.key === "total_associados") {
|
|
|
|
|
+ tableRows.value = associados;
|
|
|
|
|
+ } else if (card.key === "associados_ativos") {
|
|
|
|
|
+ tableRows.value = associados.filter((u) => getStatusValue(u.status) === "active");
|
|
|
|
|
+ } else if (card.key === "associados_pendentes") {
|
|
|
|
|
+ tableRows.value = associados.filter((u) => getStatusValue(u.status) === "pending");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ tableLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
- setTimeout(() => {
|
|
|
|
|
- isLoading.value = false;
|
|
|
|
|
- }, 1000);
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ stats.value = await getDashboardStats();
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ statsLoading.value = false;
|
|
|
|
|
+ }
|
|
|
});
|
|
});
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
-.gap {
|
|
|
|
|
- gap: 16px;
|
|
|
|
|
|
|
+.stat-card {
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ transition: box-shadow 0.2s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stat-card:hover {
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(102, 29, 117, 0.15);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stat-card--active {
|
|
|
|
|
+ box-shadow: 0 0 0 2px var(--q-violet-normal);
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|