Forráskód Böngészése

Merge branch 'feat/GINC-GAB-estilizacao-graficos-dashboard-franquia' of Softpar/sfp_vue_franchisee_ginastica_cerebro into development

Gabriel Alves 4 hete
szülő
commit
5e4021adf8

+ 2 - 6
index.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html>
   <head>
-    <title><%= productName %></title>
+    <title>Ginastica do Cerebro - Franqueada</title>
 
     <meta charset="utf-8">
     <meta name="description" content="<%= productDescription %>">
@@ -9,11 +9,7 @@
     <meta name="msapplication-tap-highlight" content="no">
     <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
 
-    <link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
-    <link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
-    <link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
-    <link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
-    <link rel="icon" type="image/ico" href="favicon.ico">
+    <link rel="icon" type="image/png" sizes="32x32"  href="logo.png">
   </head>
   <body>
     <!-- quasar:entry-point -->

BIN
public/logo.png


+ 88 - 0
src/components/charts/AniversariantesCard.vue

@@ -0,0 +1,88 @@
+<template>
+  <q-card flat class="dashboard-chart-card card-ring">
+    <div class="flex justify-between items-center no-wrap q-mb-sm">
+      <span class="text-subtitle2 text-weight-regular">Aniversariantes do Mês</span>
+      <q-icon name="mdi-cake-variant-outline" color="secondary" size="sm" />
+    </div>
+
+    <div class="header-row text-caption text-grey-6 q-mb-xs">
+      <div class="flex items-center" style="gap: 4px">
+        <q-icon name="mdi-calendar-outline" size="14px" color="secondary" />
+        <span>Nome</span>
+      </div>
+      <span>Ações</span>
+    </div>
+    <q-separator />
+
+    <div class="list-scroll">
+      <q-list dense>
+        <template v-for="(person, index) in people" :key="index">
+          <q-item class="q-px-none person-item">
+            <q-item-section avatar style="min-width: 36px">
+              <div class="day-badge">{{ person.day }}</div>
+            </q-item-section>
+            <q-item-section>
+              <q-item-label class="text-body2">{{ person.name }}</q-item-label>
+            </q-item-section>
+            <q-item-section side>
+              <div class="flex items-center no-wrap" style="gap: 4px">
+                <q-btn flat round dense size="sm" icon="mdi-whatsapp" color="dark" />
+                <q-btn flat round dense size="sm" icon="mdi-email-outline" color="dark" />
+              </div>
+            </q-item-section>
+          </q-item>
+          <q-separator v-if="index < people.length - 1" />
+        </template>
+      </q-list>
+    </div>
+  </q-card>
+</template>
+
+<script setup>
+defineProps({
+  people: {
+    type: Array,
+    default: () => [],
+  },
+});
+</script>
+
+<style scoped>
+.dashboard-chart-card {
+  border-radius: 12px;
+  padding: 20px 24px;
+  display: flex;
+  flex-direction: column;
+}
+
+.header-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.list-scroll {
+  flex: 1;
+  overflow-y: auto;
+  min-height: 0;
+}
+
+.person-item {
+  padding-top: 6px;
+  padding-bottom: 6px;
+}
+
+.day-badge {
+  width: 30px;
+  height: 30px;
+  border-radius: 50%;
+  background-color: #f97316;
+  color: #fff;
+  font-size: 12px;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+</style>

+ 37 - 0
src/components/charts/DashboardChartCard.vue

@@ -0,0 +1,37 @@
+<template>
+  <q-card flat class="dashboard-chart-card card-ring">
+    <div class="flex justify-between items-center no-wrap q-mb-sm">
+      <span class="text-subtitle2 text-weight-regular">{{ title }}</span>
+      <q-icon :name="icon" color="dark" size="sm" />
+    </div>
+    <div class="chart-slot-wrapper">
+      <slot />
+    </div>
+  </q-card>
+</template>
+
+<script setup>
+const { title, icon } = defineProps({
+  title: { type: String, required: true },
+  icon: { type: String, default: "mdi-book-open-blank-variant-outline" },
+});
+
+defineEmits(["export"]);
+</script>
+
+<style scoped>
+.dashboard-chart-card {
+  border-radius: 12px;
+  padding: 20px 28px 24px 28px;
+  display: flex;
+  flex-direction: column;
+  max-height: 420px;
+}
+
+.chart-slot-wrapper {
+  flex: 1;
+  min-height: 280px;
+  max-height: 360px;
+  position: relative;
+}
+</style>

+ 67 - 0
src/components/charts/DashboardStatCard.vue

@@ -0,0 +1,67 @@
+<template>
+  <q-card class="stat-card">
+    <div class="flex justify-between items-start no-wrap">
+      <span class="text-subtitle2 text-dark">{{ title }}</span>
+      <q-icon :name="icon" size="22px" color="dark" />
+    </div>
+
+    <div class="value-area">
+      <span class="text-h5 text-primary value-text">{{ value }}</span>
+
+      <q-badge
+        v-if="badge"
+        :color="badgeColor"
+        :label="badge"
+        :style="customStyle"
+        class="stat-badge"
+      />
+      <span v-else-if="subtitle" class="text-caption text-foreground">{{
+        subtitle
+      }}</span>
+    </div>
+  </q-card>
+</template>
+
+<script setup>
+defineProps({
+  title: { type: String, required: true },
+  icon: { type: String, default: "mdi-information-outline" },
+  value: { type: [String, Number], required: true },
+  subtitle: { type: String, default: "" },
+  badge: { type: String, default: "" },
+  badgeColor: { type: String, default: "accent-1" },
+  customStyle: { type: String, default: "padding: 4px" },
+});
+</script>
+
+<style scoped lang="scss">
+.stat-card {
+  flex: 1 1 0;
+  min-width: 0;
+  height: 140px;
+  border-radius: 12px;
+  box-shadow: 0 0 0 1px #c0c0c0c0 !important;
+  padding: 16px 20px;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+}
+
+.value-area {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.value-text {
+  font-weight: 600;
+  line-height: 1.2;
+}
+
+.stat-badge {
+  font-size: 12px;
+  border-radius: 8px;
+  width: fit-content;
+  color: $primary;
+}
+</style>

+ 214 - 0
src/components/charts/normal/GroupedBarChart.vue

@@ -0,0 +1,214 @@
+<template>
+  <div v-bind="$attrs" class="chart-wrapper full-width full-height">
+    <q-resize-observer @resize="onResize" />
+    <div v-if="hasData" class="chart-container">
+      <Bar
+        ref="chart_ref"
+        :options="chartOptions"
+        :data="chartData"
+        :plugins="activePlugins"
+      />
+    </div>
+    <div v-else class="no-data-container">
+      <span class="text-grey-6">{{ $t("http.errors.no_records_found") }}</span>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import { Bar } from "vue-chartjs";
+import {
+  Chart as ChartJS,
+  Title,
+  Tooltip,
+  Legend,
+  BarElement,
+  CategoryScale,
+  LinearScale,
+} from "chart.js";
+import ChartDataLabels from "chartjs-plugin-datalabels";
+import { base64ToJPEG } from "src/helpers/convertBase64Image";
+
+ChartJS.register(
+  Title,
+  Tooltip,
+  Legend,
+  BarElement,
+  CategoryScale,
+  LinearScale,
+);
+
+const chart_ref = ref(null);
+
+const props = defineProps({
+  labels: {
+    type: Array,
+    default: () => [],
+  },
+  datasets: {
+    type: Array,
+    default: () => [],
+  },
+  title: {
+    type: String,
+    default: "",
+  },
+  labelY: {
+    type: String,
+    default: "",
+  },
+  barRadius: {
+    type: Number,
+    default: 4,
+  },
+  showDatalabels: {
+    type: Boolean,
+    default: false,
+  },
+  tickFormatter: {
+    type: Function,
+    default: null,
+  },
+  tooltipFormatter: {
+    type: Function,
+    default: null,
+  },
+  maxBarThickness: {
+    type: Number,
+    default: null,
+  },
+  categoryPercentage: {
+    type: Number,
+    default: 0.8,
+  },
+  barPercentage: {
+    type: Number,
+    default: 0.9,
+  },
+});
+
+const onResize = () => {
+  if (chart_ref.value?.chart) {
+    setTimeout(() => chart_ref.value.chart.resize(), 50);
+  }
+};
+
+const hasData = computed(
+  () => props.labels.length > 0 && props.datasets.length > 0,
+);
+
+const activePlugins = computed(() => [ChartDataLabels]);
+
+const chartData = computed(() => ({
+  labels: props.labels,
+  datasets: props.datasets.map((ds) => ({
+    label: ds.label,
+    data: ds.data,
+    backgroundColor: ds.color,
+    borderColor: ds.color,
+    borderRadius: props.barRadius,
+    borderSkipped: false,
+    borderWidth: 0,
+    ...(props.maxBarThickness != null
+      ? { maxBarThickness: props.maxBarThickness }
+      : {}),
+    categoryPercentage: props.categoryPercentage,
+    barPercentage: props.barPercentage,
+  })),
+}));
+
+const labelColor = "#555555";
+const gridColor = "rgba(0,0,0,0.07)";
+
+const chartOptions = computed(() => ({
+  responsive: true,
+  maintainAspectRatio: false,
+  layout: {
+    padding: {
+      top: props.showDatalabels ? 28 : 8,
+      left: 12,
+      right: 12,
+      bottom: 4,
+    },
+  },
+  plugins: {
+    legend: {
+      display: false,
+    },
+    datalabels: {
+      display: props.showDatalabels,
+      anchor: "end",
+      align: "top",
+      offset: 4,
+      color: "#333",
+      font: { size: 12, weight: "bold" },
+      formatter: (v) => (v > 0 ? v : ""),
+    },
+    tooltip: {
+      backgroundColor: "rgba(255,255,255,0.95)",
+      titleColor: "#333",
+      bodyColor: "#555",
+      borderColor: "rgba(0,0,0,0.1)",
+      borderWidth: 1,
+      callbacks: props.tooltipFormatter
+        ? { label: props.tooltipFormatter }
+        : {},
+    },
+  },
+  scales: {
+    x: {
+      grid: { display: false },
+      ticks: { color: labelColor, font: { size: 11 } },
+    },
+    y: {
+      display: true,
+      title: {
+        display: !!props.labelY,
+        text: props.labelY,
+        color: labelColor,
+        font: { size: 12 },
+      },
+      grid: { color: gridColor },
+      ticks: {
+        color: labelColor,
+        font: { size: 11 },
+        callback: props.tickFormatter ?? ((v) => v),
+      },
+      suggestedMin: 0,
+    },
+  },
+}));
+
+const downloadImage = () => {
+  const image = chart_ref.value?.chart?.toBase64Image("image/jpeg", 1);
+  if (image) base64ToJPEG(image, props.title || "grouped-bar-chart");
+};
+
+defineExpose({ downloadImage, chart_ref });
+</script>
+
+<style scoped>
+.chart-wrapper {
+  position: relative;
+}
+
+.chart-container {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.no-data-container {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 1.5rem;
+}
+</style>

+ 22 - 5
src/components/layout/DefaultHeaderPage.vue

@@ -2,7 +2,7 @@
   <div>
     <q-breadcrumbs
       v-if="displayBreadcrumbs != null"
-      class="q-mb-xs text-secondary"
+      class="q-mb-xs text-secondary flex items-center"
       :class="$q.screen.lt.sm ? '' : 'q-pl-lg'"
     >
       <q-breadcrumbs-el
@@ -21,15 +21,29 @@
     </div>
 
     <div class="flex items-center justify-between">
-      <div class="column q-pl-xs" :class="$q.screen.lt.sm ? '' : 'q-pt-md'">
-        <span v-if="displayTitle" class="text-h6 text-primary q-mb-xs">
+      <div
+        class="flex items-center q-pl-xs"
+        :class="$q.screen.lt.sm ? '' : 'q-pt-md'"
+      >
+        <span v-if="displayTitle" class="text-h6 text-primary text-weight-regular">
           {{ displayTitle }}
         </span>
         <div v-else style="width: 280px">
           <q-skeleton type="text" height="40px" />
         </div>
+        <q-icon
+          v-if="showFilterIcon"
+          name="mdi-filter-outline"
+          color="background"
+          size="sm"
+          class="q-ml-sm bg-primary"
+          style="border-radius: 8px; padding: 2px"
+        />
       </div>
-      <div>
+      <div
+        class="flex items-center q-pr-sm"
+        :class="$q.screen.lt.sm ? '' : 'q-pt-md'"
+      >
         <slot name="after" />
       </div>
     </div>
@@ -47,11 +61,14 @@ const { title, breadcrumbs } = defineProps({
     type: Object,
     default: null,
   },
-
   breadcrumbs: {
     type: Object,
     default: null,
   },
+  showFilterIcon: {
+    type: Boolean,
+    default: false,
+  },
 });
 
 const route = useRoute();

+ 1 - 1
src/components/layout/LeftMenuLayout.vue

@@ -31,7 +31,7 @@
         <div
           v-if="!$q.screen.lt.md"
           class="toggle-button-wrapper absolute"
-          style="top: 2px; right: -32px; z-index: 1"
+          style="top: 10px; right: -38px; z-index: 1"
         >
           <div @click="miniState = !miniState">
             <q-icon

+ 4 - 0
src/css/quasar.variables.scss

@@ -53,6 +53,10 @@ $colors: (
   "declined-4": #ffcecf,
 
   "warning": #BF6A02,
+
+  "accent-1": #E38B37,
+
+  "foreground": #505050,
 );
 
 @each $name, $color in $colors {

+ 165 - 213
src/pages/dashboard/DashboardPage.vue

@@ -1,86 +1,82 @@
 <template>
   <div>
-    <DefaultHeaderPage />
-
-    <div class="q-pa-md">
-      <!-- Row 1: Stats -->
-      <div class="row q-col-gutter-md q-mb-md">
-        <div class="col-12 col-sm-6 col-lg-3">
-          <q-card flat bordered>
-            <q-card-section class="row justify-between items-start q-pb-none">
-              <span class="text-caption text-grey-6">Total alunos (contratos ativos)</span>
-              <q-icon name="mdi-account-multiple" color="grey-4" size="sm" />
-            </q-card-section>
-            <q-card-section class="q-pt-sm">
-              <div class="text-h5 text-bold">0</div>
-              <q-badge color="orange" class="q-mt-sm q-pa-xs" style="font-size: 11px">
-                0 ativos
-              </q-badge>
-            </q-card-section>
-          </q-card>
-        </div>
-
-        <div class="col-12 col-sm-6 col-lg-3">
-          <q-card flat bordered>
-            <q-card-section class="row justify-between items-start q-pb-none">
-              <span class="text-caption text-grey-6">Receita Total</span>
-              <q-icon name="mdi-currency-usd" color="grey-4" size="sm" />
-            </q-card-section>
-            <q-card-section class="q-pt-sm">
-              <div class="text-h5 text-bold">R$ 0,00</div>
-              <div class="text-caption text-grey-5 q-mt-sm">0 pagamentos pendentes</div>
-            </q-card-section>
-          </q-card>
-        </div>
+    <DefaultHeaderPage class="q-pa-sm">
+      <template #after>
+        <div class="flex items-center no-wrap" style="gap: 12px">
+          <template v-if="$q.screen.gt.sm">
+            <div
+              class="column"
+              style="line-height: 1.2; white-space: nowrap; flex-shrink: 0"
+            >
+              <span class="text-caption text-grey-6 text-primary text-center"
+                >Ultimo acesso</span
+              >
+              <span class="text-caption text-primary text-center"
+                >16/02/2026, 14:16</span
+              >
+            </div>
+          </template>
 
-        <div class="col-12 col-sm-6 col-lg-3">
-          <q-card flat bordered>
-            <q-card-section class="row justify-between items-start q-pb-none">
-              <span class="text-caption text-grey-6">Ticket Médio</span>
-              <q-icon name="mdi-receipt-outline" color="grey-4" size="sm" />
-            </q-card-section>
-            <q-card-section class="q-pt-sm">
-              <div class="text-h5 text-bold">R$ 12,00</div>
-              <div class="text-caption text-grey-5 q-mt-sm">Estável</div>
-            </q-card-section>
-          </q-card>
+          <div
+            class="flex items-center no-wrap"
+            style="gap: 2px; flex-shrink: 0"
+          >
+            <q-btn flat round dense icon="mdi-bell-badge" color="secondary" />
+            <q-btn flat round dense icon="mdi-account" color="secondary" />
+            <q-btn flat round dense icon="mdi-cog-outline" color="secondary" />
+          </div>
         </div>
+      </template>
+    </DefaultHeaderPage>
 
-        <div class="col-12 col-sm-6 col-lg-3">
-          <q-card flat bordered>
-            <q-card-section class="row justify-between items-start q-pb-none">
-              <span class="text-caption text-grey-6">Aniversariantes</span>
-              <q-icon name="mdi-emoticon-outline" color="grey-4" size="sm" />
-            </q-card-section>
-            <q-card-section class="q-pt-sm">
-              <div class="text-h5 text-bold">0</div>
-              <div class="text-caption text-grey-5 q-mt-sm">Fortaleça seus relacionamentos</div>
-            </q-card-section>
-          </q-card>
-        </div>
+    <div class="q-pa-sm">
+      <div class="stat-cards-row q-mb-md">
+        <DashboardStatCard
+          title="Total alunos (contratos ativos)"
+          icon="mdi-account-multiple-outline"
+          value="0"
+          badge="0 ativos"
+        />
+        <DashboardStatCard
+          title="Receita Total"
+          icon="mdi-currency-usd"
+          value="R$ 0,00"
+          subtitle="0 pagamentos pendentes"
+        />
+        <DashboardStatCard
+          title="Ticket Médio"
+          icon="mdi-calendar-blank"
+          value="R$ 12,00"
+          subtitle="Estável"
+        />
+        <DashboardStatCard
+          title="Aniversariantes"
+          icon="mdi-emoticon-happy-outline"
+          value="0"
+          subtitle="Fortaleça seus relacionamentos"
+        />
       </div>
 
-      <!-- Row 2: Charts -->
-      <div class="row q-col-gutter-md q-mb-md">
-        <!-- Faturamento Serviço / Materiais -->
+      <div class="row q-col-gutter-md q-mb-md items-stretch">
         <div class="col-12 col-md-5">
-          <q-card flat bordered style="height: 280px">
-            <q-card-section class="row justify-between items-center q-pb-xs">
-              <span class="text-subtitle2 text-weight-medium">Faturamento Serviço / Materiais</span>
-              <q-icon name="mdi-book-open-outline" color="grey-5" />
-            </q-card-section>
-            <q-separator />
-            <q-card-section style="height: calc(100% - 57px)" class="q-pt-sm q-px-sm">
-              <Bar :data="faturamentoData" :options="faturamentoOptions" />
-            </q-card-section>
-          </q-card>
+          <DashboardChartCard title="Faturamento Serviço / Materiais" style="height: 100%">
+            <GroupedBarChart
+              :labels="faturamentoChart.labels"
+              :datasets="faturamentoChart.datasets"
+              label-y="R$"
+              :tick-formatter="formatCurrencyTick"
+              :tooltip-formatter="formatCurrencyTooltip"
+              class="full-width full-height"
+            />
+          </DashboardChartCard>
         </div>
 
-        <!-- Contratos Ativos -->
         <div class="col-12 col-md-4">
-          <q-card flat bordered style="height: 280px">
+          <q-card flat bordered class="full-height">
             <q-card-section class="row justify-between items-center q-pb-xs">
-              <span class="text-subtitle2 text-weight-medium">Contratos Ativos</span>
+              <span class="text-subtitle2 text-weight-medium"
+                >Contratos Ativos</span
+              >
               <q-icon name="mdi-trending-up" color="grey-5" />
             </q-card-section>
             <q-separator />
@@ -103,11 +99,12 @@
           </q-card>
         </div>
 
-        <!-- Atalhos rápidos -->
         <div class="col-12 col-md-3">
-          <q-card flat bordered style="height: 280px">
+          <q-card flat bordered class="full-height">
             <q-card-section class="row justify-between items-center q-pb-xs">
-              <span class="text-subtitle2 text-weight-medium">Atalhos rápidos</span>
+              <span class="text-subtitle2 text-weight-medium"
+                >Atalhos rápidos</span
+              >
               <q-icon name="mdi-apps" color="grey-5" />
             </q-card-section>
             <q-separator />
@@ -139,66 +136,32 @@
       </div>
 
       <!-- Row 3: Bottom -->
-      <div class="row q-col-gutter-md">
-        <!-- Matrículas por Período -->
-        <div class="col-12 col-md-4">
-          <q-card flat bordered style="height: 320px">
-            <q-card-section class="row justify-between items-center q-pb-xs">
-              <span class="text-subtitle2 text-weight-medium">Matrículas por Período</span>
-              <q-icon name="mdi-book-open-outline" color="grey-5" />
-            </q-card-section>
-            <q-separator />
-            <q-card-section style="height: calc(100% - 57px)" class="q-pt-sm q-px-sm">
-              <Bar
-                :data="matriculasData"
-                :options="matriculasOptions"
-                :plugins="[ChartDataLabels]"
-              />
-            </q-card-section>
-          </q-card>
+      <div class="row q-col-gutter-md items-stretch">
+        <div class="col-12 col-md-5">
+          <DashboardChartCard title="Matrículas por Período" style="height: 100%">
+            <GroupedBarChart
+              :labels="matriculasChart.labels"
+              :datasets="matriculasChart.datasets"
+              :bar-radius="50"
+              :show-datalabels="true"
+              :max-bar-thickness="44"
+              :category-percentage="0.6"
+              :bar-percentage="0.85"
+              class="full-width full-height"
+            />
+          </DashboardChartCard>
         </div>
 
-        <!-- Aniversariantes do Mês -->
         <div class="col-12 col-md-4">
-          <q-card flat bordered style="height: 320px">
-            <q-card-section class="row justify-between items-center q-pb-xs">
-              <span class="text-subtitle2 text-weight-medium">Aniversariantes do Mês</span>
-              <q-icon name="mdi-gift-outline" color="grey-5" />
-            </q-card-section>
-            <q-separator />
-            <div class="row justify-between items-center q-px-md q-py-xs">
-              <span class="text-caption text-grey-6">Nome</span>
-              <span class="text-caption text-grey-6">Ações</span>
-            </div>
-            <q-separator />
-            <div style="height: calc(100% - 105px); overflow-y: auto">
-              <q-list separator>
-                <q-item v-for="(pessoa, i) in aniversariantes" :key="i" dense class="q-py-sm">
-                  <q-item-section avatar>
-                    <q-avatar :color="pessoa.color" text-color="white" size="34px" class="text-caption text-bold">
-                      {{ pessoa.nome[0] }}
-                    </q-avatar>
-                  </q-item-section>
-                  <q-item-section>
-                    <q-item-label>{{ pessoa.nome }}</q-item-label>
-                  </q-item-section>
-                  <q-item-section side>
-                    <div class="row">
-                      <q-btn flat dense round icon="mdi-whatsapp" color="green" size="sm" />
-                      <q-btn flat dense round icon="mdi-email-outline" color="grey-6" size="sm" />
-                    </div>
-                  </q-item-section>
-                </q-item>
-              </q-list>
-            </div>
-          </q-card>
+          <AniversariantesCard :people="aniversariantes" style="height: 100%" />
         </div>
 
-        <!-- Feriados do mês -->
-        <div class="col-12 col-md-4">
-          <q-card flat bordered style="height: 320px">
+        <div class="col-12 col-md-3">
+          <q-card flat bordered class="full-height">
             <q-card-section class="row justify-between items-center q-pb-xs">
-              <span class="text-subtitle2 text-weight-medium">Feriados do mês</span>
+              <span class="text-subtitle2 text-weight-medium"
+                >Feriados do mês</span
+              >
               <q-icon name="mdi-calendar-outline" color="grey-5" />
             </q-card-section>
             <q-separator />
@@ -224,7 +187,9 @@
                   >
                     {{ feriado.dia }}
                   </q-badge>
-                  <div class="text-caption q-mt-xs text-center">{{ feriado.nome }}</div>
+                  <div class="text-caption q-mt-xs text-center">
+                    {{ feriado.nome }}
+                  </div>
                 </div>
               </div>
             </q-card-section>
@@ -237,72 +202,60 @@
 
 <script setup>
 import { ref } from "vue";
-import { Bar, Doughnut } from "vue-chartjs";
-import {
-  Chart as ChartJS,
-  Title,
-  Tooltip,
-  Legend,
-  BarElement,
-  CategoryScale,
-  LinearScale,
-  ArcElement,
-} from "chart.js";
-import ChartDataLabels from "chartjs-plugin-datalabels";
+import { Doughnut } from "vue-chartjs";
+import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
 import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
+import DashboardStatCard from "src/components/charts/DashboardStatCard.vue";
+import DashboardChartCard from "src/components/charts/DashboardChartCard.vue";
+import GroupedBarChart from "src/components/charts/normal/GroupedBarChart.vue";
+import AniversariantesCard from "src/components/charts/AniversariantesCard.vue";
 
-ChartJS.register(
-  Title,
-  Tooltip,
-  Legend,
-  BarElement,
-  CategoryScale,
-  LinearScale,
-  ArcElement,
-);
+ChartJS.register(ArcElement, Tooltip, Legend);
 
-// ── Faturamento Serviço / Materiais ───────────────────────────────────────────
-const faturamentoData = ref({
-  labels: ["17/02", "18/02", "19/02", "20/02", "21/02", "22/02", "23/02", "24/02", "25/02", "26/02", "27/02", "28/02"],
+const faturamentoChart = {
+  labels: [
+    "17/02", "18/02", "19/02", "20/02", "21/02",
+    "22/02", "23/02", "24/02", "25/02", "26/02",
+    "27/02", "28/02",
+  ],
   datasets: [
     {
-      label: "Serviços",
+      label: "Serviço",
       data: [120, 185, 95, 210, 155, 200, 170, 130, 195, 160, 145, 180],
-      backgroundColor: "rgba(99, 102, 241, 0.75)",
-      borderColor: "rgba(99, 102, 241, 1)",
-      borderWidth: 1,
+      color: "#a274f1",
     },
     {
       label: "Materiais",
       data: [75, 115, 60, 140, 95, 125, 105, 85, 135, 100, 90, 115],
-      backgroundColor: "rgba(236, 72, 153, 0.75)",
-      borderColor: "rgba(236, 72, 153, 1)",
-      borderWidth: 1,
+      color: "#ff9999",
     },
   ],
-});
+};
 
-const faturamentoOptions = ref({
-  responsive: true,
-  maintainAspectRatio: false,
-  plugins: {
-    legend: { display: true, position: "top", labels: { font: { size: 11 } } },
-    tooltip: { mode: "index", intersect: false },
-    datalabels: { display: false },
-  },
-  scales: {
-    x: { ticks: { font: { size: 9 } }, grid: { display: false } },
-    y: { ticks: { font: { size: 10 } }, suggestedMin: 0 },
-  },
-});
+const formatCurrencyTick = (value) => {
+  if (value >= 1000) return `R$ ${(value / 1000).toFixed(0)}k`;
+  return `R$ ${value}`;
+};
+
+const formatCurrencyTooltip = (context) => {
+  const value = context.parsed.y;
+  return ` ${context.dataset.label}: R$ ${value.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`;
+};
 
-// ── Contratos Ativos (gauge) ──────────────────────────────────────────────────
 const gaugeData = ref({
   datasets: [
     {
       backgroundColor: [
-        "#00a550", "#4dbb7e", "#9ad2ad", "#cce156",
-        "#fff100", "#ffbe00", "#ff8c00", "#FC3D23", "#D01616", "#8A0000",
+        "#00a550",
+        "#4dbb7e",
+        "#9ad2ad",
+        "#cce156",
+        "#fff100",
+        "#ffbe00",
+        "#ff8c00",
+        "#FC3D23",
+        "#D01616",
+        "#8A0000",
       ],
       data: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
       needleValue: 7,
@@ -314,13 +267,17 @@ const gaugeData = ref({
 const gaugeOptions = ref({
   rotation: 270,
   circumference: 180,
-  cutout: "55%",
+  cutout: "50%",
   responsive: true,
   maintainAspectRatio: false,
   plugins: {
     tooltip: { enabled: false },
     legend: { display: false },
-    datalabels: { display: false },
+    datalabels: {
+      color: "black",
+      font: { size: 14, weight: "bold" },
+      formatter: (_value, ctx) => ctx.dataIndex,
+    },
   },
 });
 
@@ -355,51 +312,26 @@ const gaugeNeedlePlugin = {
   },
 };
 
-// ── Matrículas por Período ────────────────────────────────────────────────────
-const matriculasData = ref({
+const matriculasChart = {
   labels: ["JAN", "FEV", "MAR", "ABR", "MAI", "JUN"],
   datasets: [
     {
       label: "Matrículas",
       data: [120, 200, 150, 80, 70, 110],
-      backgroundColor: ["#2196F3", "#F44336", "#9C27B0", "#FFC107", "#212121", "#009688"],
-      borderColor: ["#2196F3", "#F44336", "#9C27B0", "#FFC107", "#212121", "#009688"],
-      borderWidth: 1,
-      borderRadius: 2,
+      color: ["#3B82F6", "#EF4444", "#A855F7", "#374151", "#EAB308", "#06B6D4"],
     },
   ],
-});
-
-const matriculasOptions = ref({
-  responsive: true,
-  maintainAspectRatio: false,
-  plugins: {
-    legend: { display: false },
-    datalabels: {
-      anchor: "end",
-      align: "top",
-      color: "#666",
-      font: { size: 11, weight: "bold" },
-      formatter: (v) => v,
-    },
-  },
-  scales: {
-    x: { ticks: { font: { size: 11 } }, grid: { display: false } },
-    y: { display: false, suggestedMin: 0, suggestedMax: 230 },
-  },
-});
+};
 
-// ── Aniversariantes do Mês ────────────────────────────────────────────────────
 const aniversariantes = ref([
-  { nome: "Heloisa Faria", color: "orange" },
-  { nome: "Juliana Costa", color: "green" },
-  { nome: "Juliana Costa", color: "green" },
-  { nome: "Fernando Almeida", color: "blue" },
-  { nome: "Lucas Pereira", color: "purple" },
-  { nome: "Sofia Martins", color: "teal" },
+  { day: 10, name: "Heloisa Faria" },
+  { day: 11, name: "Juliana Costa" },
+  { day: 16, name: "Juliana Costa" },
+  { day: 23, name: "Fernando Almeida" },
+  { day: 29, name: "Lucas Pereira" },
+  { day: 34, name: "Sofia Martins" },
 ]);
 
-// ── Feriados do mês ───────────────────────────────────────────────────────────
 const feriados = ref([
   { dia: 17, nome: "Carnaval", color: "amber" },
   { dia: 17, nome: "Carnaval", color: "amber" },
@@ -408,6 +340,26 @@ const feriados = ref([
 </script>
 
 <style scoped>
+.stat-cards-row {
+  display: flex;
+  flex-wrap: nowrap;
+  gap: 16px;
+}
+
+.stat-cards-row > * {
+  flex: 1 1 0;
+  min-width: 0;
+}
+
+@media (max-width: 599px) {
+  .stat-cards-row {
+    flex-wrap: wrap;
+  }
+  .stat-cards-row > * {
+    flex: 1 1 calc(50% - 8px);
+  }
+}
+
 .gauge-label {
   position: absolute;
   bottom: 28%;