DashboardPage.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. <template>
  2. <div>
  3. <DefaultHeaderPage class="q-pa-sm" />
  4. <div class="q-pa-sm">
  5. <div class="stat-cards-row q-mb-md">
  6. <DashboardStatCard
  7. title="Total alunos (contratos ativos)"
  8. icon="mdi-account-multiple-outline"
  9. :value="String(totalAlunos)"
  10. :badge="`${totalAlunos} ativos`"
  11. />
  12. <DashboardStatCard
  13. title="Receita Total"
  14. icon="mdi-currency-usd"
  15. value="R$ 0,00"
  16. subtitle="0 pagamentos pendentes"
  17. />
  18. <DashboardStatCard
  19. title="Ticket Médio"
  20. icon="mdi-calendar-blank"
  21. value="R$ 12,00"
  22. subtitle="Estável"
  23. />
  24. <DashboardStatCard
  25. title="Aniversariantes"
  26. icon="mdi-emoticon-happy-outline"
  27. value="0"
  28. subtitle="Fortaleça seus relacionamentos"
  29. />
  30. </div>
  31. <div class="row q-col-gutter-md q-mb-md items-stretch">
  32. <div class="col-12 col-md-5">
  33. <DashboardChartCard title="Faturamento Serviço / Materiais" style="height: 100%">
  34. <GroupedBarChart
  35. :labels="faturamentoChart.labels"
  36. :datasets="faturamentoChart.datasets"
  37. label-y="R$"
  38. :tick-formatter="formatCurrencyTick"
  39. :tooltip-formatter="formatCurrencyTooltip"
  40. class="full-width full-height"
  41. />
  42. </DashboardChartCard>
  43. </div>
  44. <div class="col-12 col-md-4">
  45. <q-card flat bordered class="full-height">
  46. <q-card-section class="row justify-between items-center q-pb-xs">
  47. <span class="text-subtitle2 text-weight-medium"
  48. >Contratos Ativos</span
  49. >
  50. <q-icon name="mdi-trending-up" color="grey-5" />
  51. </q-card-section>
  52. <q-separator />
  53. <q-card-section
  54. class="flex flex-center q-pt-sm"
  55. style="height: calc(100% - 57px); position: relative"
  56. >
  57. <div style="height: 100%; max-width: 280px; width: 100%">
  58. <Doughnut
  59. :data="gaugeData"
  60. :options="gaugeOptions"
  61. :plugins="[gaugeNeedlePlugin]"
  62. />
  63. </div>
  64. <div class="gauge-label">
  65. <div class="text-h5 text-bold">70</div>
  66. <div class="text-caption text-grey-6">Grade</div>
  67. </div>
  68. </q-card-section>
  69. </q-card>
  70. </div>
  71. <div class="col-12 col-md-3">
  72. <q-card flat bordered class="full-height">
  73. <q-card-section class="row justify-between items-center q-pb-xs">
  74. <span class="text-subtitle2 text-weight-medium"
  75. >Atalhos rápidos</span
  76. >
  77. <q-icon name="mdi-apps" color="grey-5" />
  78. </q-card-section>
  79. <q-separator />
  80. <q-card-section class="q-pt-md column q-gutter-sm">
  81. <q-btn
  82. unelevated
  83. color="primary"
  84. label="Criar contrato"
  85. no-caps
  86. class="full-width"
  87. />
  88. <q-btn
  89. unelevated
  90. color="primary"
  91. label="Registrar presença"
  92. no-caps
  93. class="full-width"
  94. />
  95. <q-btn
  96. unelevated
  97. color="primary"
  98. label="Novo pedido"
  99. no-caps
  100. class="full-width"
  101. />
  102. </q-card-section>
  103. </q-card>
  104. </div>
  105. </div>
  106. <!-- Row 3: Bottom -->
  107. <div class="row q-col-gutter-md items-stretch">
  108. <div class="col-12 col-md-5">
  109. <DashboardChartCard title="Matrículas por Período" style="height: 100%">
  110. <GroupedBarChart
  111. :labels="matriculasChart.labels"
  112. :datasets="matriculasChart.datasets"
  113. :bar-radius="50"
  114. :show-datalabels="true"
  115. :max-bar-thickness="44"
  116. :category-percentage="0.6"
  117. :bar-percentage="0.85"
  118. class="full-width full-height"
  119. />
  120. </DashboardChartCard>
  121. </div>
  122. <div class="col-12 col-md-4">
  123. <AniversariantesCard :people="aniversariantes" style="height: 100%" />
  124. </div>
  125. <div class="col-12 col-md-3">
  126. <q-card flat bordered class="full-height">
  127. <q-card-section class="row justify-between items-center q-pb-xs">
  128. <span class="text-subtitle2 text-weight-medium"
  129. >Feriados do mês</span
  130. >
  131. <q-btn
  132. flat
  133. round
  134. dense
  135. icon="mdi-calendar-star"
  136. color="grey-5"
  137. @click="openFeriadosDialog"
  138. />
  139. </q-card-section>
  140. <q-separator />
  141. <q-card-section class="q-pt-md">
  142. <q-btn
  143. unelevated
  144. color="primary"
  145. label="Nova data"
  146. no-caps
  147. class="full-width q-mb-md"
  148. @click="openFeriadosDialog"
  149. />
  150. <div v-if="feriadosLoading" class="flex flex-center q-py-md">
  151. <q-spinner color="primary" size="24px" />
  152. </div>
  153. <div v-else-if="feriadosMes.length === 0" class="text-caption text-grey-5 text-center">
  154. Nenhum feriado neste mês.
  155. </div>
  156. <div v-else class="row q-gutter-sm">
  157. <div
  158. v-for="feriado in feriadosMes"
  159. :key="feriado.id"
  160. class="column items-center"
  161. style="min-width: 52px"
  162. >
  163. <q-badge
  164. color="deep-orange"
  165. class="text-subtitle1 text-bold q-pa-sm"
  166. style="min-width: 40px; justify-content: center"
  167. >
  168. {{ feriado.dia }}
  169. </q-badge>
  170. <div class="text-caption q-mt-xs text-center">
  171. {{ feriado.nome }}
  172. </div>
  173. </div>
  174. </div>
  175. </q-card-section>
  176. </q-card>
  177. </div>
  178. </div>
  179. </div>
  180. </div>
  181. </template>
  182. <script setup>
  183. import { ref, computed, onMounted } from "vue";
  184. import { Doughnut } from "vue-chartjs";
  185. import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
  186. import { useQuasar } from "quasar";
  187. import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
  188. import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
  189. import DashboardChartCard from "src/components/charts/DashboardChartCard.vue";
  190. import GroupedBarChart from "src/components/charts/normal/GroupedBarChart.vue";
  191. import AniversariantesCard from "src/components/charts/AniversariantesCard.vue";
  192. import FeriadosDialog from "./components/FeriadosDialog.vue";
  193. import { getHolidays } from "src/api/holiday";
  194. import { getStudents } from "src/api/student";
  195. ChartJS.register(ArcElement, Tooltip, Legend);
  196. const $q = useQuasar();
  197. const faturamentoChart = {
  198. labels: [
  199. "17/02", "18/02", "19/02", "20/02", "21/02",
  200. "22/02", "23/02", "24/02", "25/02", "26/02",
  201. "27/02", "28/02",
  202. ],
  203. datasets: [
  204. {
  205. label: "Serviço",
  206. data: [120, 185, 95, 210, 155, 200, 170, 130, 195, 160, 145, 180],
  207. color: "#a274f1",
  208. },
  209. {
  210. label: "Materiais",
  211. data: [75, 115, 60, 140, 95, 125, 105, 85, 135, 100, 90, 115],
  212. color: "#ff9999",
  213. },
  214. ],
  215. };
  216. const formatCurrencyTick = (value) => {
  217. if (value >= 1000) return `R$ ${(value / 1000).toFixed(0)}k`;
  218. return `R$ ${value}`;
  219. };
  220. const formatCurrencyTooltip = (context) => {
  221. const value = context.parsed.y;
  222. return ` ${context.dataset.label}: R$ ${value.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`;
  223. };
  224. const gaugeData = ref({
  225. datasets: [
  226. {
  227. backgroundColor: [
  228. "#00a550",
  229. "#4dbb7e",
  230. "#9ad2ad",
  231. "#cce156",
  232. "#fff100",
  233. "#ffbe00",
  234. "#ff8c00",
  235. "#FC3D23",
  236. "#D01616",
  237. "#8A0000",
  238. ],
  239. data: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
  240. needleValue: 7,
  241. borderColor: "transparent",
  242. },
  243. ],
  244. });
  245. const gaugeOptions = ref({
  246. rotation: 270,
  247. circumference: 180,
  248. cutout: "50%",
  249. responsive: true,
  250. maintainAspectRatio: false,
  251. plugins: {
  252. tooltip: { enabled: false },
  253. legend: { display: false },
  254. datalabels: {
  255. color: "black",
  256. font: { size: 14, weight: "bold" },
  257. formatter: (_value, ctx) => ctx.dataIndex,
  258. },
  259. },
  260. });
  261. const gaugeNeedlePlugin = {
  262. id: "gaugeNeedle",
  263. afterDatasetsDraw(chart) {
  264. const { ctx, data } = chart;
  265. ctx.save();
  266. const needleValue = data.datasets[0].needleValue;
  267. const meta = chart.getDatasetMeta(0).data[0];
  268. const xCenter = meta.x;
  269. const yCenter = meta.y;
  270. const outerRadius = meta.outerRadius - 20;
  271. const circumference =
  272. (meta.circumference / Math.PI / data.datasets[0].data[0]) * needleValue;
  273. const angle = Math.PI;
  274. ctx.translate(xCenter, yCenter);
  275. ctx.rotate(angle * (circumference + 1.5));
  276. ctx.beginPath();
  277. ctx.strokeStyle = "grey";
  278. ctx.fillStyle = "grey";
  279. ctx.moveTo(-3, 0);
  280. ctx.lineTo(0, -outerRadius);
  281. ctx.lineTo(3, 0);
  282. ctx.stroke();
  283. ctx.fill();
  284. ctx.beginPath();
  285. ctx.arc(0, 0, 6, 0, 2 * Math.PI);
  286. ctx.fillStyle = "grey";
  287. ctx.fill();
  288. ctx.restore();
  289. },
  290. };
  291. const matriculasChart = {
  292. labels: ["JAN", "FEV", "MAR", "ABR", "MAI", "JUN"],
  293. datasets: [
  294. {
  295. label: "Matrículas",
  296. data: [120, 200, 150, 80, 70, 110],
  297. color: ["#3B82F6", "#EF4444", "#A855F7", "#374151", "#EAB308", "#06B6D4"],
  298. },
  299. ],
  300. };
  301. const aniversariantes = ref([
  302. { day: 10, name: "Heloisa Faria" },
  303. { day: 11, name: "Juliana Costa" },
  304. { day: 16, name: "Juliana Costa" },
  305. { day: 23, name: "Fernando Almeida" },
  306. { day: 29, name: "Lucas Pereira" },
  307. { day: 34, name: "Sofia Martins" },
  308. ]);
  309. // Alunos
  310. const totalAlunos = ref(0);
  311. async function fetchAlunos() {
  312. try {
  313. const students = await getStudents();
  314. totalAlunos.value = students.length;
  315. } catch {
  316. // silencioso
  317. }
  318. }
  319. // Feriados
  320. const allHolidays = ref([]);
  321. const feriadosLoading = ref(false);
  322. const feriadosMes = computed(() => {
  323. const now = new Date();
  324. const month = now.getMonth() + 1;
  325. const year = now.getFullYear();
  326. return allHolidays.value
  327. .filter((h) => {
  328. const d = new Date(h.holiday_date + "T00:00:00");
  329. return d.getMonth() + 1 === month && d.getFullYear() === year;
  330. })
  331. .sort((a, b) => new Date(a.holiday_date) - new Date(b.holiday_date))
  332. .map((h) => ({
  333. id: h.id,
  334. dia: new Date(h.holiday_date + "T00:00:00").getDate(),
  335. nome: h.description,
  336. }));
  337. });
  338. async function fetchHolidays() {
  339. feriadosLoading.value = true;
  340. try {
  341. allHolidays.value = await getHolidays();
  342. } catch {
  343. $q.notify({ type: "negative", message: "Erro ao carregar feriados." });
  344. } finally {
  345. feriadosLoading.value = false;
  346. }
  347. }
  348. function openFeriadosDialog() {
  349. $q.dialog({ component: FeriadosDialog }).onOk(() => {
  350. fetchHolidays();
  351. });
  352. }
  353. onMounted(() => {
  354. fetchHolidays();
  355. fetchAlunos();
  356. });
  357. </script>
  358. <style scoped>
  359. .stat-cards-row {
  360. display: flex;
  361. flex-wrap: nowrap;
  362. gap: 16px;
  363. }
  364. .stat-cards-row > * {
  365. flex: 1 1 0;
  366. min-width: 0;
  367. }
  368. @media (max-width: 599px) {
  369. .stat-cards-row {
  370. flex-wrap: wrap;
  371. }
  372. .stat-cards-row > * {
  373. flex: 1 1 calc(50% - 8px);
  374. }
  375. }
  376. .gauge-label {
  377. position: absolute;
  378. bottom: 28%;
  379. left: 50%;
  380. transform: translateX(-50%);
  381. text-align: center;
  382. pointer-events: none;
  383. }
  384. </style>