ソースを参照

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

Gabriel Alves 1 ヶ月 前
コミット
bd92ff5c22

+ 2 - 6
index.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <html>
   <head>
-    <title><%= productName %></title>
+    <title>Ginastica do Cerebro - Franqueadora</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/favicon.ico


BIN
public/logo.png


+ 37 - 12
src/components/charts/AniversariantesCard.vue

@@ -1,21 +1,33 @@
 <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-medium">Aniversariantes do Mês</span>
-      <q-btn flat round dense icon="mdi-book-open-outline" color="grey-6" size="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="text-caption text-grey-6 q-mb-xs">Nome</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 />
 
-    <q-list>
+    <q-list dense>
       <template v-for="(person, index) in people" :key="index">
-        <q-item class="q-px-none q-py-md">
-          <q-item-section avatar>
+        <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>{{ person.name }}</q-item-label>
+            <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" />
@@ -35,19 +47,32 @@ defineProps({
 
 <style scoped>
 .dashboard-chart-card {
-  border-radius: 8px;
-  padding: 16px;
+  border-radius: 12px;
+  padding: 20px 24px;
   display: flex;
   flex-direction: column;
+  max-height: 370px;
+  overflow: hidden;
+}
+
+.header-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.person-item {
+  padding-top: 6px;
+  padding-bottom: 6px;
 }
 
 .day-badge {
-  width: 32px;
-  height: 32px;
+  width: 30px;
+  height: 30px;
   border-radius: 50%;
   background-color: #f97316;
   color: #fff;
-  font-size: 13px;
+  font-size: 12px;
   font-weight: 600;
   display: flex;
   align-items: center;

+ 8 - 13
src/components/charts/DashboardChartCard.vue

@@ -1,16 +1,8 @@
 <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-medium">{{ title }}</span>
-      <q-btn
-        flat
-        round
-        dense
-        icon="mdi-book-open-outline"
-        color="grey-6"
-        size="sm"
-        @click="$emit('export')"
-      />
+      <span class="text-subtitle2 text-weight-regular">{{ title }}</span>
+      <q-icon :name="icon" color="dark" size="sm" />
     </div>
     <div class="chart-slot-wrapper">
       <slot />
@@ -19,8 +11,9 @@
 </template>
 
 <script setup>
-defineProps({
+const { title, icon } = defineProps({
   title: { type: String, required: true },
+  icon: { type: String, default: "mdi-book-open-blank-variant-outline" },
 });
 
 defineEmits(["export"]);
@@ -28,15 +21,17 @@ defineEmits(["export"]);
 
 <style scoped>
 .dashboard-chart-card {
-  border-radius: 8px;
-  padding: 16px;
+  border-radius: 12px;
+  padding: 20px 28px 24px 28px;
   display: flex;
   flex-direction: column;
+  max-height: 370px;
 }
 
 .chart-slot-wrapper {
   flex: 1;
   min-height: 220px;
+  max-height: 300px;
   position: relative;
 }
 </style>

+ 37 - 17
src/components/charts/DashboardStatCard.vue

@@ -1,22 +1,23 @@
 <template>
-  <q-card class="stat-card q-pa-lg">
-    <div class="column full-width" style="gap: 8px">
-      <div class="flex justify-between items-start no-wrap">
-        <span class="text-body2 text-grey-6">{{ title }}</span>
-        <q-icon :name="icon" size="22px" color="grey-5" />
-      </div>
+  <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>
 
-      <span class="text-h4 text-primary" style="font-weight: 600; line-height: 1.2">
-        {{ value }}
-      </span>
+    <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 class="text-caption text-grey-6">{{ subtitle }}</span>
+      <span v-else-if="subtitle" class="text-caption text-foreground">{{
+        subtitle
+      }}</span>
     </div>
   </q-card>
 </template>
@@ -28,21 +29,40 @@ defineProps({
   value: { type: [String, Number], required: true },
   subtitle: { type: String, default: "" },
   badge: { type: String, default: "" },
-  badgeColor: { type: String, default: "secondary" },
+  badgeColor: { type: String, default: "accent-1" },
+  customStyle: { type: String, default: "padding: 4px" },
 });
 </script>
 
-<style scoped>
+<style scoped lang="scss">
 .stat-card {
-  flex: 1 1 200px;
-  border-radius: 8px;
+  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: 0.75rem;
-  padding: 4px 8px;
-  border-radius: 4px;
+  font-size: 12px;
+
+  border-radius: 8px;
   width: fit-content;
+  color: $primary;
 }
 </style>

+ 39 - 11
src/components/charts/normal/GroupedBarChart.vue

@@ -6,7 +6,7 @@
         ref="chart_ref"
         :options="chartOptions"
         :data="chartData"
-        :plugins="[ChartDataLabels]"
+        :plugins="activePlugins"
       />
     </div>
     <div v-else class="no-data-container">
@@ -30,22 +30,22 @@ import {
 import ChartDataLabels from "chartjs-plugin-datalabels";
 import { base64ToJPEG } from "src/helpers/convertBase64Image";
 
-ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale);
+ChartJS.register(
+  Title,
+  Tooltip,
+  Legend,
+  BarElement,
+  CategoryScale,
+  LinearScale,
+);
 
 const chart_ref = ref(null);
 
 const props = defineProps({
-  /**
-   * Array de labels do eixo X
-   */
   labels: {
     type: Array,
     default: () => [],
   },
-  /**
-   * Array de datasets no formato:
-   * [{ label: String, data: Number[], color: String }]
-   */
   datasets: {
     type: Array,
     default: () => [],
@@ -58,12 +58,10 @@ const props = defineProps({
     type: String,
     default: "",
   },
-  /** Raio das bordas das barras. Use valores altos (ex: 50) para efeito "pill". */
   barRadius: {
     type: Number,
     default: 4,
   },
-  /** Exibe datalabels em cima de cada barra */
   showDatalabels: {
     type: Boolean,
     default: false,
@@ -76,6 +74,21 @@ const props = defineProps({
     type: Function,
     default: null,
   },
+  /** Espessura máxima de cada barra em px */
+  maxBarThickness: {
+    type: Number,
+    default: null,
+  },
+  /** Percentual da categoria ocupado pelas barras (0-1). Maior = barras mais juntas. */
+  categoryPercentage: {
+    type: Number,
+    default: 0.8,
+  },
+  /** Percentual do espaço da categoria ocupado pela barra (0-1). */
+  barPercentage: {
+    type: Number,
+    default: 0.9,
+  },
 });
 
 const onResize = () => {
@@ -88,6 +101,8 @@ 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) => ({
@@ -98,6 +113,11 @@ const chartData = computed(() => ({
     borderRadius: props.barRadius,
     borderSkipped: false,
     borderWidth: 0,
+    ...(props.maxBarThickness != null
+      ? { maxBarThickness: props.maxBarThickness }
+      : {}),
+    categoryPercentage: props.categoryPercentage,
+    barPercentage: props.barPercentage,
   })),
 }));
 
@@ -107,6 +127,14 @@ 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,

+ 2 - 2
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: -32px; z-index: 1"
         >
           <div @click="miniState = !miniState">
             <q-icon
@@ -41,7 +41,7 @@
                   ? 'mdi-page-layout-sidebar-right'
                   : 'mdi-page-layout-sidebar-left'
               "
-              color="dark"
+              color="secondary"
             />
             <q-tooltip
               anchor="center right"

+ 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 {

+ 47 - 27
src/pages/dashboard/DashboardPage.vue

@@ -11,7 +11,6 @@
             label="Unidade"
             style="width: 250px; flex-shrink: 0"
             color="secondary"
-            label-color="secondary"
             hide-dropdown-icon
           >
             <template #append>
@@ -164,7 +163,7 @@
       <div class="stat-cards-row">
         <DashboardStatCard
           title="Total alunos (contratos ativos)"
-          icon="mdi-account-multiple"
+          icon="mdi-account-multiple-outline"
           value="4.527"
           badge="3.200 ativos"
         />
@@ -206,6 +205,9 @@
             :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>
@@ -219,7 +221,8 @@
           icon="mdi-account-multiple-outline"
           value="87%"
           badge="Alta"
-          badge-color="positive"
+          badge-color="approved"
+          custom-style="padding: 6px 24px"
         />
         <DashboardStatCard
           title="Estoque Geral de Produtos"
@@ -275,16 +278,15 @@ const periodOptions = [
   { label: "Personalizado", value: "custom" },
 ];
 
-// --- Aniversariantes do Mês (hardcoded) ---
 const aniversariantes = [
   { day: 10, name: "Heloisa Faria" },
-  { day: 7, name: "Juliana Costa" },
-  { day: 24, name: "Fernando Almeida" },
-  { day: 28, name: "Patrícia Lima" },
+  { 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" },
 ];
-// -------------------------------------------
 
-// --- Matrículas por Período (hardcoded) ---
 const matriculasChart = {
   labels: ["JAN", "FEV", "MAR", "ABR", "MAI", "JUN"],
   datasets: [
@@ -295,21 +297,32 @@ const matriculasChart = {
     },
   ],
 };
-// ---------------------------------------------------
 
-// --- Faturamento Serviço / Materiais (hardcoded) ---
 const faturamentoChart = {
-  labels: ["17/02", "20/02", "23/02", "26/02"],
+  labels: [
+    "17/02",
+    "18/02",
+    "19/02",
+    "20/02",
+    "21/02",
+    "22/02",
+    "23/02",
+    "24/02",
+    "25/02",
+    "26/02",
+  ],
   datasets: [
     {
       label: "Serviço",
-      data: [18500, 22300, 15800, 27600],
-      color: "#7C3AED",
+      data: [
+        18500, 21000, 16400, 22300, 19800, 17200, 15800, 24100, 20500, 27600,
+      ],
+      color: "#a274f1",
     },
     {
       label: "Materiais",
-      data: [9200, 11400, 8700, 13100],
-      color: "#EC4899",
+      data: [9200, 10500, 8100, 11400, 9800, 8400, 8700, 12200, 10100, 13100],
+      color: "#ff9999",
     },
   ],
 };
@@ -323,7 +336,6 @@ const formatCurrencyTooltip = (context) => {
   const value = context.parsed.y;
   return ` ${context.dataset.label}: R$ ${value.toLocaleString("pt-BR", { minimumFractionDigits: 2 })}`;
 };
-// ---------------------------------------------------
 
 const ordersChart = ref({});
 const participantsChart = ref({});
@@ -500,18 +512,21 @@ onMounted(async () => {
 
 .stat-cards-row {
   display: flex;
-  flex-wrap: wrap;
+  flex-wrap: nowrap;
   gap: 16px;
 }
 
 .stat-cards-row > * {
-  flex: 1 1 200px;
-  min-width: 180px;
+  flex: 1 1 0;
+  min-width: 0;
 }
 
 @media (max-width: 599px) {
+  .stat-cards-row {
+    flex-wrap: wrap;
+  }
   .stat-cards-row > * {
-    flex: 1 1 100%;
+    flex: 1 1 calc(50% - 8px);
   }
 }
 
@@ -521,14 +536,19 @@ onMounted(async () => {
   gap: 16px;
 }
 
-.charts-row > * {
-  flex: 1 1 350px;
-  min-width: 280px;
+.charts-row > *:nth-child(1) {
+  flex: 0 0 calc(41.6667% - 11px);
+  min-width: 0;
+}
+
+.charts-row > *:nth-child(2) {
+  flex: 0 0 calc(33.3333% - 11px);
+  min-width: 0;
 }
 
-.charts-row > *:last-child {
-  flex: 0 1 240px;
-  min-width: 200px;
+.charts-row > *:nth-child(3) {
+  flex: 0 0 calc(25% - 11px);
+  min-width: 0;
 }
 
 @media (max-width: 599px) {

+ 1 - 1
src/pages/login/LoginPage.vue

@@ -108,7 +108,7 @@ const {
   validationErrors,
   execute: submitForm,
 } = useSubmitHandler({
-  onSuccess: () => router.push({ name: "HomePage" }),
+  onSuccess: () => router.push({ name: "DashboardPage" }),
   formRef: formRef,
 });