Browse Source

feat: dashboard estatico

ebagabee 1 tháng trước cách đây
mục cha
commit
da4e5ac955
1 tập tin đã thay đổi với 389 bổ sung153 xóa
  1. 389 153
      src/pages/dashboard/DashboardPage.vue

+ 389 - 153
src/pages/dashboard/DashboardPage.vue

@@ -1,183 +1,419 @@
 <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>
+
+        <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>
+
+        <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>
+
+      <!-- Row 2: Charts -->
+      <div class="row q-col-gutter-md q-mb-md">
+        <!-- Faturamento Serviço / Materiais -->
+        <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>
+        </div>
+
+        <!-- Contratos Ativos -->
+        <div class="col-12 col-md-4">
+          <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">Contratos Ativos</span>
+              <q-icon name="mdi-trending-up" color="grey-5" />
+            </q-card-section>
+            <q-separator />
+            <q-card-section
+              class="flex flex-center q-pt-sm"
+              style="height: calc(100% - 57px); position: relative"
+            >
+              <div style="height: 100%; max-width: 280px; width: 100%">
+                <Doughnut
+                  :data="gaugeData"
+                  :options="gaugeOptions"
+                  :plugins="[gaugeNeedlePlugin]"
+                />
+              </div>
+              <div class="gauge-label">
+                <div class="text-h5 text-bold">70</div>
+                <div class="text-caption text-grey-6">Grade</div>
+              </div>
+            </q-card-section>
+          </q-card>
+        </div>
+
+        <!-- Atalhos rápidos -->
+        <div class="col-12 col-md-3">
+          <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">Atalhos rápidos</span>
+              <q-icon name="mdi-apps" color="grey-5" />
+            </q-card-section>
+            <q-separator />
+            <q-card-section class="q-pt-md column q-gutter-sm">
+              <q-btn
+                unelevated
+                color="primary"
+                label="Criar contrato"
+                no-caps
+                class="full-width"
+              />
+              <q-btn
+                unelevated
+                color="primary"
+                label="Registrar presença"
+                no-caps
+                class="full-width"
+              />
+              <q-btn
+                unelevated
+                color="primary"
+                label="Novo pedido"
+                no-caps
+                class="full-width"
+              />
+            </q-card-section>
+          </q-card>
+        </div>
+      </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>
+
+        <!-- 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>
+        </div>
+
+        <!-- Feriados 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">Feriados do mês</span>
+              <q-icon name="mdi-calendar-outline" color="grey-5" />
+            </q-card-section>
+            <q-separator />
+            <q-card-section class="q-pt-md">
+              <q-btn
+                unelevated
+                color="primary"
+                label="Nova data"
+                no-caps
+                class="full-width q-mb-md"
+              />
+              <div class="row q-gutter-sm">
+                <div
+                  v-for="(feriado, i) in feriados"
+                  :key="i"
+                  class="column items-center"
+                  style="min-width: 52px"
+                >
+                  <q-badge
+                    :color="feriado.color"
+                    class="text-subtitle1 text-bold q-pa-sm"
+                    style="min-width: 40px; justify-content: center"
+                  >
+                    {{ feriado.dia }}
+                  </q-badge>
+                  <div class="text-caption q-mt-xs text-center">{{ feriado.nome }}</div>
+                </div>
+              </div>
+            </q-card-section>
+          </q-card>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
 <script setup>
-import { onMounted, ref, watch } from "vue";
-import { useI18n } from "vue-i18n";
+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 DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 
-const { t } = useI18n();
-
-const isLoading = ref(true);
-const defaultPeriod = ref("month");
-const defaultEventId = ref(1);
-
-const ordersChart = ref({});
-const participantsChart = ref({});
-const paymentsChart = ref({});
-const ticketsSoldChart = ref({});
-const eventTicketsByTypeChart = ref({});
-const eventParticipantsByCNPJAndCPF = ref({});
-const salesOverTimeLineChart = ref({});
-const eventSourcePieChart = ref({});
-
-const generateMockData = () => {
-  const createMiniChartData = (currentTotal, percentage) => ({
-    current_total: currentTotal,
-    percentage_change: percentage,
-    trend_data: Array.from({ length: 10 }, () =>
-      Math.floor(Math.random() * 100),
-    ),
-  });
-
-  const barChartDataRaw = [
-    {
-      label: t("dashboard.charts.tickets_by_type.labels.vip"),
-      value: Math.floor(Math.random() * 300),
-    },
-    {
-      label: t("dashboard.charts.tickets_by_type.labels.track"),
-      value: Math.floor(Math.random() * 800),
-    },
-    {
-      label: t("dashboard.charts.tickets_by_type.labels.box"),
-      value: Math.floor(Math.random() * 400),
-    },
-    {
-      label: t("dashboard.charts.tickets_by_type.labels.courtesy"),
-      value: Math.floor(Math.random() * 50),
-    },
-  ];
+ChartJS.register(
+  Title,
+  Tooltip,
+  Legend,
+  BarElement,
+  CategoryScale,
+  LinearScale,
+  ArcElement,
+);
 
-  const doughnutDataRaw = [
+// ── 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"],
+  datasets: [
     {
-      label: t("common.terms.cpf"),
-      value: Math.floor(Math.random() * 900 + 100),
+      label: "Serviços",
+      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,
     },
     {
-      label: t("common.terms.cnpj"),
-      value: Math.floor(Math.random() * 100 + 10),
+      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,
     },
-  ];
-  const doughnutTotal = doughnutDataRaw.reduce(
-    (sum, item) => sum + item.value,
-    0,
-  );
+  ],
+});
 
-  const lineChartDataRaw = [
-    {
-      label: t("common.months.january"),
-      value: Math.floor(1200 + Math.random() * 500),
-    },
-    {
-      label: t("common.months.february"),
-      value: Math.floor(1900 + Math.random() * 500),
-    },
-    {
-      label: t("common.months.march"),
-      value: Math.floor(3000 + Math.random() * 500),
-    },
-    {
-      label: t("common.months.april"),
-      value: Math.floor(5000 + Math.random() * 500),
-    },
-    {
-      label: t("common.months.may"),
-      value: Math.floor(2300 + Math.random() * 500),
-    },
-    {
-      label: t("common.months.june"),
-      value: Math.floor(3200 + Math.random() * 500),
-    },
-  ];
+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 pieDataRaw = [
+// ── Contratos Ativos (gauge) ──────────────────────────────────────────────────
+const gaugeData = ref({
+  datasets: [
     {
-      label: t("dashboard.charts.registration_source.sources.instagram"),
-      value: Math.floor(450 + Math.random() * 50),
-    },
-    {
-      label: t("dashboard.charts.registration_source.sources.facebook"),
-      value: Math.floor(250 + Math.random() * 50),
-    },
-    {
-      label: t("dashboard.charts.registration_source.sources.google"),
-      value: Math.floor(180 + Math.random() * 50),
-    },
-    {
-      label: t("dashboard.charts.registration_source.sources.referral"),
-      value: Math.floor(120 + Math.random() * 50),
-    },
-  ];
-  const pieTotal = pieDataRaw.reduce((sum, item) => sum + item.value, 0);
-
-  return {
-    payments: createMiniChartData(
-      (Math.random() * 20000 + 5000).toFixed(2),
-      (Math.random() * 20 - 5).toFixed(2),
-    ),
-    orders: createMiniChartData(
-      Math.floor(Math.random() * 500 + 50),
-      (Math.random() * 15 - 5).toFixed(2),
-    ),
-    tickets_sold: createMiniChartData(
-      Math.floor(Math.random() * 1500 + 200),
-      (Math.random() * 25 - 5).toFixed(2),
-    ),
-    participants: createMiniChartData(
-      Math.floor(Math.random() * 1000 + 100),
-      (Math.random() * 10 - 5).toFixed(2),
-    ),
-    barData: {
-      chart_data: barChartDataRaw,
+      backgroundColor: [
+        "#00a550", "#4dbb7e", "#9ad2ad", "#cce156",
+        "#fff100", "#ffbe00", "#ff8c00", "#FC3D23", "#D01616", "#8A0000",
+      ],
+      data: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
+      needleValue: 7,
+      borderColor: "transparent",
     },
-    doughnutData: {
-      chart_data: doughnutDataRaw,
-      current_total: doughnutTotal,
-    },
-    lineData: {
-      chart_data: lineChartDataRaw,
-    },
-    pieData: {
-      chart_data: pieDataRaw,
-      current_total: pieTotal,
-    },
-  };
-};
-
-const updateDashboardData = async () => {
-  isLoading.value = true;
-  setTimeout(() => {
-    const mockData = generateMockData();
-
-    ordersChart.value = mockData.orders;
-    participantsChart.value = mockData.participants;
-    paymentsChart.value = mockData.payments;
-    ticketsSoldChart.value = mockData.tickets_sold;
+  ],
+});
 
-    eventTicketsByTypeChart.value = mockData.barData;
-    eventParticipantsByCNPJAndCPF.value = mockData.doughnutData;
-    salesOverTimeLineChart.value = mockData.lineData;
-    eventSourcePieChart.value = mockData.pieData;
+const gaugeOptions = ref({
+  rotation: 270,
+  circumference: 180,
+  cutout: "55%",
+  responsive: true,
+  maintainAspectRatio: false,
+  plugins: {
+    tooltip: { enabled: false },
+    legend: { display: false },
+    datalabels: { display: false },
+  },
+});
 
-    isLoading.value = false;
-  }, 500);
+const gaugeNeedlePlugin = {
+  id: "gaugeNeedle",
+  afterDatasetsDraw(chart) {
+    const { ctx, data } = chart;
+    ctx.save();
+    const needleValue = data.datasets[0].needleValue;
+    const meta = chart.getDatasetMeta(0).data[0];
+    const xCenter = meta.x;
+    const yCenter = meta.y;
+    const outerRadius = meta.outerRadius - 20;
+    const circumference =
+      (meta.circumference / Math.PI / data.datasets[0].data[0]) * needleValue;
+    const angle = Math.PI;
+    ctx.translate(xCenter, yCenter);
+    ctx.rotate(angle * (circumference + 1.5));
+    ctx.beginPath();
+    ctx.strokeStyle = "grey";
+    ctx.fillStyle = "grey";
+    ctx.moveTo(-3, 0);
+    ctx.lineTo(0, -outerRadius);
+    ctx.lineTo(3, 0);
+    ctx.stroke();
+    ctx.fill();
+    ctx.beginPath();
+    ctx.arc(0, 0, 6, 0, 2 * Math.PI);
+    ctx.fillStyle = "grey";
+    ctx.fill();
+    ctx.restore();
+  },
 };
 
-watch([defaultPeriod, defaultEventId], async () => {
-  await updateDashboardData();
+// ── Matrículas por Período ────────────────────────────────────────────────────
+const matriculasData = ref({
+  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,
+    },
+  ],
 });
 
-onMounted(async () => {
-  await updateDashboardData();
+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" },
+]);
+
+// ── Feriados do mês ───────────────────────────────────────────────────────────
+const feriados = ref([
+  { dia: 17, nome: "Carnaval", color: "amber" },
+  { dia: 17, nome: "Carnaval", color: "amber" },
+  { dia: 17, nome: "Carnaval", color: "amber" },
+]);
 </script>
 
 <style scoped>
-.gap {
-  gap: 16px;
+.gauge-label {
+  position: absolute;
+  bottom: 28%;
+  left: 50%;
+  transform: translateX(-50%);
+  text-align: center;
+  pointer-events: none;
 }
 </style>