| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559 |
- <template>
- <div>
- <DefaultHeaderPage show-filter-icon class="q-pa-sm">
- <template #after>
- <div class="flex items-center no-wrap" style="gap: 12px">
- <q-select
- v-if="$q.screen.gt.xs"
- v-model="selectedUnit"
- dense
- :options="[]"
- label="Unidade"
- style="width: 250px; flex-shrink: 0"
- color="secondary"
- label-color="secondary"
- hide-dropdown-icon
- >
- <template #append>
- <q-icon name="mdi-map-marker-outline" color="secondary" />
- </template>
- </q-select>
- <div
- class="flex items-center no-wrap q-gutter-x-md q-px-sm q-ml-md"
- style="flex-shrink: 0"
- >
- <q-img src="icons/user-icon.jpg" class="avatar-circle" />
- <div
- v-if="$q.screen.gt.xs"
- class="column q-gutter-y-none"
- style="white-space: nowrap"
- >
- <span class="text-body2 text-center">Ana Laura</span>
- <span
- v-if="$q.screen.gt.sm"
- class="text-overline text-center"
- style="line-height: 1rem; font-weight: 400"
- >Gerente</span
- >
- </div>
- </div>
- <template v-if="$q.screen.gt.sm">
- <q-separator
- vertical
- style="height: 36px; width: 2px; flex-shrink: 0"
- color="dark"
- />
- <div
- class="column"
- style="line-height: 1.2; white-space: nowrap; flex-shrink: 0"
- >
- <span class="text-caption text-grey-6 text-primary text-center"
- >Ultimo acesso</span
- >
- <span class="text-caption text-primary text-center"
- >16/02/2026, 14:16</span
- >
- </div>
- </template>
- <div
- class="flex items-center no-wrap"
- style="gap: 2px; flex-shrink: 0"
- >
- <q-btn flat round dense icon="mdi-bell-badge" color="secondary" />
- <q-btn flat round dense icon="mdi-account" color="secondary" />
- <q-btn flat round dense icon="mdi-cog-outline" color="secondary" />
- </div>
- </div>
- </template>
- </DefaultHeaderPage>
- <div class="q-pa-sm">
- <div class="filter-row">
- <q-select
- v-model="selectedPeriod"
- :options="periodOptions"
- option-value="value"
- option-label="label"
- emit-value
- map-options
- dense
- label="Selecione o Período"
- color="secondary"
- class="filter-item"
- hide-dropdown-icon
- >
- <template #append>
- <q-icon name="mdi-chevron-down" color="secondary" />
- </template>
- </q-select>
- <template v-if="selectedPeriod === 'custom'">
- <q-input
- v-model="startDate"
- dense
- label="Data Inicial"
- mask="##/##/####"
- placeholder="DD/MM/AAAA"
- color="secondary"
- class="filter-item"
- >
- <template #append>
- <q-icon
- name="mdi-calendar"
- color="secondary"
- class="cursor-pointer"
- >
- <q-popup-proxy
- cover
- transition-show="scale"
- transition-hide="scale"
- >
- <q-date
- v-model="startDate"
- mask="DD/MM/YYYY"
- color="secondary"
- >
- <div class="row items-center justify-end">
- <q-btn v-close-popup label="OK" color="secondary" flat />
- </div>
- </q-date>
- </q-popup-proxy>
- </q-icon>
- </template>
- </q-input>
- <q-input
- v-model="endDate"
- dense
- label="Data Final"
- mask="##/##/####"
- placeholder="DD/MM/AAAA"
- color="secondary"
- class="filter-item"
- >
- <template #append>
- <q-icon
- name="mdi-calendar"
- color="secondary"
- class="cursor-pointer"
- >
- <q-popup-proxy
- cover
- transition-show="scale"
- transition-hide="scale"
- >
- <q-date v-model="endDate" mask="DD/MM/YYYY" color="secondary">
- <div class="row items-center justify-end">
- <q-btn v-close-popup label="OK" color="secondary" flat />
- </div>
- </q-date>
- </q-popup-proxy>
- </q-icon>
- </template>
- </q-input>
- </template>
- </div>
- </div>
- <div v-if="!isLoading" class="column gap q-pa-sm">
- <div class="stat-cards-row">
- <DashboardStatCard
- title="Total alunos (contratos ativos)"
- icon="mdi-account-multiple"
- value="4.527"
- badge="3.200 ativos"
- />
- <DashboardStatCard
- title="Contratos Congelados"
- icon="mdi-snowflake"
- value="57"
- subtitle="É hora de incentivar nossos alunos"
- />
- <DashboardStatCard
- title="Contratos Cancelados"
- icon="mdi-cancel"
- value="57"
- subtitle="É hora de incentivar nossos alunos"
- />
- <DashboardStatCard
- title="Receita Geral"
- icon="mdi-currency-usd"
- value="R$ 51.548,80"
- subtitle="0 pagamentos pendentes"
- />
- </div>
- <div class="charts-row">
- <DashboardChartCard title="Faturamento Serviço / Materiais">
- <GroupedBarChart
- :labels="faturamentoChart.labels"
- :datasets="faturamentoChart.datasets"
- label-y="R$"
- :tick-formatter="formatCurrencyTick"
- :tooltip-formatter="formatCurrencyTooltip"
- class="full-width full-height"
- />
- </DashboardChartCard>
- <DashboardChartCard title="Matrículas por Período">
- <GroupedBarChart
- :labels="matriculasChart.labels"
- :datasets="matriculasChart.datasets"
- :bar-radius="50"
- :show-datalabels="true"
- class="full-width full-height"
- />
- </DashboardChartCard>
- <AniversariantesCard :people="aniversariantes" />
- </div>
- <div class="stat-cards-row">
- <DashboardStatCard
- title="Frequência Média"
- icon="mdi-account-multiple-outline"
- value="87%"
- badge="Alta"
- badge-color="positive"
- />
- <DashboardStatCard
- title="Estoque Geral de Produtos"
- icon="mdi-currency-usd"
- value="56"
- />
- <DashboardStatCard
- title="Tarefas Pendentes"
- icon="mdi-draw"
- value="4"
- subtitle="Não deixe para amanhã"
- />
- <DashboardStatCard
- title="Tickets Abertos"
- icon="mdi-calendar-outline"
- value="2"
- subtitle="Estável"
- />
- </div>
- </div>
- <div v-else class="flex flex-center full-width q-pa-xl">
- <q-spinner color="primary" size="50px" />
- </div>
- </div>
- </template>
- <script setup>
- import { onMounted, ref, watch } from "vue";
- import { useI18n } from "vue-i18n";
- import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
- import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
- import DashboardChartCard from "src/components/charts/DashboardChartCard.vue";
- import GroupedBarChart from "src/components/charts/normal/GroupedBarChart.vue";
- import AniversariantesCard from "src/components/charts/AniversariantesCard.vue";
- const { t } = useI18n();
- const isLoading = ref(true);
- const selectedUnit = ref(null);
- const defaultPeriod = ref("month");
- const defaultEventId = ref(1);
- const selectedPeriod = ref("custom");
- const startDate = ref("");
- const endDate = ref("");
- const periodOptions = [
- { label: "Hoje", value: "today" },
- { label: "Esta semana", value: "week" },
- { label: "Este mês", value: "month" },
- { label: "Este ano", value: "year" },
- { label: "Personalizado", value: "custom" },
- ];
- // --- Aniversariantes do Mês (hardcoded) ---
- const aniversariantes = [
- { day: 10, name: "Heloisa Faria" },
- { day: 7, name: "Juliana Costa" },
- { day: 24, name: "Fernando Almeida" },
- { day: 28, name: "Patrícia Lima" },
- ];
- // -------------------------------------------
- // --- Matrículas por Período (hardcoded) ---
- const matriculasChart = {
- labels: ["JAN", "FEV", "MAR", "ABR", "MAI", "JUN"],
- datasets: [
- {
- label: "Matrículas",
- data: [120, 200, 150, 80, 70, 110],
- color: ["#3B82F6", "#EF4444", "#A855F7", "#374151", "#EAB308", "#06B6D4"],
- },
- ],
- };
- // ---------------------------------------------------
- // --- Faturamento Serviço / Materiais (hardcoded) ---
- const faturamentoChart = {
- labels: ["17/02", "20/02", "23/02", "26/02"],
- datasets: [
- {
- label: "Serviço",
- data: [18500, 22300, 15800, 27600],
- color: "#7C3AED",
- },
- {
- label: "Materiais",
- data: [9200, 11400, 8700, 13100],
- color: "#EC4899",
- },
- ],
- };
- const formatCurrencyTick = (value) => {
- if (value >= 1000) return `R$ ${(value / 1000).toFixed(0)}k`;
- return `R$ ${value}`;
- };
- const formatCurrencyTooltip = (context) => {
- const value = context.parsed.y;
- return ` ${context.dataset.label}: R$ ${value.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`;
- };
- // ---------------------------------------------------
- const ordersChart = ref({});
- const participantsChart = ref({});
- const paymentsChart = ref({});
- const ticketsSoldChart = ref({});
- const eventTicketsByTypeChart = ref({});
- const eventParticipantsByCNPJAndCPF = ref({});
- const salesOverTimeLineChart = ref({});
- const eventSourcePieChart = ref({});
- const generateMockData = () => {
- const createMiniChartData = (currentTotal, percentage) => ({
- current_total: currentTotal,
- percentage_change: percentage,
- trend_data: Array.from({ length: 10 }, () =>
- Math.floor(Math.random() * 100),
- ),
- });
- const barChartDataRaw = [
- {
- label: t("dashboard.charts.tickets_by_type.labels.vip"),
- value: Math.floor(Math.random() * 300),
- },
- {
- label: t("dashboard.charts.tickets_by_type.labels.track"),
- value: Math.floor(Math.random() * 800),
- },
- {
- label: t("dashboard.charts.tickets_by_type.labels.box"),
- value: Math.floor(Math.random() * 400),
- },
- {
- label: t("dashboard.charts.tickets_by_type.labels.courtesy"),
- value: Math.floor(Math.random() * 50),
- },
- ];
- const doughnutDataRaw = [
- {
- label: t("common.terms.cpf"),
- value: Math.floor(Math.random() * 900 + 100),
- },
- {
- label: t("common.terms.cnpj"),
- value: Math.floor(Math.random() * 100 + 10),
- },
- ];
- const doughnutTotal = doughnutDataRaw.reduce(
- (sum, item) => sum + item.value,
- 0,
- );
- const lineChartDataRaw = [
- {
- label: t("common.months.january"),
- value: Math.floor(1200 + Math.random() * 500),
- },
- {
- label: t("common.months.february"),
- value: Math.floor(1900 + Math.random() * 500),
- },
- {
- label: t("common.months.march"),
- value: Math.floor(3000 + Math.random() * 500),
- },
- {
- label: t("common.months.april"),
- value: Math.floor(5000 + Math.random() * 500),
- },
- {
- label: t("common.months.may"),
- value: Math.floor(2300 + Math.random() * 500),
- },
- {
- label: t("common.months.june"),
- value: Math.floor(3200 + Math.random() * 500),
- },
- ];
- const pieDataRaw = [
- {
- label: t("dashboard.charts.registration_source.sources.instagram"),
- value: Math.floor(450 + Math.random() * 50),
- },
- {
- label: t("dashboard.charts.registration_source.sources.facebook"),
- value: Math.floor(250 + Math.random() * 50),
- },
- {
- label: t("dashboard.charts.registration_source.sources.google"),
- value: Math.floor(180 + Math.random() * 50),
- },
- {
- label: t("dashboard.charts.registration_source.sources.referral"),
- value: Math.floor(120 + Math.random() * 50),
- },
- ];
- const pieTotal = pieDataRaw.reduce((sum, item) => sum + item.value, 0);
- return {
- payments: createMiniChartData(
- (Math.random() * 20000 + 5000).toFixed(2),
- (Math.random() * 20 - 5).toFixed(2),
- ),
- orders: createMiniChartData(
- Math.floor(Math.random() * 500 + 50),
- (Math.random() * 15 - 5).toFixed(2),
- ),
- tickets_sold: createMiniChartData(
- Math.floor(Math.random() * 1500 + 200),
- (Math.random() * 25 - 5).toFixed(2),
- ),
- participants: createMiniChartData(
- Math.floor(Math.random() * 1000 + 100),
- (Math.random() * 10 - 5).toFixed(2),
- ),
- barData: {
- chart_data: barChartDataRaw,
- },
- doughnutData: {
- chart_data: doughnutDataRaw,
- current_total: doughnutTotal,
- },
- lineData: {
- chart_data: lineChartDataRaw,
- },
- pieData: {
- chart_data: pieDataRaw,
- current_total: pieTotal,
- },
- };
- };
- const updateDashboardData = async () => {
- isLoading.value = true;
- setTimeout(() => {
- const mockData = generateMockData();
- ordersChart.value = mockData.orders;
- participantsChart.value = mockData.participants;
- paymentsChart.value = mockData.payments;
- ticketsSoldChart.value = mockData.tickets_sold;
- eventTicketsByTypeChart.value = mockData.barData;
- eventParticipantsByCNPJAndCPF.value = mockData.doughnutData;
- salesOverTimeLineChart.value = mockData.lineData;
- eventSourcePieChart.value = mockData.pieData;
- isLoading.value = false;
- }, 500);
- };
- watch([defaultPeriod, defaultEventId], async () => {
- await updateDashboardData();
- });
- onMounted(async () => {
- await updateDashboardData();
- });
- </script>
- <style scoped>
- .gap {
- gap: 16px;
- }
- .avatar-circle {
- width: 36px;
- height: 36px;
- border-radius: 50%;
- flex-shrink: 0;
- }
- .stat-cards-row {
- display: flex;
- flex-wrap: wrap;
- gap: 16px;
- }
- .stat-cards-row > * {
- flex: 1 1 200px;
- min-width: 180px;
- }
- @media (max-width: 599px) {
- .stat-cards-row > * {
- flex: 1 1 100%;
- }
- }
- .charts-row {
- display: flex;
- flex-wrap: wrap;
- gap: 16px;
- }
- .charts-row > * {
- flex: 1 1 350px;
- min-width: 280px;
- }
- .charts-row > *:last-child {
- flex: 0 1 240px;
- min-width: 200px;
- }
- @media (max-width: 599px) {
- .charts-row > * {
- flex: 1 1 100%;
- }
- }
- .filter-row {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
- align-items: center;
- }
- .filter-item {
- flex: 1 1 200px;
- min-width: 180px;
- max-width: 300px;
- }
- @media (max-width: 599px) {
- .filter-item {
- flex: 1 1 100%;
- max-width: 100%;
- }
- }
- </style>
|