|
|
@@ -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 {
|