DashboardPage.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. <template>
  2. <div>
  3. <DefaultHeaderPage class="q-pa-sm">
  4. <template #after>
  5. <div class="flex items-center no-wrap" style="gap: 12px">
  6. <template v-if="$q.screen.gt.sm">
  7. <div
  8. class="column"
  9. style="line-height: 1.2; white-space: nowrap; flex-shrink: 0"
  10. >
  11. <span class="text-caption text-grey-6 text-primary text-center"
  12. >Ultimo acesso</span
  13. >
  14. <span class="text-caption text-primary text-center"
  15. >16/02/2026, 14:16</span
  16. >
  17. </div>
  18. </template>
  19. <div
  20. class="flex items-center no-wrap"
  21. style="gap: 2px; flex-shrink: 0"
  22. >
  23. <q-btn flat round dense icon="mdi-bell-badge" color="secondary" />
  24. <q-btn flat round dense icon="mdi-account" color="secondary" />
  25. <q-btn flat round dense icon="mdi-cog-outline" color="secondary" />
  26. </div>
  27. </div>
  28. </template>
  29. </DefaultHeaderPage>
  30. <div class="q-pa-sm">
  31. <div class="stat-cards-row q-mb-md">
  32. <DashboardStatCard
  33. title="Total alunos (contratos ativos)"
  34. icon="mdi-account-multiple-outline"
  35. value="0"
  36. badge="0 ativos"
  37. />
  38. <DashboardStatCard
  39. title="Receita Total"
  40. icon="mdi-currency-usd"
  41. value="R$ 0,00"
  42. subtitle="0 pagamentos pendentes"
  43. />
  44. <DashboardStatCard
  45. title="Ticket Médio"
  46. icon="mdi-calendar-blank"
  47. value="R$ 12,00"
  48. subtitle="Estável"
  49. />
  50. <DashboardStatCard
  51. title="Aniversariantes"
  52. icon="mdi-emoticon-happy-outline"
  53. value="0"
  54. subtitle="Fortaleça seus relacionamentos"
  55. />
  56. </div>
  57. <div class="row q-col-gutter-md q-mb-md">
  58. <div class="col-12 col-md-5">
  59. <q-card flat bordered style="height: 280px">
  60. <q-card-section class="row justify-between items-center q-pb-xs">
  61. <span class="text-subtitle2 text-weight-medium"
  62. >Faturamento Serviço / Materiais</span
  63. >
  64. <q-icon name="mdi-book-open-outline" color="grey-5" />
  65. </q-card-section>
  66. <q-separator />
  67. <q-card-section
  68. style="height: calc(100% - 57px)"
  69. class="q-pt-sm q-px-sm"
  70. >
  71. <Bar :data="faturamentoData" :options="faturamentoOptions" />
  72. </q-card-section>
  73. </q-card>
  74. </div>
  75. <div class="col-12 col-md-4">
  76. <q-card flat bordered style="height: 280px">
  77. <q-card-section class="row justify-between items-center q-pb-xs">
  78. <span class="text-subtitle2 text-weight-medium"
  79. >Contratos Ativos</span
  80. >
  81. <q-icon name="mdi-trending-up" color="grey-5" />
  82. </q-card-section>
  83. <q-separator />
  84. <q-card-section
  85. class="flex flex-center q-pt-sm"
  86. style="height: calc(100% - 57px); position: relative"
  87. >
  88. <div style="height: 100%; max-width: 280px; width: 100%">
  89. <Doughnut
  90. :data="gaugeData"
  91. :options="gaugeOptions"
  92. :plugins="[gaugeNeedlePlugin]"
  93. />
  94. </div>
  95. <div class="gauge-label">
  96. <div class="text-h5 text-bold">70</div>
  97. <div class="text-caption text-grey-6">Grade</div>
  98. </div>
  99. </q-card-section>
  100. </q-card>
  101. </div>
  102. <div class="col-12 col-md-3">
  103. <q-card flat bordered style="height: 280px">
  104. <q-card-section class="row justify-between items-center q-pb-xs">
  105. <span class="text-subtitle2 text-weight-medium"
  106. >Atalhos rápidos</span
  107. >
  108. <q-icon name="mdi-apps" color="grey-5" />
  109. </q-card-section>
  110. <q-separator />
  111. <q-card-section class="q-pt-md column q-gutter-sm">
  112. <q-btn
  113. unelevated
  114. color="primary"
  115. label="Criar contrato"
  116. no-caps
  117. class="full-width"
  118. />
  119. <q-btn
  120. unelevated
  121. color="primary"
  122. label="Registrar presença"
  123. no-caps
  124. class="full-width"
  125. />
  126. <q-btn
  127. unelevated
  128. color="primary"
  129. label="Novo pedido"
  130. no-caps
  131. class="full-width"
  132. />
  133. </q-card-section>
  134. </q-card>
  135. </div>
  136. </div>
  137. <!-- Row 3: Bottom -->
  138. <div class="row q-col-gutter-md">
  139. <div class="col-12 col-md-4">
  140. <q-card flat bordered style="height: 320px">
  141. <q-card-section class="row justify-between items-center q-pb-xs">
  142. <span class="text-subtitle2 text-weight-medium"
  143. >Matrículas por Período</span
  144. >
  145. <q-icon name="mdi-book-open-outline" color="grey-5" />
  146. </q-card-section>
  147. <q-separator />
  148. <q-card-section
  149. style="height: calc(100% - 57px)"
  150. class="q-pt-sm q-px-sm"
  151. >
  152. <Bar
  153. :data="matriculasData"
  154. :options="matriculasOptions"
  155. :plugins="[ChartDataLabels]"
  156. />
  157. </q-card-section>
  158. </q-card>
  159. </div>
  160. <div class="col-12 col-md-4">
  161. <q-card flat bordered style="height: 320px">
  162. <q-card-section class="row justify-between items-center q-pb-xs">
  163. <span class="text-subtitle2 text-weight-medium"
  164. >Aniversariantes do Mês</span
  165. >
  166. <q-icon name="mdi-gift-outline" color="grey-5" />
  167. </q-card-section>
  168. <q-separator />
  169. <div class="row justify-between items-center q-px-md q-py-xs">
  170. <span class="text-caption text-grey-6">Nome</span>
  171. <span class="text-caption text-grey-6">Ações</span>
  172. </div>
  173. <q-separator />
  174. <div style="height: calc(100% - 105px); overflow-y: auto">
  175. <q-list separator>
  176. <q-item
  177. v-for="(pessoa, i) in aniversariantes"
  178. :key="i"
  179. dense
  180. class="q-py-sm"
  181. >
  182. <q-item-section avatar>
  183. <q-avatar
  184. :color="pessoa.color"
  185. text-color="white"
  186. size="34px"
  187. class="text-caption text-bold"
  188. >
  189. {{ pessoa.nome[0] }}
  190. </q-avatar>
  191. </q-item-section>
  192. <q-item-section>
  193. <q-item-label>{{ pessoa.nome }}</q-item-label>
  194. </q-item-section>
  195. <q-item-section side>
  196. <div class="row">
  197. <q-btn
  198. flat
  199. dense
  200. round
  201. icon="mdi-whatsapp"
  202. color="green"
  203. size="sm"
  204. />
  205. <q-btn
  206. flat
  207. dense
  208. round
  209. icon="mdi-email-outline"
  210. color="grey-6"
  211. size="sm"
  212. />
  213. </div>
  214. </q-item-section>
  215. </q-item>
  216. </q-list>
  217. </div>
  218. </q-card>
  219. </div>
  220. <div class="col-12 col-md-4">
  221. <q-card flat bordered style="height: 320px">
  222. <q-card-section class="row justify-between items-center q-pb-xs">
  223. <span class="text-subtitle2 text-weight-medium"
  224. >Feriados do mês</span
  225. >
  226. <q-icon name="mdi-calendar-outline" color="grey-5" />
  227. </q-card-section>
  228. <q-separator />
  229. <q-card-section class="q-pt-md">
  230. <q-btn
  231. unelevated
  232. color="primary"
  233. label="Nova data"
  234. no-caps
  235. class="full-width q-mb-md"
  236. />
  237. <div class="row q-gutter-sm">
  238. <div
  239. v-for="(feriado, i) in feriados"
  240. :key="i"
  241. class="column items-center"
  242. style="min-width: 52px"
  243. >
  244. <q-badge
  245. :color="feriado.color"
  246. class="text-subtitle1 text-bold q-pa-sm"
  247. style="min-width: 40px; justify-content: center"
  248. >
  249. {{ feriado.dia }}
  250. </q-badge>
  251. <div class="text-caption q-mt-xs text-center">
  252. {{ feriado.nome }}
  253. </div>
  254. </div>
  255. </div>
  256. </q-card-section>
  257. </q-card>
  258. </div>
  259. </div>
  260. </div>
  261. </div>
  262. </template>
  263. <script setup>
  264. import { ref } from "vue";
  265. import { Bar, Doughnut } from "vue-chartjs";
  266. import {
  267. Chart as ChartJS,
  268. Title,
  269. Tooltip,
  270. Legend,
  271. BarElement,
  272. CategoryScale,
  273. LinearScale,
  274. ArcElement,
  275. } from "chart.js";
  276. import ChartDataLabels from "chartjs-plugin-datalabels";
  277. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  278. import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
  279. ChartJS.register(
  280. Title,
  281. Tooltip,
  282. Legend,
  283. BarElement,
  284. CategoryScale,
  285. LinearScale,
  286. ArcElement,
  287. );
  288. const faturamentoData = ref({
  289. labels: [
  290. "17/02",
  291. "18/02",
  292. "19/02",
  293. "20/02",
  294. "21/02",
  295. "22/02",
  296. "23/02",
  297. "24/02",
  298. "25/02",
  299. "26/02",
  300. "27/02",
  301. "28/02",
  302. ],
  303. datasets: [
  304. {
  305. label: "Serviços",
  306. data: [120, 185, 95, 210, 155, 200, 170, 130, 195, 160, 145, 180],
  307. backgroundColor: "rgba(99, 102, 241, 0.75)",
  308. borderColor: "rgba(99, 102, 241, 1)",
  309. borderWidth: 1,
  310. },
  311. {
  312. label: "Materiais",
  313. data: [75, 115, 60, 140, 95, 125, 105, 85, 135, 100, 90, 115],
  314. backgroundColor: "rgba(236, 72, 153, 0.75)",
  315. borderColor: "rgba(236, 72, 153, 1)",
  316. borderWidth: 1,
  317. },
  318. ],
  319. });
  320. const faturamentoOptions = ref({
  321. responsive: true,
  322. maintainAspectRatio: false,
  323. plugins: {
  324. legend: { display: true, position: "top", labels: { font: { size: 11 } } },
  325. tooltip: { mode: "index", intersect: false },
  326. datalabels: { display: false },
  327. },
  328. scales: {
  329. x: { ticks: { font: { size: 9 } }, grid: { display: false } },
  330. y: { ticks: { font: { size: 10 } }, suggestedMin: 0 },
  331. },
  332. });
  333. const gaugeData = ref({
  334. datasets: [
  335. {
  336. backgroundColor: [
  337. "#00a550",
  338. "#4dbb7e",
  339. "#9ad2ad",
  340. "#cce156",
  341. "#fff100",
  342. "#ffbe00",
  343. "#ff8c00",
  344. "#FC3D23",
  345. "#D01616",
  346. "#8A0000",
  347. ],
  348. data: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  349. needleValue: 7,
  350. borderColor: "transparent",
  351. },
  352. ],
  353. });
  354. const gaugeOptions = ref({
  355. rotation: 270,
  356. circumference: 180,
  357. cutout: "55%",
  358. responsive: true,
  359. maintainAspectRatio: false,
  360. plugins: {
  361. tooltip: { enabled: false },
  362. legend: { display: false },
  363. datalabels: { display: false },
  364. },
  365. });
  366. const gaugeNeedlePlugin = {
  367. id: "gaugeNeedle",
  368. afterDatasetsDraw(chart) {
  369. const { ctx, data } = chart;
  370. ctx.save();
  371. const needleValue = data.datasets[0].needleValue;
  372. const meta = chart.getDatasetMeta(0).data[0];
  373. const xCenter = meta.x;
  374. const yCenter = meta.y;
  375. const outerRadius = meta.outerRadius - 20;
  376. const circumference =
  377. (meta.circumference / Math.PI / data.datasets[0].data[0]) * needleValue;
  378. const angle = Math.PI;
  379. ctx.translate(xCenter, yCenter);
  380. ctx.rotate(angle * (circumference + 1.5));
  381. ctx.beginPath();
  382. ctx.strokeStyle = "grey";
  383. ctx.fillStyle = "grey";
  384. ctx.moveTo(-3, 0);
  385. ctx.lineTo(0, -outerRadius);
  386. ctx.lineTo(3, 0);
  387. ctx.stroke();
  388. ctx.fill();
  389. ctx.beginPath();
  390. ctx.arc(0, 0, 6, 0, 2 * Math.PI);
  391. ctx.fillStyle = "grey";
  392. ctx.fill();
  393. ctx.restore();
  394. },
  395. };
  396. const matriculasData = ref({
  397. labels: ["JAN", "FEV", "MAR", "ABR", "MAI", "JUN"],
  398. datasets: [
  399. {
  400. label: "Matrículas",
  401. data: [120, 200, 150, 80, 70, 110],
  402. backgroundColor: [
  403. "#2196F3",
  404. "#F44336",
  405. "#9C27B0",
  406. "#FFC107",
  407. "#212121",
  408. "#009688",
  409. ],
  410. borderColor: [
  411. "#2196F3",
  412. "#F44336",
  413. "#9C27B0",
  414. "#FFC107",
  415. "#212121",
  416. "#009688",
  417. ],
  418. borderWidth: 1,
  419. borderRadius: 2,
  420. },
  421. ],
  422. });
  423. const matriculasOptions = ref({
  424. responsive: true,
  425. maintainAspectRatio: false,
  426. plugins: {
  427. legend: { display: false },
  428. datalabels: {
  429. anchor: "end",
  430. align: "top",
  431. color: "#666",
  432. font: { size: 11, weight: "bold" },
  433. formatter: (v) => v,
  434. },
  435. },
  436. scales: {
  437. x: { ticks: { font: { size: 11 } }, grid: { display: false } },
  438. y: { display: false, suggestedMin: 0, suggestedMax: 230 },
  439. },
  440. });
  441. const aniversariantes = ref([
  442. { nome: "Heloisa Faria", color: "orange" },
  443. { nome: "Juliana Costa", color: "green" },
  444. { nome: "Juliana Costa", color: "green" },
  445. { nome: "Fernando Almeida", color: "blue" },
  446. { nome: "Lucas Pereira", color: "purple" },
  447. { nome: "Sofia Martins", color: "teal" },
  448. ]);
  449. const feriados = ref([
  450. { dia: 17, nome: "Carnaval", color: "amber" },
  451. { dia: 17, nome: "Carnaval", color: "amber" },
  452. { dia: 17, nome: "Carnaval", color: "amber" },
  453. ]);
  454. </script>
  455. <style scoped>
  456. .stat-cards-row {
  457. display: flex;
  458. flex-wrap: nowrap;
  459. gap: 16px;
  460. }
  461. .stat-cards-row > * {
  462. flex: 1 1 0;
  463. min-width: 0;
  464. }
  465. @media (max-width: 599px) {
  466. .stat-cards-row {
  467. flex-wrap: wrap;
  468. }
  469. .stat-cards-row > * {
  470. flex: 1 1 calc(50% - 8px);
  471. }
  472. }
  473. .gauge-label {
  474. position: absolute;
  475. bottom: 28%;
  476. left: 50%;
  477. transform: translateX(-50%);
  478. text-align: center;
  479. pointer-events: none;
  480. }
  481. </style>