DashboardPage.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. <template>
  2. <div>
  3. <DefaultHeaderPage show-filter-icon class="q-pa-sm">
  4. <template #after>
  5. <div class="flex items-center no-wrap" style="gap: 12px">
  6. <q-select
  7. v-if="$q.screen.gt.xs"
  8. v-model="selectedUnit"
  9. dense
  10. :options="[]"
  11. label="Unidade"
  12. style="width: 250px; flex-shrink: 0"
  13. color="secondary"
  14. hide-dropdown-icon
  15. >
  16. <template #append>
  17. <q-icon name="mdi-map-marker-outline" color="secondary" />
  18. </template>
  19. </q-select>
  20. <div
  21. class="flex items-center no-wrap q-gutter-x-md q-px-sm q-ml-md"
  22. style="flex-shrink: 0"
  23. >
  24. <q-img src="icons/user-icon.jpg" class="avatar-circle" />
  25. <div
  26. v-if="$q.screen.gt.xs"
  27. class="column q-gutter-y-none"
  28. style="white-space: nowrap"
  29. >
  30. <span class="text-body2 text-center">Ana Laura</span>
  31. <span
  32. v-if="$q.screen.gt.sm"
  33. class="text-overline text-center"
  34. style="line-height: 1rem; font-weight: 400"
  35. >Gerente</span
  36. >
  37. </div>
  38. </div>
  39. <template v-if="$q.screen.gt.sm">
  40. <q-separator
  41. vertical
  42. style="height: 36px; width: 2px; flex-shrink: 0"
  43. color="dark"
  44. />
  45. <div
  46. class="column"
  47. style="line-height: 1.2; white-space: nowrap; flex-shrink: 0"
  48. >
  49. <span class="text-caption text-grey-6 text-primary text-center"
  50. >Ultimo acesso</span
  51. >
  52. <span class="text-caption text-primary text-center"
  53. >16/02/2026, 14:16</span
  54. >
  55. </div>
  56. </template>
  57. <div
  58. class="flex items-center no-wrap"
  59. style="gap: 2px; flex-shrink: 0"
  60. >
  61. <q-btn flat round dense icon="mdi-bell-badge" color="secondary" />
  62. <q-btn flat round dense icon="mdi-account" color="secondary" />
  63. <q-btn flat round dense icon="mdi-cog-outline" color="secondary" />
  64. </div>
  65. </div>
  66. </template>
  67. </DefaultHeaderPage>
  68. <div class="q-pa-sm">
  69. <div class="filter-row">
  70. <q-select
  71. v-model="selectedPeriod"
  72. :options="periodOptions"
  73. option-value="value"
  74. option-label="label"
  75. emit-value
  76. map-options
  77. dense
  78. label="Selecione o Período"
  79. color="secondary"
  80. class="filter-item"
  81. hide-dropdown-icon
  82. >
  83. <template #append>
  84. <q-icon name="mdi-chevron-down" color="secondary" />
  85. </template>
  86. </q-select>
  87. <template v-if="selectedPeriod === 'custom'">
  88. <q-input
  89. v-model="startDate"
  90. dense
  91. label="Data Inicial"
  92. mask="##/##/####"
  93. placeholder="DD/MM/AAAA"
  94. color="secondary"
  95. class="filter-item"
  96. >
  97. <template #append>
  98. <q-icon
  99. name="mdi-calendar"
  100. color="secondary"
  101. class="cursor-pointer"
  102. >
  103. <q-popup-proxy
  104. cover
  105. transition-show="scale"
  106. transition-hide="scale"
  107. >
  108. <q-date
  109. v-model="startDate"
  110. mask="DD/MM/YYYY"
  111. color="secondary"
  112. >
  113. <div class="row items-center justify-end">
  114. <q-btn v-close-popup label="OK" color="secondary" flat />
  115. </div>
  116. </q-date>
  117. </q-popup-proxy>
  118. </q-icon>
  119. </template>
  120. </q-input>
  121. <q-input
  122. v-model="endDate"
  123. dense
  124. label="Data Final"
  125. mask="##/##/####"
  126. placeholder="DD/MM/AAAA"
  127. color="secondary"
  128. class="filter-item"
  129. >
  130. <template #append>
  131. <q-icon
  132. name="mdi-calendar"
  133. color="secondary"
  134. class="cursor-pointer"
  135. >
  136. <q-popup-proxy
  137. cover
  138. transition-show="scale"
  139. transition-hide="scale"
  140. >
  141. <q-date v-model="endDate" mask="DD/MM/YYYY" color="secondary">
  142. <div class="row items-center justify-end">
  143. <q-btn v-close-popup label="OK" color="secondary" flat />
  144. </div>
  145. </q-date>
  146. </q-popup-proxy>
  147. </q-icon>
  148. </template>
  149. </q-input>
  150. </template>
  151. </div>
  152. </div>
  153. <div v-if="!isLoading" class="column gap q-pa-sm">
  154. <div class="stat-cards-row">
  155. <DashboardStatCard
  156. title="Total alunos (contratos ativos)"
  157. icon="mdi-account-multiple-outline"
  158. value="4.527"
  159. badge="3.200 ativos"
  160. clickable
  161. @click="openAlunosDialog"
  162. />
  163. <DashboardStatCard
  164. title="Contratos Congelados"
  165. icon="mdi-snowflake"
  166. value="57"
  167. subtitle="É hora de incentivar nossos alunos"
  168. clickable
  169. @click="openContratosCongeladosDialog"
  170. />
  171. <DashboardStatCard
  172. title="Contratos Cancelados"
  173. icon="mdi-cancel"
  174. value="57"
  175. subtitle="É hora de incentivar nossos alunos"
  176. clickable
  177. @click="openContratosCanceladosDialog"
  178. />
  179. <DashboardStatCard
  180. title="Receita Geral"
  181. icon="mdi-currency-usd"
  182. value="R$ 51.548,80"
  183. subtitle="0 pagamentos pendentes"
  184. />
  185. </div>
  186. <div class="charts-row">
  187. <DashboardChartCard title="Faturamento Serviço / Materiais">
  188. <GroupedBarChart
  189. :labels="faturamentoChart.labels"
  190. :datasets="faturamentoChart.datasets"
  191. label-y="R$"
  192. :tick-formatter="formatCurrencyTick"
  193. :tooltip-formatter="formatCurrencyTooltip"
  194. class="full-width full-height"
  195. />
  196. </DashboardChartCard>
  197. <DashboardChartCard title="Matrículas por Período">
  198. <GroupedBarChart
  199. :labels="matriculasChart.labels"
  200. :datasets="matriculasChart.datasets"
  201. :bar-radius="50"
  202. :show-datalabels="true"
  203. :max-bar-thickness="44"
  204. :category-percentage="0.6"
  205. :bar-percentage="0.85"
  206. class="full-width full-height"
  207. />
  208. </DashboardChartCard>
  209. <AniversariantesCard :people="aniversariantes" />
  210. </div>
  211. <div class="stat-cards-row">
  212. <DashboardStatCard
  213. title="Frequência Média"
  214. icon="mdi-account-multiple-outline"
  215. value="87%"
  216. badge="Alta"
  217. badge-color="approved"
  218. custom-style="padding: 6px 24px"
  219. clickable
  220. @click="openFrequenciaMediaDialog"
  221. />
  222. <DashboardStatCard
  223. title="Estoque Geral de Produtos"
  224. icon="mdi-currency-usd"
  225. value="56"
  226. clickable
  227. @click="openEstoqueProdutosDialog"
  228. />
  229. <DashboardStatCard
  230. title="Tarefas Pendentes"
  231. icon="mdi-draw"
  232. value="4"
  233. subtitle="Não deixe para amanhã"
  234. />
  235. <DashboardStatCard
  236. title="Tickets Abertos"
  237. icon="mdi-calendar-outline"
  238. value="2"
  239. subtitle="Estável"
  240. clickable
  241. @click="openTicketsAbertoDialog"
  242. />
  243. </div>
  244. </div>
  245. <div v-else class="flex flex-center full-width q-pa-xl">
  246. <q-spinner color="primary" size="50px" />
  247. </div>
  248. </div>
  249. </template>
  250. <script setup>
  251. import { onMounted, ref, watch } from "vue";
  252. import { useQuasar } from "quasar";
  253. import { useI18n } from "vue-i18n";
  254. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  255. import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
  256. import DashboardChartCard from "src/components/charts/DashboardChartCard.vue";
  257. import GroupedBarChart from "src/components/charts/normal/GroupedBarChart.vue";
  258. import AniversariantesCard from "src/components/charts/AniversariantesCard.vue";
  259. import AlunosAtivosDialog from "src/pages/dashboard/components/AlunosAtivosDialog.vue";
  260. import ContratosCongeladosDialog from "src/pages/dashboard/components/ContratosCongeladosDialog.vue";
  261. import ContratosCanceladosDialog from "src/pages/dashboard/components/ContratosCanceladosDialog.vue";
  262. import FrequenciaMediaDialog from "src/pages/dashboard/components/FrequenciaMediaDialog.vue";
  263. import EstoqueProdutosDialog from "src/pages/dashboard/components/EstoqueProdutosDialog.vue";
  264. import TicketsAbertoDialog from "src/pages/dashboard/components/TicketsAbertoDialog.vue";
  265. const { t } = useI18n();
  266. const $q = useQuasar();
  267. const isLoading = ref(true);
  268. const openAlunosDialog = () => {
  269. $q.dialog({ component: AlunosAtivosDialog });
  270. };
  271. const openContratosCongeladosDialog = () => {
  272. $q.dialog({ component: ContratosCongeladosDialog });
  273. };
  274. const openContratosCanceladosDialog = () => {
  275. $q.dialog({ component: ContratosCanceladosDialog });
  276. };
  277. const openFrequenciaMediaDialog = () => {
  278. $q.dialog({ component: FrequenciaMediaDialog });
  279. };
  280. const openEstoqueProdutosDialog = () => {
  281. $q.dialog({ component: EstoqueProdutosDialog });
  282. };
  283. const openTicketsAbertoDialog = () => {
  284. $q.dialog({ component: TicketsAbertoDialog });
  285. };
  286. const selectedUnit = ref(null);
  287. const defaultPeriod = ref("month");
  288. const defaultEventId = ref(1);
  289. const selectedPeriod = ref("custom");
  290. const startDate = ref("");
  291. const endDate = ref("");
  292. const periodOptions = [
  293. { label: "Hoje", value: "today" },
  294. { label: "Esta semana", value: "week" },
  295. { label: "Este mês", value: "month" },
  296. { label: "Este ano", value: "year" },
  297. { label: "Personalizado", value: "custom" },
  298. ];
  299. const aniversariantes = [
  300. { day: 10, name: "Heloisa Faria" },
  301. { day: 11, name: "Juliana Costa" },
  302. { day: 16, name: "Juliana Costa" },
  303. { day: 23, name: "Fernando Almeida" },
  304. { day: 29, name: "Lucas Pereira" },
  305. { day: 34, name: "Sofia Martins" },
  306. ];
  307. const matriculasChart = {
  308. labels: ["JAN", "FEV", "MAR", "ABR", "MAI", "JUN"],
  309. datasets: [
  310. {
  311. label: "Matrículas",
  312. data: [120, 200, 150, 80, 70, 110],
  313. color: ["#3B82F6", "#EF4444", "#A855F7", "#374151", "#EAB308", "#06B6D4"],
  314. },
  315. ],
  316. };
  317. const faturamentoChart = {
  318. labels: [
  319. "17/02",
  320. "18/02",
  321. "19/02",
  322. "20/02",
  323. "21/02",
  324. "22/02",
  325. "23/02",
  326. "24/02",
  327. "25/02",
  328. "26/02",
  329. ],
  330. datasets: [
  331. {
  332. label: "Serviço",
  333. data: [
  334. 18500, 21000, 16400, 22300, 19800, 17200, 15800, 24100, 20500, 27600,
  335. ],
  336. color: "#a274f1",
  337. },
  338. {
  339. label: "Materiais",
  340. data: [9200, 10500, 8100, 11400, 9800, 8400, 8700, 12200, 10100, 13100],
  341. color: "#ff9999",
  342. },
  343. ],
  344. };
  345. const formatCurrencyTick = (value) => {
  346. if (value >= 1000) return `R$ ${(value / 1000).toFixed(0)}k`;
  347. return `R$ ${value}`;
  348. };
  349. const formatCurrencyTooltip = (context) => {
  350. const value = context.parsed.y;
  351. return ` ${context.dataset.label}: R$ ${value.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`;
  352. };
  353. const ordersChart = ref({});
  354. const participantsChart = ref({});
  355. const paymentsChart = ref({});
  356. const ticketsSoldChart = ref({});
  357. const eventTicketsByTypeChart = ref({});
  358. const eventParticipantsByCNPJAndCPF = ref({});
  359. const salesOverTimeLineChart = ref({});
  360. const eventSourcePieChart = ref({});
  361. const generateMockData = () => {
  362. const createMiniChartData = (currentTotal, percentage) => ({
  363. current_total: currentTotal,
  364. percentage_change: percentage,
  365. trend_data: Array.from({ length: 10 }, () =>
  366. Math.floor(Math.random() * 100),
  367. ),
  368. });
  369. const barChartDataRaw = [
  370. {
  371. label: t("dashboard.charts.tickets_by_type.labels.vip"),
  372. value: Math.floor(Math.random() * 300),
  373. },
  374. {
  375. label: t("dashboard.charts.tickets_by_type.labels.track"),
  376. value: Math.floor(Math.random() * 800),
  377. },
  378. {
  379. label: t("dashboard.charts.tickets_by_type.labels.box"),
  380. value: Math.floor(Math.random() * 400),
  381. },
  382. {
  383. label: t("dashboard.charts.tickets_by_type.labels.courtesy"),
  384. value: Math.floor(Math.random() * 50),
  385. },
  386. ];
  387. const doughnutDataRaw = [
  388. {
  389. label: t("common.terms.cpf"),
  390. value: Math.floor(Math.random() * 900 + 100),
  391. },
  392. {
  393. label: t("common.terms.cnpj"),
  394. value: Math.floor(Math.random() * 100 + 10),
  395. },
  396. ];
  397. const doughnutTotal = doughnutDataRaw.reduce(
  398. (sum, item) => sum + item.value,
  399. 0,
  400. );
  401. const lineChartDataRaw = [
  402. {
  403. label: t("common.months.january"),
  404. value: Math.floor(1200 + Math.random() * 500),
  405. },
  406. {
  407. label: t("common.months.february"),
  408. value: Math.floor(1900 + Math.random() * 500),
  409. },
  410. {
  411. label: t("common.months.march"),
  412. value: Math.floor(3000 + Math.random() * 500),
  413. },
  414. {
  415. label: t("common.months.april"),
  416. value: Math.floor(5000 + Math.random() * 500),
  417. },
  418. {
  419. label: t("common.months.may"),
  420. value: Math.floor(2300 + Math.random() * 500),
  421. },
  422. {
  423. label: t("common.months.june"),
  424. value: Math.floor(3200 + Math.random() * 500),
  425. },
  426. ];
  427. const pieDataRaw = [
  428. {
  429. label: t("dashboard.charts.registration_source.sources.instagram"),
  430. value: Math.floor(450 + Math.random() * 50),
  431. },
  432. {
  433. label: t("dashboard.charts.registration_source.sources.facebook"),
  434. value: Math.floor(250 + Math.random() * 50),
  435. },
  436. {
  437. label: t("dashboard.charts.registration_source.sources.google"),
  438. value: Math.floor(180 + Math.random() * 50),
  439. },
  440. {
  441. label: t("dashboard.charts.registration_source.sources.referral"),
  442. value: Math.floor(120 + Math.random() * 50),
  443. },
  444. ];
  445. const pieTotal = pieDataRaw.reduce((sum, item) => sum + item.value, 0);
  446. return {
  447. payments: createMiniChartData(
  448. (Math.random() * 20000 + 5000).toFixed(2),
  449. (Math.random() * 20 - 5).toFixed(2),
  450. ),
  451. orders: createMiniChartData(
  452. Math.floor(Math.random() * 500 + 50),
  453. (Math.random() * 15 - 5).toFixed(2),
  454. ),
  455. tickets_sold: createMiniChartData(
  456. Math.floor(Math.random() * 1500 + 200),
  457. (Math.random() * 25 - 5).toFixed(2),
  458. ),
  459. participants: createMiniChartData(
  460. Math.floor(Math.random() * 1000 + 100),
  461. (Math.random() * 10 - 5).toFixed(2),
  462. ),
  463. barData: {
  464. chart_data: barChartDataRaw,
  465. },
  466. doughnutData: {
  467. chart_data: doughnutDataRaw,
  468. current_total: doughnutTotal,
  469. },
  470. lineData: {
  471. chart_data: lineChartDataRaw,
  472. },
  473. pieData: {
  474. chart_data: pieDataRaw,
  475. current_total: pieTotal,
  476. },
  477. };
  478. };
  479. const updateDashboardData = async () => {
  480. isLoading.value = true;
  481. setTimeout(() => {
  482. const mockData = generateMockData();
  483. ordersChart.value = mockData.orders;
  484. participantsChart.value = mockData.participants;
  485. paymentsChart.value = mockData.payments;
  486. ticketsSoldChart.value = mockData.tickets_sold;
  487. eventTicketsByTypeChart.value = mockData.barData;
  488. eventParticipantsByCNPJAndCPF.value = mockData.doughnutData;
  489. salesOverTimeLineChart.value = mockData.lineData;
  490. eventSourcePieChart.value = mockData.pieData;
  491. isLoading.value = false;
  492. }, 500);
  493. };
  494. watch([defaultPeriod, defaultEventId], async () => {
  495. await updateDashboardData();
  496. });
  497. onMounted(async () => {
  498. await updateDashboardData();
  499. });
  500. </script>
  501. <style scoped>
  502. .gap {
  503. gap: 16px;
  504. }
  505. .avatar-circle {
  506. width: 36px;
  507. height: 36px;
  508. border-radius: 50%;
  509. flex-shrink: 0;
  510. }
  511. .stat-cards-row {
  512. display: flex;
  513. flex-wrap: nowrap;
  514. gap: 16px;
  515. }
  516. .stat-cards-row > * {
  517. flex: 1 1 0;
  518. min-width: 0;
  519. }
  520. @media (max-width: 599px) {
  521. .stat-cards-row {
  522. flex-wrap: wrap;
  523. }
  524. .stat-cards-row > * {
  525. flex: 1 1 calc(50% - 8px);
  526. }
  527. }
  528. .charts-row {
  529. display: flex;
  530. flex-wrap: wrap;
  531. gap: 16px;
  532. }
  533. .charts-row > *:nth-child(1) {
  534. flex: 0 0 calc(41.6667% - 11px);
  535. min-width: 0;
  536. }
  537. .charts-row > *:nth-child(2) {
  538. flex: 0 0 calc(33.3333% - 11px);
  539. min-width: 0;
  540. }
  541. .charts-row > *:nth-child(3) {
  542. flex: 0 0 calc(25% - 11px);
  543. min-width: 0;
  544. }
  545. @media (max-width: 599px) {
  546. .charts-row > * {
  547. flex: 1 1 100%;
  548. }
  549. }
  550. .filter-row {
  551. display: flex;
  552. flex-wrap: wrap;
  553. gap: 12px;
  554. align-items: center;
  555. }
  556. .filter-item {
  557. flex: 1 1 200px;
  558. min-width: 180px;
  559. max-width: 300px;
  560. }
  561. @media (max-width: 599px) {
  562. .filter-item {
  563. flex: 1 1 100%;
  564. max-width: 100%;
  565. }
  566. }
  567. </style>