DashboardPage.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  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. />
  169. <DashboardStatCard
  170. title="Contratos Cancelados"
  171. icon="mdi-cancel"
  172. value="57"
  173. subtitle="É hora de incentivar nossos alunos"
  174. />
  175. <DashboardStatCard
  176. title="Receita Geral"
  177. icon="mdi-currency-usd"
  178. value="R$ 51.548,80"
  179. subtitle="0 pagamentos pendentes"
  180. />
  181. </div>
  182. <div class="charts-row">
  183. <DashboardChartCard title="Faturamento Serviço / Materiais">
  184. <GroupedBarChart
  185. :labels="faturamentoChart.labels"
  186. :datasets="faturamentoChart.datasets"
  187. label-y="R$"
  188. :tick-formatter="formatCurrencyTick"
  189. :tooltip-formatter="formatCurrencyTooltip"
  190. class="full-width full-height"
  191. />
  192. </DashboardChartCard>
  193. <DashboardChartCard title="Matrículas por Período">
  194. <GroupedBarChart
  195. :labels="matriculasChart.labels"
  196. :datasets="matriculasChart.datasets"
  197. :bar-radius="50"
  198. :show-datalabels="true"
  199. :max-bar-thickness="44"
  200. :category-percentage="0.6"
  201. :bar-percentage="0.85"
  202. class="full-width full-height"
  203. />
  204. </DashboardChartCard>
  205. <AniversariantesCard :people="aniversariantes" />
  206. </div>
  207. <div class="stat-cards-row">
  208. <DashboardStatCard
  209. title="Frequência Média"
  210. icon="mdi-account-multiple-outline"
  211. value="87%"
  212. badge="Alta"
  213. badge-color="approved"
  214. custom-style="padding: 6px 24px"
  215. />
  216. <DashboardStatCard
  217. title="Estoque Geral de Produtos"
  218. icon="mdi-currency-usd"
  219. value="56"
  220. />
  221. <DashboardStatCard
  222. title="Tarefas Pendentes"
  223. icon="mdi-draw"
  224. value="4"
  225. subtitle="Não deixe para amanhã"
  226. />
  227. <DashboardStatCard
  228. title="Tickets Abertos"
  229. icon="mdi-calendar-outline"
  230. value="2"
  231. subtitle="Estável"
  232. />
  233. </div>
  234. </div>
  235. <div v-else class="flex flex-center full-width q-pa-xl">
  236. <q-spinner color="primary" size="50px" />
  237. </div>
  238. </div>
  239. </template>
  240. <script setup>
  241. import { onMounted, ref, watch } from "vue";
  242. import { useQuasar } from "quasar";
  243. import { useI18n } from "vue-i18n";
  244. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  245. import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
  246. import DashboardChartCard from "src/components/charts/DashboardChartCard.vue";
  247. import GroupedBarChart from "src/components/charts/normal/GroupedBarChart.vue";
  248. import AniversariantesCard from "src/components/charts/AniversariantesCard.vue";
  249. import AlunosAtivosDialog from "src/pages/dashboard/components/AlunosAtivosDialog.vue";
  250. const { t } = useI18n();
  251. const $q = useQuasar();
  252. const isLoading = ref(true);
  253. const openAlunosDialog = () => {
  254. $q.dialog({ component: AlunosAtivosDialog });
  255. };
  256. const selectedUnit = ref(null);
  257. const defaultPeriod = ref("month");
  258. const defaultEventId = ref(1);
  259. const selectedPeriod = ref("custom");
  260. const startDate = ref("");
  261. const endDate = ref("");
  262. const periodOptions = [
  263. { label: "Hoje", value: "today" },
  264. { label: "Esta semana", value: "week" },
  265. { label: "Este mês", value: "month" },
  266. { label: "Este ano", value: "year" },
  267. { label: "Personalizado", value: "custom" },
  268. ];
  269. const aniversariantes = [
  270. { day: 10, name: "Heloisa Faria" },
  271. { day: 11, name: "Juliana Costa" },
  272. { day: 16, name: "Juliana Costa" },
  273. { day: 23, name: "Fernando Almeida" },
  274. { day: 29, name: "Lucas Pereira" },
  275. { day: 34, name: "Sofia Martins" },
  276. ];
  277. const matriculasChart = {
  278. labels: ["JAN", "FEV", "MAR", "ABR", "MAI", "JUN"],
  279. datasets: [
  280. {
  281. label: "Matrículas",
  282. data: [120, 200, 150, 80, 70, 110],
  283. color: ["#3B82F6", "#EF4444", "#A855F7", "#374151", "#EAB308", "#06B6D4"],
  284. },
  285. ],
  286. };
  287. const faturamentoChart = {
  288. labels: [
  289. "17/02",
  290. "18/02",
  291. "19/02",
  292. "20/02",
  293. "21/02",
  294. "22/02",
  295. "23/02",
  296. "24/02",
  297. "25/02",
  298. "26/02",
  299. ],
  300. datasets: [
  301. {
  302. label: "Serviço",
  303. data: [
  304. 18500, 21000, 16400, 22300, 19800, 17200, 15800, 24100, 20500, 27600,
  305. ],
  306. color: "#a274f1",
  307. },
  308. {
  309. label: "Materiais",
  310. data: [9200, 10500, 8100, 11400, 9800, 8400, 8700, 12200, 10100, 13100],
  311. color: "#ff9999",
  312. },
  313. ],
  314. };
  315. const formatCurrencyTick = (value) => {
  316. if (value >= 1000) return `R$ ${(value / 1000).toFixed(0)}k`;
  317. return `R$ ${value}`;
  318. };
  319. const formatCurrencyTooltip = (context) => {
  320. const value = context.parsed.y;
  321. return ` ${context.dataset.label}: R$ ${value.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`;
  322. };
  323. const ordersChart = ref({});
  324. const participantsChart = ref({});
  325. const paymentsChart = ref({});
  326. const ticketsSoldChart = ref({});
  327. const eventTicketsByTypeChart = ref({});
  328. const eventParticipantsByCNPJAndCPF = ref({});
  329. const salesOverTimeLineChart = ref({});
  330. const eventSourcePieChart = ref({});
  331. const generateMockData = () => {
  332. const createMiniChartData = (currentTotal, percentage) => ({
  333. current_total: currentTotal,
  334. percentage_change: percentage,
  335. trend_data: Array.from({ length: 10 }, () =>
  336. Math.floor(Math.random() * 100),
  337. ),
  338. });
  339. const barChartDataRaw = [
  340. {
  341. label: t("dashboard.charts.tickets_by_type.labels.vip"),
  342. value: Math.floor(Math.random() * 300),
  343. },
  344. {
  345. label: t("dashboard.charts.tickets_by_type.labels.track"),
  346. value: Math.floor(Math.random() * 800),
  347. },
  348. {
  349. label: t("dashboard.charts.tickets_by_type.labels.box"),
  350. value: Math.floor(Math.random() * 400),
  351. },
  352. {
  353. label: t("dashboard.charts.tickets_by_type.labels.courtesy"),
  354. value: Math.floor(Math.random() * 50),
  355. },
  356. ];
  357. const doughnutDataRaw = [
  358. {
  359. label: t("common.terms.cpf"),
  360. value: Math.floor(Math.random() * 900 + 100),
  361. },
  362. {
  363. label: t("common.terms.cnpj"),
  364. value: Math.floor(Math.random() * 100 + 10),
  365. },
  366. ];
  367. const doughnutTotal = doughnutDataRaw.reduce(
  368. (sum, item) => sum + item.value,
  369. 0,
  370. );
  371. const lineChartDataRaw = [
  372. {
  373. label: t("common.months.january"),
  374. value: Math.floor(1200 + Math.random() * 500),
  375. },
  376. {
  377. label: t("common.months.february"),
  378. value: Math.floor(1900 + Math.random() * 500),
  379. },
  380. {
  381. label: t("common.months.march"),
  382. value: Math.floor(3000 + Math.random() * 500),
  383. },
  384. {
  385. label: t("common.months.april"),
  386. value: Math.floor(5000 + Math.random() * 500),
  387. },
  388. {
  389. label: t("common.months.may"),
  390. value: Math.floor(2300 + Math.random() * 500),
  391. },
  392. {
  393. label: t("common.months.june"),
  394. value: Math.floor(3200 + Math.random() * 500),
  395. },
  396. ];
  397. const pieDataRaw = [
  398. {
  399. label: t("dashboard.charts.registration_source.sources.instagram"),
  400. value: Math.floor(450 + Math.random() * 50),
  401. },
  402. {
  403. label: t("dashboard.charts.registration_source.sources.facebook"),
  404. value: Math.floor(250 + Math.random() * 50),
  405. },
  406. {
  407. label: t("dashboard.charts.registration_source.sources.google"),
  408. value: Math.floor(180 + Math.random() * 50),
  409. },
  410. {
  411. label: t("dashboard.charts.registration_source.sources.referral"),
  412. value: Math.floor(120 + Math.random() * 50),
  413. },
  414. ];
  415. const pieTotal = pieDataRaw.reduce((sum, item) => sum + item.value, 0);
  416. return {
  417. payments: createMiniChartData(
  418. (Math.random() * 20000 + 5000).toFixed(2),
  419. (Math.random() * 20 - 5).toFixed(2),
  420. ),
  421. orders: createMiniChartData(
  422. Math.floor(Math.random() * 500 + 50),
  423. (Math.random() * 15 - 5).toFixed(2),
  424. ),
  425. tickets_sold: createMiniChartData(
  426. Math.floor(Math.random() * 1500 + 200),
  427. (Math.random() * 25 - 5).toFixed(2),
  428. ),
  429. participants: createMiniChartData(
  430. Math.floor(Math.random() * 1000 + 100),
  431. (Math.random() * 10 - 5).toFixed(2),
  432. ),
  433. barData: {
  434. chart_data: barChartDataRaw,
  435. },
  436. doughnutData: {
  437. chart_data: doughnutDataRaw,
  438. current_total: doughnutTotal,
  439. },
  440. lineData: {
  441. chart_data: lineChartDataRaw,
  442. },
  443. pieData: {
  444. chart_data: pieDataRaw,
  445. current_total: pieTotal,
  446. },
  447. };
  448. };
  449. const updateDashboardData = async () => {
  450. isLoading.value = true;
  451. setTimeout(() => {
  452. const mockData = generateMockData();
  453. ordersChart.value = mockData.orders;
  454. participantsChart.value = mockData.participants;
  455. paymentsChart.value = mockData.payments;
  456. ticketsSoldChart.value = mockData.tickets_sold;
  457. eventTicketsByTypeChart.value = mockData.barData;
  458. eventParticipantsByCNPJAndCPF.value = mockData.doughnutData;
  459. salesOverTimeLineChart.value = mockData.lineData;
  460. eventSourcePieChart.value = mockData.pieData;
  461. isLoading.value = false;
  462. }, 500);
  463. };
  464. watch([defaultPeriod, defaultEventId], async () => {
  465. await updateDashboardData();
  466. });
  467. onMounted(async () => {
  468. await updateDashboardData();
  469. });
  470. </script>
  471. <style scoped>
  472. .gap {
  473. gap: 16px;
  474. }
  475. .avatar-circle {
  476. width: 36px;
  477. height: 36px;
  478. border-radius: 50%;
  479. flex-shrink: 0;
  480. }
  481. .stat-cards-row {
  482. display: flex;
  483. flex-wrap: nowrap;
  484. gap: 16px;
  485. }
  486. .stat-cards-row > * {
  487. flex: 1 1 0;
  488. min-width: 0;
  489. }
  490. @media (max-width: 599px) {
  491. .stat-cards-row {
  492. flex-wrap: wrap;
  493. }
  494. .stat-cards-row > * {
  495. flex: 1 1 calc(50% - 8px);
  496. }
  497. }
  498. .charts-row {
  499. display: flex;
  500. flex-wrap: wrap;
  501. gap: 16px;
  502. }
  503. .charts-row > *:nth-child(1) {
  504. flex: 0 0 calc(41.6667% - 11px);
  505. min-width: 0;
  506. }
  507. .charts-row > *:nth-child(2) {
  508. flex: 0 0 calc(33.3333% - 11px);
  509. min-width: 0;
  510. }
  511. .charts-row > *:nth-child(3) {
  512. flex: 0 0 calc(25% - 11px);
  513. min-width: 0;
  514. }
  515. @media (max-width: 599px) {
  516. .charts-row > * {
  517. flex: 1 1 100%;
  518. }
  519. }
  520. .filter-row {
  521. display: flex;
  522. flex-wrap: wrap;
  523. gap: 12px;
  524. align-items: center;
  525. }
  526. .filter-item {
  527. flex: 1 1 200px;
  528. min-width: 180px;
  529. max-width: 300px;
  530. }
  531. @media (max-width: 599px) {
  532. .filter-item {
  533. flex: 1 1 100%;
  534. max-width: 100%;
  535. }
  536. }
  537. </style>