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

feat: add possibilidade de alterar endereco tambem usando geolocalizao no perfil do prestador

Gustavo Mantovani 1 тиждень тому
батько
коміт
073bc4ebcb

+ 8 - 6
src/components/login/LoginStep3Panel.vue

@@ -404,12 +404,12 @@ const fillAddressFields = (data) => {
   form.value.state = data.state ?? "";
   form.value.state = data.state ?? "";
   form.value.zip_code = data.zip_code || form.value.zip_code || "";
   form.value.zip_code = data.zip_code || form.value.zip_code || "";
 
 
-  if (hasField("lat")) {
-    form.value.latitude = data.lat;
+  if (hasField("lat") || hasField("latitude")) {
+    form.value.latitude = data.lat ?? data.latitude;
   }
   }
 
 
-  if (hasField("lng")) {
-    form.value.longitude = data.lng;
+  if (hasField("lng") || hasField("longitude")) {
+    form.value.longitude = data.lng ?? data.longitude;
   }
   }
 };
 };
 
 
@@ -433,8 +433,6 @@ const fetchCep = async (rawCep) => {
         district: data.bairro,
         district: data.bairro,
         city: data.localidade,
         city: data.localidade,
         state: data.uf,
         state: data.uf,
-        lat: null,
-        lng: null,
       });
       });
     } else {
     } else {
       clearCepFields();
       clearCepFields();
@@ -450,6 +448,8 @@ const onCepChange = (value) => {
   const cleaned = value?.replace(/\D/g, "") || "";
   const cleaned = value?.replace(/\D/g, "") || "";
 
 
   if (cleaned.length === 8) {
   if (cleaned.length === 8) {
+    form.value.latitude = null;
+    form.value.longitude = null;
     fetchCep(value);
     fetchCep(value);
   } else {
   } else {
     clearCepFields();
     clearCepFields();
@@ -481,6 +481,8 @@ const useLocation = async () => {
       },
       },
     }).onOk((geoData) => {
     }).onOk((geoData) => {
       fillAddressFields(geoData);
       fillAddressFields(geoData);
+      form.value.latitude = geoData.lat ?? geoData.latitude ?? position.lat;
+      form.value.longitude = geoData.lng ?? geoData.longitude ?? position.lng;
     });
     });
   } catch (err) {
   } catch (err) {
     const isPermissionError =
     const isPermissionError =

+ 314 - 144
src/components/profile/ProfileAddressDialog.vue

@@ -1,100 +1,195 @@
 <template>
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
+  <q-dialog
+    ref="dialogRef"
+    maximized
+    persistent
+    transition-hide="slide-right"
+    transition-show="slide-left"
+  >
     <div class="bg-page full-height no-shadow">
     <div class="bg-page full-height no-shadow">
-      <div class="row items-center q-px-md q-pb-sm bg-white shadow-profile bg-surface dialog-safe-header">
-        <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
+      <div
+        class="row items-center q-px-md q-pb-sm bg-white shadow-profile bg-surface dialog-safe-header"
+      >
+        <q-btn
+          v-close-popup
+          color="primary"
+          dense
+          flat
+          icon="mdi-chevron-left"
+          round
+        />
+
         <q-space />
         <q-space />
-        <span class="font16 fontbold gradient-diarista">{{ $t('profile.address.title') }}</span>
+
+        <span class="font16 fontbold gradient-diarista">
+          {{ $t("profile.address.title") }}
+        </span>
+
         <q-space />
         <q-space />
+
         <div style="width: 32px"></div>
         <div style="width: 32px"></div>
       </div>
       </div>
+
       <q-card-section class="col q-pa-lg">
       <q-card-section class="col q-pa-lg">
         <div class="text-text">
         <div class="text-text">
-          <div class="gradient-diarista font16 fontbold">{{ $t('profile.address.address_subtitle') }}</div>
-          <div class="text-grey-7 q-mb-lg font12">{{ $t('profile.address.address_description') }}</div>
- 
+          <div class="gradient-diarista font16 fontbold">
+            {{ $t("profile.address.address_subtitle") }}
+          </div>
 
 
-          <q-card class="q-pa-lg bg-white shadow-card" style="border-radius: 25px;" :flat="false">
+          <div class="text-grey-7 q-mb-lg font12">
+            {{ $t("profile.address.address_description") }}
+          </div>
+
+          <q-card
+            class="q-pa-lg bg-white shadow-card"
+            style="border-radius: 25px"
+            :flat="false"
+          >
             <q-input
             <q-input
               v-model="search"
               v-model="search"
-              outlined
-              dense
               class="q-mb-md bg-white card-border"
               class="q-mb-md bg-white card-border"
+              dense
               input-class="text-text"
               input-class="text-text"
+              outlined
               :placeholder="$t('profile.address.search_placeholder')"
               :placeholder="$t('profile.address.search_placeholder')"
             >
             >
               <template #append>
               <template #append>
-                <q-icon name="mdi-magnify" color="grey-7"/>
+                <q-icon color="grey-7" name="mdi-magnify" />
               </template>
               </template>
             </q-input>
             </q-input>
 
 
-            <div class="text-text q-mb-xs font14 fontbold">{{ $t('profile.address.cep') }}</div>
+            <q-btn
+              class="full-width q-mb-md"
+              color="primary-button"
+              padding="8px 12px"
+              rounded
+              :label="$t('profile.address.use_location')"
+              :loading="loadingLocation"
+              @click="useCurrentLocation"
+            />
+
+            <div class="text-text q-mb-xs font14 fontbold">
+              {{ $t("profile.address.cep") }}
+            </div>
+
             <q-input
             <q-input
               v-model="form.zip_code"
               v-model="form.zip_code"
-              input-class="text-text"
-              outlined
+              class="q-mb-md"
               dense
               dense
+              input-class="text-text"
               mask="#####-###"
               mask="#####-###"
-              unmasked-value
+              outlined
               placeholder="00000-000"
               placeholder="00000-000"
-              class="q-mb-md"
+              unmasked-value
               @update:model-value="onCepChange"
               @update:model-value="onCepChange"
             >
             >
               <template #append>
               <template #append>
-                <q-spinner v-if="loadingCep" size="xs" color="primary" />
+                <q-spinner v-if="loadingCep" color="primary" size="xs" />
               </template>
               </template>
             </q-input>
             </q-input>
 
 
-            <div class="q-mb-xs text-text font14 fontbold">{{ $t('profile.address.address_label') }}</div>
-            <q-input 
-              v-model="form.address" 
-              outlined 
-              dense 
-              class="q-mb-md" 
-              input-class="text-text" 
+            <div class="q-mb-xs text-text font14 fontbold">
+              {{ $t("profile.address.address_label") }}
+            </div>
+
+            <q-input
+              v-model="form.address"
+              class="q-mb-md"
+              dense
+              input-class="text-text"
+              outlined
               :placeholder="$t('profile.address.address_placeholder')"
               :placeholder="$t('profile.address.address_placeholder')"
             />
             />
 
 
             <div class="row q-col-gutter-sm q-mb-md">
             <div class="row q-col-gutter-sm q-mb-md">
               <div class="col-4">
               <div class="col-4">
-                <div class="q-mb-xs text-text font14 fontbold">{{ $t('profile.address.number') }}</div>
-                <q-input v-model="form.number" outlined dense input-class="text-text" placeholder="0000"/>
+                <div class="q-mb-xs text-text font14 fontbold">
+                  {{ $t("profile.address.number") }}
+                </div>
+
+                <q-input
+                  v-model="form.number"
+                  dense
+                  input-class="text-text"
+                  outlined
+                  placeholder="0000"
+                />
               </div>
               </div>
+
               <div class="col-8">
               <div class="col-8">
-                <div class="q-mb-xs text-text font14 fontbold">{{ $t('profile.address.complement') }}</div>
-                <q-input v-model="form.complement" outlined dense input-class="text-text" :placeholder="$t('profile.address.complement_placeholder')"/>
+                <div class="q-mb-xs text-text font14 fontbold">
+                  {{ $t("profile.address.complement") }}
+                </div>
+
+                <q-input
+                  v-model="form.complement"
+                  dense
+                  input-class="text-text"
+                  outlined
+                  :placeholder="$t('profile.address.complement_placeholder')"
+                />
               </div>
               </div>
             </div>
             </div>
 
 
-            <div class="q-mb-xs text-text font14 fontbold">{{ $t('profile.address.district_label') }}</div>
-            <q-input v-model="form.district" outlined dense class="q-mb-md" input-class="text-text"/>
+            <div class="q-mb-xs text-text font14 fontbold">
+              {{ $t("profile.address.district_label") }}
+            </div>
+
+            <q-input
+              v-model="form.district"
+              class="q-mb-md"
+              dense
+              input-class="text-text"
+              outlined
+            />
 
 
             <div class="row q-col-gutter-sm q-mb-lg">
             <div class="row q-col-gutter-sm q-mb-lg">
               <div class="col-8">
               <div class="col-8">
-                <div class="q-mb-xs text-text font14 fontbold">{{ $t('profile.address.city_label') }}</div>
-                <q-input :model-value="form.city?.name" readonly outlined dense input-class="text-text"/>
+                <div class="q-mb-xs text-text font14 fontbold">
+                  {{ $t("profile.address.city_label") }}
+                </div>
+
+                <q-input
+                  dense
+                  input-class="text-text"
+                  outlined
+                  readonly
+                  :model-value="form.city?.name"
+                />
               </div>
               </div>
+
               <div class="col-4">
               <div class="col-4">
-                <div class="q-mb-xs text-text font14 fontbold">{{ $t('profile.address.state_label') }}</div>
-                <q-input :model-value="form.state?.name" readonly outlined dense input-class="text-text"/>
+                <div class="q-mb-xs text-text font14 fontbold">
+                  {{ $t("profile.address.state_label") }}
+                </div>
+
+                <q-input
+                  dense
+                  input-class="text-text"
+                  outlined
+                  readonly
+                  :model-value="form.state?.name"
+                />
               </div>
               </div>
             </div>
             </div>
 
 
             <div v-if="missingCoords" class="q-mb-md">
             <div v-if="missingCoords" class="q-mb-md">
-              <q-banner rounded dense class="bg-orange-1 text-warning q-mb-sm">
+              <q-banner class="bg-orange-1 text-warning q-mb-sm" dense rounded>
                 <template #avatar>
                 <template #avatar>
-                  <q-icon name="mdi-map-marker-off" color="orange-7" />
+                  <q-icon color="orange-7" name="mdi-map-marker-off" />
                 </template>
                 </template>
-                {{ $t('profile.address.missing_coords') }}
+
+                {{ $t("profile.address.missing_coords") }}
               </q-banner>
               </q-banner>
+
               <q-btn
               <q-btn
-                unelevated
-                rounded
-                no-caps
-                outline
+                class="full-width q-mb-md"
                 color="primary"
                 color="primary"
                 icon="mdi-map-marker"
                 icon="mdi-map-marker"
-                class="full-width q-mb-md"
+                no-caps
+                outline
+                rounded
+                unelevated
                 :label="$t('profile.address.update_on_map')"
                 :label="$t('profile.address.update_on_map')"
                 :loading="geocodingCep"
                 :loading="geocodingCep"
                 @click="openMapDialog"
                 @click="openMapDialog"
@@ -102,19 +197,20 @@
             </div>
             </div>
 
 
             <q-btn
             <q-btn
-              unelevated
-              rounded
-              no-caps
-              color="primary"
               class="full-width q-py-md"
               class="full-width q-py-md"
+              color="primary"
+              no-caps
               padding="8px 16px"
               padding="8px 16px"
+              rounded
+              unelevated
+              :disable="!hasUpdatedFields"
               :label="$t('profile.address.update_address')"
               :label="$t('profile.address.update_address')"
               :loading="saving"
               :loading="saving"
-              :disable="!hasUpdatedFields"
               @click="save"
               @click="save"
             />
             />
           </q-card>
           </q-card>
         </div>
         </div>
+
         <div class="q-pb-xl"></div>
         <div class="q-pb-xl"></div>
       </q-card-section>
       </q-card-section>
     </div>
     </div>
@@ -122,73 +218,119 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, computed, onMounted } from 'vue';
-import { useDialogPluginComponent, useQuasar } from 'quasar';
-import { searchAddressByCEP, updateAddress, createAddress, getAddresses } from 'src/api/address';
-import { userStore } from 'src/stores/user';
-import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker';
-import { useGeocodingApi } from 'src/composables/useGeocodingApi';
-import LocationMapDialog from 'src/components/shared/LocationMapDialog.vue';
+import { computed, onMounted, ref } from "vue";
+
+import {
+  createAddress,
+  getAddresses,
+  searchAddressByCEP,
+  updateAddress,
+} from "src/api/address";
+
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
+import { useGeocodingApi } from "src/composables/useGeocodingApi";
+import { useGeolocation } from "src/composables/useGeolocation";
+import { useI18n } from "vue-i18n";
+import { userStore } from "src/stores/user";
+
+import LocationMapDialog from "src/components/shared/LocationMapDialog.vue";
 
 
 defineEmits([...useDialogPluginComponent.emits]);
 defineEmits([...useDialogPluginComponent.emits]);
 
 
 const { dialogRef, onDialogOK } = useDialogPluginComponent();
 const { dialogRef, onDialogOK } = useDialogPluginComponent();
+const { geocodeFullAddress } = useGeocodingApi();
+const { requestPermission, getCurrentPosition } = useGeolocation();
+const { t } = useI18n();
 const $q = useQuasar();
 const $q = useQuasar();
+
 const user = userStore();
 const user = userStore();
-const providerId = user.user.provider.id;
-const { geocodeFullAddress } = useGeocodingApi();
-const {
-  form,
-  hasUpdatedFields,
-  getUpdatedFields,
-  setUpdateFormAsOriginal,
-} = useFormUpdateTracker({
-  zip_code: '',
-  address: '',
-  number: '',
-  complement: '',
-  district: '',
-  city_id: null,
-  state_id: null,
-  city: null,
-  state: null,
-  source: 'provider',
-  source_id: providerId,
-  address_type: 'home',
-  latitude: null,
-  longitude: null,
-});
 
 
-const search = ref('');
-const loadingCep = ref(false);
-const saving = ref(false);
 const addressId = ref(null);
 const addressId = ref(null);
 const geocodingCep = ref(false);
 const geocodingCep = ref(false);
+const loadingCep = ref(false);
+const loadingLocation = ref(false);
+const saving = ref(false);
+const search = ref("");
+
+const providerId = user.user.provider.id;
 
 
-const missingCoords = computed(() =>
-  addressId.value !== null && form.latitude == null && form.longitude == null
+const { form, getUpdatedFields, hasUpdatedFields, setUpdateFormAsOriginal } =
+  useFormUpdateTracker({
+    address: "",
+    address_type: "home",
+    city: null,
+    city_id: null,
+    complement: "",
+    district: "",
+    latitude: null,
+    longitude: null,
+    number: "",
+    source: "provider",
+    source_id: providerId,
+    state: null,
+    state_id: null,
+    zip_code: "",
+  });
+
+const missingCoords = computed(
+  () =>
+    addressId.value !== null &&
+    (form.latitude == null || form.longitude == null),
 );
 );
 
 
-const onCepChange = async (val) => {
-  if (val?.length === 8) {
-    loadingCep.value = true;
+const applyGeoData = async (geoData) => {
+  form.latitude = geoData.lat;
+  form.longitude = geoData.lng;
+  form.address = geoData.address || form.address;
+  form.number = geoData.number || form.number;
+  form.district = geoData.district || form.district;
+
+  if (geoData.zip_code) {
+    form.zip_code = geoData.zip_code.replace(/\D/g, "");
+
     try {
     try {
-      const data = await searchAddressByCEP(val);
-      if (data) {
-        form.address = data.address;
-        form.district = data.district;
-        form.city_id = data.city_id;
-        form.state_id = data.state_id;
-        form.city = data.city;
-        form.state = data.state;
-        form.latitude = null;
-        form.longitude = null;
-      } else {
-        $q.notify({ type: 'negative', message: 'CEP não encontrado' });
+      const addressData = await searchAddressByCEP(form.zip_code);
+
+      if (addressData) {
+        form.city_id = addressData.city_id;
+        form.state_id = addressData.state_id;
+        form.city = addressData.city;
+        form.state = addressData.state;
       }
       }
-    } finally {
-      loadingCep.value = false;
+    } catch {
+      // Mantém cidade/estado atual se o lookup falhar.
+    }
+  }
+};
+
+const onCepChange = async (value) => {
+  if (value?.length !== 8) {
+    return;
+  }
+
+  loadingCep.value = true;
+
+  try {
+    const data = await searchAddressByCEP(value);
+
+    if (data) {
+      form.address = data.address;
+      form.district = data.district;
+      form.city_id = data.city_id;
+      form.state_id = data.state_id;
+      form.city = data.city;
+      form.state = data.state;
+      form.latitude = null;
+      form.longitude = null;
+    } else {
+      $q.notify({
+        message: "CEP não encontrado",
+        type: "negative",
+      });
     }
     }
+  } finally {
+    loadingCep.value = false;
   }
   }
 };
 };
 
 
@@ -200,61 +342,86 @@ const openMapDialog = async () => {
 
 
   if (hasAddress) {
   if (hasAddress) {
     geocodingCep.value = true;
     geocodingCep.value = true;
+
     try {
     try {
       const geo = await geocodeFullAddress({
       const geo = await geocodeFullAddress({
-        address:  form.address,
-        number:   form.number,
+        address: form.address,
+        city: form.city?.name,
         district: form.district,
         district: form.district,
+        number: form.number,
+        state: form.state?.name,
         zip_code: form.zip_code,
         zip_code: form.zip_code,
-        city:     form.city?.name,
-        state:    form.state?.name,
       });
       });
+
       if (geo) {
       if (geo) {
         initialLat = geo.lat;
         initialLat = geo.lat;
         initialLng = geo.lng;
         initialLng = geo.lng;
       }
       }
-    } catch (err) {
-      console.error('geocodeFullAddress falhou, usando coordenada default:', err);
+    } catch (error) {
+      console.error(
+        "geocodeFullAddress falhou, usando coordenada default:",
+        error,
+      );
     } finally {
     } finally {
       geocodingCep.value = false;
       geocodingCep.value = false;
     }
     }
   }
   }
 
 
-  const dialogProps = initialLat !== null
-    ? { initialLat, initialLng }
-    : {};
+  const dialogProps = initialLat !== null ? { initialLat, initialLng } : {};
 
 
   $q.dialog({
   $q.dialog({
     component: LocationMapDialog,
     component: LocationMapDialog,
     componentProps: dialogProps,
     componentProps: dialogProps,
-  }).onOk(async (geoData) => {
-    form.latitude = geoData.lat;
-    form.longitude = geoData.lng;
-    form.address = geoData.address || form.address;
-    form.number = geoData.number || form.number;
-    form.district = geoData.district || form.district;
-
-    if (geoData.zip_code) {
-      form.zip_code = geoData.zip_code.replace(/\D/g, '');
-      try {
-        const addressData = await searchAddressByCEP(form.zip_code);
-        if (addressData) {
-          form.city_id = addressData.city_id;
-          form.state_id = addressData.state_id;
-          form.city = addressData.city;
-          form.state = addressData.state;
-        }
-      } catch {
-        // mantém cidade/estado atual se lookup falhar
-      }
+  }).onOk(applyGeoData);
+};
+
+const useCurrentLocation = async () => {
+  loadingLocation.value = true;
+
+  try {
+    const granted = await requestPermission();
+
+    if (!granted) {
+      $q.notify({
+        message: t("profile.address.location_permission_denied"),
+        type: "warning",
+      });
+
+      return;
     }
     }
-  });
+
+    const position = await getCurrentPosition();
+
+    $q.dialog({
+      component: LocationMapDialog,
+      componentProps: {
+        initialLat: position.lat,
+        initialLng: position.lng,
+      },
+    }).onOk(applyGeoData);
+  } catch (error) {
+    const isPermissionError =
+      error?.code === 1 || error?.message?.toLowerCase().includes("denied");
+
+    $q.notify({
+      message: isPermissionError
+        ? t("profile.address.location_permission_denied")
+        : t("profile.address.location_error"),
+      type: "warning",
+    });
+  } finally {
+    loadingLocation.value = false;
+  }
 };
 };
 
 
+//
+
 const save = async () => {
 const save = async () => {
   saving.value = true;
   saving.value = true;
+
   try {
   try {
     let response;
     let response;
+
     if (addressId.value) {
     if (addressId.value) {
       response = await updateAddress(getUpdatedFields.value, addressId.value);
       response = await updateAddress(getUpdatedFields.value, addressId.value);
     } else {
     } else {
@@ -266,39 +433,42 @@ const save = async () => {
       onDialogOK(response);
       onDialogOK(response);
     }
     }
   } catch (error) {
   } catch (error) {
-    console.error('Erro ao salvar endereço:', error);
+    console.error("Erro ao salvar endereço:", error);
   } finally {
   } finally {
     saving.value = false;
     saving.value = false;
   }
   }
 };
 };
 
 
 onMounted(async () => {
 onMounted(async () => {
-  const providersAddresses = await getAddresses('provider', providerId);
+  const providersAddresses = await getAddresses("provider", providerId);
+
   if (providersAddresses && providersAddresses.length > 0) {
   if (providersAddresses && providersAddresses.length > 0) {
     const current = providersAddresses[0];
     const current = providersAddresses[0];
+
     addressId.value = current.id;
     addressId.value = current.id;
+
     const initialData = {
     const initialData = {
-      zip_code: current.zip_code || '',
-      address: current.address || '',
-      number: current.number || '',
-      complement: current.complement || '',
-      district: current.district || '',
-      city_id: current.city_id || null,
-      state_id: current.state_id || null,
+      address: current.address || "",
+      address_type: current.address_type || "home",
       city: current.city || null,
       city: current.city || null,
-      state: current.state || null,
-      source: 'provider',
-      source_id: providerId,
-      address_type: current.address_type || 'home',
+      city_id: current.city_id || null,
+      complement: current.complement || "",
+      district: current.district || "",
       latitude: current.latitude ?? null,
       latitude: current.latitude ?? null,
       longitude: current.longitude ?? null,
       longitude: current.longitude ?? null,
+      number: current.number || "",
+      source: "provider",
+      source_id: providerId,
+      state: current.state || null,
+      state_id: current.state_id || null,
+      zip_code: current.zip_code || "",
     };
     };
 
 
     Object.assign(form, initialData);
     Object.assign(form, initialData);
+
     setUpdateFormAsOriginal();
     setUpdateFormAsOriginal();
   }
   }
 });
 });
 </script>
 </script>
 
 
-<style scoped>
-</style>
+<style scoped></style>

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

@@ -598,7 +598,10 @@
       "update_address": "Change address",
       "update_address": "Change address",
       "search_placeholder": "Search",
       "search_placeholder": "Search",
       "missing_coords": "Address without location. Set the pin on the map.",
       "missing_coords": "Address without location. Set the pin on the map.",
-      "update_on_map": "Update on map"
+      "update_on_map": "Update on map",
+      "use_location": "Use my location",
+      "location_permission_denied": "Location permission denied.",
+      "location_error": "Could not get your location."
     },
     },
     "help": {
     "help": {
       "title": "Help",
       "title": "Help",

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

@@ -598,7 +598,10 @@
       "update_address": "Cambiar dirección",
       "update_address": "Cambiar dirección",
       "search_placeholder": "Buscar",
       "search_placeholder": "Buscar",
       "missing_coords": "Dirección sin ubicación. Defina el punto en el mapa.",
       "missing_coords": "Dirección sin ubicación. Defina el punto en el mapa.",
-      "update_on_map": "Actualizar en el mapa"
+      "update_on_map": "Actualizar en el mapa",
+      "use_location": "Usar mi ubicación",
+      "location_permission_denied": "Permiso de ubicación denegado.",
+      "location_error": "No fue posible obtener su ubicación."
     },
     },
     "help": {
     "help": {
       "title": "Ayuda",
       "title": "Ayuda",

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

@@ -601,7 +601,10 @@
       "update_address": "Alterar endereço",
       "update_address": "Alterar endereço",
       "search_placeholder": "Buscar",
       "search_placeholder": "Buscar",
       "missing_coords": "Endereço sem localização. Defina o ponto no mapa.",
       "missing_coords": "Endereço sem localização. Defina o ponto no mapa.",
-      "update_on_map": "Atualizar no mapa"
+      "update_on_map": "Atualizar no mapa",
+      "use_location": "Usar minha localização",
+      "location_permission_denied": "Permissão de localização negada.",
+      "location_error": "Não foi possível obter sua localização."
     },
     },
     "help": {
     "help": {
       "title": "Ajuda",
       "title": "Ajuda",

+ 79 - 9
src/pages/LoginPage.vue

@@ -18,7 +18,7 @@
       <div v-else key="flow" class="flow-screen">
       <div v-else key="flow" class="flow-screen">
         <div v-if="!showSubStep" class="flow-header row items-center">
         <div v-if="!showSubStep" class="flow-header row items-center">
           <q-btn
           <q-btn
-            v-if="steps >= 4"
+            v-if="showBackButton"
             class="flow-back-btn"
             class="flow-back-btn"
             color="primary"
             color="primary"
             dense
             dense
@@ -110,6 +110,7 @@
 import { computed, onMounted, ref } from "vue";
 import { computed, onMounted, ref } from "vue";
 import { createUserAndProvider, sendCode, validateCode } from "src/api/user";
 import { createUserAndProvider, sendCode, validateCode } from "src/api/user";
 import { useAuth } from "src/composables/useAuth";
 import { useAuth } from "src/composables/useAuth";
+import { useGeocodingApi } from "src/composables/useGeocodingApi";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 import { useRouter } from "vue-router";
 import { useRouter } from "vue-router";
@@ -127,7 +128,8 @@ import LoginStep6Panel from "src/components/login/LoginStep6Panel.vue";
 
 
 const $q = useQuasar();
 const $q = useQuasar();
 const router = useRouter();
 const router = useRouter();
-const { setAuthDataFromPayload } = useAuth();
+const { clearAuthData, setAuthDataFromPayload } = useAuth();
+const { geocodeFullAddress } = useGeocodingApi();
 const { t } = useI18n();
 const { t } = useI18n();
 
 
 const clicked = ref(false);
 const clicked = ref(false);
@@ -139,7 +141,7 @@ const phone = ref("");
 const registrationCompleted = ref(false);
 const registrationCompleted = ref(false);
 const showSubStep = ref(false);
 const showSubStep = ref(false);
 
 
-const stepFiveForm = ref({
+const createInitialStepFiveForm = () => ({
   daily_price_2h: null,
   daily_price_2h: null,
   daily_price_4h: null,
   daily_price_4h: null,
   daily_price_6h: null,
   daily_price_6h: null,
@@ -147,13 +149,13 @@ const stepFiveForm = ref({
   services_types_ids: [],
   services_types_ids: [],
 });
 });
 
 
-const stepFourForm = ref({
+const createInitialStepFourForm = () => ({
   document_back: null,
   document_back: null,
   document_front: null,
   document_front: null,
   selfie: null,
   selfie: null,
 });
 });
 
 
-const stepSixForm = ref({
+const createInitialStepSixForm = () => ({
   account_check_digit: "",
   account_check_digit: "",
   account_number: "",
   account_number: "",
   account_type: "checking",
   account_type: "checking",
@@ -164,7 +166,7 @@ const stepSixForm = ref({
   working_days: {},
   working_days: {},
 });
 });
 
 
-const stepThreeForm = ref({
+const createInitialStepThreeForm = () => ({
   address: "",
   address: "",
   address_type: "home",
   address_type: "home",
   birth_date: "",
   birth_date: "",
@@ -186,6 +188,11 @@ const stepThreeForm = ref({
   zip_code: "",
   zip_code: "",
 });
 });
 
 
+const stepFiveForm = ref(createInitialStepFiveForm());
+const stepFourForm = ref(createInitialStepFourForm());
+const stepSixForm = ref(createInitialStepSixForm());
+const stepThreeForm = ref(createInitialStepThreeForm());
+
 const steps = ref(1);
 const steps = ref(1);
 
 
 const submitting = ref(false);
 const submitting = ref(false);
@@ -198,7 +205,18 @@ const actionLabel = computed(() => {
   return t("provider.login.steps.step_3.action");
   return t("provider.login.steps.step_3.action");
 });
 });
 
 
-const goBack = () => {
+const showBackButton = computed(
+  () => registrationCompleted.value || steps.value >= 4,
+);
+
+const goBack = async () => {
+  if (registrationCompleted.value) {
+    await resetToLoginStart();
+    router.replace({ name: "LoginPage" });
+
+    return;
+  }
+
   if (steps.value === 4) {
   if (steps.value === 4) {
     steps.value = 3;
     steps.value = 3;
   } else if (steps.value === 5) {
   } else if (steps.value === 5) {
@@ -208,8 +226,52 @@ const goBack = () => {
   }
   }
 };
 };
 
 
+const resetToLoginStart = async () => {
+  await clearAuthData();
+
+  code.value = "";
+  email.value = "";
+  isLogin.value = false;
+  phone.value = "";
+  registrationCompleted.value = false;
+  showSubStep.value = false;
+  submitting.value = false;
+  stepFiveForm.value = createInitialStepFiveForm();
+  stepFourForm.value = createInitialStepFourForm();
+  stepSixForm.value = createInitialStepSixForm();
+  stepThreeForm.value = createInitialStepThreeForm();
+  steps.value = 1;
+  loginForm.value?.resetValidation?.();
+};
+
 const hasWorkingDaySelected = () => mapWorkingDays().length > 0;
 const hasWorkingDaySelected = () => mapWorkingDays().length > 0;
 
 
+const hasCoordinateValue = (value) =>
+  value !== null && value !== undefined && String(value).trim() !== "";
+
+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 mapWorkingDays = () => {
 const mapWorkingDays = () => {
   const mapped = [];
   const mapped = [];
 
 
@@ -303,6 +365,8 @@ const onSubmit = async () => {
 };
 };
 
 
 const registerUserAndProvider = async () => {
 const registerUserAndProvider = async () => {
+  await ensureStepThreeCoordinates();
+
   const workingDays = mapWorkingDays();
   const workingDays = mapWorkingDays();
 
 
   const form = new FormData();
   const form = new FormData();
@@ -326,6 +390,12 @@ const registerUserAndProvider = async () => {
     form.append(key, val);
     form.append(key, val);
   };
   };
 
 
+  const appendCoordinate = (key, val) => {
+    if (!hasCoordinateValue(val)) return;
+
+    form.append(key, String(val));
+  };
+
   append("name", stepThreeForm.value.name);
   append("name", stepThreeForm.value.name);
   append("email", stepThreeForm.value.email || email.value);
   append("email", stepThreeForm.value.email || email.value);
   append("phone", stepThreeForm.value.phone || phone.value);
   append("phone", stepThreeForm.value.phone || phone.value);
@@ -348,8 +418,8 @@ const registerUserAndProvider = async () => {
   append("instructions", stepThreeForm.value.instructions);
   append("instructions", stepThreeForm.value.instructions);
   append("city", stepThreeForm.value.city);
   append("city", stepThreeForm.value.city);
   append("state", stepThreeForm.value.state);
   append("state", stepThreeForm.value.state);
-  append("latitude", stepThreeForm.value.latitude);
-  append("longitude", stepThreeForm.value.longitude);
+  appendCoordinate("latitude", stepThreeForm.value.latitude);
+  appendCoordinate("longitude", stepThreeForm.value.longitude);
   append("address_type", stepThreeForm.value.address_type);
   append("address_type", stepThreeForm.value.address_type);
 
 
   append("daily_price_8h", Number(stepFiveForm.value.daily_price_8h));
   append("daily_price_8h", Number(stepFiveForm.value.daily_price_8h));