Просмотр исходного кода

Merge branch 'development' of gogs.softpar.inf.br:Softpar/sfp_front_vue_diarista_prestador into development

Gustavo Zanatta 22 часов назад
Родитель
Сommit
4de78e270c

+ 2 - 2
src/components/dashboard/ProposalAcceptedDialog.vue

@@ -3,7 +3,7 @@
     ref="dialogRef"
     @hide="onDialogHide"
   >
-    <q-card class="dialog-card">
+    <q-card class="dialog-card bg-white">
 
       <q-card-section class="text-center q-pt-xl">
 
@@ -58,4 +58,4 @@ const {
   max-width: 92vw;
   border-radius: 20px;
 }
-</style>
+</style>

+ 106 - 31
src/components/login/LoginStep3Panel.vue

@@ -7,6 +7,7 @@
     </div>
 
     <q-input
+      :ref="fieldRefs.name"
       v-model="form.name"
       class="bg-surface q-mb-md q-mt-sm"
       hide-bottom-space
@@ -15,8 +16,11 @@
       no-error-icon
       outlined
       rounded
+      :error="!!serverErrors.name"
+      :error-message="serverErrors.name"
       :placeholder="$t('provider.login.steps.step_3.full_name')"
       :rules="[inputRules.required]"
+      @update:model-value="clearServerError('name')"
     />
 
     <div class="text-text">
@@ -26,6 +30,7 @@
     </div>
 
     <q-input
+      :ref="fieldRefs.phone"
       v-model="form.phone"
       class="bg-surface q-mb-md q-mt-sm"
       hide-bottom-space
@@ -36,7 +41,10 @@
       outlined
       placeholder="(00) 00000-0000"
       rounded
+      :error="!!serverErrors.phone"
+      :error-message="serverErrors.phone"
       :rules="[inputRules.required]"
+      @update:model-value="clearServerError('phone')"
     />
 
     <div class="text-text">
@@ -46,6 +54,7 @@
     </div>
 
     <q-input
+      :ref="fieldRefs.email"
       v-model="form.email"
       class="bg-surface q-mb-md q-mt-sm"
       hide-bottom-space
@@ -57,7 +66,10 @@
       readonly
       rounded
       type="email"
+      :error="!!serverErrors.email"
+      :error-message="serverErrors.email"
       :rules="[inputRules.required, inputRules.email]"
+      @update:model-value="clearServerError('email')"
     />
 
     <div class="text-text">
@@ -67,6 +79,7 @@
     </div>
 
     <q-input
+      :ref="fieldRefs.rg"
       v-model="form.rg"
       class="bg-surface q-mb-md q-mt-sm"
       hide-bottom-space
@@ -78,7 +91,10 @@
       placeholder="00.000.000-0"
       rounded
       type="tel"
+      :error="!!serverErrors.rg"
+      :error-message="serverErrors.rg"
       :rules="[inputRules.required, validateRG]"
+      @update:model-value="clearServerError('rg')"
     />
 
     <div class="text-text">
@@ -88,6 +104,7 @@
     </div>
 
     <q-input
+      :ref="fieldRefs.document"
       v-model="form.document"
       class="bg-surface q-mb-md q-mt-sm"
       hide-bottom-space
@@ -99,7 +116,10 @@
       placeholder="000.000.000-00"
       rounded
       type="tel"
+      :error="!!serverErrors.document"
+      :error-message="serverErrors.document"
       :rules="[inputRules.required, inputRules.cpf]"
+      @update:model-value="clearServerError('document')"
     />
 
     <div class="text-text">
@@ -109,6 +129,7 @@
     </div>
 
     <q-input
+      :ref="fieldRefs.birth_date"
       v-model="form.birth_date"
       class="bg-surface q-mb-md q-mt-sm"
       hide-bottom-space
@@ -118,8 +139,11 @@
       no-error-icon
       outlined
       rounded
+      :error="!!serverErrors.birth_date"
+      :error-message="serverErrors.birth_date"
       :placeholder="$t('provider.login.steps.step_3.birth_date_placeholder')"
       :rules="[inputRules.required, validateBirthDate]"
+      @update:model-value="clearServerError('birth_date')"
     />
 
     <div class="text-text">
@@ -129,6 +153,7 @@
     </div>
 
     <q-option-group
+      :ref="fieldRefs.gender"
       v-model="form.gender"
       class="gender-options font12 q-mb-md q-mt-sm text-text"
       color="primary"
@@ -143,6 +168,7 @@
     </div>
 
     <q-input
+      :ref="fieldRefs.zip_code"
       v-model="form.zip_code"
       class="bg-surface q-mb-md q-mt-sm"
       hide-bottom-space
@@ -154,9 +180,11 @@
       placeholder="00000-000"
       rounded
       type="tel"
+      :error="!!serverErrors.zip_code"
+      :error-message="serverErrors.zip_code"
       :loading="loadingCep"
       :rules="[inputRules.required, inputRules.cep]"
-      @update:model-value="onCepChange"
+      @update:model-value="(val) => { clearServerError('zip_code'); onCepChange(val) }"
     />
 
     <q-btn
@@ -176,6 +204,7 @@
     </div>
 
     <q-input
+      :ref="fieldRefs.address"
       v-model="form.address"
       class="bg-surface q-mb-md q-mt-sm"
       hide-bottom-space
@@ -185,8 +214,11 @@
       outlined
       readonly
       rounded
+      :error="!!serverErrors.address"
+      :error-message="serverErrors.address"
       :placeholder="`${$t('common.terms.address')}...`"
       :rules="[inputRules.required]"
+      @update:model-value="clearServerError('address')"
     />
 
     <div class="row q-col-gutter-sm">
@@ -198,6 +230,7 @@
         </div>
 
         <q-input
+          :ref="fieldRefs.number"
           v-model="form.number"
           class="bg-surface q-mb-md q-mt-sm"
           hide-bottom-space
@@ -207,7 +240,10 @@
           outlined
           placeholder="0000"
           rounded
+          :error="!!serverErrors.number"
+          :error-message="serverErrors.number"
           :rules="[inputRules.required]"
+          @update:model-value="clearServerError('number')"
         />
       </div>
 
@@ -219,6 +255,7 @@
         </div>
 
         <q-input
+          :ref="fieldRefs.district"
           v-model="form.district"
           class="bg-surface q-mb-md q-mt-sm"
           hide-bottom-space
@@ -228,8 +265,11 @@
           outlined
           readonly
           rounded
+          :error="!!serverErrors.district"
+          :error-message="serverErrors.district"
           :placeholder="`${$t('common.terms.district')}...`"
           :rules="[inputRules.required]"
+          @update:model-value="clearServerError('district')"
         />
       </div>
     </div>
@@ -243,6 +283,7 @@
         </div>
 
         <q-input
+          :ref="fieldRefs.city"
           v-model="form.city"
           class="bg-surface q-mb-md q-mt-sm"
           hide-bottom-space
@@ -252,7 +293,10 @@
           outlined
           readonly
           rounded
+          :error="!!serverErrors.city"
+          :error-message="serverErrors.city"
           :rules="[inputRules.required]"
+          @update:model-value="clearServerError('city')"
         />
       </div>
 
@@ -264,6 +308,7 @@
         </div>
 
         <q-input
+          :ref="fieldRefs.state"
           v-model="form.state"
           class="bg-surface q-mb-md q-mt-sm"
           hide-bottom-space
@@ -273,41 +318,36 @@
           outlined
           readonly
           rounded
+          :error="!!serverErrors.state"
+          :error-message="serverErrors.state"
           :rules="[inputRules.required]"
+          @update:model-value="clearServerError('state')"
         />
       </div>
     </div>
 
-    <div>
-      <q-checkbox
-        v-model="form.no_complement"
-        class="q-mb-md text-text font12 fontbold"
-        color="primary"
-        keep-color
-        :label="$t('provider.login.steps.step_3.no_complement')"
-      />
+    <div class="text-text">
+      <span class="font12 fontbold">
+        {{ $t("common.terms.complement") }}
+      </span>
     </div>
 
-    <template v-if="!form.no_complement">
-      <div class="text-text">
-        <span class="font12 fontbold">
-          {{ $t("common.terms.complement") }}
-        </span>
-      </div>
-
-      <q-input
-        v-model="form.complement"
-        class="bg-surface q-mb-md q-mt-sm"
-        hide-bottom-space
-        input-class="font12 fontmedium text-text"
-        lazy-rules
-        no-error-icon
-        outlined
-        rounded
-        :placeholder="$t('provider.login.steps.step_3.complement_placeholder')"
-        :rules="[inputRules.required]"
-      />
-    </template>
+    <q-input
+      :ref="fieldRefs.complement"
+      v-model="form.complement"
+      class="bg-surface q-mb-md q-mt-sm"
+      hide-bottom-space
+      input-class="font12 fontmedium text-text"
+      lazy-rules
+      no-error-icon
+      outlined
+      rounded
+      :error="!!serverErrors.complement"
+      :error-message="serverErrors.complement"
+      :placeholder="$t('provider.login.steps.step_3.complement_placeholder')"
+      :rules="[inputRules.required]"
+      @update:model-value="clearServerError('complement')"
+    />
 
     <div class="text-text">
       <span class="font12 fontbold">
@@ -316,6 +356,7 @@
     </div>
 
     <q-input
+      :ref="fieldRefs.nickname"
       v-model="form.nickname"
       class="bg-surface q-mb-md q-mt-sm"
       hide-bottom-space
@@ -324,7 +365,10 @@
       no-error-icon
       outlined
       rounded
+      :error="!!serverErrors.nickname"
+      :error-message="serverErrors.nickname"
       :placeholder="`${$t('common.ui.misc.example')}: Casa`"
+      @update:model-value="clearServerError('nickname')"
     />
 
     <div class="text-text">
@@ -334,6 +378,7 @@
     </div>
 
     <q-input
+      :ref="fieldRefs.instructions"
       v-model="form.instructions"
       autogrow
       class="bg-surface q-mb-md q-mt-sm"
@@ -345,9 +390,12 @@
       rounded
       rows="3"
       type="textarea"
+      :error="!!serverErrors.instructions"
+      :error-message="serverErrors.instructions"
+      @update:model-value="clearServerError('instructions')"
     />
 
-    <div class="row q-gutter-sm q-mt-xs">
+    <div :ref="fieldRefs.address_type" class="row q-gutter-sm q-mt-xs">
       <q-chip
         v-for="type in addressTypes"
         :key="type.value"
@@ -368,7 +416,7 @@
 
 <script setup>
 import axios from "axios";
-import { computed, ref } from "vue";
+import { computed, inject, ref } from "vue";
 import { genderOptions } from "src/helpers/genderOptions";
 import { useGeolocation } from "src/composables/useGeolocation";
 import { useI18n } from "vue-i18n";
@@ -391,6 +439,33 @@ const $q = useQuasar();
 const loadingCep = ref(false);
 const loadingLocation = ref(false);
 
+const serverErrors = inject("serverErrors", ref({}));
+const clearServerError = inject("clearServerError", () => {});
+
+const fieldRefs = {
+  address: ref(null),
+  birth_date: ref(null),
+  city: ref(null),
+  complement: ref(null),
+  district: ref(null),
+  document: ref(null),
+  email: ref(null),
+  gender: ref(null),
+  instructions: ref(null),
+  name: ref(null),
+  nickname: ref(null),
+  number: ref(null),
+  phone: ref(null),
+  rg: ref(null),
+  state: ref(null),
+  zip_code: ref(null),
+  address_type: ref(null),
+};
+
+defineExpose({
+  getFieldRef: (field) => fieldRefs[field],
+});
+
 const translatedGenderOptions = computed(() =>
   genderOptions.map((option) => ({
     ...option,

+ 31 - 2
src/components/login/LoginStep4Panel.vue

@@ -3,6 +3,7 @@
     <div class="column items-center q-gutter-y-xl q-pt-md">
       <div class="column items-center q-gutter-y-sm">
         <div
+          :ref="fieldRefs.selfie"
           class="photo-circle"
           :class="selfieDone ? 'photo-circle--done' : 'photo-circle--pending'"
           @click="takeSelfie"
@@ -14,6 +15,13 @@
           />
         </div>
 
+        <div
+          v-if="serverErrors.selfie"
+          class="text-negative font12 text-center"
+        >
+          {{ serverErrors.selfie }}
+        </div>
+
         <template v-if="!selfieDone">
           <q-btn
             color="primary-button"
@@ -47,6 +55,7 @@
 
       <div class="column items-center q-gutter-y-sm">
         <div
+          :ref="fieldRefs.document_front"
           class="photo-circle"
           :class="docDone ? 'photo-circle--done' : 'photo-circle--pending'"
           @click="takeDocFront"
@@ -58,6 +67,13 @@
           />
         </div>
 
+        <div
+          v-if="serverErrors.document_front"
+          class="text-negative font12 text-center"
+        >
+          {{ serverErrors.document_front }}
+        </div>
+
         <template v-if="!docDone">
           <q-btn
             color="primary-button"
@@ -165,7 +181,7 @@ import {
   CameraResultType,
   CameraSource,
 } from '@capacitor/camera';
-import { computed, ref } from 'vue';
+import { computed, inject, ref } from 'vue';
 import { useI18n } from 'vue-i18n';
 
 import LogoDiaria from 'src/assets/logo_diaria_campos_login.svg';
@@ -179,6 +195,19 @@ const form = defineModel({
 
 const { t } = useI18n();
 
+const serverErrors = inject("serverErrors", ref({}));
+
+const fieldRefs = {
+  selfie: ref(null),
+  document_front: ref(null),
+};
+
+defineExpose({
+  getFieldRef: (field) => field === 'document_back'
+    ? fieldRefs.document_front
+    : fieldRefs[field],
+});
+
 const loadingDoc = ref(false);
 const loadingSelfie = ref(false);
 const resultError = ref(false);
@@ -401,4 +430,4 @@ const takeSelfie = async () => {
     background-color: rgba(139, 92, 246, 0.18);
   }
 }
-</style>
+</style>

+ 18 - 3
src/components/login/LoginStep5Panel.vue

@@ -5,6 +5,7 @@
     </div>
 
     <DefaultCurrencyInput
+      :ref="fieldRefs.daily_price_8h"
       v-model="form.daily_price_8h"
       class="bg-surface q-mb-sm custom-currency-input"
       hide-bottom-space
@@ -13,9 +14,10 @@
       no-error-icon
       placeholder="R$ 0,00"
       rounded
-      :error="!!priceError"
-      :error-message="priceError"
+      :error="!!serverErrors.daily_price_8h || !!priceError"
+      :error-message="serverErrors.daily_price_8h || priceError"
       :rules="[validateDailyPrice]"
+      @update:model-value="clearServerError('daily_price_8h')"
     />
 
     <div class="font10 fontmedium q-mb-md text-center text-grey-7">
@@ -119,7 +121,7 @@
 </template>
 
 <script setup>
-import { computed, onMounted, ref, watch } from "vue";
+import { computed, inject, onMounted, ref, watch } from "vue";
 import { getProviderDailyPriceMin } from "src/api/provider";
 import { getPublicServiceTypes } from "src/api/serviceType";
 
@@ -130,6 +132,19 @@ const form = defineModel({
   type: Object,
 });
 
+const serverErrors = inject("serverErrors", ref({}));
+const clearServerError = inject("clearServerError", () => {});
+
+const fieldRefs = {
+  daily_price_8h: ref(null),
+};
+
+defineExpose({
+  getFieldRef: (field) => field.startsWith("daily_price_")
+    ? fieldRefs.daily_price_8h
+    : fieldRefs[field],
+});
+
 const DEFAULT_DAILY_PRICE_MIN = 100;
 const DAILY_PRICE_MAX = 500;
 

+ 21 - 1
src/components/login/LoginStep6Panel.vue

@@ -4,7 +4,15 @@
       {{ $t("provider.login.steps.step_6.title") }}
     </div>
 
+    <div
+      v-if="serverErrors.working_days"
+      class="text-negative font12 q-mb-md text-center"
+    >
+      {{ serverErrors.working_days }}
+    </div>
+
     <q-card
+      ref="workingDaysRef"
       class="availability-card bg-surface q-mb-lg q-pa-lg shadow-card"
       flat
     >
@@ -111,13 +119,25 @@
 </template>
 
 <script setup>
-import { onMounted } from "vue";
+import { inject, onMounted, ref } from "vue";
 
 const form = defineModel({
   required: true,
   type: Object,
 });
 
+const serverErrors = inject("serverErrors", ref({}));
+
+const workingDaysRef = ref(null);
+
+const fieldRefs = {
+  working_days: workingDaysRef,
+};
+
+defineExpose({
+  getFieldRef: (field) => fieldRefs[field],
+});
+
 const days = [
   { value: 0, label: "DOM" },
   { value: 1, label: "SEG" },

+ 225 - 129
src/pages/LoginPage.vue

@@ -25,29 +25,53 @@
       </div>
 
       <div v-else key="flow" class="flow-screen">
-        <div v-if="!showSubStep" class="flow-header row items-center">
-          <q-btn
-            v-if="showBackButton"
-            class="flow-back-btn"
-            color="primary"
-            dense
-            flat
-            round
-            :aria-label="registrationCompleted ? t('auth.logout') : undefined"
-            :icon="registrationCompleted ? 'mdi-logout' : 'mdi-arrow-left'"
-            @click="goBack"
-          />
-        </div>
+        <div
+          v-if="registrationCompleted"
+          class="registration-flow"
+        >
+          <div class="flow-header row items-center">
+            <q-btn
+              class="flow-back-btn"
+              color="primary"
+              dense
+              flat
+              round
+              :aria-label="t('auth.logout')"
+              icon="mdi-logout"
+              @click="goBack"
+            />
+          </div>
+
+          <div class="flow-logo q-my-xl">
+            <q-img
+              style="max-width: 180px"
+              :src="LogoDiariaCampos"
+            />
+          </div>
 
-        <div v-if="!showSubStep" class="flow-logo q-my-xl">
-          <q-img
-            style="max-width: 180px"
-            :src="LogoDiariaCampos"
-          />
+          <div class="registration-complete">
+            <div class="registration-complete__card row no-wrap items-start">
+              <q-icon
+                class="registration-complete__icon"
+                name="mdi-clock-time-four-outline"
+                size="32px"
+              />
+
+              <div class="registration-complete__content">
+                <div class="registration-complete__title fontbold">
+                  {{ t("provider.login.registration_complete.title") }}
+                </div>
+
+                <div class="registration-complete__message font12">
+                  {{ t("provider.login.registration_complete.message") }}
+                </div>
+              </div>
+            </div>
+          </div>
         </div>
 
         <q-form
-          v-if="!registrationCompleted"
+          v-else
           ref="loginForm"
           autocapitalize="off"
           autocomplete="off"
@@ -55,48 +79,80 @@
           class="flow-form"
           spellcheck="false"
           @submit="onSubmit"
+          @validation-error="onValidationError"
         >
-          <div
-            class="flow-content"
-            :class="{
-              'flow-content--centered': steps <= 2 && !showSubStep,
-              'flow-content--email-only':
-                steps === 1 && !smsEnabled && !showSubStep,
-            }"
+          <q-scroll-area
+            ref="flowContentRef"
+            class="flow-scroll"
           >
-            <LoginStep1Panel
-              v-if="steps === 1"
-              v-model:email="email"
-              v-model:phone="phone"
-              :sms-enabled="smsEnabled"
-            />
-
-            <LoginStep2Panel
-              v-else-if="steps === 2"
-              v-model:code="code"
-            />
-
-            <LoginStep3Panel
-              v-else-if="steps === 3"
-              v-model="stepThreeForm"
-            />
-
-            <LoginStep4Panel
-              v-else-if="steps === 4"
-              v-model="stepFourForm"
-              @update:show-sub-step="showSubStep = $event"
-            />
+            <div class="flow-screen-content">
+              <div v-if="!showSubStep" class="flow-header row items-center">
+                <q-btn
+                  v-if="showBackButton"
+                  class="flow-back-btn"
+                  color="primary"
+                  dense
+                  flat
+                  round
+                  icon="mdi-arrow-left"
+                  @click="goBack"
+                />
+              </div>
 
-            <LoginStep5Panel
-              v-else-if="steps === 5"
-              v-model="stepFiveForm"
-            />
+              <div v-if="!showSubStep" class="flow-logo q-my-xl">
+                <q-img
+                  style="max-width: 180px"
+                  :src="LogoDiariaCampos"
+                />
+              </div>
 
-            <LoginStep6Panel
-              v-else-if="steps === 6"
-              v-model="stepSixForm"
-            />
-          </div>
+              <div
+                class="flow-content"
+                :class="{
+                  'flow-content--centered': steps <= 2 && !showSubStep,
+                  'flow-content--email-only':
+                    steps === 1 && !smsEnabled && !showSubStep,
+                }"
+              >
+                <LoginStep1Panel
+                  v-if="steps === 1"
+                  v-model:email="email"
+                  v-model:phone="phone"
+                  :sms-enabled="smsEnabled"
+                />
+
+                <LoginStep2Panel
+                  v-else-if="steps === 2"
+                  v-model:code="code"
+                />
+
+                <LoginStep3Panel
+                  v-else-if="steps === 3"
+                  ref="stepThreeRef"
+                  v-model="stepThreeForm"
+                />
+
+                <LoginStep4Panel
+                  v-else-if="steps === 4"
+                  ref="stepFourRef"
+                  v-model="stepFourForm"
+                  @update:show-sub-step="showSubStep = $event"
+                />
+
+                <LoginStep5Panel
+                  v-else-if="steps === 5"
+                  ref="stepFiveRef"
+                  v-model="stepFiveForm"
+                />
+
+                <LoginStep6Panel
+                  v-else-if="steps === 6"
+                  ref="stepSixRef"
+                  v-model="stepSixForm"
+                />
+              </div>
+            </div>
+          </q-scroll-area>
 
           <div v-if="!showSubStep" class="flow-footer">
             <q-btn
@@ -106,7 +162,7 @@
               rounded
               type="submit"
               :label="actionLabel"
-              :loading="submitting"
+              :loading="loading"
             >
               <template #loading>
                 <q-spinner />
@@ -115,39 +171,21 @@
           </div>
         </q-form>
 
-        <div v-else class="registration-complete">
-          <div class="registration-complete__card row no-wrap items-start">
-            <q-icon
-              class="registration-complete__icon"
-              name="mdi-clock-time-four-outline"
-              size="32px"
-            />
-
-            <div class="registration-complete__content">
-              <div class="registration-complete__title fontbold">
-                {{ t("provider.login.registration_complete.title") }}
-              </div>
-
-              <div class="registration-complete__message font12">
-                {{ t("provider.login.registration_complete.message") }}
-              </div>
-            </div>
-          </div>
-        </div>
       </div>
     </Transition>
   </q-page>
 </template>
 
 <script setup>
-import { computed, onMounted, ref } from "vue";
+import { computed, nextTick, onMounted, provide, ref, watch } from "vue";
 import { createUserAndProvider, sendCode, validateCode } from "src/api/user";
 import { getServiceConfiguration } from "src/api/serviceConfiguration";
 import { useAuth } from "src/composables/useAuth";
-import { useGeocodingApi } from "src/composables/useGeocodingApi";
 import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
 import { useRouter } from "vue-router";
+import { useScroll } from "src/composables/useScroll";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
 import { userStore } from "src/stores/user";
 
 import BackgroundLogin from "src/assets/background-login.svg";
@@ -165,8 +203,59 @@ const $q = useQuasar();
 const router = useRouter();
 const user = userStore();
 const { clearAuthData, logout, setAuthDataFromPayload } = useAuth();
-const { geocodeFullAddress } = useGeocodingApi();
 const { t } = useI18n();
+const { scrollToComponent } = useScroll();
+
+const {
+  loading,
+  serverErrors,
+  execute: submitForm,
+} = useSubmitHandler();
+
+const clearServerError = (field) => {
+  serverErrors.value[field] = null;
+};
+
+provide("serverErrors", serverErrors);
+provide("clearServerError", clearServerError);
+
+const FIELD_TO_STEP = {
+  name: 3, email: 3, phone: 3, rg: 3, document: 3, birth_date: 3, gender: 3,
+  zip_code: 3, address: 3, number: 3, district: 3, city: 3, state: 3,
+  complement: 3, nickname: 3, instructions: 3, address_type: 3,
+  selfie: 4, document_front: 4, document_back: 4,
+  daily_price_8h: 5, daily_price_6h: 5, daily_price_4h: 5, daily_price_2h: 5,
+  working_days: 6,
+};
+
+watch(serverErrors, () => {
+  const firstErrorField = Object.keys(serverErrors.value).find(
+    (field) => !!serverErrors.value[field] && FIELD_TO_STEP[field],
+  );
+
+  if (!firstErrorField) return;
+
+  const targetStep = FIELD_TO_STEP[firstErrorField];
+
+  if (targetStep !== steps.value) {
+    steps.value = targetStep;
+  }
+
+  nextTick(() => {
+    const panelRef =
+      targetStep === 3 ? stepThreeRef
+      : targetStep === 4 ? stepFourRef
+      : targetStep === 5 ? stepFiveRef
+      : targetStep === 6 ? stepSixRef
+      : null;
+
+    const fieldRef = panelRef?.value?.getFieldRef?.(firstErrorField);
+
+    if (fieldRef) {
+      scrollToComponent(fieldRef, flowContentRef);
+    }
+  });
+}, { deep: true });
 
 const clicked = ref(false);
 const code = ref("");
@@ -224,7 +313,6 @@ const createInitialStepThreeForm = () => ({
   longitude: null,
   name: "",
   nickname: "Principal",
-  no_complement: false,
   number: "",
   phone: "",
   rg: "",
@@ -239,7 +327,11 @@ const stepThreeForm = ref(createInitialStepThreeForm());
 
 const steps = ref(1);
 
-const submitting = ref(false);
+const flowContentRef = ref(null);
+const stepThreeRef = ref(null);
+const stepFourRef = ref(null);
+const stepFiveRef = ref(null);
+const stepSixRef = ref(null);
 
 const actionLabel = computed(() => {
   if (steps.value === 1) return t("provider.login.steps.step_1.action");
@@ -282,7 +374,7 @@ const resetToLoginStart = async () => {
   phone.value = "";
   registrationCompleted.value = false;
   showSubStep.value = false;
-  submitting.value = false;
+  loading.value = false;
   stepFiveForm.value = createInitialStepFiveForm();
   stepFourForm.value = createInitialStepFourForm();
   stepSixForm.value = createInitialStepSixForm();
@@ -293,33 +385,10 @@ const resetToLoginStart = async () => {
 
 //
 
-const hasCoordinateValue = (value) =>
-  value !== null && value !== undefined && String(value).trim() !== "";
-
 const hasWorkingDaySelected = () => mapWorkingDays().length > 0;
 
-const ensureStepThreeCoordinates = async () => {
-  const hasLatitude = hasCoordinateValue(stepThreeForm.value.latitude);
-  const hasLongitude = hasCoordinateValue(stepThreeForm.value.longitude);
-
-  if (hasLatitude && hasLongitude) return;
-
-  const geo = await geocodeFullAddress({
-    address: stepThreeForm.value.address,
-    number: stepThreeForm.value.number,
-    district: stepThreeForm.value.district,
-    zip_code: stepThreeForm.value.zip_code,
-    city: stepThreeForm.value.city,
-    state: stepThreeForm.value.state,
-  });
-
-  if (!hasCoordinateValue(geo?.lat) || !hasCoordinateValue(geo?.lng)) {
-    throw new Error("Não foi possível localizar as coordenadas do endereço.");
-  }
-
-  stepThreeForm.value.latitude = geo.lat;
-  stepThreeForm.value.longitude = geo.lng;
-};
+const hasCoordinateValue = (value) =>
+  value !== null && value !== undefined && String(value).trim() !== "";
 
 //
 
@@ -356,6 +425,11 @@ const mapWorkingDays = () => {
   return mapped;
 };
 
+const onValidationError = async (field) => {
+  await nextTick();
+  scrollToComponent(field, flowContentRef);
+};
+
 const onSubmit = async () => {
   if (registrationCompleted.value) return;
   if (showSubStep.value) return;
@@ -364,7 +438,7 @@ const onSubmit = async () => {
 
   if (!isValid) return;
 
-  submitting.value = true;
+  loading.value = true;
 
   try {
     switch (steps.value) {
@@ -407,7 +481,7 @@ const onSubmit = async () => {
 
       case 6: {
         if (await validateCurrentStep()) {
-          await registerUserAndProvider();
+          await submitForm(registerUserAndProvider);
         }
 
         break;
@@ -426,13 +500,11 @@ const onSubmit = async () => {
 
     $q.notify({ message: msg, type: "negative" });
   } finally {
-    submitting.value = false;
+    loading.value = false;
   }
 };
 
 const registerUserAndProvider = async () => {
-  await ensureStepThreeCoordinates();
-
   const workingDays = mapWorkingDays();
 
   const form = new FormData();
@@ -474,12 +546,8 @@ const registerUserAndProvider = async () => {
   append("address", stepThreeForm.value.address);
   append("number", stepThreeForm.value.number);
   append("district", stepThreeForm.value.district);
-  append("has_complement", !stepThreeForm.value.no_complement);
-
-  append(
-    "complement",
-    stepThreeForm.value.no_complement ? null : stepThreeForm.value.complement,
-  );
+  append("has_complement", true);
+  append("complement", stepThreeForm.value.complement);
 
   append("nickname", stepThreeForm.value.nickname);
   append("instructions", stepThreeForm.value.instructions);
@@ -568,10 +636,12 @@ const registerUserAndProvider = async () => {
 
   const response = await createUserAndProvider(form);
 
-  if (response.status >= 200 && response.status < 300) {
+  if (response?.status >= 200 && response.status < 300) {
     await setAuthDataFromPayload(response.data.payload);
     registrationCompleted.value = true;
   }
+
+  return response;
 };
 
 const sendValidationCode = async () => {
@@ -737,12 +807,20 @@ onMounted(() => {
 }
 
 .flow-screen {
-  display: flex;
-  flex-direction: column;
   width: 100%;
   max-width: 500px;
-  min-height: 100vh;
-  padding: 16px 20px;
+  height: 100vh;
+  height: 100dvh;
+  overflow: hidden;
+  background: var(--q-surface-dark);
+}
+
+.flow-screen-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 100%;
+  padding: 16px 20px 0;
   background: var(--q-surface-dark);
 }
 
@@ -752,26 +830,35 @@ onMounted(() => {
 }
 
 .flow-back-btn {
-  margin-left: -16px;
-  margin-right: 16px;
+  margin-left: -4px;
 }
 
 .flow-logo {
   display: flex;
   justify-content: center;
+  width: 100%;
   padding: 12px 0 20px;
 }
 
 .flow-form {
-  flex: 1;
+  height: 100%;
   display: flex;
   flex-direction: column;
+}
+
+.flow-scroll {
+  flex: 1;
+  height: 0;
   min-height: 0;
+
+  :deep(.q-scrollarea__content) {
+    display: flex;
+    flex-direction: column;
+  }
 }
 
 .flow-content {
   flex: 1;
-  overflow-y: auto;
 
   &--centered {
     display: flex;
@@ -787,7 +874,16 @@ onMounted(() => {
 }
 
 .flow-footer {
-  padding: 20px 0 calc(12px + env(safe-area-inset-bottom));
+  flex-shrink: 0;
+  padding: 8px 20px calc(12px + env(safe-area-inset-bottom));
+  background: var(--q-surface-dark);
+}
+
+.registration-flow {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 16px 20px;
 }
 
 .registration-complete {

+ 5 - 1
src/pages/dashboard/DashboardPage.vue

@@ -174,8 +174,12 @@ const openRatingDialog = (schedule) => {
 
 watch(
   () => route.query.showSuccessModal,
-  (val) => {
+  async (val) => {
     if (val === "true") {
+      if (!loading.value) {
+        await loadDashboard();
+      }
+
       $q.dialog({ component: OpportunityDialog }).onDismiss(() => {
         router.replace({ path: route.path, query: {} });
       });