Procházet zdrojové kódy

fix: correcoes gerais dos enderecos e calculos

Gustavo Zanatta před 2 týdny
rodič
revize
6bf1014148

+ 10 - 0
src/api/payment.js

@@ -10,6 +10,16 @@ export const getServicePackagePix = async (servicePackageId) => {
   return data.payload;
 };
 
+export const payScheduleProposal = async (proposalId, payload) => {
+  const { data } = await api.post(`/payment/schedule-proposal/${proposalId}/pay`, payload);
+  return data.payload;
+};
+
+export const getScheduleProposalPix = async (proposalId) => {
+  const { data } = await api.get(`/payment/schedule-proposal/${proposalId}/pix`);
+  return data.payload;
+};
+
 export const getPaymentPlatformFees = async () => {
   const { data } = await api.get('/payment/platform-fees');
   return data.payload;

+ 42 - 3
src/components/dashboard/DashboardProvidersClose.vue

@@ -5,7 +5,10 @@
         {{ $t("dashboard_client.providers_close.title") }}
       </div>
 
-      <div class="row items-center no-wrap text-text">
+      <div
+        v-if="data.length"
+        class="row items-center no-wrap text-text"
+      >
         <q-btn
           color="text"
           dense
@@ -32,7 +35,25 @@
       </div>
     </div>
 
-    <div class="column">
+    <div
+      v-if="!data.length"
+      class="empty-alert row no-wrap items-start"
+    >
+      <q-icon
+        class="empty-alert__icon"
+        name="mdi-alert-outline"
+        size="28px"
+      />
+
+      <div class="empty-alert__text font10">
+        {{ $t("dashboard_client.providers_close.empty_alert_text") }}
+      </div>
+    </div>
+
+    <div
+      v-else
+      class="column"
+    >
       <q-card
         v-for="p in data"
         :key="p.provider_id"
@@ -229,4 +250,22 @@ const goToScheduling = (provider) => {
 };
 </script>
 
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+.empty-alert {
+  background: #e3efff;
+  border-radius: 8px;
+  gap: 12px;
+  padding: 13px 14px;
+}
+
+.empty-alert__icon {
+  color: #6554d9;
+  flex: 0 0 auto;
+}
+
+.empty-alert__text {
+  color: #6554d9;
+  line-height: 1.2;
+  padding-top: 1px;
+}
+</style>

+ 7 - 1
src/components/dashboard/SchedulePaymentDialog.vue

@@ -127,6 +127,10 @@ const props = defineProps({
     type: Object,
     required: true,
   },
+  targetType: {
+    type: String,
+    default: 'service_package',
+  },
 })
 
 defineEmits([...useDialogPluginComponent.emits])
@@ -214,6 +218,7 @@ const openPixPayment = () => {
     componentProps: {
       servicePackage: item.value,
       total: selectedTotal.value,
+      targetType: props.targetType,
     },
   }).onOk(() => {
     onDialogOK()
@@ -233,6 +238,7 @@ const onConfirm = () => {
       servicePackage: item.value,
       clientPaymentMethodId,
       total: selectedTotal.value,
+      targetType: props.targetType,
     },
   }).onOk(() => {
     onDialogOK()
@@ -242,7 +248,7 @@ const onConfirm = () => {
 onMounted(() => {
   loadPlatformFees().catch(() => {})
 
-  const existingPix = paymentStore.getValidPixPaymentForServicePackage(item.value.id)
+  const existingPix = paymentStore.getValidPixPaymentForServicePackage(`${props.targetType}:${item.value.id}`)
 
   if (existingPix) {
     openPixPayment()

+ 13 - 8
src/components/dashboard/SchedulePaymentPixDialog.vue

@@ -110,12 +110,13 @@
 import { computed, ref, onMounted, onUnmounted } from 'vue'
 import { useDialogPluginComponent, useQuasar, copyToClipboard } from 'quasar'
 import { formatCurrency } from 'src/helpers/utils'
-import { getServicePackagePix, payServicePackage } from 'src/api/payment'
+import { getServicePackagePix, payServicePackage, getScheduleProposalPix, payScheduleProposal } from 'src/api/payment'
 import { usePaymentStore } from 'src/stores/payment'
 
 const props = defineProps({
   servicePackage: { type: Object, required: true },
   total: { type: Number, required: true },
+  targetType: { type: String, default: 'service_package' },
 })
 
 defineEmits([...useDialogPluginComponent.emits])
@@ -124,8 +125,12 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginC
 const $q = useQuasar()
 const paymentStore = usePaymentStore()
 
+const pay = props.targetType === 'schedule_proposal' ? payScheduleProposal : payServicePackage
+const getPix = props.targetType === 'schedule_proposal' ? getScheduleProposalPix : getServicePackagePix
+
 const item = computed(() => props.servicePackage)
 const itemId = computed(() => item.value.id)
+const cacheKey = computed(() => `${props.targetType}:${itemId.value}`)
 
 const payment = ref(null)
 const success = ref(false)
@@ -173,7 +178,7 @@ const applyPaymentStatus = (nextPayment) => {
     position: 'top',
   })
 
-  paymentStore.clearPixPaymentForServicePackage(itemId.value)
+  paymentStore.clearPixPaymentForServicePackage(cacheKey.value)
   stopPolling()
 
   onDialogOK()
@@ -184,14 +189,14 @@ const applyPaymentStatus = (nextPayment) => {
 
   if (['failed', 'cancelled'].includes(nextPayment.status)) {
     processing.value = false
-    paymentStore.clearPixPaymentForServicePackage(itemId.value)
+    paymentStore.clearPixPaymentForServicePackage(cacheKey.value)
     stopPolling()
     $q.notify({ type: 'negative', message: nextPayment.failure_message || 'Pagamento Pix não confirmado.' })
     onDialogCancel()
     return
   }
 
-  paymentStore.setPixPaymentForServicePackage(itemId.value, nextPayment)
+  paymentStore.setPixPaymentForServicePackage(cacheKey.value, nextPayment)
 }
 
 const checkPaymentStatus = async () => {
@@ -199,7 +204,7 @@ const checkPaymentStatus = async () => {
 
   pollingInFlight = true
   try {
-    const result = await getServicePackagePix(itemId.value)
+    const result = await getPix(itemId.value)
     applyPaymentStatus(result)
     updateCountdown()
   } catch (e) {
@@ -227,7 +232,7 @@ const updateCountdown = () => {
   if (!pixExpiresAt.value && totalSeconds.value > 0) totalSeconds.value--
 
   if (pixExpiresAt.value && totalSeconds.value <= 0) {
-    paymentStore.clearPixPaymentForServicePackage(itemId.value)
+    paymentStore.clearPixPaymentForServicePackage(cacheKey.value)
     stopPolling()
   }
 }
@@ -237,7 +242,7 @@ onMounted(async () => {
   countdownTimer = setInterval(updateCountdown, 1000)
 
   try {
-    const cachedPayment = paymentStore.getValidPixPaymentForServicePackage(itemId.value)
+    const cachedPayment = paymentStore.getValidPixPaymentForServicePackage(cacheKey.value)
 
     if (cachedPayment) {
       applyPaymentStatus(cachedPayment)
@@ -246,7 +251,7 @@ onMounted(async () => {
       return
     }
 
-    const paymentResult = await payServicePackage(itemId.value, { payment_method: 'pix' })
+    const paymentResult = await pay(itemId.value, { payment_method: 'pix' })
 
     applyPaymentStatus(paymentResult)
     updateCountdown()

+ 5 - 2
src/components/dashboard/SchedulePaymentProcessingDialog.vue

@@ -47,12 +47,13 @@
 import { ref, computed, onMounted } from 'vue'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import LogoDiariaSucesso from 'src/assets/diarinho-success-payment.svg';
-import { payServicePackage } from 'src/api/payment'
+import { payServicePackage, payScheduleProposal } from 'src/api/payment'
 
 const props = defineProps({
   servicePackage: { type: Object, required: true },
   clientPaymentMethodId: { type: Number, required: true },
   total: { type: Number, default: null },
+  targetType: { type: String, default: 'service_package' },
 })
 
 defineEmits([...useDialogPluginComponent.emits])
@@ -60,13 +61,15 @@ defineEmits([...useDialogPluginComponent.emits])
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
 
+const pay = props.targetType === 'schedule_proposal' ? payScheduleProposal : payServicePackage
+
 const item = computed(() => props.servicePackage)
 
 const success = ref(false)
 
 onMounted(async () => {
   try {
-    const payment = await payServicePackage(item.value.id, {
+    const payment = await pay(item.value.id, {
       payment_method: 'credit_card',
       client_payment_method_id: props.clientPaymentMethodId,
     })

+ 9 - 3
src/components/profile/ProfileAddressFormDialog.vue

@@ -226,7 +226,13 @@
                 outline
                 rounded
                 unelevated
-                :label="$t('profile.address.update_on_map')"
+                :label="
+                  $t(
+                    isEditing
+                      ? 'profile.address.update_on_map'
+                      : 'profile.address.select_on_map',
+                  )
+                "
                 :loading="geocodingCep"
                 @click="openMapDialog"
               />
@@ -300,9 +306,9 @@ const saving = ref(false);
 
 const missingCoords = computed(
   () =>
-    props.isEditing &&
     form.latitude == null &&
-    form.longitude == null,
+    form.longitude == null &&
+    Boolean(form.address || form.zip_code),
 );
 
 const clientId = user.user.client.id;

+ 65 - 0
src/components/shared/AddressIncompleteBanner.vue

@@ -0,0 +1,65 @@
+<template>
+  <div
+    class="incomplete-banner q-mx-md q-mb-md"
+    @click="openAddresses"
+  >
+    <div class="row items-center no-wrap q-pa-sm q-px-md">
+      <q-icon
+        class="q-mr-md"
+        color="primary"
+        name="mdi-alert-outline"
+        size="26px"
+      />
+
+      <div class="col banner-text font12 fontmedium text-primary">
+        {{ $t("common.address_incomplete_banner.title") }}
+      </div>
+
+      <q-btn
+        class="q-ml-sm resolver-btn font9"
+        color="primary"
+        no-caps
+        text-color="white"
+        unelevated
+        :label="$t('common.address_incomplete_banner.cta')"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { useQuasar } from "quasar";
+
+import ProfileAddressDialog from "src/components/profile/ProfileAddressDialog.vue";
+
+const emit = defineEmits(["resolved"]);
+
+const $q = useQuasar();
+
+const openAddresses = () => {
+  $q.dialog({ component: ProfileAddressDialog }).onDismiss(() => {
+    emit("resolved");
+  });
+};
+</script>
+
+<style lang="scss" scoped>
+@use "src/css/quasar.variables.scss";
+
+.incomplete-banner {
+  border-radius: 12px;
+  background: $card-incomplete-profile;
+  cursor: pointer;
+}
+
+.banner-text {
+  line-height: 1.3;
+}
+
+.resolver-btn {
+  border-radius: 20px;
+  padding: 0px 4px;
+  white-space: nowrap;
+  flex-shrink: 0;
+}
+</style>

+ 8 - 2
src/i18n/locales/en.json

@@ -3,6 +3,10 @@
     "confirm": "Confirm",
     "expand": "expand",
     "collapse": "collapse",
+    "address_incomplete_banner": {
+      "title": "Complete your profile address!",
+      "cta": "Resolve now"
+    },
     "actions": {
       "save": "Save",
       "cancel": "Cancel",
@@ -511,7 +515,8 @@
       "until_4h": "Up to 4h",
       "until_2h": "Up to 2h",
       "place_home": "Home",
-      "no_price": "to arrange"
+      "no_price": "to arrange",
+      "empty_alert_text": "There are no cleaning providers near you yet."
     },
     "pending_schedules": {
       "title": "Awaiting",
@@ -667,7 +672,8 @@
         "other": "Other"
       },
       "missing_coords": "Coordinates not found for this address.",
-      "update_on_map": "Update on map"
+      "update_on_map": "Update on map",
+      "select_on_map": "Select on map"
     },
     "help": {
       "title": "Help",

+ 8 - 2
src/i18n/locales/es.json

@@ -3,6 +3,10 @@
     "confirm": "Confirmar",
     "expand": "expandir",
     "collapse": "contraer",
+    "address_incomplete_banner": {
+      "title": "¡Completa la dirección de tu perfil!",
+      "cta": "Resolver ahora"
+    },
     "actions": {
       "save": "Guardar",
       "cancel": "Cancelar",
@@ -508,7 +512,8 @@
       "until_4h": "Hasta 4h",
       "until_2h": "Hasta 2h",
       "place_home": "Casa",
-      "no_price": "a convenir"
+      "no_price": "a convenir",
+      "empty_alert_text": "Todavía no hay diaristas cerca de ti."
     },
     "pending_schedules": {
       "title": "En espera de confirmación",
@@ -664,7 +669,8 @@
         "other": "Otro"
       },
       "missing_coords": "Coordenadas no encontradas para esta dirección.",
-      "update_on_map": "Actualizar en el mapa"
+      "update_on_map": "Actualizar en el mapa",
+      "select_on_map": "Seleccionar en el mapa"
     },
     "help": {
       "title": "Ayuda",

+ 8 - 2
src/i18n/locales/pt.json

@@ -3,6 +3,10 @@
     "confirm": "Confirmar",
     "expand": "expandir",
     "collapse": "recolher",
+    "address_incomplete_banner": {
+      "title": "Complete o endereço do seu perfil!",
+      "cta": "Resolver agora"
+    },
     "actions": {
       "save": "Salvar",
       "cancel": "Cancelar",
@@ -511,7 +515,8 @@
       "until_4h": "Até 4h",
       "until_2h": "Até 2h",
       "place_home": "Casa",
-      "no_price": "a combinar"
+      "no_price": "a combinar",
+      "empty_alert_text": "Ainda não há diaristas perto de você."
     },
     "pending_schedules": {
       "title": "Aguardando confirmação",
@@ -667,7 +672,8 @@
         "other": "Outro"
       },
       "missing_coords": "Coordenadas não encontradas para este endereço.",
-      "update_on_map": "Atualizar no mapa"
+      "update_on_map": "Atualizar no mapa",
+      "select_on_map": "Selecionar no mapa"
     },
     "help": {
       "title": "Ajuda",

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

@@ -14,6 +14,7 @@
         <DashboardRegistrationIncomplete v-if="!registrationComplete" />
         <DashboardSummaryInfos v-else :data="summaryInfos" />
         <DashboardPaymentIncomplete v-if="showPaymentBanner" />
+        <AddressIncompleteBanner v-if="!hasLocation" @resolved="reloadDashboard" />
         <DashboardPendingCustomSchedules v-if="customSchedulesNoProposals.length > 0" />
         <DashboardClientProposals v-if="clientProposals.length > 0" :data="clientProposals" @refresh-data="reloadDashboard" />
         <DashboardPendingSchedules
@@ -34,7 +35,7 @@
         <DashboardNextSchedules v-if="nextSchedules.length > 0" :data="nextSchedules" @view-details="openNextScheduleDialog" />
         <DashboardLastDoneSchedules v-if="lastDoneSchedules.length > 0" :data="lastDoneSchedules" />
         <DashboardFavoriteProviders v-if="favoriteProviders.length > 0" :data="favoriteProviders" />
-        <DashboardProvidersClose v-if="providersClose.length > 0" :data="providersClose" />
+        <DashboardProvidersClose v-if="hasLocation" :data="providersClose" />
       </q-pull-to-refresh>
     </template>
   </q-page>
@@ -53,6 +54,7 @@ import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedu
 import DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
 import DashboardFavoriteProviders from 'src/components/dashboard/DashboardFavoriteProviders.vue';
 import DashboardProvidersClose from 'src/components/dashboard/DashboardProvidersClose.vue';
+import AddressIncompleteBanner from 'src/components/shared/AddressIncompleteBanner.vue';
 import DashboardTodaySchedules from 'src/components/dashboard/DashboardTodaySchedules.vue';
 import FinalSuccesModal from '../schedules/components/FinalSuccesModal.vue';
 import DashboardPendingCustomSchedules from 'src/pages/dashboard/components/DashboardPendingCustomSchedules.vue';
@@ -89,6 +91,7 @@ const notifications = ref([]);
 const loading = ref(true);
 const registrationComplete = computed(() => store.user?.registration_complete ?? true);
 const showPaymentBanner = ref(false);
+const hasLocation = ref(true);
 
 const successModalKey = ref(
   route.query.success ??
@@ -186,6 +189,7 @@ const reloadDashboard = async (showLoader = true) => {
     todaySchedules.value = response.todaySchedules ?? [];
     notifications.value = response.notifications ?? [];
     hasPaymentMethods.value = response.has_payment_methods ?? true;
+    hasLocation.value = response.has_location ?? true;
   }
   loading.value = false;
 

+ 18 - 31
src/pages/dashboard/components/DashboardClientProposals.vue

@@ -144,7 +144,7 @@
                 label="aceitar"
                 padding="4px 12px"
                 size="sm"
-                @click="() => handleAcceptProposal(item.id)"
+                @click="() => handleAcceptProposal(item)"
               />
             </div>
           </div>
@@ -155,7 +155,7 @@
 </template>
 
 <script setup>
-import { acceptProposal, refuseProposal } from "src/api/customSchedules";
+import { refuseProposal } from "src/api/customSchedules";
 import { avatarColors } from "src/helpers/avatarColors";
 import { useQuasar } from "quasar";
 import SchedulePaymentDialog from "src/components/dashboard/SchedulePaymentDialog.vue";
@@ -223,35 +223,22 @@ const formatWeekday = (iso) => {
   return w.charAt(0).toUpperCase() + w.slice(1);
 };
 
-const handleAcceptProposal = async (proposalId) => {
-  try {
-    const response = await acceptProposal(proposalId);
-
-    const schedule = response.payload;
-
-    const servicePackage = {
-      id: schedule.id,
-      total_amount: schedule.total_amount,
-      provider: schedule.provider,
-      schedules: [
-        {
-          ...schedule,
-        },
-      ],
-    };
-
-    $q.dialog({
-      component: SchedulePaymentDialog,
-      componentProps: {
-        servicePackage,
-      },
-    }).onOk(() => {
-      emit("refreshData");
-    });
-
-  } catch (error) {
-    console.log(error);
-  }
+const handleAcceptProposal = (item) => {
+  const servicePackage = {
+    id: item.id,
+    total_amount: chooseprice(item.period_type, item.daily_price_8h),
+    address: item.address ?? null,
+  };
+
+  $q.dialog({
+    component: SchedulePaymentDialog,
+    componentProps: {
+      servicePackage,
+      targetType: "schedule_proposal",
+    },
+  }).onOk(() => {
+    emit("refreshData");
+  });
 };
 
 const handleRefuseProposal = async (proposalId) => {

+ 26 - 17
src/pages/search/SearchPage.vue

@@ -89,7 +89,10 @@
       />
     </div>
 
-    <div class="row items-center justify-between no-wrap q-px-md q-pb-sm">
+    <div
+      v-if="!loading && hasLocation"
+      class="row items-center justify-between no-wrap q-px-md q-pb-sm"
+    >
       <div class="dashboard-section-title font16 fontbold gradient-diarista">
         {{ $t("search_page.choose_provider") }}
       </div>
@@ -129,6 +132,10 @@
       />
     </div>
 
+    <template v-else-if="!hasLocation">
+      <AddressIncompleteBanner @resolved="loadProviders" />
+    </template>
+
     <template v-else>
       <div
         v-if="sortedProviders.length === 0"
@@ -276,6 +283,7 @@ import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
 import { useRouter } from "vue-router";
 
+import AddressIncompleteBanner from "src/components/shared/AddressIncompleteBanner.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 import SearchFilterDialog from "src/pages/search/components/SearchFilterDialog.vue";
 
@@ -287,6 +295,7 @@ const activeDate = ref(null);
 const activeSort = ref(null);
 const allProviders = ref([]);
 const currentPeriodType = ref(8);
+const hasLocation = ref(true);
 const loading = ref(true);
 const searchName = ref("");
 
@@ -322,6 +331,14 @@ const sortedProviders = computed(() => {
       return list.sort(
         (a, b) => Number(b[priceKey] ?? 0) - Number(a[priceKey] ?? 0),
       );
+    case "distance_asc":
+      return list.sort(
+        (a, b) => Number(a.distance_km ?? 0) - Number(b.distance_km ?? 0),
+      );
+    case "distance_desc":
+      return list.sort(
+        (a, b) => Number(b.distance_km ?? 0) - Number(a.distance_km ?? 0),
+      );
     case "rating_desc":
       return list.sort(
         (a, b) => Number(b.average_rating ?? 0) - Number(a.average_rating ?? 0),
@@ -361,17 +378,6 @@ const sortedProviders = computed(() => {
   }
 });
 
-// eslint-disable-next-line no-unused-vars
-const formatDistance = (distance) => {
-  if (distance === null || distance === undefined || distance === "")
-    return "—";
-
-  const numericDistance = Number(distance);
-  if (!Number.isFinite(numericDistance)) return "—";
-
-  return `${numericDistance.toLocaleString("pt-BR", { maximumFractionDigits: 1 })} km`;
-};
-
 const goToScheduling = (provider) => {
   $q.dialog({
     component: SchedulingDialog,
@@ -383,13 +389,16 @@ const loadProviders = async () => {
   loading.value = true;
 
   try {
-    allProviders.value =
-      (await buscaPrestadores({
-        date: activeDate.value ?? "",
-        name: searchName.value,
-      })) ?? [];
+    const result = await buscaPrestadores({
+      date: activeDate.value ?? "",
+      name: searchName.value,
+    });
+
+    allProviders.value = result?.providers ?? [];
+    hasLocation.value = result?.has_location ?? true;
   } catch {
     allProviders.value = [];
+    hasLocation.value = true;
   } finally {
     loading.value = false;
   }

+ 2 - 10
src/pages/search/components/SearchFilterDialog.vue

@@ -137,16 +137,8 @@ const sortGroups = computed(() => [
     key: "distance",
     label: t("search_filter.groups.distance"),
     options: [
-      {
-        value: "distance_desc",
-        label: t("search_filter.sort.higher"),
-        disable: true,
-      },
-      {
-        value: "distance_asc",
-        label: t("search_filter.sort.lower"),
-        disable: true,
-      },
+      { value: "distance_desc", label: t("search_filter.sort.higher") },
+      { value: "distance_asc", label: t("search_filter.sort.lower") },
     ],
   },
   {