Przeglądaj źródła

feat: adiciona grafico de faturamento novamente

ebagabee 4 tygodni temu
rodzic
commit
8780d768fd

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

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

+ 29 - 49
src/pages/dashboard/DashboardPage.vue

@@ -59,21 +59,16 @@
 
       <div class="row q-col-gutter-md q-mb-md">
         <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">
+            <GroupedBarChart
+              :labels="faturamentoChart.labels"
+              :datasets="faturamentoChart.datasets"
+              label-y="R$"
+              :tick-formatter="formatCurrencyTick"
+              :tooltip-formatter="formatCurrencyTooltip"
+              class="full-width full-height"
+            />
+          </DashboardChartCard>
         </div>
 
         <div class="col-12 col-md-4">
@@ -285,6 +280,8 @@ import {
 import ChartDataLabels from "chartjs-plugin-datalabels";
 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";
 
 ChartJS.register(
   Title,
@@ -296,52 +293,35 @@ ChartJS.register(
   ArcElement,
 );
 
-const faturamentoData = ref({
+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",
+    "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 })}`;
+};
 
 const gaugeData = ref({
   datasets: [