Переглянути джерело

feat: :sparkles: login e cadastro iniciais

feito login e cadastro iniciais (login baseado no fluxo do cliente, nao esta pronto)
Gustavo Zanatta 1 місяць тому
батько
коміт
b813482b0d

+ 15 - 0
src/api/user.js

@@ -29,3 +29,18 @@ export const userTypes = async () => {
   const { data } = await api.get("/user-types");
   return data.payload;
 };
+
+export const sendCode = async (email, phone, type = 'PROVIDER') => {
+  const data = await api.post("/user-send-code", { email, phone, type });
+  return data;
+}
+
+export const validateCode = async (email, phone, code) => {
+  const data = await api.post("/user-validate-code", { email, phone, code });
+  return data;
+}
+
+export const createUserAndProvider = async (data) => {
+  const response = await api.post("/register-provider", data);
+  return response;
+}

+ 99 - 0
src/components/login/LoginStepFourPanel.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="column col-12">
+    <q-card class="step4-card col-12">
+      <div class="bg-surface q-pa-lg">
+        <div class="text-center q-mb-md">
+          <div class="text-primary text-weight-bold">
+            {{ $t('auth.step4_title') }}
+          </div>
+        </div>
+
+        <q-input
+          v-model="cep"
+          no-error-icon
+          outlined
+          rounded
+          class="bg-surface q-mb-md"
+          input-class="text-text"
+          placeholder="Digite seu CEP"
+          hide-bottom-space
+          :rules="[inputRules.requiredHideMessage, inputRules.cep]"
+          lazy-rules
+          mask="#####-###"
+          :loading="loadingCep"
+          :bottom-slots="false"
+          @update:model-value="onCepChange"
+        >
+          <template #prepend>
+            <q-icon name="mdi-map-marker-outline" color="grey-5" class="q-mr-sm" />
+          </template>
+        </q-input>
+
+        <q-btn
+          color="primary-button"
+          :label="$t('auth.use_location')"
+          rounded
+          padding="6px 16px"
+          class="full-width"
+          @click="useLocation"
+        />
+
+      </div>
+    </q-card>
+    <div class="text-center q-pt-sm">
+      <q-btn
+        flat
+        rounded
+        color="surface"
+        :label="$t('auth.back_to_register')"
+        :icon="'mdi-chevron-left-circle-outline'"
+        @click="emit('back')"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { useInputRules } from 'src/composables/useInputRules';
+import axios from 'axios';
+
+const emit = defineEmits(['back']);
+const cep = defineModel('cep', { type: String, default: '' });
+
+const { inputRules } = useInputRules();
+const loadingCep = ref(false);
+
+const fetchCep = async (rawCep) => {
+  const cleaned = rawCep.replace(/\D/g, '');
+  if (cleaned.length !== 8) return;
+  loadingCep.value = true;
+  try {
+    const { data } = await axios.get(`https://viacep.com.br/ws/${cleaned}/json/`);
+    if (data.erro) cep.value = '';
+  } catch (error) {
+    console.log(error)
+  } finally {
+    loadingCep.value = false;
+  }
+};
+
+const onCepChange = (val) => {
+  const cleaned = val?.replace(/\D/g, '') ?? '';
+  if (cleaned.length === 8) fetchCep(val);
+};
+
+const useLocation = () => {
+  // getlocationfromdevice
+};
+</script>
+
+<style lang="scss" scoped>
+.step4-card {
+  width: 100%;
+  max-width: 340px;
+  border-radius: 30px;
+  position: relative;
+  z-index: 1;
+}
+</style>

+ 56 - 0
src/components/login/LoginStepOnePanel.vue

@@ -0,0 +1,56 @@
+<template>
+  <q-card-section class="full-width">
+    <div class="text-text">
+      <span class="text-weight-medium">{{ $t('common.terms.email') }}</span>
+    </div>
+    <q-input
+      v-model="email"
+      no-error-icon
+      outlined
+      rounded
+      class="bg-surface q-mt-sm"
+      input-class="text-text"
+      :placeholder="$t('common.terms.email')"
+      hide-bottom-space
+      :rules="!phone ? [inputRules.requiredHideMessage, inputRules.email] : []"
+      lazy-rules
+      :bottom-slots="false"
+    />
+
+    <div class="col-12 row q-py-lg">
+      <div class="col-5 q-my-auto">
+        <q-separator class="q-my-sm bg-grey-4" />
+      </div>
+      <span class="col text-center text-text">{{ $t('common.ui.misc.or') }}</span>
+      <div class="col-5 q-my-auto">
+        <q-separator class="q-my-sm bg-grey-4" />
+      </div>
+    </div>
+
+    <div class="text-text">
+      <span class="text-weight-medium">{{ $t('common.terms.phone') }}</span>
+      <span class="text-weight-light">{{ ' (' + $t('common.terms.celular') + ')' }}</span>
+    </div>
+    <q-input
+      v-model="phone"
+      no-error-icon
+      class="bg-surface q-mt-sm"
+      :placeholder="$t('common.terms.phone')"
+      input-class="text-text"
+      hide-bottom-space
+      :rules="!email ? [inputRules.requiredHideMessage, inputRules.email] : []"
+      lazy-rules
+      mask="(##) #####-####"
+      :bottom-slots="false"
+    />
+  </q-card-section>
+</template>
+
+<script setup>
+import { useInputRules } from 'src/composables/useInputRules';
+
+const email = defineModel('email', { type: String, required: true });
+const phone = defineModel('phone', { type: String, required: true });
+
+const { inputRules } = useInputRules();
+</script>

+ 208 - 0
src/components/login/LoginStepThreePanel.vue

@@ -0,0 +1,208 @@
+<template>
+  <q-card-section class="no-padding">
+    <div class="">
+      <div class="text-text">
+        <span class="text-weight-medium">{{ $t('auth.full_name') }}</span>
+      </div>
+      <q-input
+        v-model="form.name"
+        no-error-icon
+        outlined
+        rounded
+        class="bg-surface q-mt-sm q-mb-md"
+        input-class="text-text"
+        :placeholder="$t('auth.full_name')"
+        hide-bottom-space
+        :rules="[inputRules.required]"
+        lazy-rules
+      />
+    </div>
+
+    <div class="">
+      <div class="text-text">
+        <span class="text-weight-medium">{{ $t('common.terms.cpf') }}</span>
+      </div>
+      <q-input
+        v-model="form.document"
+        no-error-icon
+        outlined
+        rounded
+        class="bg-surface q-mt-sm q-mb-md"
+        input-class="text-text"
+        placeholder="000.000.000-00"
+        hide-bottom-space
+        :rules="[inputRules.required, inputRules.cpf]"
+        lazy-rules
+        mask="###.###.###-##"
+      />
+    </div>
+
+    <div class="">
+      <div class="text-text">
+        <span class="text-weight-medium">{{ $t('common.terms.cep') }}</span>
+      </div>
+      <q-input
+        v-model="form.zip_code"
+        no-error-icon
+        outlined
+        rounded
+        class="bg-surface q-mt-sm q-mb-md"
+        input-class="text-text"
+        placeholder="00000-00"
+        hide-bottom-space
+        :rules="[inputRules.required, inputRules.cep]"
+        lazy-rules
+        mask="#####-###"
+        :loading="loadingCep"
+        @update:model-value="onCepChange"
+      />
+    </div>
+
+    <div class="">
+      <div class="text-text">
+        <span class="text-weight-medium">{{ $t('common.terms.address') }}</span>
+      </div>
+      <q-input
+        v-model="form.address"
+        no-error-icon
+        outlined
+        rounded
+        class="bg-surface q-mt-sm q-mb-md"
+        input-class="text-text"
+        :placeholder="`${$t('common.terms.address')}...`"
+        hide-bottom-space
+        :rules="[inputRules.required]"
+        lazy-rules
+        readonly
+      />
+    </div>
+
+    <div class="">
+      <q-checkbox
+        v-model="form.no_complement"
+        :label="$t('auth.no_complement')"
+        color="primary"
+        class="q-mb-md text-text"
+      />
+    </div>
+    <div class="">
+      <template v-if="!form.no_complement">
+        <div class="text-text">
+          <span class="text-weight-medium">{{ $t('common.terms.complement') }}</span>
+        </div>
+        <q-input
+          v-model="form.complement"
+          no-error-icon
+          outlined
+          rounded
+          class="bg-surface q-mt-sm q-mb-md"
+          input-class="text-text"
+          :placeholder="`${$t('common.ui.misc.example')}: Apartamento, Conjunto, Casa`"
+          hide-bottom-space
+          :rules="!form.no_complement ? [inputRules.required] : []"
+          lazy-rules
+        />
+      </template>
+    </div>
+    <div class="">
+      <div class="text-text">
+        <span class="text-weight-medium">{{ $t('auth.address_nickname') }}</span>
+      </div>
+      <q-input
+        v-model="form.nickname"
+        no-error-icon
+        outlined
+        rounded
+        class="bg-surface q-mt-sm q-mb-md"
+        input-class="text-text"
+        :placeholder="`${$t('common.ui.misc.example')}: Casa`"
+        hide-bottom-space
+        lazy-rules
+      />
+    </div>
+
+    <div class="">
+      <div class="text-text">
+        <span class="text-weight-medium">{{ $t('auth.address_instructions') }}</span>
+      </div>
+      <q-input
+        v-model="form.instructions"
+        no-error-icon
+        outlined
+        rounded
+        class="bg-surface q-mt-sm q-mb-md"
+        input-class="text-text"
+        type="textarea"
+        rows="3"
+        autogrow
+        hide-bottom-space
+        lazy-rules
+      />
+    </div>
+    <div class="">
+      <div class="row q-gutter-sm q-mt-xs">
+        <q-chip
+          v-for="type in addressTypes"
+          :key="type.value"
+          :selected="form.address_type === type.value"
+          clickable
+          color="primary"
+          :outline="form.address_type !== type.value"
+          text-color="surface"
+          :icon="type.icon"
+          :icon-selected="type.icon"
+          @click="form.address_type = type.value"
+        >
+          {{ $t(type.label) }}
+        </q-chip>
+      </div>
+    </div>
+  </q-card-section>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { useInputRules } from 'src/composables/useInputRules';
+import axios from 'axios';
+
+const form = defineModel({ type: Object, required: true });
+
+const { inputRules } = useInputRules();
+const loadingCep = ref(false);
+
+const addressTypes = [
+  { value: 'home', label: 'auth.address_type_home', icon: 'mdi-home-outline' },
+  { value: 'commercial', label: 'auth.address_type_commercial', icon: 'mdi-briefcase-variant-outline' },
+  { value: 'other', label: 'auth.address_type_other', icon: 'mdi-map-marker-outline' },
+];
+
+const fetchCep = async (rawCep) => {
+  const cleaned = rawCep.replace(/\D/g, '');
+  if (cleaned.length !== 8) return;
+
+  loadingCep.value = true;
+  try {
+    const { data } = await axios.get(`https://viacep.com.br/ws/${cleaned}/json/`);
+    if (!data.erro) {
+      form.value.address = `${data.logradouro}, ${data.bairro} - ${data.localidade}/${data.uf}`;
+      form.value.city = data.localidade;
+      form.value.state = data.uf;
+    } else {
+      form.value.address = '';
+    }
+  } catch {
+    form.value.address = '';
+  } finally {
+    loadingCep.value = false;
+  }
+};
+
+const onCepChange = (val) => {
+  const cleaned = val?.replace(/\D/g, '') ?? '';
+  if (cleaned.length === 8) {
+    fetchCep(val);
+  } else {
+    form.value.address = '';
+  }
+};
+</script>

+ 83 - 0
src/components/login/LoginStepTwoPanel.vue

@@ -0,0 +1,83 @@
+<template>
+  <q-card-section class="no-padding q-mb-xl">
+    <div class="text-text q-mb-xs text-left">
+      <span class="text-weight-medium">{{ $t('auth.validation_code') }}</span>
+    </div>
+
+    <div class="code-boxes-wrapper" @click="focusCodeInput">
+      <div
+        v-for="i in 6"
+        :key="i"
+        class="code-box"
+        :class="{ 'code-box--active': code.length === i - 1 }"
+      >
+        <span v-if="code[i - 1]" class="text-text">{{ code[i - 1] }}</span>
+        <span v-else class="text-text-light text-h6">{{ placeholder }}</span>
+      </div>
+    </div>
+
+    <q-input
+      ref="codeInputRef"
+      v-model="code"
+      no-error-icon
+      class="code-hidden-input"
+      hide-bottom-space
+      :rules="[inputRules.required]"
+      lazy-rules
+      mask="######"
+      inputmode="numeric"
+    />
+  </q-card-section>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import { useInputRules } from 'src/composables/useInputRules';
+
+const code = defineModel('code', { type: String, required: true });
+
+const { inputRules } = useInputRules();
+const codeInputRef = ref(null);
+const placeholder = 'x';
+
+const focusCodeInput = () => {
+  codeInputRef.value?.$el.querySelector('input')?.focus();
+};
+</script>
+
+<style lang="scss" scoped>
+.code-boxes-wrapper {
+  display: flex;
+  gap: 10px;
+  justify-content: center;
+  cursor: text;
+
+  .code-box {
+    width: 44px;
+    height: 45px;
+    border: 2px solid #e4e4e4;
+    background-color: #F9FAFB;
+    border-radius: 8px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 1.4rem;
+    font-weight: 600;
+    color: var(--q-text);
+    transition: border-color 0.2s ease;
+
+    &--active {
+      border-color: var(--q-primary);
+    }
+  }
+}
+
+.code-hidden-input {
+  position: absolute;
+  opacity: 0;
+  pointer-events: none;
+  width: 0;
+  height: 0;
+  overflow: hidden;
+}
+</style>

+ 2 - 0
src/css/quasar.variables.scss

@@ -57,6 +57,8 @@ $colors: (
 
   // Text Colors
   "text": #555555,
+  // Text Colors light
+  "text-light": #BBBBBB,
   // Very Dark Gray (almost black)
 
   // Status Colors with Variants

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

@@ -134,7 +134,22 @@
     "continue": "Continue",
     "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",
+    "enter_code": "Enter the code sent to your email",
+    "code_placeholder": "Code",
+    "validation_code": "Validation Code",
+    "register_later": "Register later",
+    "register": "Register",
+    "full_name": "Full Name",
+    "no_complement": "Address without complement",
+    "address_nickname": "Address nickname",
+    "address_instructions": "Instructions (optional)",
+    "address_type_home": "Home",
+    "address_type_commercial": "Commercial",
+    "address_type_other": "Other",
+    "step4_title": "Add your ZIP code and see the nearest cleaners",
+    "use_location": "use my location",
+    "back_to_register": "Back to registration"
   },
   "business": {
     "advertise": "Advertise",

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

@@ -134,7 +134,22 @@
     "continue": "Continuar",
     "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",
+    "enter_code": "Ingresa el código enviado a tu correo",
+    "code_placeholder": "Código",
+    "validation_code": "Código de Validación",
+    "register_later": "Registrar más tarde",
+    "register": "Registrar",
+    "full_name": "Nombre Completo",
+    "no_complement": "Dirección sin complemento",
+    "address_nickname": "Apodo de la dirección",
+    "address_instructions": "Instrucciones (opcional)",
+    "address_type_home": "Casa",
+    "address_type_commercial": "Comercial",
+    "address_type_other": "Otro",
+    "step4_title": "Agrega tu código postal y ve los limpiadores más cercanos",
+    "use_location": "usar mi ubicación",
+    "back_to_register": "Volver al registro"
   },
   "business": {
     "advertise": "Anunciar",

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

@@ -134,7 +134,22 @@
     "continue": "Continuar",
     "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",
+    "enter_code": "Digite o código enviado para seu e-mail",
+    "code_placeholder": "Código",
+    "validation_code": "Código de Validação",
+    "register_later": "Cadastrar mais tarde",
+    "register": "Cadastrar",
+    "full_name": "Nome Completo",
+    "no_complement": "Endereço sem complemento",
+    "address_nickname": "Apelido do endereço",
+    "address_instructions": "Instruções (opcional)",
+    "address_type_home": "Casa",
+    "address_type_commercial": "Comercial",
+    "address_type_other": "Outro",
+    "step4_title": "Adicione seu CEP e veja os diaristas mais próximos",
+    "use_location": "usar minha localização",
+    "back_to_register": "Voltar para o cadastro"
   },
   "business": {
     "advertise": "Anunciar",

+ 3 - 2
src/layouts/MainLayout.vue

@@ -10,7 +10,7 @@
       }"
     >
       <q-toolbar
-        class="flex justify-between bg-surface"
+        class="flex justify-between bg-primary"
         style="border-radius: 0 0 6px 6px !important"
       >
         <q-btn dense flat @click="toggleLeftDrawer">
@@ -108,7 +108,8 @@ const router = useRouter();
 let oldValue = route.path;
 
 const someAvatar = () => {
-  return "https://cdn.quasar.dev/img/avatar4.jpg";
+  let random = Math.floor(Math.random() * 5) + 1;
+  return "https://cdn.quasar.dev/img/avatar" + random + ".jpg";
 };
 
 const logoutFn = async () => {

+ 242 - 156
src/pages/LoginPage.vue

@@ -1,198 +1,284 @@
 <template>
-  <q-page :padding="clicked" class="login-page bg-surface-dark">
+  <q-page class="login-page bg-surface-dark">
     <Transition name="fade-slide" mode="out-in">
-    <q-card v-if="clicked" key="login" flat class="login-card q-pa-md q-pt-xl bg-surface-dark column">
-      <div class="text-center col-4">
-        <q-img :src="LogoDiariaCampos" style="max-width: 200px" />
+
+      <div v-if="!clicked" key="splash" class="splash-screen" @click="clicked = true">
+        <img :src="BackgroundLogin" class="splash-layer splash-layer--bg" />
+        <img :src="FotoDiarista" class="splash-layer splash-layer--photo" />
+        <img :src="LogoLogin" class="splash-layer splash-layer--logo" />
       </div>
-      <!-- <q-space></q-space> -->
-      <q-form
-        ref="loginForm"
-        class="col column"
-        autocorrect="off"
-        autocapitalize="off"
-        autocomplete="off"
-        spellcheck="false"
-        @submit="submitCode"
-      >
-        <q-card-section>
-          <div class="text-text">
-            <span class="text-weight-medium">{{ $t('common.terms.email') }}</span>
-          </div>
-          <q-input
-            v-model="email"
-            no-error-icon
-            outlined
-            rounded
-            class="bg-surface q-mt-sm"
-            input-class="text-text"
-            :placeholder="$t('common.terms.email')"
-            hide-bottom-space
+
+      <div v-else-if="steps === 4" key="step4" class="splash-screen">
+        <img :src="BackgroundLogin" class="splash-layer splash-layer--bg" />
+        <img :src="FotoDiarista" class="splash-layer splash-layer--photo" />
+        <img :src="LogoLogin" class="splash-layer splash-layer--logo-small" />
+        <div class="step4-card-wrapper">
+          <LoginStepFourPanel v-model:cep="stepFourCep" @back="steps = 3" />
+        </div>
+      </div>
+
+      <div v-else key="flow" class="flow-screen">
+
+        <div class="flow-header">
+          <q-btn
+            v-if="steps === 3"
+            flat
+            dense
+            color="primary"
+            :label="$t('auth.register_later')"
+            icon-right="mdi-chevron-right-circle-outline"
+            class="text-caption"
+            @click="steps = 4"
           />
-          <div class="col-12 row q-py-lg">
-            <div class="col-5 q-my-auto">
-              <q-separator
-                class="q-my-sm bg-grey-4"
-              />
-            </div>
-            <span class="col text-center text-text">{{ $t('common.ui.misc.or') }}</span>
-            <div class="col-5 q-my-auto">
-              <q-separator
-                class="q-my-sm bg-grey-4"
-              />          
-            </div>
+        </div>
+
+        <div class="flow-logo">
+          <q-img :src="LogoDiariaCampos" style="max-width: 180px;" />
+        </div>
+
+        <q-form
+          ref="loginForm"
+          class="flow-form"
+          autocorrect="off"
+          autocapitalize="off"
+          autocomplete="off"
+          spellcheck="false"
+          @submit="onSubmit"
+        >
+          <div class="flow-content" :class="{ 'flow-content--centered': steps < 3 }">
+            <LoginStepOnePanel v-if="steps === 1" v-model:email="email" v-model:phone="phone" />
+            <LoginStepTwoPanel v-else-if="steps === 2" v-model:code="code" />
+            <LoginStepThreePanel v-else-if="steps === 3" v-model="stepThreeForm" />
           </div>
-          <div class="text-text">
-            <span class="text-weight-medium">{{ $t('common.terms.phone') }}</span>
-            <span class="text-weight-light">{{ ' (' + $t('common.terms.celular') + ')' }}</span>
+
+          <div class="flow-footer">
+            <q-btn
+              color="primary-button"
+              :label="$t('auth.continue')"
+              rounded
+              padding="14px 16px"
+              type="submit"
+              class="full-width"
+              :loading="submitting"
+            >
+              <template #loading>
+                <q-spinner />
+              </template>
+            </q-btn>
           </div>
-          <q-input
-            v-model="phone"
-            no-error-icon
-            class="bg-surface q-mt-sm"
-            :placeholder="$t('common.terms.phone')"
-            input-class="text-text"
-            hide-bottom-space
-          />
-        </q-card-section>
-      <q-space></q-space>
-        <q-card-actions align="center">
-          <q-btn
-            color="primary-button"
-            :label="$t('auth.continue')"
-            rounded
-            padding="12px 16px"
-            type="submit"
-            class="col-12"
-            :loading="submitting"
-          >
-            <template #loading>
-              <q-spinner />
-            </template>
-          </q-btn>
-        </q-card-actions>
-      </q-form>
-    </q-card>
-    <q-card v-else-if="!clicked" key="splash" class="bg-login" @click="clicked = true">
-      <img :src="BackgroundLogin" class="layer-background" />
-      <img :src="FotoDiarista" class="layer-photo" />
-      <img :src="LogoLogin" class="layer-logo" />
-    </q-card>
+        </q-form>
+
+      </div>
+
     </Transition>
   </q-page>
 </template>
 
 <script setup>
-import { ref, onMounted } from "vue";
-// import { useAuth } from "src/composables/useAuth";
-// import { useRouter } from "vue-router";
-// import { useInputRules } from "src/composables/useInputRules";
-
-import LogoDiariaCampos from "src/assets/logo_diaria_campos_login.svg";
-import BackgroundLogin from "src/assets/background-login.svg";
-import FotoDiarista from "src/assets/foto_diarista_login.svg";
-import LogoLogin from "src/assets/logo_diaria_login.svg";
-// import { Preferences } from "@capacitor/preferences";
-
-// const router = useRouter();
-
-// const { inputRules } = useInputRules();
-const email = ref("");
-const phone = ref("");
-const submitting = ref(false);
-const loginForm = ref(null);
-// const checkbox = ref(false);
-const clicked = ref(false);
+import { ref } from 'vue';
+import { createUserAndProvider, sendCode, validateCode } from 'src/api/user';
+import { useRouter } from 'vue-router';
+import { userStore } from 'src/stores/user';
+import { permissionStore } from 'src/stores/permission';
 
-// const submitLogin = async () => {
-//   try {
-//     submitting.value = true;
+import BackgroundLogin from 'src/assets/background-login.svg';
+import FotoDiarista from 'src/assets/foto_diarista_login.svg';
+import LogoLogin from 'src/assets/logo_diaria_login.svg';
+import LogoDiariaCampos from 'src/assets/logo_diaria_campos_login.svg';
 
-//     const validate = await loginForm.value.validate();
+import LoginStepOnePanel from 'src/components/login/LoginStepOnePanel.vue';
+import LoginStepTwoPanel from 'src/components/login/LoginStepTwoPanel.vue';
+import LoginStepThreePanel from 'src/components/login/LoginStepThreePanel.vue';
+import LoginStepFourPanel from 'src/components/login/LoginStepFourPanel.vue';
 
-//     if (!validate) {
-//       return;
-//     }
+const router = useRouter();
 
-//     await useAuth().login(email.value, phone.value);
+const email = ref('');
+const phone = ref('');
+const code = ref('');
+const stepFourCep = ref('');
 
-//     submitting.value = false;
+const stepThreeForm = ref({
+  name: '',
+  document: '',
+  zip_code: '',
+  address: '',
+  complement: '',
+  nickname: '',
+  instructions: '',
+  address_type: 'home',
+  no_complement: false,
+  city: '',
+  state: '',
+});
 
-//     router.push({ name: "DashboardPage" });
-//   } catch (error) {
-//     console.error(error);
-//     submitting.value = false;
-//   }
-// };
+const steps = ref(1); // 1 = credentials | 2 = code | 3 = user fields | 4 = cep only
+const clicked = ref(false);
+const submitting = ref(false);
+const loginForm = ref(null);
 
-const submitCode = () => {
-  console.log("submit code", email.value, phone.value);
-}
+const sendValidationCode = async () => {
+  const response = await sendCode(email.value, phone.value);
+  if (response.status === 201) {
+    steps.value = 2;
+  } else {
+    console.error('Failed to send validation code');
+  }
+};
 
-onMounted(async () => {
-});
+const validateCodeInput = async () => {
+  const response = await validateCode(email.value, phone.value, code.value);
+  if (response.status === 200) {
+    steps.value = 3;
+  } else {
+    console.error('Invalid validation code');
+  }
+};
+
+const setUserAndPermissions = async (response) => {
+  const { user, access_token } = response;
+  userStore().user = user;
+  userStore().accessToken = access_token;
+  await permissionStore().fetchScopes();
+};
+
+const registerUserAndProvider = async () => {
+  const payload = {
+    ...stepThreeForm.value,
+    email: email.value,
+    phone: phone.value,
+    code: code.value,
+    has_complement: !stepThreeForm.value.no_complement,
+  };
+
+  const response = await createUserAndProvider(payload);
+  if (response.status === 200) {
+    await setUserAndPermissions(response.data.payload);
+    router.push({ name: 'DashboardPage' });
+  } else {
+    console.error('Failed to create user and provider');
+  }
+};
+
+const onSubmit = async () => {
+  switch (steps.value) {
+    case 1: await sendValidationCode(); break;
+    case 2: await validateCodeInput(); break;
+    case 3: await registerUserAndProvider(); break;
+    default: break;
+  }
+};
 </script>
 
 <style lang="scss" scoped>
 .fade-slide-enter-active,
 .fade-slide-leave-active {
-  transition: opacity 0.4s ease, transform 0.4s ease;
-}
-
-.fade-slide-enter-from {
-  opacity: 0;
-  transform: translateY(24px);
-}
-
-.fade-slide-leave-to {
-  opacity: 0;
-  transform: translateY(-24px);
+  transition: opacity 0.35s ease, transform 0.35s ease;
 }
+.fade-slide-enter-from { opacity: 0; transform: translateY(6px); }
+.fade-slide-leave-to   { opacity: 0; transform: translateY(-6px); }
 
 .login-page {
+  min-height: 100vh;
   display: flex;
   justify-content: center;
-  align-items: center;
-
-  .login-card {
-    width: 100vw;
-    max-width: 500px;
-    height: 90vh;
-    max-height: 800px;
-  }
+  background: var(--q-surface-dark);
 }
 
-.bg-login {
+.splash-screen {
   position: relative;
   width: 100vw;
-  height: 100vh;
+  min-height: 100vh;
   overflow: hidden;
+  cursor: pointer;
 
-  .layer-background {
+  .splash-layer {
     position: absolute;
-    inset: 0;
-    width: 100%;
-    height: 100%;
-    object-fit: cover;
-  }
 
-  .layer-photo {
-    position: absolute;
-    inset: 0;
-    width: 100%;
-    height: 100%;
-    object-fit: cover;
-    opacity: 0.15;
-    mix-blend-mode: multiply;
+    &--bg {
+      inset: 0;
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+
+    &--photo {
+      inset: 0;
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+      opacity: 0.15;
+      mix-blend-mode: multiply;
+    }
+
+    &--logo {
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 180px;
+      z-index: 1;
+    }
+
+    &--logo-small {
+      top: 18%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 100px;
+      z-index: 1;
+    }
   }
+}
 
-  .layer-logo {
-    position: absolute;
-    top: 50%;
-    left: 50%;
-    transform: translate(-50%, -50%);
-    width: 180px;
-    z-index: 1;
+.step4-card-wrapper {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -30%);
+  z-index: 2;
+  width: 90%;
+  max-width: 360px;
+}
+
+.flow-screen {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  max-width: 500px;
+  min-height: 100vh;
+  padding: 16px 20px;
+  background: var(--q-surface-dark);
+}
+
+.flow-header {
+  min-height: 36px;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+}
+
+.flow-logo {
+  display: flex;
+  justify-content: center;
+  padding: 12px 0 24px;
+}
+
+.flow-form {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.flow-content {
+  flex: 1;
+
+  &--centered {
+    display: flex;
+    align-items: center;
+    justify-content: center;
   }
 }
+
+.flow-footer {
+  padding: 20px 0 12px;
+}
 </style>

+ 7 - 363
src/pages/dashboard/DashboardPage.vue

@@ -1,376 +1,20 @@
 <template>
   <div>
-    <DefaultHeaderPage>
-      <template #after>
-        <q-btn
-          outline
-          icon="mdi-calendar"
-          color="primary"
-          @click="showFilter"
-        />
-      </template>
-    </DefaultHeaderPage>
-    <q-expansion-item
-      v-model="filter"
-      dense
-      hide-expand-icon
-      class="remove-header-expansion-item"
-    >
-      <DatePeriodSelector
-        v-model:selected-period="defaultPeriod"
-        v-model:selected-event-id="defaultEventId"
-        class="q-pa-sm"
-      />
-    </q-expansion-item>
+    <!-- <DefaultHeaderPage> -->
 
-    <div v-if="!isLoading" class="column gap q-pa-sm">
-      <div class="flex full-width gap">
-        <div class="flex flex-grow gap">
-          <CardIconMiniChart
-            class="flex-grow"
-            :title="t('dashboard.cards.total_earnings')"
-            :icon="'mdi-currency-usd'"
-            :number-porcent="paymentsChart.percentage_change"
-            :number-card="
-              t('dashboard.currency_format', {
-                value: paymentsChart.current_total,
-              })
-            "
-          >
-            <template #chart>
-              <MiniLineChart
-                :data="paymentsChart.trend_data"
-                fill-color="rgba(0, 0, 0, 0)"
-              />
-            </template>
-          </CardIconMiniChart>
-          <CardIconMiniChart
-            class="flex-grow"
-            :title="t('orders.plural')"
-            :icon="'mdi-package-variant'"
-            :number-porcent="ordersChart.percentage_change"
-            :number-card="ordersChart.current_total"
-          >
-            <template #chart>
-              <MiniBarChart
-                :data="ordersChart.trend_data"
-                fill-color="rgba(0, 0, 0, 0)"
-              />
-            </template>
-          </CardIconMiniChart>
-        </div>
-        <div class="flex flex-grow gap">
-          <CardIconMiniChart
-            class="flex-grow"
-            :title="t('dashboard.cards.tickets_sold')"
-            :icon="'mdi-ticket-outline'"
-            :number-porcent="ticketsSoldChart.percentage_change"
-            :number-card="ticketsSoldChart.current_total"
-          >
-            <template #chart>
-              <MiniLineChart
-                :data="ticketsSoldChart.trend_data"
-                fill-color="rgba(0, 0, 0, 0)"
-              />
-            </template>
-          </CardIconMiniChart>
-          <CardIconMiniChart
-            class="flex-grow"
-            :title="t('dashboard.cards.registrations')"
-            :icon="'mdi-account-group-outline'"
-            :number-porcent="participantsChart.percentage_change"
-            :number-card="participantsChart.current_total"
-          >
-            <template #chart>
-              <MiniBarChart
-                :data="participantsChart.trend_data"
-                fill-color="rgba(0, 0, 0, 0)"
-              />
-            </template>
-          </CardIconMiniChart>
-        </div>
-      </div>
+    <!-- </DefaultHeaderPage> -->
+    {{ userStore().user.name + ' é o usuario logado'  }}<br>
+    {{ userStore().user }}
 
-      <div class="flex full-width gap">
-        <div class="flex flex-grow">
-          <CardIconChart
-            :title="t('dashboard.charts.tickets_by_type.title')"
-            :icon="'mdi-ticket-account'"
-            class="flex-grow"
-          >
-            <template #chart>
-              <BarChart
-                :data="eventTicketsByTypeChart"
-                :data-set-label="t('events.tickets.plural')"
-                :label-x="t('events.tickets.types_singular')"
-                :label-y="t('common.terms.quantity')"
-                :show-legend="true"
-              />
-            </template>
-          </CardIconChart>
-        </div>
-        <div class="flex flex-grow">
-          <CardIconChart
-            :title="t('dashboard.charts.participants_by_document.title')"
-            :icon="'mdi-badge-account'"
-            class="flex-grow"
-          >
-            <template #chart>
-              <DoughnutChart
-                :data="eventParticipantsByCNPJAndCPF"
-                :data-set-label="t('events.attendance.participant_plural')"
-              />
-            </template>
-          </CardIconChart>
-        </div>
-      </div>
-
-      <div class="flex full-width gap">
-        <div class="flex flex-grow">
-          <CardIconChart
-            :title="t('dashboard.charts.sales_over_time.title')"
-            :icon="'mdi-chart-line'"
-            class="flex-grow"
-          >
-            <template #chart>
-              <LineChart
-                :data="salesOverTimeLineChart"
-                :data-set-label="t('ui.navigation.sales')"
-                :label-x="t('common.terms.month')"
-                :label-y="
-                  t('dashboard.charts.sales_over_time.y_label', {
-                    currency: 'R$',
-                  })
-                "
-              />
-            </template>
-          </CardIconChart>
-        </div>
-        <div class="flex flex-grow">
-          <CardIconChart
-            :title="t('dashboard.charts.registration_source.title')"
-            :icon="'mdi-chart-pie'"
-            class="flex-grow"
-          >
-            <template #chart>
-              <PieChart
-                :data="eventSourcePieChart"
-                :data-set-label="
-                  t('dashboard.charts.registration_source.source')
-                "
-              />
-            </template>
-          </CardIconChart>
-        </div>
-      </div>
-    </div>
-
-    <div v-else class="flex flex-center full-width q-pa-xl">
-      <q-spinner color="primary" size="50px" />
-    </div>
   </div>
 </template>
 
 <script setup>
-import { onMounted, ref, watch, defineAsyncComponent } from "vue";
-import { useI18n } from "vue-i18n";
-import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
-import DatePeriodSelector from "./components/DatePeriodSelector.vue";
-
-const MiniLineChart = defineAsyncComponent(
-  () => import("src/components/charts/mini/MiniLineChart.vue"),
-);
-const MiniBarChart = defineAsyncComponent(
-  () => import("src/components/charts/mini/MiniBarChart.vue"),
-);
-const CardIconMiniChart = defineAsyncComponent(
-  () => import("src/components/charts/CardIconMiniChart.vue"),
-);
-const CardIconChart = defineAsyncComponent(
-  () => import("src/components/charts/CardIconChart.vue"),
-);
-const BarChart = defineAsyncComponent(
-  () => import("src/components/charts/normal/BarChart.vue"),
-);
-const DoughnutChart = defineAsyncComponent(
-  () => import("src/components/charts/normal/DoughnutChart.vue"),
-);
-const LineChart = defineAsyncComponent(
-  () => import("src/components/charts/normal/LineChart.vue"),
-);
-const PieChart = defineAsyncComponent(
-  () => import("src/components/charts/normal/PieChart.vue"),
-);
-
-const { t } = useI18n();
-
-const isLoading = ref(true);
-const filter = ref(false);
-const defaultPeriod = ref("month");
-const defaultEventId = ref(1);
-
-const ordersChart = ref({});
-const participantsChart = ref({});
-const paymentsChart = ref({});
-const ticketsSoldChart = ref({});
-const eventTicketsByTypeChart = ref({});
-const eventParticipantsByCNPJAndCPF = ref({});
-const salesOverTimeLineChart = ref({});
-const eventSourcePieChart = ref({});
-
-const showFilter = () => {
-  filter.value = !filter.value;
-};
-
-const generateMockData = () => {
-  const createMiniChartData = (currentTotal, percentage) => ({
-    current_total: currentTotal,
-    percentage_change: percentage,
-    trend_data: Array.from({ length: 10 }, () =>
-      Math.floor(Math.random() * 100),
-    ),
-  });
-
-  const barChartDataRaw = [
-    {
-      label: t("dashboard.charts.tickets_by_type.labels.vip"),
-      value: Math.floor(Math.random() * 300),
-    },
-    {
-      label: t("dashboard.charts.tickets_by_type.labels.track"),
-      value: Math.floor(Math.random() * 800),
-    },
-    {
-      label: t("dashboard.charts.tickets_by_type.labels.box"),
-      value: Math.floor(Math.random() * 400),
-    },
-    {
-      label: t("dashboard.charts.tickets_by_type.labels.courtesy"),
-      value: Math.floor(Math.random() * 50),
-    },
-  ];
-
-  const doughnutDataRaw = [
-    {
-      label: t("common.terms.cpf"),
-      value: Math.floor(Math.random() * 900 + 100),
-    },
-    {
-      label: t("common.terms.cnpj"),
-      value: Math.floor(Math.random() * 100 + 10),
-    },
-  ];
-  const doughnutTotal = doughnutDataRaw.reduce(
-    (sum, item) => sum + item.value,
-    0,
-  );
-
-  const lineChartDataRaw = [
-    {
-      label: t("common.months.january"),
-      value: Math.floor(1200 + Math.random() * 500),
-    },
-    {
-      label: t("common.months.february"),
-      value: Math.floor(1900 + Math.random() * 500),
-    },
-    {
-      label: t("common.months.march"),
-      value: Math.floor(3000 + Math.random() * 500),
-    },
-    {
-      label: t("common.months.april"),
-      value: Math.floor(5000 + Math.random() * 500),
-    },
-    {
-      label: t("common.months.may"),
-      value: Math.floor(2300 + Math.random() * 500),
-    },
-    {
-      label: t("common.months.june"),
-      value: Math.floor(3200 + Math.random() * 500),
-    },
-  ];
-
-  const pieDataRaw = [
-    {
-      label: t("dashboard.charts.registration_source.sources.instagram"),
-      value: Math.floor(450 + Math.random() * 50),
-    },
-    {
-      label: t("dashboard.charts.registration_source.sources.facebook"),
-      value: Math.floor(250 + Math.random() * 50),
-    },
-    {
-      label: t("dashboard.charts.registration_source.sources.google"),
-      value: Math.floor(180 + Math.random() * 50),
-    },
-    {
-      label: t("dashboard.charts.registration_source.sources.referral"),
-      value: Math.floor(120 + Math.random() * 50),
-    },
-  ];
-  const pieTotal = pieDataRaw.reduce((sum, item) => sum + item.value, 0);
-
-  return {
-    payments: createMiniChartData(
-      (Math.random() * 20000 + 5000).toFixed(2),
-      (Math.random() * 20 - 5).toFixed(2),
-    ),
-    orders: createMiniChartData(
-      Math.floor(Math.random() * 500 + 50),
-      (Math.random() * 15 - 5).toFixed(2),
-    ),
-    tickets_sold: createMiniChartData(
-      Math.floor(Math.random() * 1500 + 200),
-      (Math.random() * 25 - 5).toFixed(2),
-    ),
-    participants: createMiniChartData(
-      Math.floor(Math.random() * 1000 + 100),
-      (Math.random() * 10 - 5).toFixed(2),
-    ),
-    barData: {
-      chart_data: barChartDataRaw,
-    },
-    doughnutData: {
-      chart_data: doughnutDataRaw,
-      current_total: doughnutTotal,
-    },
-    lineData: {
-      chart_data: lineChartDataRaw,
-    },
-    pieData: {
-      chart_data: pieDataRaw,
-      current_total: pieTotal,
-    },
-  };
-};
-
-const updateDashboardData = async () => {
-  isLoading.value = true;
-  setTimeout(() => {
-    const mockData = generateMockData();
-
-    ordersChart.value = mockData.orders;
-    participantsChart.value = mockData.participants;
-    paymentsChart.value = mockData.payments;
-    ticketsSoldChart.value = mockData.tickets_sold;
-
-    eventTicketsByTypeChart.value = mockData.barData;
-    eventParticipantsByCNPJAndCPF.value = mockData.doughnutData;
-    salesOverTimeLineChart.value = mockData.lineData;
-    eventSourcePieChart.value = mockData.pieData;
-
-    isLoading.value = false;
-  }, 500);
-};
-
-watch([defaultPeriod, defaultEventId], async () => {
-  await updateDashboardData();
-});
+import { onMounted } from "vue";
+// import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
+import { userStore } from 'src/stores/user';
 
 onMounted(async () => {
-  await updateDashboardData();
 });
 </script>
 

+ 2 - 2
src/router/index.js

@@ -8,7 +8,7 @@ import {
 import routes from "./routes";
 import { Notify } from "quasar";
 import { permissionStore } from "src/stores/permission";
-import { useI18n } from "vue-i18n";
+import { i18n } from "src/boot/i18n";
 import { userStore } from "src/stores/user";
 import { useAuth } from "src/composables/useAuth";
 /*
@@ -56,7 +56,7 @@ export default route(function (/* { store, ssrContext } */) {
       const permission = getAccess(to.meta.requiredPermission, "view");
       if (!permission) {
         Notify.create({
-          message: useI18n().t("validation.permissions.view"),
+          message: i18n.global.t("validation.permissions.view"),
           type: "negative",
         });
         return next(from);