Преглед на файлове

feat: :sparkles: varios ajustes para melhorar responsividade e modo dark e light

Denis преди 1 година
родител
ревизия
f8f027bcf0

BIN
src/assets/logo_softpar.png


BIN
src/assets/logo_softpar_azul.png


+ 0 - 15
src/assets/quasar-logo-vertical.svg

@@ -1,15 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 356 360">
-	<path
-		d="M43.4 303.4c0 3.8-2.3 6.3-7.1 6.3h-15v-22h14.4c4.3 0 6.2 2.2 6.2 5.2 0 2.6-1.5 4.4-3.4 5 2.8.4 4.9 2.5 4.9 5.5zm-8-13H24.1v6.9H35c2.1 0 4-1.3 4-3.8 0-2.2-1.3-3.1-3.7-3.1zm5.1 12.6c0-2.3-1.8-3.7-4-3.7H24.2v7.7h11.7c3.4 0 4.6-1.8 4.6-4zm36.3 4v2.7H56v-22h20.6v2.7H58.9v6.8h14.6v2.3H58.9v7.5h17.9zm23-5.8v8.5H97v-8.5l-11-13.4h3.4l8.9 11 8.8-11h3.4l-10.8 13.4zm19.1-1.8V298c0-7.9 5.2-10.7 12.7-10.7 7.5 0 13 2.8 13 10.7v1.4c0 7.9-5.5 10.8-13 10.8s-12.7-3-12.7-10.8zm22.7 0V298c0-5.7-3.9-8-10-8-6 0-9.8 2.3-9.8 8v1.4c0 5.8 3.8 8.1 9.8 8.1 6 0 10-2.3 10-8.1zm37.2-11.6v21.9h-2.9l-15.8-17.9v17.9h-2.8v-22h3l15.6 18v-18h2.9zm37.9 10.2v1.3c0 7.8-5.2 10.4-12.4 10.4H193v-22h11.2c7.2 0 12.4 2.8 12.4 10.3zm-3 0c0-5.3-3.3-7.6-9.4-7.6h-8.4V307h8.4c6 0 9.5-2 9.5-7.7V298zm50.8-7.6h-9.7v19.3h-3v-19.3h-9.7v-2.6h22.4v2.6zm34.4-2.6v21.9h-3v-10.1h-16.8v10h-2.8v-21.8h2.8v9.2H296v-9.2h2.9zm34.9 19.2v2.7h-20.7v-22h20.6v2.7H316v6.8h14.5v2.3H316v7.5h17.8zM24 340.2v7.3h13.9v2.4h-14v9.6H21v-22h20v2.7H24zm41.5 11.4h-9.8v7.9H53v-22h13.3c5.1 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6H66c3.1 0 5.3-1.5 5.3-4.7 0-3.3-2.2-4.1-5.3-4.1H55.7v8.8zm47.9 6.2H89l-2 4.3h-3.2l10.7-22.2H98l10.7 22.2h-3.2l-2-4.3zm-1-2.3l-6.3-13-6 13h12.2zm46.3-15.3v21.9H146v-17.2L135.7 358h-2.1l-10.2-15.6v17h-2.8v-21.8h3l11 16.9 11.3-17h3zm35 19.3v2.6h-20.7v-22h20.6v2.7H166v6.8h14.5v2.3H166v7.6h17.8zm47-19.3l-8.3 22h-3l-7.1-18.6-7 18.6h-3l-8.2-22h3.3L204 356l6.8-18.5h3.4L221 356l6.6-18.5h3.3zm10 11.6v-1.4c0-7.8 5.2-10.7 12.7-10.7 7.6 0 13 2.9 13 10.7v1.4c0 7.9-5.4 10.8-13 10.8-7.5 0-12.7-3-12.7-10.8zm22.8 0v-1.4c0-5.7-4-8-10-8s-9.9 2.3-9.9 8v1.4c0 5.8 3.8 8.2 9.8 8.2 6.1 0 10-2.4 10-8.2zm28.3 2.4h-9.8v7.9h-2.8v-22h13.2c5.2 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6h10.2c3 0 5.2-1.5 5.2-4.7 0-3.3-2.1-4.1-5.2-4.1h-10.2v8.8zm40.3-1.5l-6.8 5.6v6.4h-2.9v-22h2.9v12.3l15.2-12.2h3.7l-9.9 8.1 10.3 13.8h-3.6l-8.9-12z" />
-	<path fill="#050A14"
-		d="M188.4 71.7a10.4 10.4 0 01-20.8 0 10.4 10.4 0 1120.8 0zM224.2 45c-2.2-3.9-5-7.5-8.2-10.7l-12 7c-3.7-3.2-8-5.7-12.6-7.3a49.4 49.4 0 00-9.7 13.9 59 59 0 0140.1 14l7.6-4.4a57 57 0 00-5.2-12.5zM178 125.1c4.5 0 9-.6 13.4-1.7v-14a40 40 0 0012.5-7.2 47.7 47.7 0 00-7.1-15.3 59 59 0 01-32.2 27.7v8.7c4.4 1.2 8.9 1.8 13.4 1.8zM131.8 45c-2.3 4-4 8.1-5.2 12.5l12 7a40 40 0 000 14.4c5.7 1.5 11.3 2 16.9 1.5a59 59 0 01-8-41.7l-7.5-4.3c-3.2 3.2-6 6.7-8.2 10.6z" />
-	<path fill="#00B4FF"
-		d="M224.2 98.4c2.3-3.9 4-8 5.2-12.4l-12-7a40 40 0 000-14.5c-5.7-1.5-11.3-2-16.9-1.5a59 59 0 018 41.7l7.5 4.4c3.2-3.2 6-6.8 8.2-10.7zm-92.4 0c2.2 4 5 7.5 8.2 10.7l12-7a40 40 0 0012.6 7.3c4-4.1 7.3-8.8 9.7-13.8a59 59 0 01-40-14l-7.7 4.4c1.2 4.3 3 8.5 5.2 12.4zm46.2-80c-4.5 0-9 .5-13.4 1.7V34a40 40 0 00-12.5 7.2c1.5 5.7 4 10.8 7.1 15.4a59 59 0 0132.2-27.7V20a53.3 53.3 0 00-13.4-1.8z" />
-	<path fill="#00B4FF"
-		d="M178 9.2a62.6 62.6 0 11-.1 125.2A62.6 62.6 0 01178 9.2m0-9.2a71.7 71.7 0 100 143.5A71.7 71.7 0 00178 0z" />
-	<path fill="#050A14"
-		d="M96.6 212v4.3c-9.2-.8-15.4-5.8-15.4-17.8V180h4.6v18.4c0 8.6 4 12.6 10.8 13.5zm16-31.9v18.4c0 8.9-4.3 12.8-10.9 13.5v4.4c9.2-.7 15.5-5.6 15.5-18v-18.3h-4.7zM62.2 199v-2.2c0-12.7-8.8-17.4-21-17.4-12.1 0-20.7 4.7-20.7 17.4v2.2c0 12.8 8.6 17.6 20.7 17.6 1.5 0 3-.1 4.4-.3l11.8 6.2 2-3.3-8.2-4-6.4-3.1a32 32 0 01-3.6.2c-9.8 0-16-3.9-16-13.3v-2.2c0-9.3 6.2-13.1 16-13.1 9.9 0 16.3 3.8 16.3 13.1v2.2c0 5.3-2.1 8.7-5.6 10.8l4.8 2.4c3.4-2.8 5.5-7 5.5-13.2zM168 215.6h5.1L156 179.7h-4.8l17 36zM143 205l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.8-3.7H143zm133.7 10.7h5.2l-17.3-35.9h-4.8l17 36zm-25-10.7l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.7-3.7h-14.8zm73.8-2.5c6-1.2 9-5.4 9-11.4 0-8-4.5-10.9-12.9-10.9h-21.4v35.5h4.6v-31.3h16.5c5 0 8.5 1.4 8.5 6.7 0 5.2-3.5 7.7-8.5 7.7h-11.4v4.1h10.7l9.3 12.8h5.5l-9.9-13.2zm-117.4 9.9c-9.7 0-14.7-2.5-18.6-6.3l-2.2 3.8c5.1 5 11 6.7 21 6.7 1.6 0 3.1-.1 4.6-.3l-1.9-4h-3zm18.4-7c0-6.4-4.7-8.6-13.8-9.4l-10.1-1c-6.7-.7-9.3-2.2-9.3-5.6 0-2.5 1.4-4 4.6-5l-1.8-3.8c-4.7 1.4-7.5 4.2-7.5 8.9 0 5.2 3.4 8.7 13 9.6l11.3 1.2c6.4.6 8.9 2 8.9 5.4 0 2.7-2.1 4.7-6 5.8l1.8 3.9c5.3-1.6 8.9-4.7 8.9-10zm-20.3-21.9c7.9 0 13.3 1.8 18.1 5.7l1.8-3.9a30 30 0 00-19.6-5.9c-2 0-4 .1-5.7.3l1.9 4 3.5-.2z" />
-	<path fill="#00B4FF"
-		d="M.5 251.9c29.6-.5 59.2-.8 88.8-1l88.7-.3 88.7.3 44.4.4 44.4.6-44.4.6-44.4.4-88.7.3-88.7-.3a7981 7981 0 01-88.8-1z" />
-	<path fill="none" d="M-565.2 324H-252v15.8h-313.2z" />
-</svg>

BIN
src/assets/softpar_logo.png


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
src/assets/softpar_logo_dark.svg


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
src/assets/softpar_logo_light.svg


+ 1 - 0
src/assets/softpar_logo_mini.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="-15 -15 80 80" fill="none"><rect width="50" height="50" rx="25" fill="#21316C"></rect><path d="M10 19.549C10 14.8784 13.1257 12 17.474 12C21.8222 12 25.0591 15.0456 25.0591 19.5768C25.0591 24.4191 21.4518 26.8101 18.1547 26.8101C16.307 26.8101 14.7743 26.0672 13.8342 24.6466V32.4509H10V19.549ZM21.1646 19.405C21.1646 17.1533 19.6874 15.5888 17.5018 15.5888C15.3161 15.5888 13.8389 17.1533 13.8389 19.405C13.8389 21.6567 15.3161 23.2213 17.5018 23.2213C19.6874 23.2213 21.1646 21.6521 21.1646 19.405Z" fill="#EEF4FF"></path><path d="M25.5635 19.5768C25.5635 15.0456 28.6892 12 33.1486 12C37.608 12 40.6226 14.962 40.6226 19.549V26.4433H37.0986V23.9084C36.3021 25.8165 34.5702 26.8147 32.4401 26.8147C29.2032 26.8147 25.5635 24.4237 25.5635 19.5815V19.5768ZM36.7883 19.405C36.7883 17.1533 35.3111 15.5888 33.1208 15.5888C30.9305 15.5888 29.4579 17.1533 29.4579 19.405C29.4579 21.6567 30.9351 23.2213 33.1208 23.2213C35.3065 23.2213 36.7883 21.6521 36.7883 19.405Z" fill="#EEF4FF"></path><path d="M26.0549 38.8485C19.9933 38.8485 16.1359 35.139 15.0801 33.9737L17.8168 31.4713C18.3957 32.1073 21.5353 35.2736 26.4022 35.1297C30.63 34.9997 34.4179 31.9541 35.1311 31.2067L37.8122 33.7694C37.7659 33.8205 32.9963 38.6442 26.5179 38.8438C26.3651 38.8485 26.2077 38.8531 26.0595 38.8531L26.0549 38.8485Z" fill="#0F86FA"></path></svg>

+ 58 - 48
src/boot/axios.js

@@ -25,62 +25,72 @@ api.interceptors.request.use(
   },
 );
 
-const errorInterceptor = (error) => {
-  return new Promise((resolve, reject) => {
-    if (!error.response) {
+let isRefreshing = false;
+let validQueue = [];
+
+const errorInterceptor = async (error) => {
+  if (!error.config.retryCount) {
+    error.config.retryCount = 0;
+  }
+
+  if (error.config.retryCount >= 3) {
+    return Promise.reject(error);
+  }
+
+  error.config.retryCount++;
+
+  if (!error.response) {
+    Notify.create({
+      message: error.message,
+      type: "negative",
+    });
+    return Promise.reject(error);
+  }
+
+  if (error.response.status === 401) {
+    if (error?.config?.url === "/login") {
       Notify.create({
-        message: error.message,
+        message: error.response.data.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) => {
-                resolve(res);
-              })
-              .catch((apiError) => {
-                reject(apiError);
-              });
-          })
-          .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");
+      return Promise.reject(error);
+    }
 
-        window.location.href = "/login";
+    if (isRefreshing) {
+      return new Promise((resolve, reject) => {
+        validQueue.push({ resolve, reject, config: error.config });
+      });
+    }
+
+    isRefreshing = true;
+
+    try {
+      await useAuth().refreshToken();
+      isRefreshing = false;
 
-        reject(error);
-      } else {
-        Notify.create({
-          message: error.response.data.message,
-          type: "negative",
-        });
-        reject(error);
+      validQueue.forEach((request) => {
+        request.resolve(api.request(request.config));
+      });
+      validQueue = [];
+
+      return await api.request(error.config);
+    } catch (error) {
+      isRefreshing = false;
+      validQueue = [];
+      Cookies.remove("access_token");
+      Cookies.remove("refresh_token");
+      if (window.location.pathname !== "/login") {
+        window.location.href = "/login";
       }
+      return Promise.reject(error);
     }
+  }
+
+  Notify.create({
+    message: error.response.data.message,
+    type: "negative",
   });
+  return Promise.reject(error);
 };
 
 const successInterceptor = (response) => {

+ 5 - 31
src/components/geral/DefaultTable.vue

@@ -140,7 +140,7 @@ const props = defineProps({
   },
 
   // rota da api, ex: /clientes
-  apiRoute: {
+  apiCall: {
     type: Function,
     required: true,
   },
@@ -211,7 +211,7 @@ const props = defineProps({
     default: true,
   },
 
-  noApiRoute: {
+  noApiCall: {
     type: Boolean,
     default: false,
   },
@@ -251,7 +251,7 @@ watch(showInativos, () => {
 });
 
 watch(
-  () => props.apiRoute,
+  () => props.apiCall,
   async () => {
     await onRequest();
   },
@@ -292,7 +292,7 @@ const onAddItem = () => {
 // busca os dados do banco com filtros e pagination
 const onRequest = async () => {
   // const filter = params.filter;
-  if (props.noApiRoute) {
+  if (props.noApiCall) {
     loading.value = false;
     return;
   }
@@ -300,7 +300,7 @@ const onRequest = async () => {
   loading.value = true;
 
   // pega os dados do servidor
-  const response = await props.apiRoute();
+  const response = await props.apiCall();
   // limpa os dados atuais e adiciona os novos
   rows.value.splice(0, rows.value.length, ...response);
 
@@ -333,30 +333,4 @@ onMounted(async () => {
 
 <style lang="scss">
 @import "src/css/table.scss";
-
-.ativo {
-  justify-content: center;
-  align-items: center;
-  padding: 5px 12px;
-  gap: 10px;
-  background: #cfdab7;
-  border-radius: 24px;
-}
-
-.chip {
-  justify-content: center;
-  align-items: center;
-  padding: 5px 12px;
-  gap: 10px;
-  border-radius: 24px;
-}
-
-.inativo {
-  justify-content: center;
-  align-items: flex-start;
-  padding: 5px 12px;
-  gap: 10px;
-  background: #f7cfbb;
-  border-radius: 24px;
-}
 </style>

+ 80 - 30
src/components/geral/LeftMenuLayout.vue

@@ -1,4 +1,3 @@
-<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
 <template>
   <q-drawer
     v-bind="$attrs"
@@ -9,13 +8,13 @@
     :width="250"
     :mini-width="64"
     :breakpoint="500"
-    :mini="miniState"
+    :mini="!$q.screen.lt.md ? miniState : true"
     :behavior="'desktop'"
     class="detached-container"
   >
     <div class="column full-height q-pa-sm no-wrap">
       <div
-        v-if="!$q.screen.lt.sm"
+        v-if="!$q.screen.lt.md"
         class="toggle-button-wrapper absolute"
         style="top: 50%; right: -32px; z-index: 1"
       >
@@ -33,7 +32,7 @@
             anchor="center right"
             self="center left"
             :offset="[10, 10]"
-            >{{ miniState ? "Expandir menu" : "Colapsar menu" }}</q-tooltip
+            >{{ miniState ? $t('navigation.expand_menu') : $t('navigation.collapse_menu') }}</q-tooltip
           >
         </q-btn>
       </div>
@@ -53,12 +52,47 @@
             <q-item-section>{{ user_store.user.name }}</q-item-section>
           </div>
           <q-tooltip
-            v-if="miniState && !$q.screen.lt.sm"
+            v-if="miniState && !$q.screen.lt.md"
             anchor="center right"
             self="center left"
             :offset="[10, 10]"
             >{{ user_store.user.name }}</q-tooltip
           >
+          <q-menu anchor="center right" self="top start">
+            <q-list class="column no-wrap overflow-hidden">
+              <q-item
+                v-ripple
+                v-close-popup
+                clickable
+                :to="{ name: 'ProfilePage' }"
+                exact
+                exact-active-class="menu-selected"
+              >
+                <div class="flex">
+                  <q-item-section avatar>
+                    <q-icon
+                      name="account_circle"
+                      color="primary"
+                      style="font-size: 18px"
+                    />
+                  </q-item-section>
+                  <q-item-section>{{ $t("navigation.perfil") }}</q-item-section>
+                </div>
+              </q-item>
+              <q-item v-ripple clickable @click="logoutFn">
+                <div class="flex">
+                  <q-item-section avatar>
+                    <q-icon
+                      name="logout"
+                      color="negative"
+                      style="font-size: 18px"
+                    />
+                  </q-item-section>
+                  <q-item-section>{{ $t("navigation.logout") }}</q-item-section>
+                </div>
+              </q-item>
+            </q-list>
+          </q-menu>
         </q-item>
       </q-list>
 
@@ -80,7 +114,7 @@
             </q-item-section>
             <q-item-section>{{ $t(menu.title) }}</q-item-section>
             <q-tooltip
-              v-if="miniState && !$q.screen.lt.sm"
+              v-if="miniState && !$q.screen.lt.md"
               anchor="center right"
               self="center left"
               :offset="[10, 10]"
@@ -91,7 +125,7 @@
           <div v-else>
             <template v-if="!miniState">
               <q-tooltip
-                v-if="miniState && !$q.screen.lt.sm"
+                v-if="miniState && !$q.screen.lt.md"
                 anchor="center right"
                 self="center left"
                 :offset="[10, 10]"
@@ -99,7 +133,7 @@
               >
               <q-expansion-item
                 v-model="isExpasionItemExpanded"
-                header-class=" menu-item--spaced"
+                header-class="menu-item--spaced"
                 :class="{
                   'menu-selected':
                     childrenAreActive(menu.children) && !isExpasionItemExpanded,
@@ -119,14 +153,14 @@
                     :to="{ name: child.name }"
                     exact
                     exact-active-class="menu-selected"
-                    class="menu-item--spaced"
+                    class="menu-item--spaced q-pl-lg"
                   >
                     <q-item-section avatar>
                       <q-icon :name="child.icon" style="font-size: 18px" />
                     </q-item-section>
                     <q-item-section>{{ $t(child.title) }}</q-item-section>
                     <q-tooltip
-                      v-if="miniState && !$q.screen.lt.sm"
+                      v-if="miniState && !$q.screen.lt.md"
                       anchor="center right"
                       self="center left"
                       :offset="[10, 10]"
@@ -149,14 +183,14 @@
                 </q-item-section>
                 <q-item-section>{{ $t(menu.title) }}</q-item-section>
                 <q-tooltip
-                  v-if="miniState && !$q.screen.lt.sm"
+                  v-if="miniState && !$q.screen.lt.md"
                   anchor="center right"
                   self="center left"
                   :offset="[10, 10]"
                   >{{ $t(menu.title) }}</q-tooltip
                 >
-                <q-menu anchor="top right" self="top left">
-                  <q-list style="min-width: 100px">
+                <q-menu anchor="center right" self="top start">
+                  <q-list>
                     <q-item
                       v-for="child in menu.childrens"
                       :key="child.name"
@@ -166,7 +200,6 @@
                       :to="{ name: child.name }"
                       exact
                       exact-active-class="menu-selected"
-                      class="menu-item--spaced"
                     >
                       <q-item-section avatar>
                         <q-icon :name="child.icon" style="font-size: 18px" />
@@ -180,40 +213,49 @@
           </div>
         </template>
       </q-list>
-      <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>
-              <q-icon name="logout" color="negative" style="font-size: 18px" />
-            </q-item-section>
-            <q-item-section>Sair</q-item-section>
+      <q-list class="q-mt-auto">
+        <q-item v-ripple clickable @click="openUrl('https://softpar.inf.br')">
+          <div class="flex full-width justify-center">
+            <q-img
+              :src="
+                miniState
+                  ? LogoSoftparMini
+                  : $q.dark.isActive
+                    ? LogoSoftparLight
+                    : LogoSoftparDark
+              "
+              style="width: 100%; height: 30px; max-width: 114px"
+            />
           </div>
-          <q-tooltip
-            v-if="miniState && !$q.screen.lt.sm"
-            anchor="center right"
-            self="center left"
-            :offset="[10, 10]"
-            >Sair</q-tooltip
-          >
         </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 } from "vue";
+import { ref, onMounted, watch } 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";
+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";
 
 const { logout } = useAuth();
 const router = useRouter();
 const route = useRoute();
 const user_store = userStore();
 
+const version = "0.0.1";
+
 const leftDrawerOpen = ref(true);
-const miniState = ref(true);
+const miniState = ref(Cookies.get("miniState") === "true" ?? false);
 
 const childrenAreActive = (children) => {
   if (!children) return false;
@@ -285,6 +327,14 @@ const logoutFn = async () => {
   router.push({ name: "LoginPage" });
 };
 
+const openUrl = (url) => {
+  window.open(url, "_blank");
+};
+
+watch(miniState, () => {
+  Cookies.set("miniState", miniState.value);
+});
+
 onMounted(() => {
   getMenuAccess();
 });

+ 178 - 0
src/components/geral/LeftMenuLayoutMobile.vue

@@ -0,0 +1,178 @@
+<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+<template>
+  <q-drawer
+    v-bind="$attrs"
+    v-model="leftDrawerOpen"
+    :width="250"
+    :breakpoint="500"
+    behavior="mobile"
+    class="detached-container"
+  >
+    <div class="column full-height q-pa-sm no-wrap">
+      <q-list class="column no-wrap">
+        <template v-for="menu in menus" :key="menu.name">
+          <!-- Single Menu -->
+          <q-item
+            v-if="menu.type === 'single'"
+            v-ripple
+            clickable
+            exact-active-class="menu-selected"
+            exact
+            active-class="menu-selected"
+            :to="{ name: menu.name }"
+            class="q-my-xs"
+          >
+            <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-item>
+          <!-- Expansive Menu with children -->
+          <q-expansion-item
+            v-else
+            v-model="isExpasionItemExpanded"
+            header-class="menu-item--spaced"
+            :class="{
+              'menu-selected':
+                childrenAreActive(menu.children) && !isExpasionItemExpanded,
+            }"
+          >
+            <template #header>
+              <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>
+            </template>
+            <div v-for="child in menu.childrens" :key="child.name">
+              <q-item
+                v-ripple
+                clickable
+                :to="{ name: child.name }"
+                exact
+                exact-active-class="menu-selected"
+                class="menu-item--spaced q-pl-lg"
+              >
+                <q-item-section avatar>
+                  <q-icon :name="child.icon" style="font-size: 18px" />
+                </q-item-section>
+                <q-item-section>{{ $t(child.title) }}</q-item-section>
+              </q-item>
+            </div>
+          </q-expansion-item>
+        </template>
+      </q-list>
+
+      <q-list class="q-mt-auto">
+        <q-item v-ripple clickable @click="openUrl('https://softpar.inf.br')">
+          <div class="flex full-width justify-center">
+            <q-img
+              :src="$q.dark.isActive ? LogoSoftparLight : LogoSoftparDark"
+              style="width: 100%; height: 30px; max-width: 114px"
+            />
+          </div>
+        </q-item>
+      </q-list>
+      <div class="full-width text-center text-subtitle3">
+        <span class="text-caption text-weight-light">1.0.0</span>
+      </div>
+    </div>
+  </q-drawer>
+</template>
+
+<script setup>
+import { ref, onMounted } from "vue";
+import { permissionStore } from "src/stores/permission";
+import { useRoute } from "vue-router";
+import LogoSoftparLight from "src/assets/softpar_logo_light.svg";
+import LogoSoftparDark from "src/assets/softpar_logo_dark.svg";
+const route = useRoute();
+
+const leftDrawerOpen = ref(false);
+
+const childrenAreActive = (children) => {
+  if (!children) return false;
+  return children.some((child) => {
+    return route.path.includes(child.path);
+  });
+};
+
+const isExpasionItemExpanded = ref(false);
+
+const menus = ref([
+  {
+    type: "single",
+    title: "navigation.dashboard",
+    name: "HomePage",
+    icon: "mdi-home-variant-outline",
+    disable: false,
+    permission: false,
+    permissionScope: "dashboard",
+  },
+  {
+    type: "expansive",
+    title: "navigation.registration",
+    icon: "mdi-cog-outline",
+    disable: false,
+    permission: false,
+    permissionScope: "config",
+    childrens: [
+      {
+        type: "single",
+        title: "navigation.users",
+        name: "UsersPage",
+        icon: "mdi-account-multiple-outline",
+        disable: false,
+        permission: false,
+        permissionScope: "config.user",
+      },
+    ],
+  },
+]);
+
+const getMenuAccess = () => {
+  const { getAccess } = permissionStore();
+  menus.value = menus.value
+    .map((menu) => {
+      if (menu.type === "expansive") {
+        if (getAccess(menu.permissionScope, "menu")) {
+          menu.permission = true;
+        }
+        menu.childrens = menu.childrens.filter((children) => {
+          children.permission = getAccess(children.permissionScope, "menu");
+          return children.permission;
+        });
+        return menu.childrens.length > 0 ? menu : null;
+      } else {
+        menu.permission = getAccess(menu.permissionScope, "menu");
+        return menu;
+      }
+    })
+    .filter((menu) => menu !== null);
+};
+
+const openUrl = (url) => {
+  window.open(url, "_blank");
+};
+
+onMounted(() => {
+  getMenuAccess();
+});
+</script>
+
+<style lang="scss" scoped>
+@import "/src/css/quasar.variables.scss";
+.text-subtitle3 {
+  font-size: 1.1rem !important;
+  font-weight: 400 !important;
+}
+
+.menu-selected {
+  background-color: rgba($primary, 0.1);
+  color: $primary;
+}
+
+.menu-item--spaced {
+  margin-top: 5px;
+  margin-bottom: 5px;
+}
+</style>

+ 0 - 2
src/composables/useAuth.js

@@ -70,8 +70,6 @@ export const useAuth = () => {
         });
         userStore().user = payload.user;
       }
-
-      return response;
     } catch (error) {
       return Promise.reject(error);
     }

+ 19 - 1
src/css/app.scss

@@ -1,4 +1,5 @@
-// app global css in SCSS format
+@use "sass:map";
+@use "src/css/quasar.variables.scss";
 
 .input-disable {
   .q-field--outlined .q-field__control::before {
@@ -7,6 +8,13 @@
   }
 }
 
+.q-toolbar {
+  position: relative;
+  padding: 0 12px;
+  min-height: 50px;
+  width: auto;
+}
+
 .q-drawer:has(.detached-container) {
   margin: 16px !important;
   margin-bottom: 16px !important;
@@ -14,3 +22,13 @@
   border-radius: 6px !important;
   transition: all;
 }
+
+.body--light {
+  .q-drawer:has(.detached-container) {
+    background: #{map.get($colors, "dark")};
+  }
+
+  .q-menu {
+    background: #{map.get($colors, "dark")};
+  }
+}

+ 4 - 1
src/css/quasar.variables.scss

@@ -25,6 +25,7 @@ $colors: (
   // Light - Blue 400
   "primary-dark": #1565c0,
 
+  "dark": #f1f1f1,
   // Dark - Blue 800
   // Secondary Colors and Variants
   "secondary": #9c27b0,
@@ -89,12 +90,14 @@ $colors: (
 // Dark Theme Color Overrides
 $colors-dark: (
   // Primary Colors - Lighter in Dark Mode
-  "primary": #90caf9,
+  "primary": #4488c0,
   // Blue 200
   "primary-light": #e3f2fd,
   // Blue 50
   "primary-dark": #42a5f5,
 
+  "dark": #1d1d1d,
+
   // Blue 400
   // Secondary Colors - Lighter in Dark Mode
   "secondary": #ce93d8,

+ 11 - 1
src/css/table.scss

@@ -11,7 +11,7 @@
   }
 
   .body--light & {
-    --table-bg-color: #{map.get($colors, "page")}; // Light background
+    --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
   }
@@ -186,3 +186,13 @@
     }
   }
 }
+
+.q-table__grid-item-card {
+  .body--light & {
+    background: #{map.get($colors, "dark")};
+  }
+
+  .body--dark & {
+    background: #{map.get($colors, "background-3")};
+  }
+}

+ 3 - 1
src/i18n/locales/en.json

@@ -36,7 +36,9 @@
     "explore": "Explore",
     "opportunities": "Opportunities",
     "interests": "Interests",
-    "negotiations": "Negotiations"
+    "negotiations": "Negotiations",
+    "expand_menu": "Expand menu",
+    "collapse_menu": "Collapse menu"
   },
   "users": {
     "user": "{something} user | {something} users",

+ 3 - 1
src/i18n/locales/es.json

@@ -36,7 +36,9 @@
     "explore": "Explorar",
     "opportunities": "Oportunidades",
     "interests": "Intereses",
-    "negotiations": "Negociaciones"
+    "negotiations": "Negociaciones",
+    "expand_menu": "Expandir menu",
+    "collapse_menu": "Colapsar menu"
   },
   "users": {
     "user": "{something} usuario | {something} usuarios",

+ 3 - 1
src/i18n/locales/pt.json

@@ -36,7 +36,9 @@
     "explore": "Explorar",
     "opportunities": "Oportunidades",
     "interests": "Interesses",
-    "negotiations": "Negociações"
+    "negotiations": "Negociações",
+    "expand_menu": "Expandir menu",
+    "collapse_menu": "Colapsar menu"
   },
   "users": {
     "user": "{something} usuario | {something} usuarios",

+ 86 - 11
src/layouts/MainLayout.vue

@@ -1,22 +1,78 @@
 <template>
   <q-layout class="relative" view="hHh lpR fFf">
-    <LeftMenuLayout v-model="leftDrawerOpen" />
-
-    <q-page-container>
-      <q-page
+    <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-toolbar
+        class="flex justify-between bg-dark"
+        style="border-radius: 6px !important"
       >
+        <q-btn dense flat @click="toggleLeftDrawer">
+          <q-icon name="menu" :color="$q.dark.isActive ? 'white' : 'black'" />
+        </q-btn>
+        <q-btn dense flat>
+          <img
+            :src="someAvatar()"
+            alt="avatar"
+            style="width: 20px; height: 20px; border-radius: 50%"
+          />
+          <q-menu anchor="center right" self="top start">
+            <q-list class="column no-wrap overflow-hidden">
+              <q-item
+                v-ripple
+                v-close-popup
+                clickable
+                :to="{ name: 'ProfilePage' }"
+                exact
+                exact-active-class="menu-selected"
+              >
+                <div class="flex">
+                  <q-item-section avatar>
+                    <q-icon
+                      name="account_circle"
+                      color="primary"
+                      style="font-size: 18px"
+                    />
+                  </q-item-section>
+                  <q-item-section>{{ $t("navigation.perfil") }}</q-item-section>
+                </div>
+              </q-item>
+              <q-item v-ripple clickable @click="logoutFn">
+                <div class="flex">
+                  <q-item-section avatar>
+                    <q-icon
+                      name="logout"
+                      color="negative"
+                      style="font-size: 18px"
+                    />
+                  </q-item-section>
+                  <q-item-section>{{ $t("navigation.logout") }}</q-item-section>
+                </div>
+              </q-item>
+            </q-list>
+          </q-menu>
+        </q-btn>
+      </q-toolbar>
+    </q-header>
+    <q-page-container>
+      <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;
+          :style="
+            $q.screen.lt.sm
+              ? 'height: calc(100dvh - 68px - env(safe-area-inset-top)) !important;'
+              : 'height: calc(100dvh - env(safe-area-inset-top)) !important;'
           "
         >
           <router-view v-slot="{ Component }">
             <Transition mode="out-in">
-              <component :is="Component" />
+              <component
+                :is="Component"
+                style="padding: 20px !important; padding-right: 10px !important"
+                :style="
+                  $q.screen.lt.sm ? 'padding-right: 10px !important;' : ''
+                "
+              />
             </Transition>
           </router-view>
         </q-scroll-area>
@@ -28,17 +84,36 @@
 <script setup>
 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";
 
 defineOptions({
   name: "MainLayout",
 });
 
+const { logout } = useAuth();
+const route = useRoute();
 const leftDrawerOpen = ref(true);
 const scrollAreaRef = useTemplateRef("scrollAreaRef");
-const route = useRoute();
+const router = useRouter();
 
 let oldValue = route.path;
+
+const someAvatar = () => {
+  return "https://cdn.quasar.dev/img/avatar4.jpg";
+};
+
+const logoutFn = async () => {
+  await logout();
+  router.push({ name: "LoginPage" });
+};
+
+const toggleLeftDrawer = () => {
+  leftDrawerOpen.value = !leftDrawerOpen.value;
+};
+
 watch(route, (value) => {
   if (oldValue.path != value.path) {
     scrollAreaRef.value.setScrollPosition("vertical", 0, 0);

+ 1 - 1
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-grey-box">
+    <q-card flat class="login-card q-pa-md q-pt-xl bg-dark">
       <div class="text-center">
         <q-img :src="Logo" style="max-width: 250px" />
         <div class="text-h6">{{ $t("general.welcome") }}</div>

+ 1 - 1
src/pages/users/UsersPage.vue

@@ -4,7 +4,7 @@
     <DefaultTable
       :key="tableKey"
       :columns="columns"
-      :api-route="getUsers"
+      :api-call="getUsers"
       :mostrar-selecao-de-colunas="false"
       :mostrar-botao-fullscreen="false"
       :mostrar-toggle-inativos="false"

Някои файлове не бяха показани, защото твърде много файлове са промени