DashboardPage.vue 15 KB

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