Browse Source

feat: permite o prestador adicionar localizacao via api do google maps

Gustavo Mantovani 1 week ago
parent
commit
4c9d4cc2d5

+ 8 - 7
quasar.config.js

@@ -26,13 +26,14 @@ const loadAppEnv = (ctx) => {
   const fileEnv = parseEnvFile(resolve(process.cwd(), envFile));
 
   return {
-    APP_ENV:           appEnv,
-    API_URL:           fileEnv.API_URL,
-    PASSWORD:          fileEnv.PASSWORD,
-    WEBSOCKET_API:     fileEnv.WEBSOCKET_API,
-    WEBSOCKET_PATH:    fileEnv.WEBSOCKET_PATH,
-    WEBSOCKET_ROOM:    fileEnv.WEBSOCKET_ROOM,
-    WEBSOCKET_API_KEY: fileEnv.WEBSOCKET_API_KEY,
+    APP_ENV:             appEnv,
+    API_URL:             fileEnv.API_URL,
+    PASSWORD:            fileEnv.PASSWORD,
+    WEBSOCKET_API:       fileEnv.WEBSOCKET_API,
+    WEBSOCKET_PATH:      fileEnv.WEBSOCKET_PATH,
+    WEBSOCKET_ROOM:      fileEnv.WEBSOCKET_ROOM,
+    WEBSOCKET_API_KEY:   fileEnv.WEBSOCKET_API_KEY,
+    GOOGLE_MAPS_API_KEY: fileEnv.GOOGLE_MAPS_API_KEY,
   };
 };
 

+ 286 - 38
src/components/login/LoginStep3Panel.vue

@@ -2,7 +2,7 @@
   <q-card-section class="no-padding">
     <div class="text-text">
       <span class="font12 fontbold">
-        {{ $t('provider.login.steps.step_3.full_name') }}
+        {{ $t("provider.login.steps.step_3.full_name") }}
       </span>
     </div>
 
@@ -21,7 +21,7 @@
 
     <div class="text-text">
       <span class="font12 fontbold">
-        {{ $t('common.terms.phone') }}
+        {{ $t("common.terms.phone") }}
       </span>
     </div>
 
@@ -41,7 +41,7 @@
 
     <div class="text-text">
       <span class="font12 fontbold">
-        {{ $t('common.terms.email') }}
+        {{ $t("common.terms.email") }}
       </span>
     </div>
 
@@ -62,7 +62,7 @@
 
     <div class="text-text">
       <span class="font12 fontbold">
-        {{ $t('provider.login.steps.step_3.rg') }}
+        {{ $t("provider.login.steps.step_3.rg") }}
       </span>
     </div>
 
@@ -83,7 +83,7 @@
 
     <div class="text-text">
       <span class="font12 fontbold">
-        {{ $t('common.terms.cpf') }}
+        {{ $t("common.terms.cpf") }}
       </span>
     </div>
 
@@ -104,7 +104,7 @@
 
     <div class="text-text">
       <span class="font12 fontbold">
-        {{ $t('provider.login.steps.step_3.birth_date') }}
+        {{ $t("provider.login.steps.step_3.birth_date") }}
       </span>
     </div>
 
@@ -124,7 +124,7 @@
 
     <div class="text-text">
       <span class="font12 fontbold">
-        {{ $t('common.terms.cep') }}
+        {{ $t("common.terms.cep") }}
       </span>
     </div>
 
@@ -145,9 +145,19 @@
       @update:model-value="onCepChange"
     />
 
+    <q-btn
+      class="full-width q-mb-md"
+      color="primary-button"
+      padding="8px 12px"
+      rounded
+      :label="$t('provider.login.steps.step_3.use_location')"
+      :loading="loadingLocation"
+      @click="useLocation"
+    />
+
     <div class="text-text">
       <span class="font12 fontbold">
-        {{ $t('common.terms.address') }}
+        {{ $t("common.terms.address") }}
       </span>
     </div>
 
@@ -159,15 +169,98 @@
       lazy-rules
       no-error-icon
       outlined
+      readonly
       rounded
-      :placeholder="$t('provider.login.steps.step_3.address_placeholder')"
+      :placeholder="`${$t('common.terms.address')}...`"
       :rules="[inputRules.required]"
     />
 
-    <div class="text-center">
+    <div class="row q-col-gutter-sm">
+      <div class="col-4">
+        <div class="text-text">
+          <span class="font12 fontbold">
+            {{ $t("common.terms.address_number") }}
+          </span>
+        </div>
+
+        <q-input
+          v-model="form.number"
+          class="bg-surface q-mb-md q-mt-sm"
+          hide-bottom-space
+          input-class="font12 fontmedium text-text"
+          lazy-rules
+          no-error-icon
+          outlined
+          placeholder="0000"
+          rounded
+          :rules="[inputRules.required]"
+        />
+      </div>
+
+      <div class="col-8">
+        <div class="text-text">
+          <span class="font12 fontbold">
+            {{ $t("common.terms.district") }}
+          </span>
+        </div>
+
+        <q-input
+          v-model="form.district"
+          class="bg-surface q-mb-md q-mt-sm"
+          hide-bottom-space
+          input-class="font12 fontmedium text-text"
+          no-error-icon
+          outlined
+          readonly
+          rounded
+          :placeholder="`${$t('common.terms.district')}...`"
+        />
+      </div>
+    </div>
+
+    <div class="row q-col-gutter-sm">
+      <div class="col-8">
+        <div class="text-text">
+          <span class="font12 fontbold">
+            {{ $t("common.terms.city") }}
+          </span>
+        </div>
+
+        <q-input
+          v-model="form.city"
+          class="bg-surface q-mb-md q-mt-sm"
+          input-class="font12 fontmedium text-text"
+          no-error-icon
+          outlined
+          readonly
+          rounded
+        />
+      </div>
+
+      <div class="col-4">
+        <div class="text-text">
+          <span class="font12 fontbold">
+            {{ $t("common.terms.state") }}
+          </span>
+        </div>
+
+        <q-input
+          v-model="form.state"
+          class="bg-surface q-mb-md q-mt-sm"
+          hide-bottom-space
+          input-class="font12 fontmedium text-text"
+          no-error-icon
+          outlined
+          readonly
+          rounded
+        />
+      </div>
+    </div>
+
+    <div>
       <q-checkbox
         v-model="form.no_complement"
-        class="q-mb-md text-text"
+        class="q-mb-md text-text font12 fontbold"
         color="primary"
         keep-color
         :label="$t('provider.login.steps.step_3.no_complement')"
@@ -177,7 +270,7 @@
     <template v-if="!form.no_complement">
       <div class="text-text">
         <span class="font12 fontbold">
-          {{ $t('common.terms.complement') }}
+          {{ $t("common.terms.complement") }}
         </span>
       </div>
 
@@ -194,14 +287,72 @@
         :rules="[inputRules.required]"
       />
     </template>
+
+    <div class="text-text">
+      <span class="font12 fontbold">
+        {{ $t("provider.login.steps.step_3.address_nickname") }}
+      </span>
+    </div>
+
+    <q-input
+      v-model="form.nickname"
+      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('common.ui.misc.example')}: Casa`"
+    />
+
+    <div class="text-text">
+      <span class="font12 fontbold">
+        {{ $t("provider.login.steps.step_3.address_instructions") }}
+      </span>
+    </div>
+
+    <q-input
+      v-model="form.instructions"
+      autogrow
+      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
+      rows="3"
+      type="textarea"
+    />
+
+    <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"
+        text-color="surface"
+        :icon="type.icon"
+        :icon-selected="type.icon"
+        :outline="form.address_type !== type.value"
+        @click="form.address_type = type.value"
+      >
+        {{ $t(type.label) }}
+      </q-chip>
+    </div>
   </q-card-section>
 </template>
 
 <script setup>
-import axios from 'axios';
-import { useInputRules } from 'src/composables/useInputRules';
-import { ref } from 'vue';
-import { useI18n } from 'vue-i18n';
+import axios from "axios";
+import { useQuasar } from "quasar";
+import LocationMapDialog from "src/components/shared/LocationMapDialog.vue";
+import { useGeolocation } from "src/composables/useGeolocation";
+import { useInputRules } from "src/composables/useInputRules";
+import { ref } from "vue";
+import { useI18n } from "vue-i18n";
 
 const form = defineModel({
   required: true,
@@ -210,11 +361,60 @@ const form = defineModel({
 
 const { inputRules } = useInputRules();
 const { t } = useI18n();
+const $q = useQuasar();
+const { requestPermission, getCurrentPosition } = useGeolocation();
 
 const loadingCep = ref(false);
+const loadingLocation = ref(false);
+
+const addressTypes = [
+  {
+    icon: "mdi-home-outline",
+    label: "provider.login.steps.step_3.address_type_home",
+    value: "home",
+  },
+  {
+    icon: "mdi-briefcase-variant-outline",
+    label: "provider.login.steps.step_3.address_type_commercial",
+    value: "commercial",
+  },
+  {
+    icon: "mdi-map-marker-outline",
+    label: "provider.login.steps.step_3.address_type_other",
+    value: "other",
+  },
+];
+
+const clearCepFields = () => {
+  form.value.address = "";
+  form.value.district = "";
+  form.value.city = "";
+  form.value.state = "";
+  form.value.latitude = null;
+  form.value.longitude = null;
+};
+
+const fillAddressFields = (data) => {
+  const hasField = (field) => Object.prototype.hasOwnProperty.call(data, field);
+
+  form.value.address = data.address ?? "";
+  form.value.number = data.number || form.value.number || "";
+  form.value.district = data.district ?? "";
+  form.value.city = data.city ?? "";
+  form.value.state = data.state ?? "";
+  form.value.zip_code = data.zip_code || form.value.zip_code || "";
+
+  if (hasField("lat")) {
+    form.value.latitude = data.lat;
+  }
+
+  if (hasField("lng")) {
+    form.value.longitude = data.lng;
+  }
+};
 
 const fetchCep = async (rawCep) => {
-  const cleaned = rawCep.replace(/\D/g, '');
+  const cleaned = rawCep.replace(/\D/g, "");
 
   if (cleaned.length !== 8) {
     return;
@@ -223,43 +423,89 @@ const fetchCep = async (rawCep) => {
   loadingCep.value = true;
 
   try {
-    const { data } = await axios.get(`https://viacep.com.br/ws/${cleaned}/json/`);
+    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;
+      fillAddressFields({
+        address: data.logradouro,
+        district: data.bairro,
+        city: data.localidade,
+        state: data.uf,
+        lat: null,
+        lng: null,
+      });
     } else {
-      form.value.address = '';
-      form.value.city = '';
-      form.value.state = '';
+      clearCepFields();
     }
   } catch {
-    form.value.address = '';
-    form.value.city = '';
-    form.value.state = '';
+    clearCepFields();
   } finally {
     loadingCep.value = false;
   }
 };
 
 const onCepChange = (value) => {
-  const cleaned = value?.replace(/\D/g, '') || '';
+  const cleaned = value?.replace(/\D/g, "") || "";
 
   if (cleaned.length === 8) {
     fetchCep(value);
+  } else {
+    clearCepFields();
+  }
+};
+
+const useLocation = async () => {
+  loadingLocation.value = true;
+
+  try {
+    const granted = await requestPermission();
+
+    if (!granted) {
+      $q.notify({
+        message: t("provider.login.steps.step_3.location_permission_denied"),
+        type: "warning",
+      });
+
+      return;
+    }
+
+    const position = await getCurrentPosition();
+
+    $q.dialog({
+      component: LocationMapDialog,
+      componentProps: {
+        initialLat: position.lat,
+        initialLng: position.lng,
+      },
+    }).onOk((geoData) => {
+      fillAddressFields(geoData);
+    });
+  } catch (err) {
+    const isPermissionError =
+      err?.code === 1 || err?.message?.toLowerCase().includes("denied");
+
+    $q.notify({
+      message: isPermissionError
+        ? t("provider.login.steps.step_3.location_permission_denied")
+        : t("provider.login.steps.step_3.location_error"),
+      type: "warning",
+    });
+  } finally {
+    loadingLocation.value = false;
   }
 };
 
 const validateBirthDate = (value) => {
   if (!value) {
-    return t('validation.rules.required');
+    return t("validation.rules.required");
   }
 
   const matches = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value);
 
   if (!matches) {
-    return t('provider.login.steps.step_3.date_invalid');
+    return t("provider.login.steps.step_3.date_invalid");
   }
 
   const day = Number(matches[1]);
@@ -268,24 +514,26 @@ const validateBirthDate = (value) => {
 
   const date = new Date(year, month - 1, day);
   const isValidDate =
-    date.getFullYear() === year
-    && date.getMonth() === month - 1
-    && date.getDate() === day;
+    date.getFullYear() === year &&
+    date.getMonth() === month - 1 &&
+    date.getDate() === day;
 
   if (!isValidDate) {
-    return t('provider.login.steps.step_3.date_invalid');
+    return t("provider.login.steps.step_3.date_invalid");
   }
 
   if (date > new Date()) {
-    return t('provider.login.steps.step_3.date_invalid');
+    return t("provider.login.steps.step_3.date_invalid");
   }
 
   return true;
 };
 
 const validateRG = (value) => {
-  const cleanedValue = value?.replace(/\D/g, '') || '';
+  const cleanedValue = value?.replace(/\D/g, "") || "";
 
-  return cleanedValue.length >= 8 || t('provider.login.steps.step_3.rg_invalid');
+  return (
+    cleanedValue.length >= 8 || t("provider.login.steps.step_3.rg_invalid")
+  );
 };
-</script>
+</script>

+ 2 - 1
src/components/shared/LocationMapDialog.vue

@@ -26,7 +26,7 @@
           :placeholder="reversing ? '' : $t('auth.geocoding_failed_short')"
         />
         <q-btn
-          color="primary"
+          color="primary-button"
           :label="$t('auth.confirm_location')"
           rounded
           padding="14px 16px"
@@ -177,6 +177,7 @@ const initMap = async () => {
         zoom: 17,
       },
     });
+    await googleMap.enableTouch();
   } catch (err) {
     $q.notify({ type: 'negative', message: `Erro ao abrir o mapa: ${err?.message ?? err}` });
     return;

+ 26 - 0
src/composables/useGeolocation.js

@@ -0,0 +1,26 @@
+import { Capacitor } from "@capacitor/core";
+import { Geolocation } from "@capacitor/geolocation";
+
+export const useGeolocation = () => {
+  const requestPermission = async () => {
+    if (!Capacitor.isNativePlatform()) return true;
+
+    const status = await Geolocation.requestPermissions();
+
+    return status.location === "granted" || status.location === "limited";
+  };
+
+  const getCurrentPosition = async () => {
+    const position = await Geolocation.getCurrentPosition({
+      enableHighAccuracy: true,
+      timeout: 10000,
+    });
+
+    return {
+      lat: position.coords.latitude,
+      lng: position.coords.longitude,
+    };
+  };
+
+  return { requestPermission, getCurrentPosition };
+};

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

@@ -43,6 +43,7 @@
       "country": "Country",
       "address": "Address",
       "address_number": "Address Number",
+      "district": "District",
       "complement": "Complement",
       "postal_code": "Postal Code",
       "phone": "Phone",
@@ -176,7 +177,15 @@
           "action": "continue",
           "rg_invalid": "Invalid RG",
           "date_invalid": "Invalid date",
-          "rg": "RG"
+          "rg": "RG",
+          "address_nickname": "Address nickname",
+          "address_instructions": "Address instructions",
+          "address_type_home": "Home",
+          "address_type_commercial": "Commercial",
+          "address_type_other": "Other",
+          "use_location": "Use my location",
+          "location_permission_denied": "Location permission denied.",
+          "location_error": "Could not get your location."
         },
         "step_4": {
           "document_front": "Side 1 (Front)",

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

@@ -43,6 +43,7 @@
       "country": "País",
       "address": "Dirección",
       "address_number": "Número",
+      "district": "Barrio",
       "complement": "Complemento",
       "postal_code": "Código postal",
       "phone": "Teléfono",
@@ -176,7 +177,15 @@
           "action": "continuar",
           "rg_invalid": "RG inválido",
           "date_invalid": "Fecha inválida",
-          "rg": "RG"
+          "rg": "RG",
+          "address_nickname": "Apodo de la dirección",
+          "address_instructions": "Instrucciones de la dirección",
+          "address_type_home": "Casa",
+          "address_type_commercial": "Comercial",
+          "address_type_other": "Otro",
+          "use_location": "Usar mi ubicación",
+          "location_permission_denied": "Permiso de ubicación denegado.",
+          "location_error": "No fue posible obtener su ubicación."
         },
         "step_4": {
           "document_front": "Lado 1 (Frente)",

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

@@ -43,6 +43,7 @@
       "country": "País",
       "address": "Endereço",
       "address_number": "Número",
+      "district": "Bairro",
       "complement": "Complemento",
       "postal_code": "Código Postal",
       "phone": "Telefone",
@@ -176,7 +177,15 @@
           "action": "continuar",
           "rg_invalid": "RG inválido",
           "date_invalid": "Data inválida",
-          "rg": "RG"
+          "rg": "RG",
+          "address_nickname": "Apelido do endereço",
+          "address_instructions": "Instruções do endereço",
+          "address_type_home": "Casa",
+          "address_type_commercial": "Comercial",
+          "address_type_other": "Outro",
+          "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."
         },
         "step_4": {
           "document_front": "Lado 1 (Frente)",

+ 11 - 12
src/pages/LoginPage.vue

@@ -16,10 +16,7 @@
       </div>
 
       <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
             v-if="steps >= 4"
             class="flow-back-btn"
@@ -58,10 +55,7 @@
 
             <LoginStep2Panel v-else-if="steps === 2" v-model:code="code" />
 
-            <LoginStep3Panel
-              v-else-if="steps === 3"
-              v-model="stepThreeForm"
-            />
+            <LoginStep3Panel v-else-if="steps === 3" v-model="stepThreeForm" />
 
             <LoginStep4Panel
               v-else-if="steps === 4"
@@ -69,10 +63,7 @@
               @update:show-sub-step="showSubStep = $event"
             />
 
-            <LoginStep5Panel
-              v-else-if="steps === 5"
-              v-model="stepFiveForm"
-            />
+            <LoginStep5Panel v-else-if="steps === 5" v-model="stepFiveForm" />
 
             <LoginStep6Panel v-else-if="steps === 6" v-model="stepSixForm" />
           </div>
@@ -180,11 +171,15 @@ const stepThreeForm = ref({
   city: "",
   complement: "",
   document: "",
+  district: "",
   email: "",
   instructions: "",
+  latitude: null,
+  longitude: null,
   name: "",
   nickname: "Principal",
   no_complement: false,
+  number: "",
   phone: "",
   rg: "",
   state: "",
@@ -340,6 +335,8 @@ const registerUserAndProvider = async () => {
   append("birth_date", toISODate(stepThreeForm.value.birth_date));
   append("zip_code", stepThreeForm.value.zip_code);
   append("address", stepThreeForm.value.address);
+  append("number", stepThreeForm.value.number);
+  append("district", stepThreeForm.value.district);
   append("has_complement", !stepThreeForm.value.no_complement);
 
   append(
@@ -351,6 +348,8 @@ const registerUserAndProvider = async () => {
   append("instructions", stepThreeForm.value.instructions);
   append("city", stepThreeForm.value.city);
   append("state", stepThreeForm.value.state);
+  append("latitude", stepThreeForm.value.latitude);
+  append("longitude", stepThreeForm.value.longitude);
   append("address_type", stepThreeForm.value.address_type);
 
   append("daily_price_8h", Number(stepFiveForm.value.daily_price_8h));