Przeglądaj źródła

feat: :sparkles: feat (login) criação da pagina de login

foi criada a página de login do serprati com diferenciação por tipo de usuário e layout adequado

fase:dev | origin:escopo
Gustavo Zanatta 2 tygodni temu
rodzic
commit
77b17d2d2f

+ 2 - 2
quasar.config.js

@@ -56,7 +56,7 @@ export default defineConfig((ctx) => {
       // analyze: true,
       env: {
         APP_NAME: "skeleton",
-        API_URL: ctx.dev ? "http://localhost:8000" : "http://localhost:8000",
+        API_URL: ctx.dev ? "http://localhost:3000" : "http://localhost:8000",
         PASSWORD: ctx.dev ? "S@ft2080." : "",
         WEBSOCKET_API: ctx.dev
           ? "http://localhost:4321/"
@@ -100,7 +100,7 @@ export default defineConfig((ctx) => {
     // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
     devServer: {
       // https: true
-      open: true, // opens browser window automatically
+      open: false, // opens browser window automatically
     },
 
     // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework

+ 5 - 3
src/App.vue

@@ -14,9 +14,11 @@ defineOptions({
 const { locale } = useI18n();
 
 const $q = useQuasar();
-const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
-  ? "dark"
-  : "light";
+// const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
+//   ? "dark"
+//   : "light";
+
+const systemTheme = "light";
 
 const theme = Cookies.get("theme") || systemTheme;
 

+ 10 - 0
src/api/auth.js

@@ -0,0 +1,10 @@
+import api from "src/api";
+
+export const forgotPassword = (email, tipo) =>
+  api.post("/forgot-password", { email, tipo });
+
+export const verifyCode = (email, codigo) =>
+  api.post("/verify-code", { email, codigo });
+
+export const resetPassword = (email, codigo, password, password_confirmation) =>
+  api.post("/reset-password", { email, codigo, password, password_confirmation });

Plik diff jest za duży
+ 6 - 0
src/assets/logo_serprati.svg


BIN
src/assets/pessoas_fundo.jpg


+ 8 - 5
src/composables/useAuth.js

@@ -10,11 +10,12 @@ export const useAuth = () => {
     await permissionStore().fetchScopes();
   };
 
-  const login = async (email, password) => {
+  const login = async (email, password, tipo) => {
     try {
       const response = await api.post("/login", {
-        email: email,
-        password: password,
+        email,
+        password,
+        tipo,
       });
 
       if (response.status === 200) {
@@ -31,9 +32,11 @@ export const useAuth = () => {
       const response = await api.post("/logout");
       if (response.status === 200) {
         userStore().resetUser();
+        permissionStore().resetScopes();
       }
-    } catch (error) {
-      console.error(error);
+    } catch {
+      userStore().resetUser();
+      permissionStore().resetScopes();
     }
   };
 

+ 12 - 12
src/css/app.scss

@@ -89,21 +89,21 @@ body.body--light {
   }
 }
 
-body.body--dark {
-  background: #{map.get($colors-dark, "page")};
+// body.body--dark {
+//   background: #{map.get($colors-dark, "page")};
 
-  .q-drawer:has(.detached-container) {
-    background: #{map.get($colors-dark, "surface")} !important;
-  }
+//   .q-drawer:has(.detached-container) {
+//     background: #{map.get($colors-dark, "surface")} !important;
+//   }
 
-  .q-menu {
-    background: #{map.get($colors-dark, "surface")};
-  }
+//   .q-menu {
+//     background: #{map.get($colors-dark, "surface")};
+//   }
 
-  .card-ring {
-    box-shadow: 0 0 0 1px #505050 !important;
-  }
-}
+//   .card-ring {
+//     box-shadow: 0 0 0 1px #505050 !important;
+//   }
+// }
 
 .q-card__actions .q-btn {
   padding: 10px 16px;

+ 112 - 107
src/css/quasar.variables.scss

@@ -2,12 +2,12 @@
 // --------------------------------------------------
 
 // Primary Theme Colors
-$primary: #1976d2; // Material Blue 700
-$secondary: #9c27b0; // Material Purple 500
-$accent: #e91e63; // Material Pink 500
+// $primary: #1976d2; // Material Blue 700
+// $secondary: #661D75; // SerpRati Brand Purple
+// $accent: #e91e63; // Material Pink 500
 
 // Dark Theme Base Colors
-$dark: #1d1d1d;
+// $dark: #1d1d1d;
 
 // Status Colors
 $positive: #2e7d32; // Material Green 800
@@ -17,39 +17,44 @@ $warning: #ed6c02; // Material Orange 800
 
 // 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 - Blue 800
-  // Secondary Colors and Variants
-  "secondary": #9c27b0,
-  // Base - Purple 500
-  "secondary-light": #ba68c8,
-  // 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
-  "page": #f1f1f1,
-
-  // Surface Colors
-  "surface": #ffffff,
-  "surface-light": #f5f5f5,
-  "surface-dark": #f1f1f1,
+  // Brand Colors
+  // $primary: #35a30a;
+  "primary": #35a30a,
+  "primary-4": #cde8c2,
+
+  "text": #161616,
+  "text-2": #505050,
+  "white-2": #fefcff,
+
+  "border": #c0c0c0,
+  // "error": #d4183d,
+
+  // Violet Colors
+  "violet-light": #f0e8f1,
+  "violet-light-hover": #e8ddea,
+  "violet-light-active": #d0b9d4,
+  "violet-normal": #661d75,
+  "violet-normal-hover": #5c1a69,
+  "violet-normal-active": #52175e,
+  "violet-dark": #4d1658,
+  "violet-dark-hover": #3d1146,
+  "violet-dark-active": #2e0d35,
+  "violet-darker": #240a29,
+
+  // Neutral Colors
+  "neutral-light": #fefefe,
+  "neutral-light-hover": #fdfdfd,
+  "neutral-light-active": #fbfbfb,
+  "neutral-normal": #f2f2f2,
+  "neutral-normal-hover": #dadada,
+  "neutral-normal-active": #c2c2c2,
+  "neutral-dark": #b6b6b6,
+  "neutral-dark-hover": #919191,
+  "neutral-dark-active": #6d6d6d,
+  "neutral-darker": #555555,
 
   //text color
-  "text": #000000,
+  // "text": #000000,
 
   // Status Colors with Variants
   "success": #2e7d32,
@@ -81,68 +86,68 @@ $colors: (
 );
 
 // Dark Theme Color Overrides
-$colors-dark: (
-  // Primary Colors and Variants
-  "primary": #1976d2,
-  // Base - Blue 700
-  "primary-light": #42a5f5,
-  // Blue 50
-  "primary-dark": #1565c0,
-
-  "dark": #1d1d1d,
-
-  // Blue 400
-  // Secondary Colors - Lighter in Dark Mode
-  "secondary": #ce93d8,
-  // Purple 200
-  "secondary-light": #f3e5f5,
-  // Purple 50
-  "secondary-dark": #ab47bc,
-
-  //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
-  "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,
-
-  // Red 700
-  "warning": #ffa726,
-  // Orange 400
-  "warning-light": #ffb74d,
-  // Orange 300
-  "warning-dark": #f57c00,
-
-  // Orange 700
-  "info": #29b6f6,
-  // Light Blue 400
-  "info-light": #4fc3f7,
-  // Light Blue 300
-  "info-dark": #0288d1 // Light Blue 700
-);
+// $colors-dark: (
+//   // Primary Colors and Variants
+//   "primary": #1976d2,
+//   // Base - Blue 700
+//   "primary-light": #42a5f5,
+//   // Blue 50
+//   "primary-dark": #1565c0,
+
+//   "dark": #1d1d1d,
+
+//   // Blue 400
+//   // Secondary Colors - Lighter in Dark Mode
+//   "secondary": #9c27b0,
+//   // Purple 500
+//   "secondary-light": #ce93d8,
+//   // Purple 200
+//   "secondary-dark": #661D75,
+
+//   //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
+//   "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,
+
+//   // Red 700
+//   "warning": #ffa726,
+//   // Orange 400
+//   "warning-light": #ffb74d,
+//   // Orange 300
+//   "warning-dark": #f57c00,
+
+//   // Orange 700
+//   "info": #29b6f6,
+//   // Light Blue 400
+//   "info-light": #4fc3f7,
+//   // Light Blue 300
+//   "info-dark": #0288d1 // Light Blue 700
+// );
 
 // Generate color utility classes for light theme
 @each $name, $color in $colors {
@@ -155,13 +160,13 @@ $colors-dark: (
 }
 
 // Generate color utility classes for dark theme
-.body--dark {
-  @each $name, $color in $colors-dark {
-    .text-#{$name} {
-      color: $color !important;
-    }
-    .bg-#{$name} {
-      background: $color !important;
-    }
-  }
-}
+// .body--dark {
+//   @each $name, $color in $colors-dark {
+//     .text-#{$name} {
+//       color: $color !important;
+//     }
+//     .bg-#{$name} {
+//       background: $color !important;
+//     }
+//   }
+// }

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

@@ -162,7 +162,32 @@
     "registration": "Registration",
     "confirm_password": "Confirm Password",
     "agreed_terms": "I agree with the terms",
-    "agreed_privacy": "I agree with the privacy policy"
+    "agreed_privacy": "I agree with the privacy policy",
+    "select_type_hint": "Select your profile and sign in",
+    "type": {
+      "administrador": "Administrator",
+      "associado": "Associate",
+      "parceiro": "Partner"
+    },
+    "forgot_password": "Forgot password?",
+    "forgot_password_title": "Enter your email to reset your password",
+    "forgot_password_description": "We will send a verification code to your email.",
+    "continue": "Continue",
+    "verify_email_title": "Check your email",
+    "verify_email_description": "Please check your inbox and click the link to proceed.",
+    "no_email_hint": "Did not receive the email? Check your",
+    "check_spam": "spam or junk folder.",
+    "enter_code": "Enter Code",
+    "enter_code_title": "Enter your code here",
+    "verify": "Verify",
+    "resend_email": "Resend email",
+    "remember_password": "Remember your password?",
+    "do_login": "Sign in",
+    "new_password_title": "Create your new password",
+    "password_hint": "Minimum 6 characters with uppercase, lowercase and numbers",
+    "confirm": "Confirm",
+    "back_to_site": "Back to Site",
+    "wrong_type": "User found, but the selected type is incorrect. Please select the correct login type."
   },
   "business": {
     "advertise": "Advertise",
@@ -185,14 +210,16 @@
       "cpf": "This field must be a valid CPF",
       "cnpj": "This field must be a valid CNPJ",
       "cep": "This field must be a valid ZIP code",
-      "value_smaller_than_zero": "Value cannot be less than zero"
+      "value_smaller_than_zero": "Value cannot be less than zero",
+      "code_length": "The code must have 6 digits"
     },
     "permissions": {
       "view": "You don't have permission to view this",
       "create": "You don't have permission to create this",
       "edit": "You don't have permission to edit this",
       "delete": "You don't have permission to delete this",
-      "add": "You don't have permission to add this"
+      "add": "You don't have permission to add this",
+      "wrong_type": "You do not have permission to access this area."
     }
   },
   "http": {
@@ -312,7 +339,20 @@
       "city": "City",
       "state": "State",
       "country": "Country",
-      "exit": "Exit"
+      "exit": "Exit",
+      "partners": "Partners & Agreements",
+      "partner_agreements": "Agreements",
+      "partner_services": "Services",
+      "store": "Store",
+      "store_items": "Items",
+      "store_orders": "Orders",
+      "appointments": "Appointments",
+      "my_appointments": "My Appointments",
+      "received_appointments": "Received Appointments",
+      "notifications": "Notifications",
+      "categories": "Categories",
+      "my_profile": "My Profile",
+      "my_services": "My Services"
     }
   },
   "charts": {
@@ -366,4 +406,4 @@
       }
     }
   }
-}
+}

+ 45 - 5
src/i18n/locales/es.json

@@ -162,7 +162,32 @@
     "registration": "Registro",
     "confirm_password": "Confirmar contraseña",
     "agreed_terms": "Acepto los términos",
-    "agreed_privacy": "Acepto la política de privacidad"
+    "agreed_privacy": "Acepto la política de privacidad",
+    "select_type_hint": "Seleccione su perfil e inicie sesión",
+    "type": {
+      "administrador": "Administrador",
+      "associado": "Asociado",
+      "parceiro": "Socio"
+    },
+    "forgot_password": "¿Olvidé mi contraseña?",
+    "forgot_password_title": "Ingrese su correo para restablecer su contraseña",
+    "forgot_password_description": "Le enviaremos un código de verificación a su correo.",
+    "continue": "Continuar",
+    "verify_email_title": "Verifique su correo",
+    "verify_email_description": "Por favor, revise su bandeja de entrada y haga clic en el enlace para continuar.",
+    "no_email_hint": "¿No recibió el correo? Revise su carpeta de",
+    "check_spam": "spam o correo no deseado.",
+    "enter_code": "Ingresar Código",
+    "enter_code_title": "Ingrese su código aquí",
+    "verify": "Verificar",
+    "resend_email": "Reenviar correo",
+    "remember_password": "¿Recuerda su contraseña?",
+    "do_login": "Iniciar sesión",
+    "new_password_title": "Cree su nueva contraseña",
+    "password_hint": "Mínimo 6 caracteres con mayúsculas, minúsculas y números",
+    "confirm": "Confirmar",
+    "back_to_site": "Volver al Sitio",
+    "wrong_type": "Usuario encontrado, pero el tipo seleccionado es incorrecto. Seleccione el tipo correcto."
   },
   "business": {
     "advertise": "Anunciar",
@@ -185,14 +210,16 @@
       "cpf": "Este campo debe ser un CPF válido",
       "cnpj": "Este campo debe ser un CNPJ válido",
       "cep": "Este campo debe ser un código postal válido",
-      "value_smaller_than_zero": "El valor no puede ser menor que cero"
+      "value_smaller_than_zero": "El valor no puede ser menor que cero",
+      "code_length": "El código debe tener 6 dígitos"
     },
     "permissions": {
       "view": "No tienes permiso para ver esto",
       "create": "No tienes permiso para crear esto",
       "edit": "No tienes permiso para editar esto",
       "delete": "No tienes permiso para eliminar esto",
-      "add": "No tienes permiso para añadir esto"
+      "add": "No tienes permiso para añadir esto",
+      "wrong_type": "No tiene permiso para acceder a esta área."
     }
   },
   "http": {
@@ -312,7 +339,20 @@
       "city": "Ciudad",
       "state": "Estado/Provincia",
       "country": "País",
-      "exit": "Salir"
+      "exit": "Salir",
+      "partners": "Socios y Convenios",
+      "partner_agreements": "Convenios",
+      "partner_services": "Servicios",
+      "store": "Tienda",
+      "store_items": "Artículos",
+      "store_orders": "Pedidos",
+      "appointments": "Citas",
+      "my_appointments": "Mis Citas",
+      "received_appointments": "Citas Recibidas",
+      "notifications": "Notificaciones",
+      "categories": "Categorías",
+      "my_profile": "Mi Perfil",
+      "my_services": "Mis Servicios"
     }
   },
   "charts": {
@@ -366,4 +406,4 @@
       }
     }
   }
-}
+}

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

@@ -162,7 +162,32 @@
     "registration": "Cadastro",
     "confirm_password": "Confirmar Senha",
     "agreed_terms": "Eu concordo com os termos",
-    "agreed_privacy": "Eu concordo com a política de privacidade"
+    "agreed_privacy": "Eu concordo com a política de privacidade",
+    "select_type_hint": "Selecione seu perfil e faça o Login",
+    "type": {
+      "administrador": "Administrador",
+      "associado": "Associado",
+      "parceiro": "Parceiro"
+    },
+    "forgot_password": "Esqueci a senha?",
+    "forgot_password_title": "Informe seu e-mail para redefinir sua senha",
+    "forgot_password_description": "Enviaremos um código de verificação para o seu e-mail.",
+    "continue": "Continuar",
+    "verify_email_title": "Verifique seu e-mail",
+    "verify_email_description": "Por favor, confira sua caixa de entrada e clique no link para prosseguir.",
+    "no_email_hint": "Não recebeu o e-mail? Verifique a caixa de",
+    "check_spam": "spam ou lixo eletrônico.",
+    "enter_code": "Digitar Código",
+    "enter_code_title": "Digite seu código aqui",
+    "verify": "Verificar",
+    "resend_email": "Reenviar e-mail",
+    "remember_password": "Lembrou sua senha?",
+    "do_login": "Faça seu Login",
+    "new_password_title": "Vamos cadastrar sua nova senha",
+    "password_hint": "Mínimo de 6 caracteres com letras maiúsculas, minúsculas e números",
+    "confirm": "Confirmar",
+    "back_to_site": "Voltar ao Site",
+    "wrong_type": "Usuário encontrado, mas o tipo selecionado está incorreto. Selecione o login correto."
   },
   "business": {
     "advertise": "Anunciar",
@@ -185,14 +210,16 @@
       "cpf": "Este campo deve ser um CPF válido",
       "cnpj": "Este campo deve ser um CNPJ válido",
       "cep": "Este campo deve ser um CEP válido",
-      "value_smaller_than_zero": "O valor não pode ser menor que zero"
+      "value_smaller_than_zero": "O valor não pode ser menor que zero",
+      "code_length": "O código deve ter 6 dígitos"
     },
     "permissions": {
       "view": "Você não tem permissão para visualizar isto",
       "create": "Você não tem permissão para criar isto",
       "edit": "Você não tem permissão para editar isto",
       "delete": "Você não tem permissão para excluir isto",
-      "add": "Você não tem permissão para adicionar isto"
+      "add": "Você não tem permissão para adicionar isto",
+      "wrong_type": "Você não tem permissão para acessar esta área."
     }
   },
   "http": {
@@ -312,7 +339,20 @@
       "city": "Cidades",
       "state": "Estados",
       "country": "Países",
-      "exit": "Sair"
+      "exit": "Sair",
+      "partners": "Parceiros e Convênios",
+      "partner_agreements": "Convênios",
+      "partner_services": "Serviços",
+      "store": "Loja",
+      "store_items": "Itens",
+      "store_orders": "Pedidos",
+      "appointments": "Agendamentos",
+      "my_appointments": "Meus Agendamentos",
+      "received_appointments": "Agendamentos Recebidos",
+      "notifications": "Notificações",
+      "categories": "Categorias",
+      "my_profile": "Meu Perfil",
+      "my_services": "Meus Serviços"
     }
   },
   "charts": {
@@ -366,4 +406,4 @@
       }
     }
   }
-}
+}

+ 151 - 0
src/pages/login/ForgotPasswordPage.vue

@@ -0,0 +1,151 @@
+<template>
+  <q-page class="login-page column items-center justify-center">
+    <div class="login-overlay" />
+
+    <div class="login-card column items-center">
+      <q-img :src="Logo" class="login-logo q-mb-lg" />
+
+      <UserTypeBadge :tipo="tipo" />
+
+      <p class="login-title">{{ $t("auth.forgot_password_title") }}</p>
+      <p class="login-description">{{ $t("auth.forgot_password_description") }}</p>
+
+      <q-form
+        ref="formRef"
+        class="full-width"
+        autocomplete="off"
+        @submit="onSubmit"
+      >
+        <DefaultInput
+          v-model="form.email"
+          v-model:error="validationErrors.email"
+          type="email"
+          autofocus
+          :label="$t('common.terms.email')"
+          :rules="[inputRules.required, inputRules.email]"
+        >
+          <template #append>
+            <q-icon name="mdi-account-outline" color="grey-5" />
+          </template>
+        </DefaultInput>
+
+        <q-btn
+          class="full-width q-mt-sm login-btn"
+          color="secondary"
+          :label="$t('auth.continue')"
+          type="submit"
+          unelevated
+          :loading
+        >
+          <template #loading>
+            <q-spinner />
+          </template>
+        </q-btn>
+      </q-form>
+
+      <a href="#" class="login-link q-mt-md" @click.prevent="goBack">
+        <q-icon name="mdi-arrow-left" size="12px" class="q-mr-xs" />
+        {{ $t("auth.back_to_site") }}
+      </a>
+    </div>
+  </q-page>
+</template>
+
+<script setup>
+import { ref, useTemplateRef } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { useInputRules } from "src/composables/useInputRules";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { forgotPassword } from "src/api/auth";
+
+import Logo from "src/assets/logo_serprati.svg";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+import UserTypeBadge from "./components/UserTypeBadge.vue";
+
+const router = useRouter();
+const route = useRoute();
+const { inputRules } = useInputRules();
+
+const tipo = route.query.tipo ?? "administrador";
+const formRef = useTemplateRef("formRef");
+
+const {
+  loading,
+  validationErrors,
+  execute: submitForm,
+} = useSubmitHandler({
+  onSuccess: () =>
+    router.push({
+      name: "VerifyEmailPage",
+      query: { email: form.value.email, tipo },
+    }),
+  formRef,
+});
+
+const form = ref({ email: null });
+
+const onSubmit = async () => {
+  await submitForm(() => forgotPassword(form.value.email, tipo));
+};
+
+const goBack = () => router.push({ name: "LoginPage" });
+</script>
+
+<style lang="scss" scoped>
+@use "src/css/quasar.variables.scss";
+
+.login-page {
+  min-height: 100dvh;
+  background-image: url("src/assets/pessoas_fundo.jpg");
+  background-size: cover;
+  background-position: center;
+  position: relative;
+}
+.login-overlay {
+  position: absolute;
+  inset: 0;
+  background: rgba(74, 20, 140, 0.72);
+  z-index: 0;
+}
+.login-card {
+  position: relative;
+  z-index: 1;
+  width: 100%;
+  max-width: 400px;
+  padding: 32px 24px 40px;
+}
+.login-logo {
+  width: 180px;
+  filter: brightness(0) invert(1);
+}
+.login-title {
+  color: #fff;
+  font-size: 18px;
+  font-weight: 700;
+  margin: 0 0 8px;
+  text-align: center;
+}
+.login-description {
+  color: rgba(255, 255, 255, 0.8);
+  font-size: 13px;
+  text-align: center;
+  margin: 0 0 20px;
+}
+.login-btn {
+  height: 44px;
+  font-size: 15px;
+  font-weight: 600;
+}
+.login-link {
+  color: rgba(255, 255, 255, 0.75);
+  font-size: 13px;
+  text-decoration: none;
+  cursor: pointer;
+  &:hover { color: #fff; text-decoration: underline; }
+}
+:deep(.q-field) {
+  .q-field__control { background: rgba(255, 255, 255, 0.12) !important; border-radius: 8px; }
+  .q-field__label, .q-field__native, .q-field__input { color: #fff !important; }
+  .q-field__bottom { color: rgba(255, 100, 100, 0.9) !important; }
+}
+</style>

+ 194 - 73
src/pages/login/LoginPage.vue

@@ -1,65 +1,85 @@
 <template>
-  <q-page class="column">
-    <div
-      flat
-      class="column justify-around items-center flex-grow full-width full-height z-top frosted-glass"
-    >
-      <div class="column flex-center full-width q-px-md">
-        <q-img :src="Logo" style="max-width: 650px" />
-        <div class="text-h5 q-mt-xl">{{ $t("auth.login") }}</div>
-      </div>
+  <q-page class="login-page column items-center justify-center">
+    <div class="login-overlay" />
+
+    <div class="login-card column items-center">
+      <div class="column items-center fields-card">
+        <q-img :src="Logo" class="login-logo q-mb-lg" />
 
-      <q-form
-        ref="formRef"
-        class="full-width q-pa-md q-pb-xl"
-        style="max-width: 400px"
-        autocorrect="off"
-        autocapitalize="off"
-        autocomplete="off"
-        spellcheck="false"
-        @submit="onSubmit"
-      >
-        <DefaultInput
-          v-model="form.email"
-          v-model:error="validationErrors.email"
-          type="email"
-          lazy-rules
-          autofocus
-          :label="$t('common.terms.email')"
-          :rules="[inputRules.required, inputRules.email]"
-        />
-        <DefaultPasswordInput
-          v-model="form.password"
-          v-model:error="validationErrors.password"
-          :rules="[inputRules.required, inputRules.min(6)]"
-          :label="$t('common.terms.password')"
-        />
-        <q-checkbox
-          v-model="checkbox"
-          size="xs"
-          label="Lembrar email"
-          class="q-mb-md"
-          style="margin-left: -6px"
-        />
-        <div>
+        <p class="login-subtitle">{{ $t("auth.select_type_hint") }}</p>
+
+        <div class="type-tabs row q-mb-lg">
           <q-btn
-            class="full-width"
-            color="primary"
+            v-for="tipo in userTypes"
+            :key="tipo.value"
+            class="type-tab column items-center justify-center"
+            :class="{ 'type-tab--active': selectedTipo === tipo.value }"
+            @click="selectedTipo = tipo.value"
+          >
+            <div class="column items-center">
+              <q-icon :name="tipo.icon" size="22px" />
+              <span class="type-tab-label">{{ $t(tipo.label) }}</span>
+            </div>
+          </q-btn>
+        </div>
+
+        <q-form
+          ref="formRef"
+          class="full-width"
+          autocorrect="off"
+          autocapitalize="off"
+          autocomplete="off"
+          spellcheck="false"
+          @submit="onSubmit"
+        >
+          <DefaultInput
+            v-model="form.email"
+            v-model:error="validationErrors.email"
+            type="email"
+            autofocus
+            :label="$t('common.terms.email')"
+            :rules="[inputRules.required, inputRules.email]"
+          >
+            <template #append>
+              <q-icon name="mdi-account-outline" color="grey-5" />
+            </template>
+          </DefaultInput>
+
+          <DefaultPasswordInput
+            v-model="form.password"
+            v-model:error="validationErrors.password"
+            :rules="[inputRules.required, inputRules.min(6)]"
+            :label="$t('common.terms.password')"
+          />
+
+          <q-btn
+            class="full-width q-mt-sm login-btn"
+            color="violet-normal"
             :label="$t('auth.sign-in')"
-            size="md"
-            padding="sm"
             type="submit"
+            unelevated
             :loading
           >
             <template #loading>
               <q-spinner />
             </template>
           </q-btn>
+        </q-form>
+
+        <div class="column items-center q-mt-md gap-xs">
+          <router-link
+            :to="{ name: 'ForgotPasswordPage', query: { tipo: selectedTipo } }"
+            class="login-link"
+          >
+            {{ $t("auth.forgot_password") }}
+          </router-link>
+          <a href="/" class="login-link q-mt-xs">
+            <q-icon name="mdi-arrow-left" size="12px" class="q-mr-xs" />
+            {{ $t("auth.back_to_site") }}
+          </a>
         </div>
-        <div style="height: 160px"></div>
-      </q-form>
+      </div>
     </div>
-    <WavePattern class="absolute-top" />
   </q-page>
 </template>
 
@@ -70,27 +90,41 @@ import { useAuth } from "src/composables/useAuth";
 import { useRouter } from "vue-router";
 import { useInputRules } from "src/composables/useInputRules";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { userStore } from "src/stores/user";
 
-import Logo from "src/assets/logo.png";
+import Logo from "src/assets/logo_serprati.svg";
 import DefaultInput from "src/components/defaults/DefaultInput.vue";
 import DefaultPasswordInput from "src/components/defaults/DefaultPasswordInput.vue";
-import WavePattern from "./component/WavePattern.vue";
 
 const router = useRouter();
 const $q = useQuasar();
-
 const { inputRules } = useInputRules();
 const { login } = useAuth();
+const store = userStore();
 
 const formRef = useTemplateRef("formRef");
+const selectedTipo = ref("administrador");
+
+const userTypes = [
+  { value: "associado",     label: "auth.type.associado",     icon: "mdi-account-outline" },
+  { value: "administrador", label: "auth.type.administrador", icon: "mdi-shield-account-outline" },
+  { value: "parceiro",      label: "auth.type.parceiro",      icon: "mdi-handshake-outline" },
+];
+
+const getRedirectByTipo = (tipo) => {
+  if (tipo === "administrador") return { name: "DashboardPage" };
+  if (tipo === "associado")     return { name: "HomePage" };
+  if (tipo === "parceiro")      return { name: "HomePage" };
+  return { name: "HomePage" };
+};
 
 const {
   loading,
   validationErrors,
   execute: submitForm,
 } = useSubmitHandler({
-  onSuccess: () => router.push({ name: "HomePage" }),
-  formRef: formRef,
+  onSuccess: () => router.push(getRedirectByTipo(store.userTipo)),
+  formRef,
 });
 
 const form = ref({
@@ -98,37 +132,124 @@ const form = ref({
   password: process.env.PASSWORD ?? null,
 });
 
-const checkbox = ref(false);
-
 const onSubmit = async () => {
-  await submitForm(() => login(form.value.email, form.value.password));
-  const email_storage = $q.cookies.get("email");
-  if (email_storage && !checkbox.value) {
-    $q.cookies.remove("email");
-  }
-  if (checkbox.value) {
-    $q.cookies.set("email", form.value.email, {
-      path: "/",
-      sameSite: "Lax",
-    });
-  }
+  await submitForm(() => login(form.value.email, form.value.password, selectedTipo.value));
 };
 
 onBeforeMount(() => {
-  const email_storage = $q.cookies.get("email");
-  if (email_storage) {
-    checkbox.value = true;
-    form.value.email = email_storage;
+  const savedEmail = $q.cookies.get("email");
+  if (savedEmail) {
+    form.value.email = savedEmail;
   }
 });
 </script>
 
 <style lang="scss" scoped>
+@use "sass:map";
+@use "src/css/quasar.variables.scss" as *;
+
 .login-page {
+  min-height: 100dvh;
+  background-image: url("src/assets/pessoas_fundo.jpg");
+  background-size: cover;
+  background-position: center;
   position: relative;
 }
 
-.frosted-glass {
-  backdrop-filter: blur(60px);
+.login-overlay {
+  position: absolute;
+  inset: 0;
+  background: rgba(74, 20, 140, 0.48);
+  z-index: 0;
+}
+
+.login-card {
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: calc(100% - 32px);
+  min-height: calc(100dvh - 32px);
+  background: rgba(255, 255, 255, 0.747);
+  border-radius: 16px;
+  box-shadow: 0 4px 32px rgba(0, 0, 0, 0.15);
 }
+
+.fields-card {
+  width: 100%;
+  max-width: 600px;
+  padding: 32px;
+  border-radius: 12px;
+}
+
+.login-logo {
+  width: 380px;
+}
+
+.login-subtitle {
+  color: map.get($colors, "violet-normal");
+  font-size: 14px;
+  font-weight: 500;
+  margin: 0 0 24px;
+  text-align: center;
+}
+
+.type-tabs {
+  display: flex;
+  gap: 10px;
+  width: 100%;
+}
+
+.type-tab {
+  flex: 1;
+  min-width: 0;
+  min-height: 72px;
+  background: map.get($colors, "violet-light");
+  border-radius: 10px;
+  padding: 10px 4px;
+  cursor: pointer;
+  color: map.get($colors, "violet-normal");
+  transition: border-color 0.2s, background 0.2s, color 0.2s;
+  gap: 5px;
+
+  &:hover {
+    background: map.get($colors, "violet-light-hover");
+  }
+
+  &--active {
+    background: map.get($colors, "violet-normal");
+    color: #fff;
+    box-shadow: 0 2px 10px rgba(102, 29, 117, 0.35);
+  }
+  &--active:hover {
+    background: map.get($colors, "violet-normal-hover");
+    box-shadow: 0 4px 20px rgba(102, 29, 117, 0.5);
+  }
+}
+
+.type-tab-label {
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.login-btn {
+  height: 44px;
+  font-size: 15px;
+  font-weight: 600;
+  letter-spacing: 0.5px;
+}
+
+.login-link {
+  color: map.get($colors, "violet-normal");
+  font-size: 13px;
+  text-decoration: none;
+  cursor: pointer;
+
+  &:hover {
+    color: map.get($colors, "violet-hover");
+    text-decoration: underline;
+  }
+}
+
+
 </style>

+ 117 - 0
src/pages/login/ResetPasswordPage.vue

@@ -0,0 +1,117 @@
+<template>
+  <q-page class="login-page column items-center justify-center">
+    <div class="login-overlay" />
+
+    <div class="login-card column items-center">
+      <q-img :src="Logo" class="login-logo q-mb-lg" />
+
+      <UserTypeBadge :tipo="tipo" />
+
+      <p class="login-title">{{ $t("auth.new_password_title") }}</p>
+
+      <q-form
+        ref="formRef"
+        class="full-width"
+        autocomplete="off"
+        @submit="onSubmit"
+      >
+        <DefaultPasswordInput
+          v-model="form.password"
+          v-model:error="validationErrors.password"
+          :label="$t('common.terms.password')"
+          :rules="[inputRules.required, inputRules.password]"
+          :hint="$t('auth.password_hint')"
+        />
+        <DefaultPasswordInput
+          v-model="form.password_confirmation"
+          v-model:error="validationErrors.password_confirmation"
+          :label="$t('auth.confirm_password')"
+          :rules="[inputRules.required, (v) => v === form.password || $t('validation.rules.same_password')]"
+        />
+
+        <q-btn
+          class="full-width q-mt-sm login-btn"
+          color="secondary"
+          :label="$t('auth.confirm')"
+          type="submit"
+          unelevated
+          :loading
+        >
+          <template #loading><q-spinner /></template>
+        </q-btn>
+      </q-form>
+
+      <a href="#" class="login-link q-mt-md" @click.prevent="goBack">
+        <q-icon name="mdi-arrow-left" size="12px" class="q-mr-xs" />
+        {{ $t("auth.back_to_site") }}
+      </a>
+    </div>
+  </q-page>
+</template>
+
+<script setup>
+import { ref, useTemplateRef } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { useInputRules } from "src/composables/useInputRules";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { resetPassword } from "src/api/auth";
+
+import Logo from "src/assets/logo_serprati.svg";
+import DefaultPasswordInput from "src/components/defaults/DefaultPasswordInput.vue";
+import UserTypeBadge from "./components/UserTypeBadge.vue";
+
+const router = useRouter();
+const route  = useRoute();
+const { inputRules } = useInputRules();
+
+const email  = route.query.email  ?? "";
+const tipo   = route.query.tipo   ?? "administrador";
+const codigo = route.query.codigo ?? "";
+
+const formRef = useTemplateRef("formRef");
+
+const {
+  loading,
+  validationErrors,
+  execute: submitForm,
+} = useSubmitHandler({
+  onSuccess: () => router.push({ name: "LoginPage" }),
+  formRef,
+});
+
+const form = ref({
+  password: null,
+  password_confirmation: null,
+});
+
+const onSubmit = async () => {
+  await submitForm(() =>
+    resetPassword(email, codigo, form.value.password, form.value.password_confirmation)
+  );
+};
+
+const goBack = () => router.push({ name: "LoginPage" });
+</script>
+
+<style lang="scss" scoped>
+@use "src/css/quasar.variables.scss";
+
+.login-page {
+  min-height: 100dvh;
+  background-image: url("src/assets/pessoas_fundo.jpg");
+  background-size: cover; background-position: center; position: relative;
+}
+.login-overlay { position: absolute; inset: 0; background: rgba(74,20,140,0.72); z-index: 0; }
+.login-card { position: relative; z-index: 1; width: 100%; max-width: 400px; padding: 32px 24px 40px; }
+.login-logo { width: 180px; filter: brightness(0) invert(1); }
+.login-title { color: #fff; font-size: 18px; font-weight: 700; margin: 0 0 20px; text-align: center; }
+.login-btn { height: 44px; font-size: 15px; font-weight: 600; }
+.login-link { color: rgba(255,255,255,0.75); font-size: 13px; text-decoration: none; cursor: pointer;
+  &:hover { color: #fff; text-decoration: underline; }
+}
+:deep(.q-field) {
+  .q-field__control { background: rgba(255,255,255,0.12) !important; border-radius: 8px; }
+  .q-field__label, .q-field__native, .q-field__input { color: #fff !important; }
+  .q-field__bottom { color: rgba(255,100,100,0.9) !important; }
+}
+</style>

+ 153 - 0
src/pages/login/VerifyCodePage.vue

@@ -0,0 +1,153 @@
+<template>
+  <q-page class="login-page column items-center justify-center">
+    <div class="login-overlay" />
+
+    <div class="login-card column items-center">
+      <q-img :src="Logo" class="login-logo q-mb-lg" />
+
+      <UserTypeBadge :tipo="tipo" />
+
+      <p class="login-title">{{ $t("auth.enter_code_title") }}</p>
+
+      <div class="email-box q-mb-lg">
+        <q-icon name="mdi-email-outline" size="16px" class="q-mr-xs" />
+        <span>{{ email }}</span>
+      </div>
+
+      <q-form
+        ref="formRef"
+        class="full-width"
+        autocomplete="off"
+        @submit="onSubmit"
+      >
+        <DefaultInput
+          v-model="form.codigo"
+          v-model:error="validationErrors.codigo"
+          autofocus
+          :label="$t('common.terms.code')"
+          mask="######"
+          :rules="[inputRules.required, (v) => (v && v.length === 6) || $t('validation.rules.code_length')]"
+        >
+          <template #append>
+            <q-icon name="mdi-numeric" color="grey-5" />
+          </template>
+        </DefaultInput>
+
+        <q-btn
+          class="full-width q-mt-sm login-btn"
+          color="secondary"
+          :label="$t('auth.verify')"
+          type="submit"
+          unelevated
+          :loading
+        >
+          <template #loading><q-spinner /></template>
+        </q-btn>
+      </q-form>
+
+      <q-btn
+        flat
+        :label="$t('common.actions.resend_email')"
+        color="white"
+        class="q-mt-md resend-btn"
+        :loading="resending"
+        @click="onResend"
+      />
+
+      <div class="column items-center q-mt-sm">
+        <a href="#" class="login-link" @click.prevent="goToLogin">
+          {{ $t("auth.remember_password") }}
+          <strong>{{ $t("auth.do_login") }}</strong>
+        </a>
+        <a href="#" class="login-link q-mt-xs" @click.prevent="goBack">
+          <q-icon name="mdi-arrow-left" size="12px" class="q-mr-xs" />
+          {{ $t("auth.back_to_site") }}
+        </a>
+      </div>
+    </div>
+  </q-page>
+</template>
+
+<script setup>
+import { ref, useTemplateRef } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { useQuasar } from "quasar";
+import { useInputRules } from "src/composables/useInputRules";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { verifyCode, forgotPassword } from "src/api/auth";
+
+import Logo from "src/assets/logo_serprati.svg";
+import DefaultInput from "src/components/defaults/DefaultInput.vue";
+import UserTypeBadge from "./components/UserTypeBadge.vue";
+
+const router = useRouter();
+const route  = useRoute();
+const $q     = useQuasar();
+const { inputRules } = useInputRules();
+
+const email  = route.query.email ?? "";
+const tipo   = route.query.tipo  ?? "administrador";
+
+const formRef  = useTemplateRef("formRef");
+const resending = ref(false);
+
+const {
+  loading,
+  validationErrors,
+  execute: submitForm,
+} = useSubmitHandler({
+  onSuccess: () =>
+    router.push({ name: "ResetPasswordPage", query: { email, tipo, codigo: form.value.codigo } }),
+  formRef,
+});
+
+const form = ref({ codigo: null });
+
+const onSubmit = async () => {
+  await submitForm(() => verifyCode(email, form.value.codigo));
+};
+
+const onResend = async () => {
+  resending.value = true;
+  try {
+    await forgotPassword(email, tipo);
+    $q.notify({ type: "positive", message: "Código reenviado!" });
+  } catch {
+    $q.notify({ type: "negative", message: "Não foi possível reenviar. Tente novamente." });
+  } finally {
+    resending.value = false;
+  }
+};
+
+const goToLogin = () => router.push({ name: "LoginPage" });
+const goBack    = () => router.push({ name: "LoginPage" });
+</script>
+
+<style lang="scss" scoped>
+@use "src/css/quasar.variables.scss";
+
+.login-page {
+  min-height: 100dvh;
+  background-image: url("src/assets/pessoas_fundo.jpg");
+  background-size: cover; background-position: center; position: relative;
+}
+.login-overlay { position: absolute; inset: 0; background: rgba(74,20,140,0.72); z-index: 0; }
+.login-card { position: relative; z-index: 1; width: 100%; max-width: 400px; padding: 32px 24px 40px; }
+.login-logo { width: 180px; filter: brightness(0) invert(1); }
+.login-title { color: #fff; font-size: 18px; font-weight: 700; margin: 0 0 16px; text-align: center; }
+.email-box {
+  display: flex; align-items: center;
+  background: rgba(255,255,255,0.15); border-radius: 8px; padding: 8px 16px;
+  color: #fff; font-size: 14px;
+}
+.login-btn { height: 44px; font-size: 15px; font-weight: 600; }
+.resend-btn { font-size: 13px; text-decoration: underline; }
+.login-link { color: rgba(255,255,255,0.75); font-size: 13px; text-decoration: none; cursor: pointer;
+  &:hover { color: #fff; text-decoration: underline; }
+}
+:deep(.q-field) {
+  .q-field__control { background: rgba(255,255,255,0.12) !important; border-radius: 8px; }
+  .q-field__label, .q-field__native, .q-field__input { color: #fff !important; }
+  .q-field__bottom { color: rgba(255,100,100,0.9) !important; }
+}
+</style>

+ 98 - 0
src/pages/login/VerifyEmailPage.vue

@@ -0,0 +1,98 @@
+<template>
+  <q-page class="login-page column items-center justify-center">
+    <div class="login-overlay" />
+
+    <div class="login-card column items-center">
+      <q-img :src="Logo" class="login-logo q-mb-lg" />
+
+      <UserTypeBadge :tipo="tipo" />
+
+      <p class="login-title">{{ $t("auth.verify_email_title") }}</p>
+      <p class="login-description">
+        {{ $t("auth.verify_email_description") }}
+      </p>
+
+      <div class="email-box q-mb-lg">
+        <q-icon name="mdi-email-outline" size="16px" class="q-mr-xs" />
+        <span>{{ email }}</span>
+      </div>
+
+      <q-btn
+        class="full-width login-btn"
+        color="secondary"
+        :label="$t('auth.enter_code')"
+        unelevated
+        @click="goToCode"
+      />
+
+      <p class="login-hint q-mt-md">
+        {{ $t("auth.no_email_hint") }}
+        <a href="#" class="login-link" @click.prevent="resend">
+          {{ $t("auth.check_spam") }}
+        </a>
+      </p>
+
+      <a href="#" class="login-link q-mt-sm" @click.prevent="goBack">
+        <q-icon name="mdi-arrow-left" size="12px" class="q-mr-xs" />
+        {{ $t("auth.back_to_site") }}
+      </a>
+    </div>
+  </q-page>
+</template>
+
+<script setup>
+import { useRouter, useRoute } from "vue-router";
+
+import Logo from "src/assets/logo_serprati.svg";
+import UserTypeBadge from "./components/UserTypeBadge.vue";
+
+const router = useRouter();
+const route = useRoute();
+
+const email = route.query.email ?? "";
+const tipo  = route.query.tipo  ?? "administrador";
+
+const goToCode = () =>
+  router.push({ name: "VerifyCodePage", query: { email, tipo } });
+
+const resend = () =>
+  router.push({ name: "ForgotPasswordPage", query: { tipo } });
+
+const goBack = () => router.push({ name: "LoginPage" });
+</script>
+
+<style lang="scss" scoped>
+@use "src/css/quasar.variables.scss";
+
+.login-page {
+  min-height: 100dvh;
+  background-image: url("src/assets/pessoas_fundo.jpg");
+  background-size: cover;
+  background-position: center;
+  position: relative;
+}
+.login-overlay {
+  position: absolute; inset: 0;
+  background: rgba(74, 20, 140, 0.72);
+  z-index: 0;
+}
+.login-card {
+  position: relative; z-index: 1;
+  width: 100%; max-width: 400px;
+  padding: 32px 24px 40px;
+}
+.login-logo { width: 180px; filter: brightness(0) invert(1); }
+.login-title { color: #fff; font-size: 18px; font-weight: 700; margin: 0 0 8px; text-align: center; }
+.login-description { color: rgba(255,255,255,0.8); font-size: 13px; text-align: center; margin: 0 0 16px; }
+.email-box {
+  display: flex; align-items: center;
+  background: rgba(255,255,255,0.15);
+  border-radius: 8px; padding: 8px 16px;
+  color: #fff; font-size: 14px;
+}
+.login-btn { height: 44px; font-size: 15px; font-weight: 600; width: 100%; }
+.login-hint { color: rgba(255,255,255,0.65); font-size: 12px; text-align: center; margin: 0; }
+.login-link { color: rgba(255,255,255,0.75); font-size: 13px; text-decoration: none; cursor: pointer;
+  &:hover { color: #fff; text-decoration: underline; }
+}
+</style>

+ 37 - 0
src/pages/login/components/UserTypeBadge.vue

@@ -0,0 +1,37 @@
+<template>
+  <div class="user-type-badge row items-center justify-center q-mb-lg">
+    <div class="badge-pill row items-center q-px-lg q-py-sm">
+      <q-icon :name="tipoData.icon" size="20px" class="q-mr-sm" />
+      <span class="badge-label">{{ $t(tipoData.label) }}</span>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from "vue";
+
+const props = defineProps({
+  tipo: { type: String, required: true },
+});
+
+const tipoMap = {
+  administrador: { icon: "mdi-shield-account-outline", label: "auth.type.administrador" },
+  associado:     { icon: "mdi-account-outline",        label: "auth.type.associado" },
+  parceiro:      { icon: "mdi-handshake-outline",       label: "auth.type.parceiro" },
+};
+
+const tipoData = computed(() => tipoMap[props.tipo] ?? tipoMap.administrador);
+</script>
+
+<style lang="scss" scoped>
+.badge-pill {
+  background: rgba(255, 255, 255, 0.2);
+  border: 1.5px solid rgba(255, 255, 255, 0.45);
+  border-radius: 999px;
+  color: #fff;
+}
+.badge-label {
+  font-size: 13px;
+  font-weight: 600;
+}
+</style>

+ 39 - 21
src/router/index.js

@@ -11,16 +11,16 @@ import { permissionStore } from "src/stores/permission";
 import { i18n } from "src/boot/i18n";
 import { userStore } from "src/stores/user";
 import { useAuth } from "src/composables/useAuth";
-/*
- * If not building with SSR mode, you can
- * directly export the Router instantiation;
- *
- * The function below can be async too; either use
- * async/await or return a Promise which resolves
- * with the Router instance.
- */
 
-export default defineRouter(function (/* { store, ssrContext } */) {
+const AUTH_ROUTES = ["LoginPage", "ForgotPasswordPage", "VerifyEmailPage", "VerifyCodePage", "ResetPasswordPage"];
+
+const HOME_BY_TIPO = {
+  administrador: "DashboardPage",
+  associado:     "HomePage",
+  parceiro:      "HomePage",
+};
+
+export default defineRouter(function () {
   const createHistory = process.env.SERVER
     ? createMemoryHistory
     : process.env.VUE_ROUTER_MODE === "history"
@@ -30,31 +30,38 @@ export default defineRouter(function (/* { store, ssrContext } */) {
   const Router = createRouter({
     scrollBehavior: () => ({ left: 0, top: 0 }),
     routes,
-
-    // Leave this as is and make changes in quasar.conf.js instead!
-    // quasar.conf.js -> build -> vueRouterMode
-    // quasar.conf.js -> build -> publicPath
     history: createHistory(process.env.VUE_ROUTER_BASE),
   });
+
   let refreshed = false;
+
   Router.beforeEach(async (to, from, next) => {
-    if (userStore().accessToken == null && !refreshed) {
+    const store = userStore();
+
+    if (store.accessToken == null && !refreshed) {
       try {
         await useAuth().refresh();
       } catch {
         refreshed = true;
-        return next({ name: "LoginPage" });
+        if (!AUTH_ROUTES.includes(to.name)) {
+          return next({ name: "LoginPage" });
+        }
+        return next();
       }
     }
-    if (userStore().accessToken) {
-      if (to.name == "LoginPage") {
-        return next({ name: "HomePage" });
-      }
+
+    if (store.accessToken && AUTH_ROUTES.includes(to.name)) {
+      const home = HOME_BY_TIPO[store.userTipo] ?? "HomePage";
+      return next({ name: home });
+    }
+
+    if (to.meta.requireAuth && !store.accessToken) {
+      return next({ name: "LoginPage" });
     }
+
     if (to.meta.requiredPermission) {
       const { getAccess } = permissionStore();
-      const permission = getAccess(to.meta.requiredPermission, "view");
-      if (!permission) {
+      if (!getAccess(to.meta.requiredPermission, "view")) {
         Notify.create({
           message: i18n.global.t("validation.permissions.view"),
           type: "negative",
@@ -62,6 +69,17 @@ export default defineRouter(function (/* { store, ssrContext } */) {
         return next(from);
       }
     }
+
+    if (to.meta.allowedTypes?.length && store.userTipo) {
+      if (!to.meta.allowedTypes.includes(store.userTipo)) {
+        Notify.create({
+          message: i18n.global.t("validation.permissions.view"),
+          type: "negative",
+        });
+        return next({ name: "HomePage" });
+      }
+    }
+
     return next();
   });
 

+ 31 - 33
src/router/routes.js

@@ -16,9 +16,7 @@ const routes = [
         path: "",
         name: "HomePage",
         component: () => import("src/pages/home/HomePage.vue"),
-        meta: {
-          requireAuth: true,
-        },
+        meta: { requireAuth: true },
       },
       {
         path: "/dashboard",
@@ -26,18 +24,10 @@ const routes = [
         component: () => import("pages/dashboard/DashboardPage.vue"),
         meta: {
           title: { value: "Dashboard" },
-          description: {
-            value: "page.system-dashboard.description",
-            translate: true,
-          },
+          description: { value: "page.system-dashboard.description", translate: true },
           requireAuth: true,
           requiredPermission: "dashboard",
-          breadcrumbs: [
-            {
-              name: "DashboardPage",
-              title: "Dashboard",
-            },
-          ],
+          breadcrumbs: [{ name: "DashboardPage", title: "Dashboard" }],
         },
       },
       {
@@ -45,22 +35,10 @@ const routes = [
         name: "SystemVersionsPage",
         component: () => import("pages/version/SystemVersionsPage.vue"),
         meta: {
-          title: {
-            value: "ui.navigation.versions",
-            translate: true,
-          },
-          description: {
-            value: "page.versions.description",
-            translate: true,
-          },
+          title: { value: "ui.navigation.versions", translate: true },
+          description: { value: "page.versions.description", translate: true },
           requireAuth: true,
-          breadcrumbs: [
-            {
-              name: "SystemVersionsPage",
-              title: "ui.navigation.versions",
-              translate: true,
-            },
-          ],
+          breadcrumbs: [{ name: "SystemVersionsPage", title: "ui.navigation.versions", translate: true }],
         },
       },
       ...sub_routes,
@@ -74,15 +52,35 @@ const routes = [
         path: "",
         name: "LoginPage",
         component: () => import("pages/login/LoginPage.vue"),
-        meta: {
-          title: "Login",
-        },
+        meta: { title: "Login" },
+      },
+      {
+        path: "/recuperar-senha",
+        name: "ForgotPasswordPage",
+        component: () => import("pages/login/ForgotPasswordPage.vue"),
+        meta: { title: "Recuperar Senha" },
+      },
+      {
+        path: "/verificar-email",
+        name: "VerifyEmailPage",
+        component: () => import("pages/login/VerifyEmailPage.vue"),
+        meta: { title: "Verifique seu E-mail" },
+      },
+      {
+        path: "/digitar-codigo",
+        name: "VerifyCodePage",
+        component: () => import("pages/login/VerifyCodePage.vue"),
+        meta: { title: "Digite o Código" },
+      },
+      {
+        path: "/nova-senha",
+        name: "ResetPasswordPage",
+        component: () => import("pages/login/ResetPasswordPage.vue"),
+        meta: { title: "Nova Senha" },
       },
     ],
   },
 
-  // Always leave this as last one,
-  // but you can also remove it
   {
     path: "/:catchAll(.*)*",
     component: () => import("pages/ErrorNotFound.vue"),

+ 183 - 12
src/stores/navigation.js

@@ -1,40 +1,43 @@
 import { defineStore } from "pinia";
 import { computed } from "vue";
 import { permissionStore } from "src/stores/permission";
+import { userStore } from "src/stores/user";
 
 export const navigationStore = defineStore("navigation", () => {
   const navigationStructure = Object.freeze([
+    // ─── Comum ───────────────────────────────────────────────
     {
       type: "single",
       title: "ui.navigation.home",
       name: "HomePage",
       icon: "mdi-home-outline",
-      disable: false,
       permission: true,
+      allowedTypes: [],
     },
+
+    // ─── ADMINISTRADOR ───────────────────────────────────────────────
     {
       type: "single",
       title: "ui.navigation.dashboard",
       name: "DashboardPage",
       icon: "mdi-poll",
-      disable: false,
       permission: false,
       permissionScope: "dashboard",
+      allowedTypes: ["administrador"],
     },
     {
       type: "expansive",
       title: "ui.navigation.registration",
       icon: "mdi-plus",
-      disable: false,
       permission: false,
       permissionScope: "config",
+      allowedTypes: ["administrador"],
       childrens: [
         {
           type: "single",
           title: "ui.navigation.users",
           name: "UsersPage",
           icon: "mdi-account-multiple-outline",
-          disable: false,
           permission: false,
           permissionScope: "config.user",
         },
@@ -43,7 +46,6 @@ export const navigationStore = defineStore("navigation", () => {
           title: "ui.navigation.city",
           name: "CityPage",
           icon: "mdi-city-variant-outline",
-          disable: false,
           permission: false,
           permissionScope: "config.city",
         },
@@ -52,7 +54,6 @@ export const navigationStore = defineStore("navigation", () => {
           title: "ui.navigation.country",
           name: "CountryPage",
           icon: "mdi-earth",
-          disable: false,
           permission: false,
           permissionScope: "config.country",
         },
@@ -61,24 +62,194 @@ export const navigationStore = defineStore("navigation", () => {
           title: "ui.navigation.state",
           name: "StatePage",
           icon: "mdi-map-marker",
-          disable: false,
           permission: false,
           permissionScope: "config.state",
         },
       ],
     },
+    {
+      type: "expansive",
+      title: "ui.navigation.partners",
+      icon: "mdi-handshake-outline",
+      permission: false,
+      permissionScope: "parceiro",
+      allowedTypes: ["administrador"],
+      childrens: [
+        {
+          type: "single",
+          title: "ui.navigation.partner_agreements",
+          name: "PartnerAgreementsPage",
+          icon: "mdi-office-building-outline",
+          permission: false,
+          permissionScope: "parceiro.convenio",
+        },
+        {
+          type: "single",
+          title: "ui.navigation.partner_services",
+          name: "PartnerServicesPage",
+          icon: "mdi-briefcase-outline",
+          permission: false,
+          permissionScope: "parceiro.servico",
+        },
+      ],
+    },
+    {
+      type: "expansive",
+      title: "ui.navigation.store",
+      icon: "mdi-store-outline",
+      permission: false,
+      permissionScope: "loja",
+      allowedTypes: ["administrador"],
+      childrens: [
+        {
+          type: "single",
+          title: "ui.navigation.store_items",
+          name: "StoreItemsPage",
+          icon: "mdi-package-variant-closed",
+          permission: false,
+          permissionScope: "loja.item",
+        },
+        {
+          type: "single",
+          title: "ui.navigation.store_orders",
+          name: "StoreOrdersPage",
+          icon: "mdi-cart-outline",
+          permission: false,
+          permissionScope: "loja.pedido",
+        },
+      ],
+    },
+    {
+      type: "single",
+      title: "ui.navigation.appointments",
+      name: "AppointmentsPage",
+      icon: "mdi-calendar-check-outline",
+      permission: false,
+      permissionScope: "agendamento",
+      allowedTypes: ["administrador"],
+    },
+    {
+      type: "single",
+      title: "ui.navigation.notifications",
+      name: "NotificationsPage",
+      icon: "mdi-bell-outline",
+      permission: false,
+      permissionScope: "notificacao",
+      allowedTypes: ["administrador"],
+    },
+    {
+      type: "single",
+      title: "ui.navigation.categories",
+      name: "CategoriesPage",
+      icon: "mdi-tag-multiple-outline",
+      permission: false,
+      permissionScope: "categoria",
+      allowedTypes: ["administrador"],
+    },
+
+    // ─── ASSOCIADO ───────────────────────────────────────────────────
+    {
+      type: "single",
+      title: "ui.navigation.dashboard",
+      name: "DashboardPage",
+      icon: "mdi-view-dashboard-outline",
+      permission: false,
+      permissionScope: "dashboard",
+      allowedTypes: ["associado"],
+    },
+    {
+      type: "single",
+      title: "ui.navigation.partner_agreements",
+      name: "PartnerAgreementsPage",
+      icon: "mdi-handshake-outline",
+      permission: false,
+      permissionScope: "parceiro.convenio",
+      allowedTypes: ["associado"],
+    },
+    {
+      type: "single",
+      title: "ui.navigation.store",
+      name: "StoreItemsPage",
+      icon: "mdi-store-outline",
+      permission: false,
+      permissionScope: "loja.item",
+      allowedTypes: ["associado"],
+    },
+    {
+      type: "single",
+      title: "ui.navigation.my_appointments",
+      name: "AppointmentsPage",
+      icon: "mdi-calendar-check-outline",
+      permission: false,
+      permissionScope: "agendamento",
+      allowedTypes: ["associado"],
+    },
+
+    // ─── PARCEIRO ────────────────────────────────────────────────────
+    {
+      type: "single",
+      title: "ui.navigation.dashboard",
+      name: "DashboardPage",
+      icon: "mdi-view-dashboard-outline",
+      permission: false,
+      permissionScope: "dashboard",
+      allowedTypes: ["parceiro"],
+    },
+    {
+      type: "single",
+      title: "ui.navigation.my_profile",
+      name: "PartnerAgreementsPage",
+      icon: "mdi-office-building-outline",
+      permission: false,
+      permissionScope: "parceiro.convenio",
+      allowedTypes: ["parceiro"],
+    },
+    {
+      type: "single",
+      title: "ui.navigation.my_services",
+      name: "PartnerServicesPage",
+      icon: "mdi-briefcase-outline",
+      permission: false,
+      permissionScope: "parceiro.servico",
+      allowedTypes: ["parceiro"],
+    },
+    {
+      type: "single",
+      title: "ui.navigation.received_appointments",
+      name: "AppointmentsPage",
+      icon: "mdi-calendar-clock-outline",
+      permission: false,
+      permissionScope: "agendamento",
+      allowedTypes: ["parceiro"],
+    },
+
+    // ─── Comum (rodapé) ───────────────────────────────────────
+    {
+      type: "single",
+      title: "ui.navigation.versions",
+      name: "SystemVersionsPage",
+      icon: "mdi-information-outline",
+      permission: true,
+      allowedTypes: [],
+    },
   ]);
 
   const getNavigationAccess = () => {
     const { getAccess } = permissionStore();
+    const { userTipo } = userStore();
+
     return navigationStructure
+      .filter((menu) => {
+        if (!menu.allowedTypes || menu.allowedTypes.length === 0) return true;
+        return menu.allowedTypes.includes(userTipo);
+      })
       .map((menu) => {
         if (menu.type === "expansive") {
           if (getAccess(menu.permissionScope, "menu")) menu.permission = true;
-          menu.childrens = menu.childrens.filter((children) => {
-            if (!children?.permissionScope) return true;
-            children.permission = getAccess(children.permissionScope, "menu");
-            return children.permission;
+          menu.childrens = menu.childrens.filter((child) => {
+            if (!child?.permissionScope) return true;
+            child.permission = getAccess(child.permissionScope, "menu");
+            return child.permission;
           });
           return menu.childrens.length > 0 ? menu : null;
         } else {
@@ -87,7 +258,7 @@ export const navigationStore = defineStore("navigation", () => {
           return menu;
         }
       })
-      .filter((menu) => menu !== null);
+      .filter((menu) => menu !== null && menu.permission);
   };
 
   const navigationItems = computed(() => getNavigationAccess());

+ 2 - 2
src/stores/permission.js

@@ -72,9 +72,9 @@ export const permissionStore = defineStore("permission", () => {
   };
 
   const getAccess = (scopeName, permissionType) => {
-    const { isAdmin } = userStore();
+    const { isAdministrador } = userStore();
 
-    if (isAdmin) {
+    if (isAdministrador) {
       return true;
     }
 

+ 17 - 7
src/stores/user.js

@@ -1,21 +1,28 @@
 import { defineStore } from "pinia";
-import { ref } from "vue";
+import { ref, computed } from "vue";
 import { getUser } from "src/api/user";
 
 export const userStore = defineStore("user", () => {
   const user = ref(null);
   const accessToken = ref(null);
-  const isAdmin = ref(false);
+
+  const isAdministrador = computed(() => user.value?.type?.value === "administrador" || user.value?.type === "administrador");
+  const isAssociado = computed(() => user.value?.type?.value === "associado" || user.value?.type === "associado");
+  const isParceiro = computed(() => user.value?.type?.value === "parceiro" || user.value?.type === "parceiro");
+
+  const userTipo = computed(() => {
+    const t = user.value?.type;
+    if (!t) return null;
+    return typeof t === "object" ? t.value : t;
+  });
 
   const setUser = (userData) => {
     user.value = userData;
-    isAdmin.value = userData.type === "admin";
   };
 
   const resetUser = () => {
     user.value = null;
-    isAdmin.value = false;
-    accessToken.value = false;
+    accessToken.value = null;
   };
 
   const fetchUser = async () => {
@@ -25,10 +32,13 @@ export const userStore = defineStore("user", () => {
 
   return {
     user,
-    isAdmin,
+    accessToken,
+    isAdministrador,
+    isAssociado,
+    isParceiro,
+    userTipo,
     setUser,
     resetUser,
     fetchUser,
-    accessToken,
   };
 });

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików