فهرست منبع

feat: :sparkles: miniCharts on the CardIconChart

Denis 1 سال پیش
والد
کامیت
9402c57799

+ 30 - 0
package-lock.json

@@ -10,10 +10,12 @@
       "dependencies": {
         "@quasar/extras": "^1.16.15",
         "axios": "^1.7.9",
+        "chart.js": "^4.4.7",
         "date-fns": "^3.6.0",
         "pinia": "^2.3.0",
         "quasar": "^2.17.4",
         "vue": "^3.5",
+        "vue-chartjs": "^5.3.2",
         "vue-i18n": "^9.14.2",
         "vue-router": "^4.5.0"
       },
@@ -1046,6 +1048,12 @@
         "@jridgewell/sourcemap-codec": "^1.4.14"
       }
     },
+    "node_modules/@kurkle/color": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+      "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+      "license": "MIT"
+    },
     "node_modules/@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2389,6 +2397,18 @@
       "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
       "dev": true
     },
+    "node_modules/chart.js": {
+      "version": "4.4.7",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz",
+      "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==",
+      "license": "MIT",
+      "dependencies": {
+        "@kurkle/color": "^0.3.0"
+      },
+      "engines": {
+        "pnpm": ">=8"
+      }
+    },
     "node_modules/chokidar": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -7099,6 +7119,16 @@
         }
       }
     },
+    "node_modules/vue-chartjs": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.2.tgz",
+      "integrity": "sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "chart.js": "^4.1.1",
+        "vue": "^3.0.0-0 || ^2.7.0"
+      }
+    },
     "node_modules/vue-demi": {
       "version": "0.14.10",
       "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",

+ 2 - 0
package.json

@@ -16,10 +16,12 @@
   "dependencies": {
     "@quasar/extras": "^1.16.15",
     "axios": "^1.7.9",
+    "chart.js": "^4.4.7",
     "date-fns": "^3.6.0",
     "pinia": "^2.3.0",
     "quasar": "^2.17.4",
     "vue": "^3.5",
+    "vue-chartjs": "^5.3.2",
     "vue-i18n": "^9.14.2",
     "vue-router": "^4.5.0"
   },

+ 6 - 5
src/boot/defaultPropsComponents.js

@@ -1,4 +1,4 @@
-import { QDialog, QInput, QSelect, QBtn, QScrollArea } from "quasar";
+import { QDialog, QInput, QSelect, QBtn, QScrollArea, QCard } from "quasar";
 import { boot } from "quasar/wrappers";
 
 /**
@@ -20,17 +20,18 @@ export default boot(() => {
     transitionHide: "slide-down",
   });
   SetComponentDefaults(QInput, {
-    filled: false,
+    filled: true,
   });
   SetComponentDefaults(QSelect, {
-    filled: false,
-    outlined: true,
-    dense: true,
+    filled: true,
   });
   SetComponentDefaults(QBtn, {
     outline: true,
     padding: "10px 16px"
   });
+  SetComponentDefaults(QCard, {
+    flat: true,
+  });
   SetComponentDefaults(QScrollArea, {
     thumbStyle: {
       borderRadius: "4px",

+ 97 - 0
src/components/charts/CardIconChart.vue

@@ -0,0 +1,97 @@
+<template>
+  <q-card flat class="q-pa-lg">
+    <div class="column no-wrap full-width">
+      <div class="flex items-center no-wrap">
+        <div class="round background q-mr-sm">
+          <q-icon
+            class="q-pa-sm"
+            :name="props.icon"
+            size="24px"
+            :color="props.color"
+          />
+        </div>
+        <span class="text-h5">{{ props.title }}</span>
+      </div>
+      <div class="flex no-wrap full-width justify-between q-pa-sm">
+        <div class="column flex-center">
+          <span class="text-h3">{{ props.numberCard }}</span>
+          <div
+            class="flex no-wrap text-subtitle2"
+            :class="props.numberPorcent > 0 ? 'text-positive' : 'text-negative'"
+          >
+            <q-icon
+              :name="
+                props.numberPorcent > 0 ? 'mdi-arrow-up' : 'mdi-arrow-down'
+              "
+              size="18px"
+              class="q-mr-xs"
+            />
+            {{ props.numberPorcent + "%" }}
+          </div>
+        </div>
+        <div class="flex justify-end" style="max-width: 120px; height: 80px;">
+          <slot name="chart">
+            <MiniLineChart
+              :data="chartData"
+              line-color="#1976D2"
+              fill-color="rgba(0, 0, 0, 0)"
+            />
+
+            <MiniBarChart
+              :data="chartData"
+              bar-color="#1976D2"
+            />
+          </slot>
+        </div>
+      </div>
+    </div>
+  </q-card>
+</template>
+
+<script setup>
+// import MiniLineChart from "./mini/MiniLineChart.vue";
+import MiniBarChart from "./mini/MiniBarChart.vue";
+const props = defineProps({
+  color: {
+    type: String,
+    default: "primary",
+  },
+  title: {
+    type: String,
+    default: "Usuários",
+  },
+  icon: {
+    type: String,
+    default: "mdi-account",
+  },
+  chartData: {
+    type: Array,
+    default: () =>
+      Array.from({ length: 7 }, () => Math.floor(Math.random() * 100)),
+  },
+  numberCard: {
+    type: Number,
+    default: () => Math.floor(Math.random() * 100),
+  },
+  numberPorcent: {
+    type: Number,
+    default: () => Math.ceil(Math.random() * 200 - 100),
+  },
+});
+</script>
+<style lang="scss" scoped>
+@use "sass:map";
+@use "src/css/quasar.variables.scss";
+
+body.body--light {
+  .background {
+    background: rgba(map.get($colors, "primary"), 0.2) !important;
+  }
+}
+
+body.body--dark {
+  .background {
+    background: rgba(map.get($colors-dark, "primary"), 0.2) !important;
+  }
+}
+</style>

+ 113 - 0
src/components/charts/mini/MiniBarChart.vue

@@ -0,0 +1,113 @@
+<template>
+  <Bar
+    :id="props.id"
+    ref="chart_ref"
+    :options="chartOptions"
+    :data="computedChartData"
+  />
+</template>
+
+<script setup>
+import {
+  Chart as ChartJS,
+  CategoryScale,
+  LinearScale,
+  BarElement,
+  Tooltip,
+} from "chart.js";
+import { computed, useTemplateRef } from "vue";
+import { Bar } from "vue-chartjs";
+
+// Register only necessary components
+ChartJS.register(
+  CategoryScale,
+  LinearScale,
+  BarElement,
+  Tooltip,
+);
+
+const chart_ref = useTemplateRef(null);
+
+// Simplified props focusing on essential functionality
+const props = defineProps({
+  // Core data props
+  data: {
+    type: Array,
+    required: true,
+  },
+
+  // Essential styling
+  barColor: {
+    type: String,
+    default: "#1976D2",
+  },
+
+  // Optional configurations for flexibility
+  horizontal: {
+    type: Boolean,
+    default: false,
+  },
+  showTooltip: {
+    type: Boolean,
+    default: true,
+  }
+});
+
+// Optimized chart options for mini-charts
+const chartOptions = computed(() => ({
+  responsive: true,
+  maintainAspectRatio: false,
+  indexAxis: props.horizontal ? "y" : "x",
+
+  plugins: {
+    legend: {
+      display: false, // Always hide legend in mini charts
+    },
+    tooltip: {
+      enabled: props.showTooltip,
+      displayColors: false,
+      callbacks: {
+        label: (context) => `${context.raw}`
+      }
+    },
+  },
+
+  scales: {
+    x: {
+      display: false,
+      grid: {
+        display: false,
+      }
+    },
+    y: {
+      display: false,
+      grid: {
+        display: false,
+      },
+      beginAtZero: true,
+    },
+  },
+
+  animation: {
+    duration: 750,
+    easing: 'easeOutQuad',
+  },
+}));
+
+// Simplified data computation
+const computedChartData = computed(() => ({
+  labels: Array(props.data.length).fill(''),
+  datasets: [{
+    data: props.data,
+    backgroundColor: props.barColor,
+    borderRadius: 2,
+    barThickness: 8,
+    maxBarThickness: 10,
+  }]
+}));
+
+// Expose essential methods
+defineExpose({
+  chart_ref,
+});
+</script>

+ 123 - 0
src/components/charts/mini/MiniLineChart.vue

@@ -0,0 +1,123 @@
+<template>
+  <Line
+    ref="chart_ref"
+    :options="chartOptions"
+    :data="computedChartData"
+  />
+</template>
+
+<script setup>
+import {
+  Chart as ChartJS,
+  CategoryScale,
+  LinearScale,
+  LineElement,
+  PointElement,
+  Tooltip,
+  Filler
+} from "chart.js";
+import { computed, useTemplateRef } from "vue";
+import { Line } from "vue-chartjs";
+
+// Register only essential components
+ChartJS.register(
+  CategoryScale,
+  LinearScale,
+  LineElement,
+  PointElement,
+  Tooltip,
+  Filler
+);
+
+const chart_ref = useTemplateRef(null);
+
+const props = defineProps({
+  // Essential data props
+  data: {
+    type: Array,
+    required: true,
+  },
+
+  // Core styling
+  lineColor: {
+    type: String,
+    default: "#1976D2",
+  },
+  fillColor: {
+    type: String,
+    default: "rgba(25, 118, 210, 0.1)",
+  },
+
+  // Optional display features
+  showTooltip: {
+    type: Boolean,
+    default: true,
+  },
+  showPoints: {
+    type: Boolean,
+    default: false,
+  }
+});
+
+const chartOptions = computed(() => ({
+  responsive: true,
+  maintainAspectRatio: false,
+
+  plugins: {
+    legend: {
+      display: false, // Always hidden for mini charts
+    },
+    tooltip: {
+      enabled: props.showTooltip,
+      displayColors: false,
+      callbacks: {
+        label: (context) => `${context.raw}`
+      }
+    },
+  },
+
+  scales: {
+    x: {
+      display: false,
+      grid: { display: false }
+    },
+    y: {
+      display: false,
+      grid: { display: false },
+      beginAtZero: true
+    }
+  },
+
+  elements: {
+    line: {
+      tension: 0.4,
+      borderWidth: 2,
+    },
+    point: {
+      radius: props.showPoints ? 3 : 0,
+      hitRadius: 5,
+      borderWidth: 0,
+      backgroundColor: props.showPoints ? props.lineColor : "rgba(0,0,0,0)",
+    }
+  },
+
+  animation: {
+    duration: 750,
+    easing: 'easeOutQuad',
+  }
+}));
+
+const computedChartData = computed(() => ({
+  labels: Array(props.data.length).fill(''),
+  datasets: [{
+    data: props.data,
+    borderColor: props.lineColor,
+    backgroundColor: props.fillColor,
+    fill: true,
+  }]
+}));
+
+defineExpose({
+  chart_ref,
+});
+</script>

+ 0 - 0
src/components/geral/DefaultHeaderPage.vue → src/components/layout/DefaultHeaderPage.vue


+ 29 - 10
src/components/geral/LeftMenuLayout.vue → src/components/layout/LeftMenuLayout.vue

@@ -8,7 +8,7 @@
     :width="250"
     :mini-width="64"
     :breakpoint="500"
-    :mini="!$q.screen.lt.md ? miniState : true"
+    :mini="miniState"
     :behavior="'desktop'"
     class="detached-container"
   >
@@ -32,7 +32,11 @@
             anchor="center right"
             self="center left"
             :offset="[10, 10]"
-            >{{ miniState ? $t('navigation.expand_menu') : $t('navigation.collapse_menu') }}</q-tooltip
+            >{{
+              miniState
+                ? $t("navigation.expand_menu")
+                : $t("navigation.collapse_menu")
+            }}</q-tooltip
           >
         </q-btn>
       </div>
@@ -52,7 +56,7 @@
             <q-item-section>{{ user_store.user.name }}</q-item-section>
           </div>
           <q-tooltip
-            v-if="miniState && !$q.screen.lt.md"
+            v-if="miniState"
             anchor="center right"
             self="center left"
             :offset="[10, 10]"
@@ -114,7 +118,7 @@
             </q-item-section>
             <q-item-section>{{ $t(menu.title) }}</q-item-section>
             <q-tooltip
-              v-if="miniState && !$q.screen.lt.md"
+              v-if="miniState"
               anchor="center right"
               self="center left"
               :offset="[10, 10]"
@@ -125,7 +129,7 @@
           <div v-else>
             <template v-if="!miniState">
               <q-tooltip
-                v-if="miniState && !$q.screen.lt.md"
+                v-if="miniState"
                 anchor="center right"
                 self="center left"
                 :offset="[10, 10]"
@@ -160,7 +164,7 @@
                     </q-item-section>
                     <q-item-section>{{ $t(child.title) }}</q-item-section>
                     <q-tooltip
-                      v-if="miniState && !$q.screen.lt.md"
+                      v-if="miniState"
                       anchor="center right"
                       self="center left"
                       :offset="[10, 10]"
@@ -183,7 +187,7 @@
                 </q-item-section>
                 <q-item-section>{{ $t(menu.title) }}</q-item-section>
                 <q-tooltip
-                  v-if="miniState && !$q.screen.lt.md"
+                  v-if="miniState"
                   anchor="center right"
                   self="center left"
                   :offset="[10, 10]"
@@ -230,14 +234,13 @@
         </q-item>
       </q-list>
       <div class="full-width text-center text-subtitle3">
-
         <span class="text-caption text-weight-light">{{ version }}</span>
       </div>
     </div>
   </q-drawer>
 </template>
 <script setup>
-import { ref, onMounted, watch } from "vue";
+import { ref, onMounted, watch, watchEffect } from "vue";
 import { useAuth } from "src/composables/useAuth";
 import { permissionStore } from "src/stores/permission";
 import { useRouter, useRoute } from "vue-router";
@@ -246,7 +249,9 @@ import LogoSoftparLight from "src/assets/softpar_logo_light.svg";
 import LogoSoftparDark from "src/assets/softpar_logo_dark.svg";
 import LogoSoftparMini from "src/assets/softpar_logo_mini.svg";
 import { Cookies } from "quasar";
+import { useQuasar } from "quasar";
 
+const $q = useQuasar();
 const { logout } = useAuth();
 const router = useRouter();
 const route = useRoute();
@@ -274,7 +279,7 @@ const menus = ref([
   {
     type: "single",
     title: "navigation.dashboard",
-    name: "HomePage",
+    name: "DashboardPage",
     icon: "mdi-home-variant-outline",
     disable: false,
     permission: false,
@@ -322,6 +327,20 @@ const getMenuAccess = () => {
     .filter((menu) => menu !== null);
 };
 
+watchEffect(() => {
+  if ($q.screen.lt.md) {
+    miniState.value = true
+    if (Array.isArray(isExpasionItemExpanded.value)) {
+      isExpasionItemExpanded.value.forEach((expansion, index) => {
+        isExpasionItemExpanded.value[index] = false;
+      });
+    } else {
+      console.log("isExpasionItemExpanded", isExpasionItemExpanded.value);
+      isExpasionItemExpanded.value = false;
+    }
+  }
+});
+
 const logoutFn = async () => {
   await logout();
   router.push({ name: "LoginPage" });

+ 1 - 1
src/components/geral/LeftMenuLayoutMobile.vue → src/components/layout/LeftMenuLayoutMobile.vue

@@ -102,7 +102,7 @@ const menus = ref([
   {
     type: "single",
     title: "navigation.dashboard",
-    name: "HomePage",
+    name: "DashboardPage",
     icon: "mdi-home-variant-outline",
     disable: false,
     permission: false,

+ 31 - 3
src/css/app.scss

@@ -1,6 +1,20 @@
 @use "sass:map";
 @use "src/css/quasar.variables.scss";
 
+.flex-grow {
+  flex-grow: 1;
+  flex-shrink: 1;
+  flex-basis: 0;
+}
+
+.round {
+  border-radius: 50%;
+}
+
+.self-center {
+  align-self: center;
+}
+
 .input-disable {
   .q-field--outlined .q-field__control::before {
     border: 1px solid #b9b9b9 !important;
@@ -23,12 +37,26 @@
   transition: all;
 }
 
-.body--light {
+body.body--light {
   .q-drawer:has(.detached-container) {
-    background: #{map.get($colors, "dark")};
+    background: #{map.get($colors, "surface")} !important;
   }
 
   .q-menu {
-    background: #{map.get($colors, "dark")};
+    background: #{map.get($colors, "surface")};
   }
+
+  background: #{map.get($colors, "page")} !important;
+}
+
+body.body--dark {
+  .q-drawer:has(.detached-container) {
+    background: #{map.get($colors-dark, "surface")} !important;
+  }
+
+  .q-menu {
+    background: #{map.get($colors-dark, "surface")};
+  }
+
+  background: #{map.get($colors-dark, "page")};
 }

+ 33 - 33
src/css/quasar.variables.scss

@@ -8,7 +8,6 @@ $accent: #e91e63; // Material Pink 500
 
 // Dark Theme Base Colors
 $dark: #1d1d1d;
-$dark-page: #121212; // Material Dark Background
 
 // Status Colors
 $positive: #2e7d32; // Material Green 800
@@ -25,7 +24,6 @@ $colors: (
   // Light - Blue 400
   "primary-dark": #1565c0,
 
-  "dark": #f1f1f1,
   // Dark - Blue 800
   // Secondary Colors and Variants
   "secondary": #9c27b0,
@@ -34,16 +32,24 @@ $colors: (
   // Light - Purple 300
   "secondary-dark": #7b1fa2,
 
+  // Terceary Colors and Variants
+  "terciary": #ff9800,
+  // Base - Orange 500
+  "terciary-light": #ffb74d,
+  // Light - Orange 300
+  "terciary-dark": #f57c00,
+
   // Dark - Purple 700
   // Background Colors
-  "background": #ffffff,
-  "background-light": #000000,
-  "background-dark": #121212,
+  "page": #f1f1f1,
 
   // Surface Colors
   "surface": #ffffff,
-  "surface-light": #fafafa,
-  "surface-dark": #1e1e1e,
+  "surface-light": #f5f5f5,
+  "surface-dark": #f1f1f1,
+
+  //text color
+  "text": #000000,
 
   // Status Colors with Variants
   "success": #2e7d32,
@@ -71,30 +77,17 @@ $colors: (
   // Light Blue 700
   "info-light": #03a9f4,
   // Light Blue 500
-  "info-dark": #01579b,
-
-  // Light Blue 900
-  // Grey Scale
-  "grey-50": #fafafa,
-  "grey-100": #f5f5f5,
-  "grey-200": #eeeeee,
-  "grey-300": #e0e0e0,
-  "grey-400": #bdbdbd,
-  "grey-500": #9e9e9e,
-  "grey-600": #757575,
-  "grey-700": #616161,
-  "grey-800": #424242,
-  "grey-900": #212121
+  "info-dark": #01579b
 );
 
 // Dark Theme Color Overrides
 $colors-dark: (
-  // Primary Colors - Lighter in Dark Mode
-  "primary": #4488c0,
-  // Blue 200
-  "primary-light": #e3f2fd,
+  // Primary Colors and Variants
+  "primary": #1976d2,
+  // Base - Blue 700
+  "primary-light": #42a5f5,
   // Blue 50
-  "primary-dark": #42a5f5,
+  "primary-dark": #1565c0,
 
   "dark": #1d1d1d,
 
@@ -106,13 +99,20 @@ $colors-dark: (
   // Purple 50
   "secondary-dark": #ab47bc,
 
-  // Purple 400
-  // Background Colors - Adjusted for Dark Mode
-  "background": #121212,
-  // Dark Background
-  "background-light": #1d1d1d,
-  // Darker Background
-  "background-dark": #000000,
+  //Terceary Colors - Lighter in Dark Mode
+  "terciary": #ffd191,
+  // Yellow 200
+  "terciary-light": #ffecb3,
+  // Yellow 50
+  "terciary-dark": #ffab40,
+
+  "page": #121212,
+
+  "surface": #1d1d1d,
+  "surface-light": #333333,
+  "surface-dark": #121212,
+
+  "text": #ffffff,
 
   // Black Background
   // Status Colors - Adjusted for Dark Mode

+ 6 - 6
src/css/table.scss

@@ -5,15 +5,15 @@
   padding-right: 16px !important;
 
   .body--dark & {
-    --table-bg-color: #{map.get($colors, "background-dark")}; // Using our dark background
-    --table-border-color: #{map.get($colors, "grey-800")}; // Darker border
-    --table-header-color: #{map.get($colors, "page")}; // Light text for dark mode
+    --table-bg-color: #{map.get($colors-dark, "surface")}; // Using our dark background
+    --table-border-color: #{map.get($colors-dark, "surface-light")}; // Darker border
+    --table-header-color: #{map.get($colors-dark, "text")}; // Light text for dark mode
   }
 
   .body--light & {
-    --table-bg-color: #{map.get($colors, "dark")}; // Light background
-    --table-border-color: #{map.get($colors, "grey-200")}; // Border color
-    --table-header-color: #{map.get($colors, "background-3")}; // Dark text for light mode
+    --table-bg-color: #{map.get($colors, "surface")}; // Light background
+    --table-border-color: #{map.get($colors, "surface-light")}; // Border color
+    --table-header-color: #{map.get($colors, "text")}; // Dark text for light mode
   }
 
   :deep(.q-table) {

+ 59 - 0
src/helpers/convertBase64Image.js

@@ -0,0 +1,59 @@
+const base64ToJPEG = (base64String, fileName) => {
+  // Remova a parte inicial "data:image/jpeg;base64,"
+  const base64WithoutHeader = base64String.replace(
+    /^data:image\/jpeg;base64,/,
+    ""
+  );
+
+  // Converte a string base64 para um array de bytes
+  const byteCharacters = atob(base64WithoutHeader);
+  const byteNumbers = new Array(byteCharacters.length);
+  for (let i = 0; i < byteCharacters.length; i++) {
+    byteNumbers[i] = byteCharacters.charCodeAt(i);
+  }
+  const byteArray = new Uint8Array(byteNumbers);
+
+  // Cria um objeto Blob contendo os dados da imagem
+  const blob = new Blob([byteArray], { type: "image/png" });
+
+  // Cria um link para download
+  const link = document.createElement("a");
+  link.href = URL.createObjectURL(blob);
+  link.download = fileName || "image.png";
+
+  // Adiciona o link ao documento, clica nele e remove-o
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+};
+
+const base64ToPNG = (base64String, fileName) => {
+  // Remova a parte inicial "data:image/jpeg;base64,"
+  const base64WithoutHeader = base64String.replace(
+    /^data:image\/png;base64,/,
+    ""
+  );
+
+  // Converte a string base64 para um array de bytes
+  const byteCharacters = atob(base64WithoutHeader);
+  const byteNumbers = new Array(byteCharacters.length);
+  for (let i = 0; i < byteCharacters.length; i++) {
+    byteNumbers[i] = byteCharacters.charCodeAt(i);
+  }
+  const byteArray = new Uint8Array(byteNumbers);
+
+  // Cria um objeto Blob contendo os dados da imagem
+  const blob = new Blob([byteArray], { type: "image/png" });
+
+  // Cria um link para download
+  const link = document.createElement("a");
+  link.href = URL.createObjectURL(blob);
+  link.download = fileName || "image.png";
+
+  // Adiciona o link ao documento, clica nele e remove-o
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+}
+
+export { base64ToJPEG, base64ToPNG };

+ 1 - 0
src/helpers/masks.js

@@ -12,6 +12,7 @@ const masks = {
     celular: "(##) # ####-####",
     telefone: "(##) ####-####",
     cep: "#####-###",
+    cnpj: "##.###.###/####-##",
   },
   Paraguay: {
     celular: "(###) ###-###",

+ 5 - 5
src/layouts/MainLayout.vue

@@ -2,9 +2,9 @@
   <q-layout class="relative" view="hHh lpR fFf">
     <LeftMenuLayout v-if="!$q.screen.lt.sm" />
     <LeftMenuLayoutMobile v-else v-model="leftDrawerOpen" />
-    <q-header v-if="$q.screen.lt.sm" class="bg-background q-pa-sm">
+    <q-header v-if="$q.screen.lt.sm" class="bg-transparent q-pa-sm">
       <q-toolbar
-        class="flex justify-between bg-dark"
+        class="flex justify-between bg-surface"
         style="border-radius: 6px !important"
       >
         <q-btn dense flat @click="toggleLeftDrawer">
@@ -70,7 +70,7 @@
                 :is="Component"
                 style="padding: 20px !important; padding-right: 10px !important"
                 :style="
-                  $q.screen.lt.sm ? 'padding-right: 10px !important;' : ''
+                  $q.screen.lt.sm ? 'padding-left: 10px !important;' : ''
                 "
               />
             </Transition>
@@ -86,8 +86,8 @@ import { ref, useTemplateRef, watch } from "vue";
 import { useRoute } from "vue-router";
 import { useAuth } from "src/composables/useAuth";
 import { useRouter } from "vue-router";
-import LeftMenuLayout from "src/components/geral/LeftMenuLayout.vue";
-import LeftMenuLayoutMobile from "src/components/geral/LeftMenuLayoutMobile.vue";
+import LeftMenuLayout from "src/components/layout/LeftMenuLayout.vue";
+import LeftMenuLayoutMobile from "src/components/layout/LeftMenuLayoutMobile.vue";
 
 defineOptions({
   name: "MainLayout",

+ 3 - 3
src/pages/LoginPage.vue

@@ -1,6 +1,6 @@
 <template>
-  <q-page padding class="login-page bg-background">
-    <q-card flat class="login-card q-pa-md q-pt-xl bg-dark">
+  <q-page padding class="login-page">
+    <q-card flat class="login-card q-pa-md q-pt-xl bg-surface">
       <div class="text-center">
         <q-img :src="Logo" style="max-width: 250px" />
         <div class="text-h6">{{ $t("general.welcome") }}</div>
@@ -113,7 +113,7 @@ const submitLogin = async () => {
 
     submitting.value = false;
 
-    router.push({ name: "HomePage" });
+    router.push({ name: "DashboardPage" });
   } catch (error) {
     submitting.value = false;
   }

+ 24 - 0
src/pages/dashboard/DashboardPage.vue

@@ -0,0 +1,24 @@
+<template>
+  <div>
+    <DefaultHeaderPage />
+    <div class="flex gap q-pa-sm">
+      <div class="flex flex-grow gap">
+        <CardIconChart class="flex-grow" />
+        <CardIconChart class="flex-grow" />
+      </div>
+      <div class="flex flex-grow gap">
+        <CardIconChart class="flex-grow" />
+        <CardIconChart class="flex-grow" />
+      </div>
+    </div>
+  </div>
+</template>
+<script setup>
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
+import CardIconChart from "src/components/charts/CardIconChart.vue";
+</script>
+<style scoped>
+.gap {
+  gap: 16px;
+}
+</style>

+ 0 - 9
src/pages/home/HomePage.vue

@@ -1,9 +0,0 @@
-<template>
-  <div>
-    <DefaultHeaderPage />
-  </div>
-</template>
-<script setup>
-import DefaultHeaderPage from "src/components/geral/DefaultHeaderPage.vue";
-
-</script>

+ 15 - 13
src/pages/users/UsersPage.vue

@@ -1,18 +1,20 @@
 <template>
   <div>
     <DefaultHeaderPage />
-    <DefaultTable
-      :key="tableKey"
-      :columns="columns"
-      :api-call="getUsers"
-      :mostrar-selecao-de-colunas="false"
-      :mostrar-botao-fullscreen="false"
-      :mostrar-toggle-inativos="false"
-      open-item
-      add-item
-      @on-row-click="onRowClick"
-      @on-add-item="onAddItem"
-    />
+    <div>
+      <DefaultTable
+        :key="tableKey"
+        :columns="columns"
+        :api-call="getUsers"
+        :mostrar-selecao-de-colunas="false"
+        :mostrar-botao-fullscreen="false"
+        :mostrar-toggle-inativos="false"
+        open-item
+        add-item
+        @on-row-click="onRowClick"
+        @on-add-item="onAddItem"
+      />
+    </div>
   </div>
 </template>
 
@@ -24,7 +26,7 @@ import { permissionStore } from "src/stores/permission";
 import { getUsers, createUser, updateUser } from "src/api/user";
 
 import DefaultTable from "src/components/geral/DefaultTable.vue";
-import DefaultHeaderPage from "src/components/geral/DefaultHeaderPage.vue";
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
 
 const AddEditUserDialog = defineAsyncComponent(
   () => import("src/pages/users/components/AddEditUserDialog.vue"),

+ 1 - 1
src/router/index.js

@@ -43,7 +43,7 @@ export default route(function (/* { store, ssrContext } */) {
     }
     if (access_token) {
       if (to.name == "LoginPage") {
-        return next({ name: "HomePage" });
+        return next({ name: "DashboardPage" });
       }
     }
     if (to.meta.requiredPermission) {

+ 3 - 3
src/router/routes.js

@@ -14,15 +14,15 @@ const routes = [
     children: [
       {
         path: "",
-        name: "HomePage",
-        component: () => import("src/pages/home/HomePage.vue"),
+        name: "DashboardPage",
+        component: () => import("src/pages/dashboard/DashboardPage.vue"),
         meta: {
           title: "navigation.dashboard",
           requireAuth: true,
           requiredPermission: "dashboard",
           breadcrumbs: [
             {
-              name: "HomePage",
+              name: "DashboardPage",
               title: "navigation.dashboard",
             },
           ],

+ 1 - 1
src/router/routes/users.route.js

@@ -9,7 +9,7 @@ const routes = [
       requiredPermission: "config.user",
       breadcrumbs: [
         {
-          name: "HomePage",
+          name: "DashboardPage",
           title: "navigation.dashboard",
         },
         {