DashboardPage.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  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 items-stretch">
  58. <div class="col-12 col-md-5">
  59. <DashboardChartCard title="Faturamento Serviço / Materiais" style="height: 100%">
  60. <GroupedBarChart
  61. :labels="faturamentoChart.labels"
  62. :datasets="faturamentoChart.datasets"
  63. label-y="R$"
  64. :tick-formatter="formatCurrencyTick"
  65. :tooltip-formatter="formatCurrencyTooltip"
  66. class="full-width full-height"
  67. />
  68. </DashboardChartCard>
  69. </div>
  70. <div class="col-12 col-md-4">
  71. <q-card flat bordered class="full-height">
  72. <q-card-section class="row justify-between items-center q-pb-xs">
  73. <span class="text-subtitle2 text-weight-medium"
  74. >Contratos Ativos</span
  75. >
  76. <q-icon name="mdi-trending-up" color="grey-5" />
  77. </q-card-section>
  78. <q-separator />
  79. <q-card-section
  80. class="flex flex-center q-pt-sm"
  81. style="height: calc(100% - 57px); position: relative"
  82. >
  83. <div style="height: 100%; max-width: 280px; width: 100%">
  84. <Doughnut
  85. :data="gaugeData"
  86. :options="gaugeOptions"
  87. :plugins="[gaugeNeedlePlugin]"
  88. />
  89. </div>
  90. <div class="gauge-label">
  91. <div class="text-h5 text-bold">70</div>
  92. <div class="text-caption text-grey-6">Grade</div>
  93. </div>
  94. </q-card-section>
  95. </q-card>
  96. </div>
  97. <div class="col-12 col-md-3">
  98. <q-card flat bordered class="full-height">
  99. <q-card-section class="row justify-between items-center q-pb-xs">
  100. <span class="text-subtitle2 text-weight-medium"
  101. >Atalhos rápidos</span
  102. >
  103. <q-icon name="mdi-apps" color="grey-5" />
  104. </q-card-section>
  105. <q-separator />
  106. <q-card-section class="q-pt-md column q-gutter-sm">
  107. <q-btn
  108. unelevated
  109. color="primary"
  110. label="Criar contrato"
  111. no-caps
  112. class="full-width"
  113. />
  114. <q-btn
  115. unelevated
  116. color="primary"
  117. label="Registrar presença"
  118. no-caps
  119. class="full-width"
  120. />
  121. <q-btn
  122. unelevated
  123. color="primary"
  124. label="Novo pedido"
  125. no-caps
  126. class="full-width"
  127. />
  128. </q-card-section>
  129. </q-card>
  130. </div>
  131. </div>
  132. <!-- Row 3: Bottom -->
  133. <div class="row q-col-gutter-md items-stretch">
  134. <div class="col-12 col-md-5">
  135. <DashboardChartCard title="Matrículas por Período" style="height: 100%">
  136. <GroupedBarChart
  137. :labels="matriculasChart.labels"
  138. :datasets="matriculasChart.datasets"
  139. :bar-radius="50"
  140. :show-datalabels="true"
  141. :max-bar-thickness="44"
  142. :category-percentage="0.6"
  143. :bar-percentage="0.85"
  144. class="full-width full-height"
  145. />
  146. </DashboardChartCard>
  147. </div>
  148. <div class="col-12 col-md-4">
  149. <AniversariantesCard :people="aniversariantes" style="height: 100%" />
  150. </div>
  151. <div class="col-12 col-md-3">
  152. <q-card flat bordered class="full-height">
  153. <q-card-section class="row justify-between items-center q-pb-xs">
  154. <span class="text-subtitle2 text-weight-medium"
  155. >Feriados do mês</span
  156. >
  157. <q-icon name="mdi-calendar-outline" color="grey-5" />
  158. </q-card-section>
  159. <q-separator />
  160. <q-card-section class="q-pt-md">
  161. <q-btn
  162. unelevated
  163. color="primary"
  164. label="Nova data"
  165. no-caps
  166. class="full-width q-mb-md"
  167. />
  168. <div class="row q-gutter-sm">
  169. <div
  170. v-for="(feriado, i) in feriados"
  171. :key="i"
  172. class="column items-center"
  173. style="min-width: 52px"
  174. >
  175. <q-badge
  176. :color="feriado.color"
  177. class="text-subtitle1 text-bold q-pa-sm"
  178. style="min-width: 40px; justify-content: center"
  179. >
  180. {{ feriado.dia }}
  181. </q-badge>
  182. <div class="text-caption q-mt-xs text-center">
  183. {{ feriado.nome }}
  184. </div>
  185. </div>
  186. </div>
  187. </q-card-section>
  188. </q-card>
  189. </div>
  190. </div>
  191. </div>
  192. </div>
  193. </template>
  194. <script setup>
  195. import { ref } from "vue";
  196. import { Doughnut } from "vue-chartjs";
  197. import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
  198. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  199. import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
  200. import DashboardChartCard from "src/components/charts/DashboardChartCard.vue";
  201. import GroupedBarChart from "src/components/charts/normal/GroupedBarChart.vue";
  202. import AniversariantesCard from "src/components/charts/AniversariantesCard.vue";
  203. ChartJS.register(ArcElement, Tooltip, Legend);
  204. const faturamentoChart = {
  205. labels: [
  206. "17/02", "18/02", "19/02", "20/02", "21/02",
  207. "22/02", "23/02", "24/02", "25/02", "26/02",
  208. "27/02", "28/02",
  209. ],
  210. datasets: [
  211. {
  212. label: "Serviço",
  213. data: [120, 185, 95, 210, 155, 200, 170, 130, 195, 160, 145, 180],
  214. color: "#a274f1",
  215. },
  216. {
  217. label: "Materiais",
  218. data: [75, 115, 60, 140, 95, 125, 105, 85, 135, 100, 90, 115],
  219. color: "#ff9999",
  220. },
  221. ],
  222. };
  223. const formatCurrencyTick = (value) => {
  224. if (value >= 1000) return `R$ ${(value / 1000).toFixed(0)}k`;
  225. return `R$ ${value}`;
  226. };
  227. const formatCurrencyTooltip = (context) => {
  228. const value = context.parsed.y;
  229. return ` ${context.dataset.label}: R$ ${value.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`;
  230. };
  231. const gaugeData = ref({
  232. datasets: [
  233. {
  234. backgroundColor: [
  235. "#00a550",
  236. "#4dbb7e",
  237. "#9ad2ad",
  238. "#cce156",
  239. "#fff100",
  240. "#ffbe00",
  241. "#ff8c00",
  242. "#FC3D23",
  243. "#D01616",
  244. "#8A0000",
  245. ],
  246. data: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  247. needleValue: 7,
  248. borderColor: "transparent",
  249. },
  250. ],
  251. });
  252. const gaugeOptions = ref({
  253. rotation: 270,
  254. circumference: 180,
  255. cutout: "50%",
  256. responsive: true,
  257. maintainAspectRatio: false,
  258. plugins: {
  259. tooltip: { enabled: false },
  260. legend: { display: false },
  261. datalabels: {
  262. color: "black",
  263. font: { size: 14, weight: "bold" },
  264. formatter: (_value, ctx) => ctx.dataIndex,
  265. },
  266. },
  267. });
  268. const gaugeNeedlePlugin = {
  269. id: "gaugeNeedle",
  270. afterDatasetsDraw(chart) {
  271. const { ctx, data } = chart;
  272. ctx.save();
  273. const needleValue = data.datasets[0].needleValue;
  274. const meta = chart.getDatasetMeta(0).data[0];
  275. const xCenter = meta.x;
  276. const yCenter = meta.y;
  277. const outerRadius = meta.outerRadius - 20;
  278. const circumference =
  279. (meta.circumference / Math.PI / data.datasets[0].data[0]) * needleValue;
  280. const angle = Math.PI;
  281. ctx.translate(xCenter, yCenter);
  282. ctx.rotate(angle * (circumference + 1.5));
  283. ctx.beginPath();
  284. ctx.strokeStyle = "grey";
  285. ctx.fillStyle = "grey";
  286. ctx.moveTo(-3, 0);
  287. ctx.lineTo(0, -outerRadius);
  288. ctx.lineTo(3, 0);
  289. ctx.stroke();
  290. ctx.fill();
  291. ctx.beginPath();
  292. ctx.arc(0, 0, 6, 0, 2 * Math.PI);
  293. ctx.fillStyle = "grey";
  294. ctx.fill();
  295. ctx.restore();
  296. },
  297. };
  298. const matriculasChart = {
  299. labels: ["JAN", "FEV", "MAR", "ABR", "MAI", "JUN"],
  300. datasets: [
  301. {
  302. label: "Matrículas",
  303. data: [120, 200, 150, 80, 70, 110],
  304. color: ["#3B82F6", "#EF4444", "#A855F7", "#374151", "#EAB308", "#06B6D4"],
  305. },
  306. ],
  307. };
  308. const aniversariantes = ref([
  309. { day: 10, name: "Heloisa Faria" },
  310. { day: 11, name: "Juliana Costa" },
  311. { day: 16, name: "Juliana Costa" },
  312. { day: 23, name: "Fernando Almeida" },
  313. { day: 29, name: "Lucas Pereira" },
  314. { day: 34, name: "Sofia Martins" },
  315. ]);
  316. const feriados = ref([
  317. { dia: 17, nome: "Carnaval", color: "amber" },
  318. { dia: 17, nome: "Carnaval", color: "amber" },
  319. { dia: 17, nome: "Carnaval", color: "amber" },
  320. ]);
  321. </script>
  322. <style scoped>
  323. .stat-cards-row {
  324. display: flex;
  325. flex-wrap: nowrap;
  326. gap: 16px;
  327. }
  328. .stat-cards-row > * {
  329. flex: 1 1 0;
  330. min-width: 0;
  331. }
  332. @media (max-width: 599px) {
  333. .stat-cards-row {
  334. flex-wrap: wrap;
  335. }
  336. .stat-cards-row > * {
  337. flex: 1 1 calc(50% - 8px);
  338. }
  339. }
  340. .gauge-label {
  341. position: absolute;
  342. bottom: 28%;
  343. left: 50%;
  344. transform: translateX(-50%);
  345. text-align: center;
  346. pointer-events: none;
  347. }
  348. </style>