فهرست منبع

feat: add tela para redefinicao de senha

Gustavo Mantovani 1 ماه پیش
والد
کامیت
b37b147bf7
5فایلهای تغییر یافته به همراه340 افزوده شده و 38 حذف شده
  1. 13 0
      src/api/forgotPassword.js
  2. 92 17
      src/pages/LoginPage.vue
  3. 207 0
      src/pages/ResetPasswordPage.vue
  4. 9 21
      src/pages/dashboard/DashboardPage.vue
  5. 19 0
      src/router/routes.js

+ 13 - 0
src/api/forgotPassword.js

@@ -0,0 +1,13 @@
+import { api } from "src/boot/axios";
+
+export const sendForgotPasswordToken = async (payload) => {
+  const { data } = await api.post("/forgot-password/send-token", payload);
+
+  return data;
+};
+
+export const resetForgotPassword = async (payload) => {
+  const { data } = await api.post("/forgot-password/reset", payload);
+
+  return data;
+};

+ 92 - 17
src/pages/LoginPage.vue

@@ -1,17 +1,8 @@
 <template>
-  <q-page
-    class="login-page"
-    padding
-  >
-    <q-card
-      class="login-card q-pa-lg bg-surface"
-      flat
-    >
+  <q-page class="login-page" padding>
+    <q-card class="login-card q-pa-lg bg-surface" flat>
       <div class="text-center">
-        <q-img
-          src="images/kizzo_logo.svg"
-          style="max-width: 220px"
-        />
+        <q-img src="images/kizzo_logo.svg" style="max-width: 220px" />
 
         <div
           class="text-weight-regular"
@@ -28,9 +19,17 @@
         autocorrect="off"
         class="q-pa-md"
         spellcheck="false"
-        @submit="submitLogin"
+        @submit="isForgotPassword ? submitForgotPassword() : submitLogin()"
       >
         <q-card-section class="q-mt-sm flex column q-gutter-y-md">
+          <q-banner
+            v-if="isForgotPassword"
+            class="bg-primary text-white"
+            rounded
+          >
+            Informe seu email para receber o link de recuperação de senha.
+          </q-banner>
+
           <q-input
             v-model="email"
             hide-bottom-space
@@ -41,21 +40,42 @@
           />
 
           <DefaultPasswordInput
+            v-if="!isForgotPassword"
             v-model="password"
             hide-bottom-space
             label="senha"
             :rules="[inputRules.required, inputRules.min(6)]"
           />
+
+          <div v-if="!isForgotPassword" class="text-right">
+            <span
+              class="text-primary cursor-pointer"
+              style="font-size: 14px"
+              @click="enableForgotPassword"
+            >
+              Trocar senha
+            </span>
+          </div>
+
+          <div v-if="isForgotPassword" class="text-right">
+            <span
+              class="text-primary cursor-pointer"
+              style="font-size: 14px"
+              @click="cancelForgotPassword"
+            >
+              Voltar para login
+            </span>
+          </div>
         </q-card-section>
 
         <q-card-actions align="right" class="q-px-md q-mt-md">
           <q-btn
             class="full-width"
             color="primary"
-            label="Entrar"
-            type="submit"
             size="md"
             style="border-radius: 8px"
+            type="submit"
+            :label="isForgotPassword ? 'Enviar link' : 'Entrar'"
             :loading="submitting"
           >
             <template #loading>
@@ -70,6 +90,7 @@
 
 <script setup>
 import { onMounted, ref } from "vue";
+import { sendForgotPasswordToken } from "src/api/forgotPassword";
 import { useAuth } from "src/composables/useAuth";
 import { useInputRules } from "src/composables/useInputRules";
 import { useQuasar } from "quasar";
@@ -84,10 +105,62 @@ const $q = useQuasar();
 const { inputRules } = useInputRules();
 
 const email = ref("");
+
 const password = ref(process.env.PASSWORD);
+
 const submitting = ref(false);
+
 const loginForm = ref(null);
 
+const isForgotPassword = ref(false);
+
+const enableForgotPassword = () => {
+  isForgotPassword.value = true;
+};
+
+const cancelForgotPassword = () => {
+  isForgotPassword.value = false;
+};
+
+const submitForgotPassword = async () => {
+  try {
+    submitting.value = true;
+
+    const validate = await loginForm.value.validate();
+
+    if (!validate) {
+      submitting.value = false;
+      return;
+    }
+
+    await sendForgotPasswordToken({
+      email: email.value,
+    });
+
+    $q.notify({
+      type: "positive",
+      message: "Enviamos um link de recuperação para seu email.",
+      position: "top-right",
+      timeout: 5000,
+    });
+
+    isForgotPassword.value = false;
+
+    submitting.value = false;
+  } catch (error) {
+    submitting.value = false;
+
+    $q.notify({
+      type: "negative",
+      message:
+        error?.response?.data?.message ||
+        "Erro ao enviar email de recuperação.",
+      position: "top-right",
+      timeout: 5000,
+    });
+  }
+};
+
 const submitLogin = async () => {
   try {
     submitting.value = true;
@@ -107,10 +180,12 @@ const submitLogin = async () => {
     router.push({ name: "DashboardPage" });
   } catch (error) {
     submitting.value = false;
-    
+
     $q.notify({
       type: "negative",
-      message: error?.response?.data?.message || "Falha no login. Verifique suas credenciais.",
+      message:
+        error?.response?.data?.message ||
+        "Falha no login. Verifique suas credenciais.",
       position: "top-right",
       timeout: 5000,
     });

+ 207 - 0
src/pages/ResetPasswordPage.vue

@@ -0,0 +1,207 @@
+<template>
+  <q-page class="reset-password-page" padding>
+    <q-card class="reset-password-card q-pa-lg bg-surface" flat>
+      <div class="text-center">
+        <q-img
+          src="images/kizzo_logo.svg"
+          style="max-width: 220px"
+        />
+
+        <div
+          class="text-weight-regular"
+          style="font-size: 20px; letter-spacing: 0.15px"
+        >
+          Redefinir senha
+        </div>
+      </div>
+
+      <q-form
+        ref="resetPasswordForm"
+        autocapitalize="off"
+        autocomplete="off"
+        autocorrect="off"
+        class="q-pa-md"
+        spellcheck="false"
+        @submit="submitResetPassword"
+      >
+        <q-card-section class="q-mt-sm flex column q-gutter-y-md">
+          <q-banner
+            class="bg-primary text-white"
+            rounded
+          >
+            Escolha uma nova senha para concluir a recuperação de acesso.
+          </q-banner>
+
+          <q-input
+            v-model="email"
+            hide-bottom-space
+            label="Email"
+            lazy-rules
+            type="email"
+            :rules="[inputRules.required, inputRules.email]"
+          />
+
+          <DefaultPasswordInput
+            v-model="password"
+            hide-bottom-space
+            label="Nova senha"
+            lazy-rules
+            :rules="[inputRules.required, inputRules.min(6)]"
+          />
+
+          <DefaultPasswordInput
+            v-model="confirmPassword"
+            hide-bottom-space
+            label="Confirmar nova senha"
+            lazy-rules
+            :rules="confirmPasswordRules"
+          />
+
+          <div class="text-right">
+            <span
+              class="text-primary cursor-pointer"
+              style="font-size: 14px"
+              @click="goToLogin"
+            >
+              Voltar para login
+            </span>
+          </div>
+        </q-card-section>
+
+        <q-card-actions align="right" class="q-px-md q-mt-md">
+          <q-btn
+            class="full-width"
+            color="primary"
+            label="Alterar senha"
+            size="md"
+            style="border-radius: 8px"
+            type="submit"
+            :loading="submitting"
+          >
+            <template #loading>
+              <q-spinner />
+            </template>
+          </q-btn>
+        </q-card-actions>
+      </q-form>
+    </q-card>
+  </q-page>
+</template>
+
+<script setup>
+import { computed, onMounted, ref } from "vue";
+import { resetForgotPassword } from "src/api/forgotPassword";
+import { useInputRules } from "src/composables/useInputRules";
+import { useQuasar } from "quasar";
+import { useRoute, useRouter } from "vue-router";
+
+import DefaultPasswordInput from "src/components/defaults/DefaultPasswordInput.vue";
+
+const route = useRoute();
+const router = useRouter();
+
+const $q = useQuasar();
+
+const { inputRules } = useInputRules();
+
+const email = ref("");
+const token = ref("");
+const password = ref("");
+const confirmPassword = ref("");
+const submitting = ref(false);
+const resetPasswordForm = ref(null);
+
+const confirmPasswordRules = computed(() => [
+  inputRules.required,
+  inputRules.samePassword(password.value),
+]);
+
+const getQueryValue = (value) => (Array.isArray(value) ? value[0] : value);
+
+const goToLogin = () => {
+  router.push({ name: "LoginPage" });
+};
+
+const notifyMissingToken = () => {
+  $q.notify({
+    type: "negative",
+    message: "Link de recuperação inválido. Solicite um novo link.",
+    position: "top-right",
+    timeout: 5000,
+  });
+};
+
+const submitResetPassword = async () => {
+  try {
+    submitting.value = true;
+
+    if (!token.value) {
+      submitting.value = false;
+
+      notifyMissingToken();
+
+      return;
+    }
+
+    const validate = await resetPasswordForm.value.validate();
+
+    if (!validate) {
+      submitting.value = false;
+
+      return;
+    }
+
+    await resetForgotPassword({
+      email: email.value,
+      token: token.value,
+      password: password.value,
+    });
+
+    $q.notify({
+      type: "positive",
+      message: "Senha alterada com sucesso.",
+      position: "top-right",
+      timeout: 5000,
+    });
+
+    submitting.value = false;
+
+    goToLogin();
+  } catch (error) {
+    submitting.value = false;
+
+    $q.notify({
+      type: "negative",
+      message:
+        error?.response?.data?.message ||
+        "Erro ao alterar senha. Verifique o token e tente novamente.",
+      position: "top-right",
+      timeout: 5000,
+    });
+  }
+};
+
+onMounted(() => {
+  email.value = getQueryValue(route.query.email) || "";
+  token.value = getQueryValue(route.query.token) || "";
+
+  if (!token.value) {
+    notifyMissingToken();
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.reset-password-page {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  .reset-password-card {
+    width: 100%;
+    max-width: 600px;
+    border-radius: 12px;
+    padding-top: 40px;
+  }
+}
+</style>

+ 9 - 21
src/pages/dashboard/DashboardPage.vue

@@ -40,10 +40,7 @@
           <span class="dashboard-section-caption">
             Indicadores
 
-            <q-tooltip
-              anchor="center left"
-              self="center right"
-            >
+            <q-tooltip anchor="center left" self="center right">
               Os números exibidos são referentes ao desempenho da reserva
             </q-tooltip>
           </span>
@@ -64,10 +61,7 @@
           <span class="dashboard-section-caption">
             Financeiro
 
-            <q-tooltip
-              anchor="center left"
-              self="center right"
-            >
+            <q-tooltip anchor="center left" self="center right">
               Os valores exibidos, são referentes ao desempenho financeiro
             </q-tooltip>
           </span>
@@ -196,6 +190,9 @@ const defaultSummary = Object.freeze({
   cleanings_count: 0,
   expenses: 0,
   reservations_count: 0,
+  checkout_reservations_count: 0,
+  crossed_reservations_count: 0,
+  crossed_cleanings_count: 0,
   available_days: 0,
   days_in_month: 30,
   properties_count: 1,
@@ -327,13 +324,9 @@ const allMetricCards = computed(() =>
   }),
 );
 
-const firstRowCards = computed(() =>
-  allMetricCards.value.slice(0, 7),
-);
+const firstRowCards = computed(() => allMetricCards.value.slice(0, 7));
 
-const secondRowCards = computed(() =>
-  allMetricCards.value.slice(7, 14),
-);
+const secondRowCards = computed(() => allMetricCards.value.slice(7, 14));
 
 const selectedReferenceLabel = computed(() => {
   if (!selectedMonth.value || !selectedYear.value) {
@@ -399,10 +392,7 @@ const availabilityItems = computed(() => {
       1,
     )})`,
 
-    percentage:
-      total > 0
-        ? Number(((item.value * 100) / total).toFixed(2))
-        : 0,
+    percentage: total > 0 ? Number(((item.value * 100) / total).toFixed(2)) : 0,
 
     color: item.color,
   }));
@@ -432,9 +422,7 @@ const channelsBarItems = computed(() => {
 
   return channels.map((item, index) => {
     const fallbackColor =
-      ranieryFallbackPalette[
-        fallbackIndex++ % ranieryFallbackPalette.length
-      ];
+      ranieryFallbackPalette[fallbackIndex++ % ranieryFallbackPalette.length];
 
     return {
       key: `${item.channel}-${index}`,

+ 19 - 0
src/router/routes.js

@@ -93,6 +93,25 @@ const routes = [
     ],
   },
 
+  {
+    path: "/reset-password",
+
+    component: () => import("layouts/LoginLayout.vue"),
+
+    children: [
+      {
+        path: "",
+        name: "ResetPasswordPage",
+
+        component: () => import("pages/ResetPasswordPage.vue"),
+
+        meta: {
+          title: "Redefinir senha",
+        },
+      },
+    ],
+  },
+
   ...versionRoutes,
 
   // Always leave this as last one,