Преглед изворни кода

feat: mudanças para resolver bugs e padrão

Denis пре 1 година
родитељ
комит
a013c088c3

Разлика између датотеке није приказан због своје велике величине
+ 280 - 291
package-lock.json


+ 15 - 15
package.json

@@ -14,26 +14,26 @@
     "build": "quasar build"
   },
   "dependencies": {
-    "@quasar/extras": "^1.16.4",
-    "axios": "^1.2.1",
+    "@quasar/extras": "^1.16.15",
+    "axios": "^1.7.9",
     "date-fns": "^3.6.0",
-    "pinia": "^2.0.11",
-    "quasar": "^2.6.0",
-    "vue": "^3.4.18",
-    "vue-i18n": "^9.13.1",
-    "vue-router": "^4.0.0"
+    "pinia": "^2.3.0",
+    "quasar": "^2.17.4",
+    "vue": "^3.5",
+    "vue-i18n": "^9.14.2",
+    "vue-router": "^4.5.0"
   },
   "devDependencies": {
-    "@intlify/eslint-plugin-vue-i18n": "^3.0.0",
+    "@intlify/eslint-plugin-vue-i18n": "^3.2.0",
     "@intlify/unplugin-vue-i18n": "^2.0.0",
     "@intlify/vue-i18n-loader": "^4.2.0",
-    "@quasar/app-vite": "^2.0.0-beta.5",
-    "autoprefixer": "^10.4.2",
-    "eslint": "^8.57.0",
-    "eslint-config-prettier": "^9.0.0",
-    "eslint-plugin-vue": "^9.0.0",
-    "postcss": "^8.4.14",
-    "prettier": "^3.0.3",
+    "@quasar/app-vite": "^2.0.1",
+    "autoprefixer": "^10.4.20",
+    "eslint": "^8.57.1",
+    "eslint-config-prettier": "^9.1.0",
+    "eslint-plugin-vue": "^9.32.0",
+    "postcss": "^8.4.49",
+    "prettier": "^3.4.2",
     "vite-plugin-checker": "^0.6.4"
   },
   "engines": {

+ 3 - 1
src/App.vue

@@ -19,7 +19,9 @@ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
   : "light";
 
 const theme = $q.cookies.get("theme") || systemTheme;
-$q.dark.set(theme === "dark");
+const localeCookie = Cookies.get("locale") || window.navigator.language;
+console.log(theme, localeCookie);
+$q.dark.set(theme == "dark");
 
 watch(
   () => $q.dark.isActive,

BIN
src/assets/logo.png


BIN
src/assets/logo_softpar.png


+ 52 - 34
src/boot/axios.js

@@ -1,13 +1,7 @@
 import { boot } from "quasar/wrappers";
 import { useAuth } from "src/composables/useAuth";
 import { Cookies, Notify } from "quasar";
-import axios, { AxiosError } from "axios";
-// Be careful when using SSR for cross-request state pollution
-// due to creating a Singleton instance here;
-// If any client changes this (global) instance, it might be a
-// good idea to move this instance creation inside of the
-// "export default () => {}" function below (which runs individually
-// for each client)
+import axios from "axios";
 
 const api = axios.create({
   baseURL: process.env.API_URL + "/api",
@@ -16,12 +10,13 @@ const api = axios.create({
 });
 
 api.interceptors.request.use(
-  (config) => {
-    const access_token = Cookies.get("access_token");
-    const language = Cookies.get("locale") || window.navigator.language;
+  async (config) => {
+    const accessToken = Cookies.get("access_token");
+    const savedLanguage = Cookies.get("locale");
+    const language = savedLanguage || window.navigator.language;
     config.headers["Accept-Language"] = language;
-    if (access_token) {
-      config.headers.Authorization = `Bearer ${access_token}`;
+    if (accessToken) {
+      config.headers.Authorization = `Bearer ${accessToken}`;
     }
     return config;
   },
@@ -32,16 +27,25 @@ api.interceptors.request.use(
 
 const errorInterceptor = (error) => {
   return new Promise((resolve, reject) => {
-    if (error.response.status === 401) {
-      useAuth()
-        .refreshToken()
-        .then((response) => {
-          if (response instanceof AxiosError) {
-            Cookies.remove("access_token");
-            Cookies.remove("refresh_token");
-            window.location.href = "/login";
-            reject(response);
-          } else {
+    if (!error.response) {
+      Notify.create({
+        message: error.message,
+        type: "negative",
+      });
+      reject(error);
+    } else {
+      if (error.response.status === 401) {
+        if(error?.config?.url == "/login") {
+          Notify.create({
+            message: error.response.data.message,
+            type: "negative",
+          });
+          reject(error);
+          return;
+        }
+        useAuth()
+          .refreshToken()
+          .then(async () => {
             api
               .request(error.config)
               .then((res) => {
@@ -50,17 +54,31 @@ const errorInterceptor = (error) => {
               .catch((apiError) => {
                 reject(apiError);
               });
-          }
-        })
-        .catch((refreshError) => {
-          reject(refreshError);
+          })
+          .catch(async (refreshError) => {
+            Cookies.remove("access_token");
+            Cookies.remove("refresh_token");
+            if (window.location.pathname !== "/login") {
+              window.location.href = "/login";
+            }
+            reject(refreshError);
+          });
+        // Verificação: se o token de acesso não está funcionando para buscar user/me
+        // é porque o token é restrito
+      } else if (error?.config?.url == "/user/me") {
+        Cookies.remove("access_token");
+        Cookies.remove("refresh_token");
+
+        window.location.href = "/login";
+
+        reject(error);
+      } else {
+        Notify.create({
+          message: error.response.data.message,
+          type: "negative",
         });
-    } else {
-      Notify.create({
-        message: error.response.data.message,
-        type: "negative",
-      });
-      reject(error);
+        reject(error);
+      }
     }
   });
 };
@@ -78,10 +96,10 @@ const successInterceptor = (response) => {
 export default boot(({ app }) => {
   api.interceptors.response.use(
     (response) => successInterceptor(response),
-    (error) => errorInterceptor(error, app),
+    (error) => errorInterceptor(error),
   );
-  // for use inside Vue files (Options API) through this.$axios and this.$api
 
+  // for use inside Vue files (Options API) through this.$axios and this.$api
   app.config.globalProperties.$axios = axios;
   // ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
   //       so you won't necessarily have to import axios in each vue file

+ 6 - 2
src/boot/defaultPropsComponents.js

@@ -1,4 +1,4 @@
-import { QDialog, QInput, QSelect, QScrollArea } from "quasar";
+import { QDialog, QInput, QSelect, QBtn, QScrollArea } from "quasar";
 import { boot } from "quasar/wrappers";
 
 /**
@@ -21,11 +21,15 @@ export default boot(() => {
   });
   SetComponentDefaults(QInput, {
     filled: false,
-    outlined: true,
   });
   SetComponentDefaults(QSelect, {
     filled: false,
     outlined: true,
+    dense: true,
+  });
+  SetComponentDefaults(QBtn, {
+    outline: true,
+    padding: "10px 16px"
   });
   SetComponentDefaults(QScrollArea, {
     thumbStyle: {

+ 1 - 1
src/components/geral/DefaultDialogHeader.vue

@@ -2,7 +2,7 @@
   <q-bar
     class="q-py-md"
     v-bind="$attrs"
-    style="min-height: 45px; max-height: 45px"
+    style="height: 50px"
   >
     <q-icon v-if="props.icon" :name="props.icon" />
     <div>{{ props.title() }}</div>

+ 3 - 3
src/components/geral/DefaultHeaderPage.vue

@@ -1,15 +1,15 @@
 <template>
-  <div class="q-ml-md">
+  <div>
     <q-breadcrumbs class="q-mb-md">
       <q-breadcrumbs-el
         v-for="breadcrumb in $route.meta?.breadcrumbs"
         :key="breadcrumb?.name"
-        :label="breadcrumb?.title"
+        :label="$t(breadcrumb?.title)"
         :to="{ name: breadcrumb?.name }"
       />
     </q-breadcrumbs>
 
-    <span class="text-h5">{{ $route.meta?.title }}</span>
+    <span class="text-h5">{{ $t($route.meta?.title) }}</span>
     <q-separator class="q-my-sm" />
   </div>
 </template>

+ 63 - 123
src/components/geral/DefaultTable.vue

@@ -2,7 +2,6 @@
   <q-table
     v-model:fullscreen="fullscreen"
     flat
-    class="softpar-table"
     :pagination="{ rowsPerPage }"
     :pagination-label="getPaginationLabel"
     row-key="id"
@@ -11,86 +10,72 @@
     :columns="props.columns"
     :visible-columns="visibleColumns"
     :filter="filter"
+    :grid="$q.screen.lt.sm"
+    class="softpar-table q-pa-sm"
     @row-click="onRowClick"
   >
     <template #top>
-      <q-input
-        v-if="mostrarCampoPesquisa"
-        v-model="filter"
-        outlined
-        debounce="500"
-        placeholder="Buscar"
-        style="min-width: 400px"
-        clearable
-        dense
-        autofocus
+      <div
+        class="flex full-width justify-between align-center q-mb-md q-pl-sm"
+        style="gap: 1rem"
       >
-        <template #append>
-          <q-icon name="mdi-magnify" />
-        </template>
-      </q-input>
-
-      <!-- <q-checkbox
-        v-if="props.mostrarToggleInativos"
-        v-model="showInativos"
-        class="q-ml-sm"
-        :label="props.labelInativo"
-        dense
-        color="secondary"
-      /> -->
-
-      <q-select
-        v-if="mostrarSelecaoDeColunas"
-        v-model="visibleColumns"
-        class="q-ml-md"
-        multiple
-        dense
-        outlined
-        options-outlined
-        :display-value="$q.lang.table.columns"
-        emit-value
-        map-options
-        :options="mapColuns"
-        option-value="name"
-        style="min-width: 150px"
-        options-selected-class="text-bold"
-      />
-
-      <q-btn
-        v-if="mostrarBotaoFullscreen"
-        flat
-        class="q-ml-md"
-        @click="fullscreen = !fullscreen"
-      >
-        <q-icon name="mdi-fullscreen" />
-      </q-btn>
-
-      <q-btn
-        outline
-        class="default-button-padding q-ml-md bg-white"
-        icon="mdi-filter-outline"
-        style="height: 40px; width: 40px"
-        @click="showFilters = !showFilters"
-      >
-      </q-btn>
-
-      <q-space />
-
-      <q-btn-dropdown
-        v-if="props.dropDown"
-        class="q-mr-md"
-        color="primary"
-        :label="$t('general.options')"
-      />
-
-      <q-btn
-        v-if="props.addItem"
-        :outline="props.outlineAdd"
-        class="default-button-padding"
-        label="Adicionar"
-        @click="onAddItem"
-      >
-      </q-btn>
+        <q-input
+          v-if="mostrarCampoPesquisa"
+          v-model="filter"
+          debounce="250"
+          :placeholder="$t('general.search')"
+          clearable
+          autofocus
+          class=""
+          color="primary"
+        >
+          <template #append>
+            <q-icon name="mdi-magnify" />
+          </template>
+        </q-input>
+
+        <q-select
+          v-if="mostrarSelecaoDeColunas"
+          v-model="visibleColumns"
+          multiple
+          dense
+          outlined
+          options-outlined
+          :display-value="$q.lang.table.columns"
+          emit-value
+          map-options
+          :options="mapColuns"
+          option-value="name"
+          style="width: 150px"
+          options-selected-class="text-bold"
+        />
+
+        <q-btn
+          v-if="mostrarBotaoFullscreen"
+          flat
+          @click="fullscreen = !fullscreen"
+        >
+          <q-icon name="mdi-fullscreen" />
+        </q-btn>
+
+        <q-space />
+
+        <q-btn-dropdown
+          v-if="props.dropDown"
+          color="primary"
+          :label="$t('general.options')"
+        />
+
+        <q-btn
+          v-if="props.addItem"
+          color="primary"
+          padding="10px 16px"
+          :outline="props.outlineAdd"
+          :label="$t('general.add')"
+          @click="onAddItem"
+        >
+        </q-btn>
+      </div>
     </template>
 
     <template #body-cell-status="{ value, row }">
@@ -126,33 +111,6 @@
       </q-td>
     </template>
 
-    <template #body-cell-principal="{ value, row }">
-      <q-td style="width: 1%">
-        <q-item-section>
-          <span class="text-center">
-            <q-icon
-              v-if="row.principal && value"
-              name="mdi-star"
-              size="1.5rem"
-              style="color: #385873"
-              onmouseover="this.style.color='#688FAF';"
-              onmouseout="this.style.color='#385873';"
-              @click.stop="togglePrincipal(row)"
-            />
-            <q-icon
-              v-if="!row.principal"
-              name="mdi-star-outline"
-              size="1.5rem"
-              style="color: #385873"
-              onmouseover="this.style.color='#688FAF';"
-              onmouseout="this.style.color='#385873';"
-              @click.stop="togglePrincipal(row)"
-            />
-          </span>
-        </q-item-section>
-      </q-td>
-    </template>
-
     <template v-if="!props.hideNoDataLabel" #no-data>
       <div class="q-my-md row justify-center full-width">
         <q-spinner v-if="loading" color="primary" size="30px" />
@@ -172,7 +130,7 @@
 import { ref, onMounted, toRaw, watch } from "vue";
 import { useRouter } from "vue-router";
 
-const emit = defineEmits(["onRowClick", "onAddItem", "noRows", "togglePrincipal"]);
+const emit = defineEmits(["onRowClick", "onAddItem", "noRows"]);
 
 const props = defineProps({
   // colunas de configuração da tabela
@@ -360,11 +318,6 @@ const onRequest = async () => {
   loading.value = false;
 };
 
-// funcao exclusiva para contatos table, para alterar o contato principal
-const togglePrincipal = async (row) => {
-  emit("togglePrincipal", row);
-};
-
 onMounted(async () => {
   // faz a primeira requisição
   await onRequest({
@@ -406,17 +359,4 @@ onMounted(async () => {
   background: #f7cfbb;
   border-radius: 24px;
 }
-
-.circulo-status {
-  width: 18px;
-  height: 18px;
-  border-radius: 50%;
-  display: inline-block;
-}
-.circulo-ativo {
-  background-color: #80f680; /* Verde */
-}
-.circulo-inativo {
-  background-color: #919191; /* Cinza */
-}
 </style>

+ 27 - 16
src/components/geral/LeftMenuLayout.vue

@@ -4,18 +4,28 @@
     v-bind="$attrs"
     v-model="leftDrawerOpen"
     show-if-above
+    no-swipe-close
+    no-swipe-open
     :width="250"
-    :mini-width="100"
+    :mini-width="64"
     :breakpoint="500"
     :mini="miniState"
+    :behavior="'desktop'"
     class="detached-container"
   >
-    <div class="q-pa-sm">
+    <div class="column full-height q-pa-sm no-wrap">
       <div
+        v-if="!$q.screen.lt.sm"
         class="toggle-button-wrapper absolute"
         style="top: 50%; right: -32px; z-index: 1"
       >
-        <q-btn flat round size="sm" @click="miniState = !miniState">
+        <q-btn
+          flat
+          round
+          size="sm"
+          padding="8px 8px"
+          @click="miniState = !miniState"
+        >
           <q-icon
             :name="miniState ? 'mdi-chevron-right' : 'mdi-chevron-left'"
           />
@@ -40,14 +50,14 @@
                 />
               </template>
             </q-item-section>
-            <q-item-section>Usuario</q-item-section>
+            <q-item-section>{{ user_store.user.name }}</q-item-section>
           </div>
           <q-tooltip
-            v-if="miniState"
+            v-if="miniState && !$q.screen.lt.sm"
             anchor="center right"
             self="center left"
             :offset="[10, 10]"
-            >Usuario</q-tooltip
+            >{{ user_store.user.name }}</q-tooltip
           >
         </q-item>
       </q-list>
@@ -70,7 +80,7 @@
             </q-item-section>
             <q-item-section>{{ $t(menu.title) }}</q-item-section>
             <q-tooltip
-              v-if="miniState"
+              v-if="miniState && !$q.screen.lt.sm"
               anchor="center right"
               self="center left"
               :offset="[10, 10]"
@@ -81,7 +91,7 @@
           <div v-else>
             <template v-if="!miniState">
               <q-tooltip
-                v-if="miniState"
+                v-if="miniState && !$q.screen.lt.sm"
                 anchor="center right"
                 self="center left"
                 :offset="[10, 10]"
@@ -116,7 +126,7 @@
                     </q-item-section>
                     <q-item-section>{{ $t(child.title) }}</q-item-section>
                     <q-tooltip
-                      v-if="miniState"
+                      v-if="miniState && !$q.screen.lt.sm"
                       anchor="center right"
                       self="center left"
                       :offset="[10, 10]"
@@ -133,14 +143,13 @@
                 exact
                 exact-active-class="menu-selected"
                 class="menu-item--spaced"
-                @click="openMenu(menu)"
               >
                 <q-item-section avatar>
                   <q-icon :name="menu.icon" style="font-size: 18px" />
                 </q-item-section>
                 <q-item-section>{{ $t(menu.title) }}</q-item-section>
                 <q-tooltip
-                  v-if="miniState"
+                  v-if="miniState && !$q.screen.lt.sm"
                   anchor="center right"
                   self="center left"
                   :offset="[10, 10]"
@@ -171,8 +180,7 @@
           </div>
         </template>
       </q-list>
-
-      <q-list class="column q-mt-md no-wrap overflow-hidden">
+      <q-list class="column no-wrap overflow-hidden q-mt-auto">
         <q-item v-ripple clickable @click="logoutFn">
           <div class="flex">
             <q-item-section avatar>
@@ -181,7 +189,7 @@
             <q-item-section>Sair</q-item-section>
           </div>
           <q-tooltip
-            v-if="miniState"
+            v-if="miniState && !$q.screen.lt.sm"
             anchor="center right"
             self="center left"
             :offset="[10, 10]"
@@ -197,12 +205,15 @@ import { ref, onMounted } from "vue";
 import { useAuth } from "src/composables/useAuth";
 import { permissionStore } from "src/stores/permission";
 import { useRouter, useRoute } from "vue-router";
+import { userStore } from "src/stores/user";
 
 const { logout } = useAuth();
-const leftDrawerOpen = ref(true);
-const miniState = ref(true);
 const router = useRouter();
 const route = useRoute();
+const user_store = userStore();
+
+const leftDrawerOpen = ref(true);
+const miniState = ref(true);
 
 const childrenAreActive = (children) => {
   if (!children) return false;

+ 2 - 2
src/composables/useAuth.js

@@ -30,7 +30,7 @@ export const useAuth = () => {
       }
       return response;
     } catch (error) {
-      return error;
+      return Promise.reject(error);
     }
   };
 
@@ -73,7 +73,7 @@ export const useAuth = () => {
 
       return response;
     } catch (error) {
-      return error;
+      return Promise.reject(error);
     }
   };
 

+ 40 - 0
src/composables/useInputRules.js

@@ -0,0 +1,40 @@
+import { useI18n } from "vue-i18n";
+
+export const useInputRules = () => {
+  const { t } = useI18n();
+
+  // Regex patterns
+  const emailPattern =
+    /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+  const cpfPattern = /^[0-9]{3}\.[0-9]{3}\.[0-9]{3}-[0-9]{2}$/;
+  const cnpjPattern = /^[0-9]{2}\.[0-9]{3}\.[0-9]{3}\/[0-9]{4}-[0-9]{2}$/;
+  const passwordPattern = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
+
+  const inputRules = {
+    required: (value) => !!value || t("rules.required"),
+    requiredNumber: (value) => !isNaN(value) || t("rules.required"),
+    requiredHideMessage: (value) => !!value,
+    min: (min) => (value) =>
+      value.length >= min ||
+      `${t("rules.min")} ${min} ${t("rules.characters")}`,
+    email: (value) => !value || emailPattern.test(value) || t("rules.email"),
+    emails: (value) => {
+      if (!value) return true;
+      const emails = value.split(";").map((email) => email.trim());
+      return (
+        emails.every((email) => inputRules.email(email) === true) ||
+        t("rules.email")
+      );
+    },
+    cpf: (value) => !value || cpfPattern.test(value) || t("rules.cpf"),
+    cnpj: (value) => !value || cnpjPattern.test(value) || t("rules.cnpj"),
+    samePassword: (otherValue) => (value) =>
+      value === otherValue || t("rules.same_password"),
+    password: (value) =>
+      !value || passwordPattern.test(value) || t("rules.password"),
+  };
+
+  return {
+    inputRules,
+  };
+};

+ 1 - 0
src/css/app.scss

@@ -12,4 +12,5 @@
   margin-bottom: 16px !important;
   margin-left: 10px !important;
   border-radius: 6px !important;
+  transition: all;
 }

+ 151 - 22
src/css/quasar.variables.scss

@@ -1,35 +1,164 @@
-// Quasar SCSS (& Sass) Variables
+// Quasar SCSS Variables with Material Design Color System
 // --------------------------------------------------
-// To customize the look and feel of this app, you can override
-// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
 
-// Check documentation for full list of Quasar variables
+// Primary Theme Colors
+$primary: #1976d2; // Material Blue 700
+$secondary: #9c27b0; // Material Purple 500
+$accent: #e91e63; // Material Pink 500
 
-// Your own variables (that are declared here) and Quasar's own
-// ones will be available out of the box in your .vue/.scss/.sass files
+// Dark Theme Base Colors
+$dark: #1d1d1d;
+$dark-page: #121212; // Material Dark Background
 
-// It's highly recommended to change the default colors
-// to match your app's branding.
-// Tip: Use the "Theme Builder" on Quasar's documentation website.
+// Status Colors
+$positive: #2e7d32; // Material Green 800
+$negative: #d32f2f; // Material Red 700
+$info: #0288d1; // Material Light Blue 700
+$warning: #ed6c02; // Material Orange 800
 
-$primary: #2d69eb;
-$secondary: #d6d6d6;
-$accent: #ff1717;
+// Extended Color System with Light/Dark Variants
+$colors: (
+  // Primary Colors and Variants
+  "primary": #1976d2,
+  // Base - Blue 700
+  "primary-light": #42a5f5,
+  // Light - Blue 400
+  "primary-dark": #1565c0,
 
-$dark: #1d1d1d;
-$dark-page: #0d0c0c;
+  // Dark - Blue 800
+  // Secondary Colors and Variants
+  "secondary": #9c27b0,
+  // Base - Purple 500
+  "secondary-light": #ba68c8,
+  // Light - Purple 300
+  "secondary-dark": #7b1fa2,
+
+  // Dark - Purple 700
+  // Background Colors
+  "background": #ffffff,
+  "background-light": #000000,
+  "background-dark": #121212,
+
+  // Surface Colors
+  "surface": #ffffff,
+  "surface-light": #fafafa,
+  "surface-dark": #1e1e1e,
+
+  // Status Colors with Variants
+  "success": #2e7d32,
+  // Green 800
+  "success-light": #4caf50,
+  // Green 500
+  "success-dark": #1b5e20,
+
+  // Green 900
+  "error": #d32f2f,
+  // Red 700
+  "error-light": #ef5350,
+  // Red 400
+  "error-dark": #c62828,
+
+  // Red 800
+  "warning": #ed6c02,
+  // Orange 800
+  "warning-light": #ff9800,
+  // Orange 500
+  "warning-dark": #e65100,
+
+  // Orange 900
+  "info": #0288d1,
+  // 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
+);
+
+// Dark Theme Color Overrides
+$colors-dark: (
+  // Primary Colors - Lighter in Dark Mode
+  "primary": #90caf9,
+  // Blue 200
+  "primary-light": #e3f2fd,
+  // Blue 50
+  "primary-dark": #42a5f5,
+
+  // Blue 400
+  // Secondary Colors - Lighter in Dark Mode
+  "secondary": #ce93d8,
+  // Purple 200
+  "secondary-light": #f3e5f5,
+  // 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,
+
+  // Black Background
+  // Status Colors - Adjusted for Dark Mode
+  "success": #66bb6a,
+  // Green 400
+  "success-light": #81c784,
+  // Green 300
+  "success-dark": #388e3c,
+
+  // Green 700
+  "error": #f44336,
+  // Red 500
+  "error-light": #e57373,
+  // Red 300
+  "error-dark": #d32f2f,
 
-$positive: #21ba45;
-$negative: #c10015;
-$info: #31ccec;
-$warning: #f2c037;
+  // Red 700
+  "warning": #ffa726,
+  // Orange 400
+  "warning-light": #ffb74d,
+  // Orange 300
+  "warning-dark": #f57c00,
 
-$page: #f5f5f5;
+  // Orange 700
+  "info": #29b6f6,
+  // Light Blue 400
+  "info-light": #4fc3f7,
+  // Light Blue 300
+  "info-dark": #0288d1 // Light Blue 700
+);
 
-.body--light {
-  --q-page: #f5f5f5;
+// Generate color utility classes for light theme
+@each $name, $color in $colors {
+  .text-#{$name} {
+    color: $color !important;
+  }
+  .bg-#{$name} {
+    background: $color !important;
+  }
 }
 
+// Generate color utility classes for dark theme
 .body--dark {
-  --q-page: #1d1d1d;
+  @each $name, $color in $colors-dark {
+    .text-#{$name} {
+      color: $color !important;
+    }
+    .bg-#{$name} {
+      background: $color !important;
+    }
+  }
 }

+ 24 - 23
src/css/table.scss

@@ -1,23 +1,24 @@
+@use "sass:map";
+@use "src/css/quasar.variables.scss";
 .softpar-table {
   padding-left: 16px !important;
   padding-right: 16px !important;
 
   .body--dark & {
-    --table-bg-color: #181818;
-    --table-border-color: #3b3b3b;
-    --table-header-color: #f9f9f9;
+    --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
   }
 
   .body--light & {
-    background-color: #eef4f8;
-    --table-bg-color: #f9f9f9;
-    --table-border-color: #e0e0e0;
-    --table-header-color: #717171;
+    --table-bg-color: #{map.get($colors, "page")}; // 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
   }
 
   :deep(.q-table) {
     thead tr:first-child th {
-      background-color: var(-q--primary) !important;
+      background-color: $primary !important;
     }
 
     thead tr th {
@@ -46,7 +47,7 @@
   }
 
   .q-table__top {
-    padding-top: 8px;
+    padding-top: 16px;
     padding-left: 0px;
     padding-right: 0px;
     padding-bottom: 16px;
@@ -75,11 +76,11 @@
 
   .body--dark & {
     background: none;
-    border: 1px solid #cfdab7;
+    border: 1px solid #{map.get($colors, "positive-1")};
   }
 
   .body--light & {
-    background: #cfdab7;
+    background: #{map.get($colors, "positive-1")};
     border: none;
   }
 }
@@ -95,11 +96,11 @@
 
   .body--dark & {
     background: none;
-    border: 1px solid #f7cfbb;
+    border: 1px solid #{map.get($colors, "negative-1")};
   }
 
   .body--light & {
-    background: #f7cfbb;
+    background: #{map.get($colors, "negative-1")};
     border: none;
   }
 }
@@ -115,11 +116,11 @@
 
   .body--dark & {
     background: none;
-    border: 1px solid #f9e3e3;
+    border: 1px solid #{map.get($colors, "negative-1")};
   }
 
   .body--light & {
-    background: #f9e3e3;
+    background: #{map.get($colors, "negative-1")};
     border: none;
   }
 }
@@ -135,11 +136,11 @@
 
   .body--dark & {
     background: none;
-    border: 1px solid #e1e9f0;
+    border: 1px solid #{map.get($colors, "primary-1")};
   }
 
   .body--light & {
-    background: #e1e9f0;
+    background: #{map.get($colors, "primary-1")};
     border: none;
   }
 }
@@ -155,11 +156,11 @@
 
   .body--dark & {
     background: none;
-    border: 1px solid #faf4bf;
+    border: 1px solid #{map.get($colors, "warning")};
   }
 
   .body--light & {
-    background: #faf4bf;
+    background: #{map.get($colors, "warning")};
     border: none;
   }
 }
@@ -167,21 +168,21 @@
 .table-bottom {
   .q-table__top {
     .body--light & {
-      background: #f9f9f9;
+      background: #{map.get($colors, "page")};
     }
 
     .body--dark & {
-      background: #181818;
+      background: #{map.get($colors, "background-3")};
     }
   }
 
   .q-table__bottom {
     .body--light & {
-      background: #f9f9f9;
+      background: #{map.get($colors, "page")};
     }
 
     .body--dark & {
-      background: #181818;
+      background: #{map.get($colors, "background-3")};
     }
   }
 }

+ 24 - 0
src/i18n/index.js

@@ -3,7 +3,31 @@ import pt from "./locales/pt.json";
 import es from "./locales/es.json";
 
 export default {
+  "en": en,
+  "en-us": en,
+  "en-gb": en,
   "en-US": en,
+  "en-GB": en,
+  "EN": en,
+  "EN-US": en,
+  "EN-GB": en,
+  "pt": pt,
+  "pt-br": pt,
+  "pt-pt": pt,
   "pt-BR": pt,
+  "pt-PT": pt,
+  "PT": pt,
+  "PT-BR": pt,
+  "PT-PT": pt,
+  "es": es,
+  "es-es": es,
+  "es-mx": es,
+  "es-ar": es,
   "es-ES": es,
+  "es-MX": es,
+  "es-AR": es,
+  "ES": es,
+  "ES-ES": es,
+  "ES-MX": es,
+  "ES-AR": es,
 };

+ 19 - 5
src/i18n/locales/en.json

@@ -27,7 +27,16 @@
     "logout": "Logout",
     "exit": "Exit",
     "registration": "Registration",
-    "users": "Users"
+    "users": "Users",
+    "perfil": "Profile",
+    "plans": "Plans",
+    "wallet": "Wallet",
+    "advertise": "Advertise",
+    "my_advertisements": "My Advertisements",
+    "explore": "Explore",
+    "opportunities": "Opportunities",
+    "interests": "Interests",
+    "negotiations": "Negotiations"
   },
   "users": {
     "user": "{something} user | {something} users",
@@ -51,9 +60,14 @@
   },
   "rules": {
     "required": "This field is required",
-    "min": "This field must be at least {min} characters",
-    "max": "This field must be at most {max} characters",
-    "email": "This field must be a valid email | These fields must be valid emails",
-    "date": "This field must be a valid date"
+    "characters": "characters",
+    "min": "This field must have at least",
+    "max": "This field must have a maximum of",
+    "email": "This field must be a valid email",
+    "date": "This field must be a valid date",
+    "cpf": "This field must be a valid CPF",
+    "cnpj": "This field must be a valid CNPJ",
+    "same_password": "Passwords must match",
+    "password": "The password must be at least 8 characters, one uppercase letter, one lowercase letter, and one number"
   }
 }

+ 20 - 6
src/i18n/locales/es.json

@@ -24,10 +24,19 @@
   "navigation": {
     "dashboard": "Tablero",
     "login": "Iniciar sesión",
-    "logout": "Cerrar sesión",
+    "logout": "Salir",
     "exit": "Salir",
     "registration": "Registro",
-    "users": "Usuarios"
+    "users": "Usuarios",
+    "perfil": "Perfil",
+    "plans": "Planes",
+    "wallet": "Cartera",
+    "advertise": "Anunciar",
+    "my_advertisements": "Mis Anuncios",
+    "explore": "Explorar",
+    "opportunities": "Oportunidades",
+    "interests": "Intereses",
+    "negotiations": "Negociaciones"
   },
   "users": {
     "user": "{something} usuario | {something} usuarios",
@@ -51,9 +60,14 @@
   },
   "rules": {
     "required": "Este campo es obligatorio",
-    "min": "Este campo debe tener al menos {min} caracteres",
-    "max": "Este campo debe tener como máximo {max} caracteres",
-    "email": "Este campo debe ser un correo electrónico válido | Estos campos deben ser correos electrónicos válidos",
-    "date": "Este campo debe ser una fecha válida"
+    "characters": "caracteres",
+    "min": "Este campo debe tener al menos",
+    "max": "Este campo debe tener como máximo",
+    "email": "Este campo debe ser un correo electrónico válido",
+    "date": "Este campo debe ser una fecha válida",
+    "cpf": "Este campo debe ser un CPF válido",
+    "cnpj": "Este campo debe ser un CNPJ válido",
+    "same_password": "Las contraseñas deben coincidir",
+    "password": "La contraseña debe tener al menos 8 caracteres, una letra mayúscula, una letra minúscula y un número"
   }
 }

+ 19 - 5
src/i18n/locales/pt.json

@@ -24,10 +24,19 @@
   "navigation": {
     "dashboard": "Dashboard",
     "login": "Login",
-    "logout": "Logout",
+    "logout": "Sair",
     "exit": "Sair",
     "registration": "Registro",
-    "users": "Usuários"
+    "users": "Usuários",
+    "perfil": "Perfil",
+    "plans": "Planos",
+    "wallet": "Carteira",
+    "advertise": "Anúnciar",
+    "my_advertisements": "Meus Anúncios",
+    "explore": "Explorar",
+    "opportunities": "Oportunidades",
+    "interests": "Interesses",
+    "negotiations": "Negociações"
   },
   "users": {
     "user": "{something} usuario | {something} usuarios",
@@ -51,9 +60,14 @@
   },
   "rules": {
     "required": "Este campo é obrigatório",
-    "min": "Este campo deve ter pelo menos {min} caracteres",
-    "max": "Este campo deve ter no máximo {max} caracteres",
+    "characters": "caracteres",
+    "min": "Este campo deve ter pelo menos",
+    "max": "Este campo deve ter no máximo",
     "email": "Este campo deve ser um email válido | Estes campos devem ser emails válidos",
-    "date": "Este campo deve ser uma data válida"
+    "date": "Este campo deve ser uma data válida",
+    "cpf": "Este campo deve ser um CPF válido",
+    "cnpj": "Este campo deve ser um CNPJ válido",
+    "same_password": "As senhas devem ser iguais",
+    "password": "A senha deve ter pelo menos 6 caracteres, uma letra maiúscula, uma letra minúscula e um número"
   }
 }

+ 9 - 5
src/layouts/MainLayout.vue

@@ -3,15 +3,19 @@
     <LeftMenuLayout v-model="leftDrawerOpen" />
 
     <q-page-container>
-      <q-page>
+      <q-page
+      >
         <q-scroll-area
           ref="scrollAreaRef"
           style="
+            padding-top: 20px !important;
+            margin-left: 20px !important;
+            margin-right: 10px !important;
             height: calc(100dvh - 50px - env(safe-area-inset-top)) !important;
           "
         >
           <router-view v-slot="{ Component }">
-            <Transition>
+            <Transition mode="out-in">
               <component :is="Component" />
             </Transition>
           </router-view>
@@ -30,15 +34,15 @@ defineOptions({
   name: "MainLayout",
 });
 
-const leftDrawerOpen = ref(false);
+const leftDrawerOpen = ref(true);
 const scrollAreaRef = useTemplateRef("scrollAreaRef");
 const route = useRoute();
 
 let oldValue = route.path;
 watch(route, (value) => {
   if (oldValue.path != value.path) {
-    scrollAreaRef.value.setScrollPosition("vertical", 0, 250);
-    scrollAreaRef.value.setScrollPosition("horizontal", 0, 250);
+    scrollAreaRef.value.setScrollPosition("vertical", 0, 0);
+    scrollAreaRef.value.setScrollPosition("horizontal", 0, 0);
   }
   oldValue = value.path;
 });

+ 5 - 6
src/pages/LoginPage.vue

@@ -2,13 +2,10 @@
   <q-page padding class="login-page bg-background">
     <q-card flat class="login-card q-pa-md q-pt-xl bg-grey-box">
       <div class="text-center">
-        <q-img src="assets/logo.png" style="max-width: 250px" />
+        <q-img :src="Logo" style="max-width: 250px" />
+        <div class="text-h6">{{ $t("general.welcome") }}</div>
       </div>
 
-      <q-card-section class="text-center">
-        <div class="text-h4">{{ $t("general.welcome") }}</div>
-      </q-card-section>
-
       <q-form
         ref="loginForm"
         class="q-pa-md"
@@ -76,6 +73,8 @@ import { useAuth } from "src/composables/useAuth";
 import { useRouter } from "vue-router";
 import { useInputRules } from "src/composables/useInputRules";
 
+import Logo from "src/assets/logo.png";
+
 const router = useRouter();
 const $q = useQuasar();
 
@@ -141,7 +140,7 @@ onMounted(() => {
   align-items: center;
 
   .login-card {
-    min-width: 585px;
+    width: 100%;
     max-width: 500px;
     border-radius: 12px;
   }

+ 6 - 2
src/pages/home/HomePage.vue

@@ -1,5 +1,9 @@
 <template>
-  <div class="q-pa-md"></div>
+  <div>
+    <DefaultHeaderPage />
+  </div>
 </template>
+<script setup>
+import DefaultHeaderPage from "src/components/geral/DefaultHeaderPage.vue";
 
-<script setup></script>
+</script>

+ 6 - 5
src/pages/users/UsersPage.vue

@@ -1,14 +1,13 @@
 <template>
-  <div class="q-pa-md">
+  <div>
     <DefaultHeaderPage />
     <DefaultTable
       :key="tableKey"
       :columns="columns"
+      :api-route="getUsers"
       :mostrar-selecao-de-colunas="false"
       :mostrar-botao-fullscreen="false"
       :mostrar-toggle-inativos="false"
-      style="padding-right: 16px"
-      :api-route="getUsers"
       open-item
       add-item
       @on-row-click="onRowClick"
@@ -18,13 +17,13 @@
 </template>
 
 <script setup>
-import DefaultTable from "src/components/geral/DefaultTable.vue";
 import { ref, defineAsyncComponent } from "vue";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 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";
 
 const AddEditUserDialog = defineAsyncComponent(
@@ -44,14 +43,16 @@ const columns = [
     align: "left",
     style: "width: 50%",
     required: true,
+    sortable: true,
   },
   {
     name: "email",
-    label: "email",
+    label: "Email",
     field: "email",
     align: "left",
     style: "width: 20%",
     required: true,
+    sortable: true,
   },
   {},
 ];

+ 48 - 29
src/pages/users/components/AddEditUserDialog.vue

@@ -2,39 +2,44 @@
   <q-dialog ref="dialogRef" @hide="onDialogHide">
     <q-card class="q-dialog-plugin">
       <DefaultDialogHeader :title="props.title" @close="onDialogCancel" />
-      <q-card-section>
-        <q-form ref="formRef" class="row q-col-gutter-sm">
+      <q-form ref="formRef" @submit="onOKClick">
+        <q-card-section class="row q-col-gutter-sm">
           <q-input
-            v-model="name"
+            v-model="form.name"
             :label="$t('users.name')"
             :hint="$t('users.name_and_surname')"
             :rules="[inputRules.required]"
             class="col-6"
           />
           <q-input
-            v-model="email"
+            v-model="form.email"
             label="Email"
             :rules="[inputRules.email]"
             class="col-6"
           />
           <q-input
-            v-model="password"
+            v-model="form.password"
             :label="$t('users.password')"
-            :rules="[inputRules.min(6)]"
+            :rules="props.user ? [] : [inputRules.required, inputRules.min(6)]"
             class="col-6"
           />
-        </q-form>
-      </q-card-section>
-      <q-card-actions align="center">
-        <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
-        <q-space />
-        <q-btn color="primary" label="OK" @click="onOKClick" />
-      </q-card-actions>
+        </q-card-section>
+        <q-card-actions align="center">
+          <q-btn color="primary" label="Cancel" @click="onDialogCancel" />
+          <q-space />
+          <q-btn
+            color="primary"
+            label="OK"
+            :type="'submit'"
+            :disable="!hasChanges"
+          />
+        </q-card-actions>
+      </q-form>
     </q-card>
   </q-dialog>
 </template>
 <script setup>
-import { ref } from "vue";
+import { ref, computed, useTemplateRef } from "vue";
 import { useInputRules } from "src/composables/useInputRules";
 import { useDialogPluginComponent } from "quasar";
 import { useI18n } from "vue-i18n";
@@ -58,16 +63,32 @@ const props = defineProps({
   },
 });
 
+const { inputRules } = useInputRules();
+
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
 
-const { inputRules } = useInputRules();
+const formRef = useTemplateRef("formRef");
 
-const formRef = ref(null);
+const form = ref({
+  name: props.user ? props.user.name : "",
+  email: props.user ? props.user.email : "",
+  password: "",
+});
+const originalForm = { ...form.value };
 
-const name = ref(props.user?.name || "");
-const email = ref(props.user?.email || "");
-const password = ref("");
+const hasChanges = computed(
+  () => Object.keys(getChangedFields.value).length > 0,
+);
+const getChangedFields = computed(() => {
+  const changedFields = {};
+  for (const key in form.value) {
+    if (form.value[key] !== originalForm[key]) {
+      changedFields[key] = form.value[key];
+    }
+  }
+  return changedFields;
+});
 
 // this is part of our example (so not required)
 const onOKClick = () => {
@@ -75,15 +96,13 @@ const onOKClick = () => {
     return;
   }
 
-  const payload = {
-    name: name.value,
-    email: email.value,
-    password: password.value,
-  };
-  // on OK, it is REQUIRED to
-  // call onDialogOK (with optional payload)
-  onDialogOK({ ...payload });
-  // or with payload: onDialogOK({ ... })
-  // ...and it will also hide the dialog automatically
+  if (props.user) {
+    // When editing, only send changed fields
+    const changedFields = getChangedFields.value;
+    onDialogOK(changedFields);
+  } else {
+    // When creating, send all fields
+    onDialogOK({ ...form.value });
+  }
 };
 </script>

+ 8 - 0
src/router/routes.js

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

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

@@ -4,17 +4,17 @@ const routes = [
     name: "UsersPage",
     component: () => import("pages/users/UsersPage.vue"),
     meta: {
-      title: "Usuários",
+      title: "navigation.users",
       requireAuth: true,
       requiredPermission: "config.user",
       breadcrumbs: [
         {
           name: "HomePage",
-          title: "Início",
+          title: "navigation.dashboard",
         },
         {
           name: "UsersPage",
-          title: "Usuários",
+          title: "navigation.users",
         },
       ],
     },

Неке датотеке нису приказане због велике количине промена