Browse Source

Merge branch 'feature/diaria-gus-4leva' of Softpar/sfp_front_vue_diarista_cliente into development

zntt 2 weeks ago
parent
commit
dce402f995

+ 5 - 0
src/api/dashboard.js

@@ -5,6 +5,11 @@ export const dadosDashboard = async () => {
   return data.payload;
   return data.payload;
 }
 }
 
 
+export const dadosPedidosCliente = async () => {
+  const { data } = await api.get("/dados-pedidos-cliente");
+  return data.payload;
+}
+
 export const getScheduleClienteDetails = async (id) => {
 export const getScheduleClienteDetails = async (id) => {
   const { data } = await api.get(`/dados-dashboard-cliente/schedule/${id}/detalhes`);
   const { data } = await api.get(`/dados-dashboard-cliente/schedule/${id}/detalhes`);
   return data.payload;
   return data.payload;

+ 10 - 0
src/api/payment.js

@@ -10,6 +10,16 @@ export const getServicePackagePix = async (servicePackageId) => {
   return data.payload;
   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 () => {
 export const getPaymentPlatformFees = async () => {
   const { data } = await api.get('/payment/platform-fees');
   const { data } = await api.get('/payment/platform-fees');
   return data.payload;
   return data.payload;

+ 1 - 19
src/components/dashboard/DashboardNextSchedules.vue

@@ -145,6 +145,7 @@
 
 
 <script setup>
 <script setup>
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
+import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 
 
 import {
 import {
   formatCurrency,
   formatCurrency,
@@ -196,25 +197,6 @@ const formatScheduleTotal = (item) => {
   );
   );
 };
 };
 
 
-const formatDayMonth = (iso) => {
-  if (!iso) return "";
-
-  return new Date(iso).toLocaleDateString("pt-BR", {
-    day: "2-digit",
-    month: "2-digit",
-  });
-};
-
-const formatWeekday = (iso) => {
-  if (!iso) return "";
-
-  const d = new Date(iso);
-
-  const w = d.toLocaleDateString("pt-BR", { weekday: "long" });
-
-  return w.charAt(0).toUpperCase() + w.slice(1);
-};
-
 onMounted(() => {
 onMounted(() => {
   loadPlatformFees().catch(() => {});
   loadPlatformFees().catch(() => {});
 });
 });

+ 2 - 1
src/components/dashboard/DashboardPendingSchedules.vue

@@ -174,6 +174,7 @@
 import { computed, ref } from "vue";
 import { computed, ref } from "vue";
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
 import { getFirstName } from "src/helpers/utils";
 import { getFirstName } from "src/helpers/utils";
+import { parseLocalDate } from "src/helpers/scheduleDate";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 
 
 const props = defineProps({
 const props = defineProps({
@@ -258,7 +259,7 @@ const displayDate = (item) => {
 
 
   if (!schedule?.date) return "—";
   if (!schedule?.date) return "—";
 
 
-  const date = new Date(schedule.date);
+  const date = parseLocalDate(schedule.date);
 
 
   return date.toLocaleDateString("pt-BR", {
   return date.toLocaleDateString("pt-BR", {
     day: "2-digit",
     day: "2-digit",

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

@@ -5,7 +5,10 @@
         {{ $t("dashboard_client.providers_close.title") }}
         {{ $t("dashboard_client.providers_close.title") }}
       </div>
       </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
         <q-btn
           color="text"
           color="text"
           dense
           dense
@@ -32,7 +35,25 @@
       </div>
       </div>
     </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
       <q-card
         v-for="p in data"
         v-for="p in data"
         :key="p.provider_id"
         :key="p.provider_id"
@@ -229,4 +250,22 @@ const goToScheduling = (provider) => {
 };
 };
 </script>
 </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>

+ 52 - 4
src/components/dashboard/DashboardTodaySchedules.vue

@@ -1,5 +1,9 @@
 <template>
 <template>
   <div class="q-mx-md q-mb-md">
   <div class="q-mx-md q-mb-md">
+    <div class="font16 fontbold gradient-diarista q-mb-sm">
+      {{ $t("dashboard_client.today_schedules.title") }}
+    </div>
+
     <div class="scroll-wrapper">
     <div class="scroll-wrapper">
       <div class="scroll-track">
       <div class="scroll-track">
         <q-card
         <q-card
@@ -31,7 +35,11 @@
 
 
               <div class="col-7 column no-wrap overflow-hidden justify-center">
               <div class="col-7 column no-wrap overflow-hidden justify-center">
                 <span class="font12 fontmedium text-text leading-tight">
                 <span class="font12 fontmedium text-text leading-tight">
-                  <template v-if="cardState(item) === 'awaiting_code'">
+                  <template v-if="cardState(item) === 'awaiting_code' && !hasStarted(item)">
+                    {{ $t("dashboard_client.today_schedules.not_started_label") }}
+                  </template>
+
+                  <template v-else-if="cardState(item) === 'awaiting_code'">
                     {{ $t("dashboard_client.today_schedules.start_with") }}
                     {{ $t("dashboard_client.today_schedules.start_with") }}
                   </template>
                   </template>
 
 
@@ -71,7 +79,17 @@
               <div
               <div
                 class="flex-shrink-0 q-ml-sm column items-center justify-start"
                 class="flex-shrink-0 q-ml-sm column items-center justify-start"
               >
               >
-                <template v-if="cardState(item) === 'awaiting_code'">
+                <template v-if="cardState(item) === 'awaiting_code' && !hasStarted(item)">
+                  <div class="clock-badge">
+                    <q-icon
+                      color="white"
+                      name="mdi-clock-outline"
+                      size="15px"
+                    />
+                  </div>
+                </template>
+
+                <template v-else-if="cardState(item) === 'awaiting_code' && hasStarted(item)">
                   <div class="column items-center">
                   <div class="column items-center">
                     <span class="font9 fontbold text-primary q-mb-xs">
                     <span class="font9 fontbold text-primary q-mb-xs">
                       {{ $t("dashboard_client.today_schedules.code_label") }}
                       {{ $t("dashboard_client.today_schedules.code_label") }}
@@ -117,7 +135,7 @@
 
 
                   <q-btn
                   <q-btn
                     v-else
                     v-else
-                    class="rate-btn"
+                    class="rate-btn font10 fontmedium"
                     no-caps
                     no-caps
                     unelevated
                     unelevated
                     @click.stop="emit('rate', item)"
                     @click.stop="emit('rate', item)"
@@ -201,7 +219,7 @@
                     {{ $t("dashboard_client.today_schedules.end_time_label") }}
                     {{ $t("dashboard_client.today_schedules.end_time_label") }}
 
 
                     <span class="gradient-diarista">
                     <span class="gradient-diarista">
-                      {{ item.end_time?.slice(0, 5) }}
+                      {{ calculateTimeUntilEnd(item.end_time) }}
                     </span>
                     </span>
                   </span>
                   </span>
                 </template>
                 </template>
@@ -277,6 +295,36 @@ const cardState = (item) => {
   return "awaiting_code";
   return "awaiting_code";
 };
 };
 
 
+const hasStarted = (item) => {
+  const [h, m] = (item.start_time || "00:00").slice(0, 5).split(":").map(Number);
+
+  const startTime = new Date();
+
+  startTime.setHours(h, m, 0, 0);
+
+  return now.value >= startTime.getTime();
+};
+
+const calculateTimeUntilEnd = (endTimeStr) => {
+  const [h, m] = (endTimeStr || "23:59").slice(0, 5).split(":").map(Number);
+
+  const endTime = new Date();
+
+  endTime.setHours(h, m, 0, 0);
+
+  const diffMs = endTime.getTime() - now.value;
+
+  if (diffMs <= 0) return "00:00:00";
+
+  const diffH = Math.floor(diffMs / (1000 * 60 * 60));
+
+  const diffM = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
+
+  const diffS = Math.floor((diffMs % (1000 * 60)) / 1000);
+
+  return `${String(diffH).padStart(2, "0")}:${String(diffM).padStart(2, "0")}:${String(diffS).padStart(2, "0")}`;
+};
+
 const visibleItems = computed(() => {
 const visibleItems = computed(() => {
   return props.data.filter((item) => !item.client_reviewed);
   return props.data.filter((item) => !item.client_reviewed);
 });
 });

+ 22 - 0
src/components/dashboard/NextSchedulesDetailsDialog.vue

@@ -117,6 +117,19 @@
           </span>
           </span>
         </div>
         </div>
 
 
+        <div
+          v-if="priceRangeLabel"
+          class="detail-row"
+        >
+          <span class="detail-label text-primary q-pr-sm">
+            {{ $t("dashboard_client.pending_schedules.detail_price_range") }}
+          </span>
+
+          <span class="detail-value">
+            {{ priceRangeLabel }}
+          </span>
+        </div>
+
         <div class="detail-row-total font16 fontbold">
         <div class="detail-row-total font16 fontbold">
           <span class="detail-label text-primary q-pr-sm">
           <span class="detail-label text-primary q-pr-sm">
             {{ $t("dashboard_client.pending_schedules.detail_total") }}
             {{ $t("dashboard_client.pending_schedules.detail_total") }}
@@ -274,6 +287,15 @@ const total = computed(() =>
   ),
   ),
 );
 );
 
 
+const priceRangeLabel = computed(() => {
+  const minPrice = details.value?.min_price;
+  const maxPrice = details.value?.max_price;
+
+  if (minPrice == null || maxPrice == null) return null;
+
+  return `${formatCurrency(minPrice)} - ${formatCurrency(maxPrice)}`;
+});
+
 const openCancelDialog = () => {
 const openCancelDialog = () => {
   $q.dialog({
   $q.dialog({
     component: ScheduleCancelDialog,
     component: ScheduleCancelDialog,

+ 10 - 1
src/components/dashboard/ScheduleAcceptedDialog.vue

@@ -13,7 +13,15 @@
           size="80px"
           size="80px"
           :style="avatarStyle"
           :style="avatarStyle"
         >
         >
-          {{ displayProviderName.slice(0, 2).toUpperCase() || "??" }}
+          <img
+            v-if="getProfileMediaUrl(item)"
+            :src="getProfileMediaUrl(item)"
+            style="object-fit: cover"
+          />
+
+          <span v-else>
+            {{ displayProviderName.slice(0, 2).toUpperCase() || "??" }}
+          </span>
         </q-avatar>
         </q-avatar>
 
 
         <div class="font16 fontbold provider-name">
         <div class="font16 fontbold provider-name">
@@ -159,6 +167,7 @@
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
 import { computed, onMounted } from "vue";
 import { computed, onMounted } from "vue";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
+import { getProfileMediaUrl } from "src/helpers/profileMedia";
 
 
 import {
 import {
   getSchedulePlatformFeeRate,
   getSchedulePlatformFeeRate,

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

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

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

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

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

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

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

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

+ 24 - 0
src/helpers/scheduleDate.js

@@ -0,0 +1,24 @@
+const parseLocalDate = (dateStr) => {
+  if (!dateStr) return null;
+
+  const iso = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
+  if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
+  const dmy = dateStr.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
+  if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
+  return null;
+};
+
+const formatWeekday = (dateStr) => {
+  const d = parseLocalDate(dateStr);
+  if (!d) return "";
+  const w = d.toLocaleDateString("pt-BR", { weekday: "long" });
+  return w.charAt(0).toUpperCase() + w.slice(1);
+};
+
+const formatDayMonth = (dateStr) => {
+  const d = parseLocalDate(dateStr);
+  if (!d) return "";
+  return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
+};
+
+export { parseLocalDate, formatWeekday, formatDayMonth };

+ 16 - 5
src/i18n/locales/en.json

@@ -3,6 +3,10 @@
     "confirm": "Confirm",
     "confirm": "Confirm",
     "expand": "expand",
     "expand": "expand",
     "collapse": "collapse",
     "collapse": "collapse",
+    "address_incomplete_banner": {
+      "title": "Complete your profile address!",
+      "cta": "Resolve now"
+    },
     "actions": {
     "actions": {
       "save": "Save",
       "save": "Save",
       "cancel": "Cancel",
       "cancel": "Cancel",
@@ -437,12 +441,14 @@
       "my_schedules": "My schedules"
       "my_schedules": "My schedules"
     },
     },
     "today_schedules": {
     "today_schedules": {
+      "title": "Today",
       "start_with": "Service starting with",
       "start_with": "Service starting with",
-      "started_by": "Service in progress",
+      "not_started_label": "Starting soon with",
+      "started_by": "Service started by",
       "finished_by": "Service completed by",
       "finished_by": "Service completed by",
       "code_label": "Code",
       "code_label": "Code",
       "in_progress": "in progress",
       "in_progress": "in progress",
-      "end_time_label": "Ends at",
+      "end_time_label": "Ends in:",
       "rate_btn": "Rate",
       "rate_btn": "Rate",
       "help_btn": "help",
       "help_btn": "help",
       "cancelled": "Service cancelled"
       "cancelled": "Service cancelled"
@@ -495,7 +501,8 @@
       "btn_reschedule": "reschedule",
       "btn_reschedule": "reschedule",
       "empty_upcoming": "No scheduled services",
       "empty_upcoming": "No scheduled services",
       "empty_completed": "No completed services",
       "empty_completed": "No completed services",
-      "waiting_proposals": "Waiting for proposals"
+      "waiting_proposals": "Waiting for proposals",
+      "proposals_received_count": "{count} proposal(s) received"
     },
     },
     "favorites": {
     "favorites": {
       "title": "Favorites",
       "title": "Favorites",
@@ -511,7 +518,8 @@
       "until_4h": "Up to 4h",
       "until_4h": "Up to 4h",
       "until_2h": "Up to 2h",
       "until_2h": "Up to 2h",
       "place_home": "Home",
       "place_home": "Home",
-      "no_price": "to arrange"
+      "no_price": "to arrange",
+      "empty_alert_text": "There are no cleaning providers near you yet."
     },
     },
     "pending_schedules": {
     "pending_schedules": {
       "title": "Awaiting",
       "title": "Awaiting",
@@ -537,6 +545,7 @@
       "detail_service_fee_pix": "Service fee (Pix):",
       "detail_service_fee_pix": "Service fee (Pix):",
       "detail_service_fee_credit_card": "Service fee (card):",
       "detail_service_fee_credit_card": "Service fee (card):",
       "detail_total": "Total:",
       "detail_total": "Total:",
+      "detail_price_range": "Price range:",
       "detail_package_total": "Package total (Pix):",
       "detail_package_total": "Package total (Pix):",
       "detail_pix_total": "Pix total:",
       "detail_pix_total": "Pix total:",
       "detail_credit_card_total": "Credit card total:",
       "detail_credit_card_total": "Credit card total:",
@@ -667,7 +676,8 @@
         "other": "Other"
         "other": "Other"
       },
       },
       "missing_coords": "Coordinates not found for this address.",
       "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": {
     "help": {
       "title": "Help",
       "title": "Help",
@@ -828,6 +838,7 @@
     "other_provider_blocked": "Your order already has time slots with another professional. Finish or empty the current order before booking with another professional.",
     "other_provider_blocked": "Your order already has time slots with another professional. Finish or empty the current order before booking with another professional.",
     "local_cart_title": "Orders",
     "local_cart_title": "Orders",
     "orders_title": "Orders",
     "orders_title": "Orders",
+    "pending_custom_title": "Requests in progress",
     "no_orders": "No orders created.",
     "no_orders": "No orders created.",
     "order_title": "Order #{id}",
     "order_title": "Order #{id}",
     "view_favorites": "view favorites",
     "view_favorites": "view favorites",

+ 16 - 5
src/i18n/locales/es.json

@@ -3,6 +3,10 @@
     "confirm": "Confirmar",
     "confirm": "Confirmar",
     "expand": "expandir",
     "expand": "expandir",
     "collapse": "contraer",
     "collapse": "contraer",
+    "address_incomplete_banner": {
+      "title": "¡Completa la dirección de tu perfil!",
+      "cta": "Resolver ahora"
+    },
     "actions": {
     "actions": {
       "save": "Guardar",
       "save": "Guardar",
       "cancel": "Cancelar",
       "cancel": "Cancelar",
@@ -437,12 +441,14 @@
       "my_schedules": "Mis jornadas"
       "my_schedules": "Mis jornadas"
     },
     },
     "today_schedules": {
     "today_schedules": {
+      "title": "Hoy",
       "start_with": "Servicio iniciado con",
       "start_with": "Servicio iniciado con",
-      "started_by": "Servicio en curso",
+      "not_started_label": "Comienza pronto con",
+      "started_by": "Servicio iniciado por",
       "finished_by": "Servicio concluido por",
       "finished_by": "Servicio concluido por",
       "code_label": "Código",
       "code_label": "Código",
       "in_progress": "en progreso",
       "in_progress": "en progreso",
-      "end_time_label": "Término a las",
+      "end_time_label": "Termina en:",
       "rate_btn": "Avaliar",
       "rate_btn": "Avaliar",
       "help_btn": "ayuda",
       "help_btn": "ayuda",
       "cancelled": "Servicio cancelado"
       "cancelled": "Servicio cancelado"
@@ -492,7 +498,8 @@
       "btn_reschedule": "reprogramar",
       "btn_reschedule": "reprogramar",
       "empty_upcoming": "Sin servicios programados",
       "empty_upcoming": "Sin servicios programados",
       "empty_completed": "Sin servicios completados",
       "empty_completed": "Sin servicios completados",
-      "waiting_proposals": "Esperando propuestas"
+      "waiting_proposals": "Esperando propuestas",
+      "proposals_received_count": "{count} propuesta(s) recibida(s)"
     },
     },
     "favorites": {
     "favorites": {
       "title": "Favoritos",
       "title": "Favoritos",
@@ -508,7 +515,8 @@
       "until_4h": "Hasta 4h",
       "until_4h": "Hasta 4h",
       "until_2h": "Hasta 2h",
       "until_2h": "Hasta 2h",
       "place_home": "Casa",
       "place_home": "Casa",
-      "no_price": "a convenir"
+      "no_price": "a convenir",
+      "empty_alert_text": "Todavía no hay diaristas cerca de ti."
     },
     },
     "pending_schedules": {
     "pending_schedules": {
       "title": "En espera de confirmación",
       "title": "En espera de confirmación",
@@ -534,6 +542,7 @@
       "detail_service_fee_pix": "Tarifa de servicio (Pix):",
       "detail_service_fee_pix": "Tarifa de servicio (Pix):",
       "detail_service_fee_credit_card": "Tarifa de servicio (tarjeta):",
       "detail_service_fee_credit_card": "Tarifa de servicio (tarjeta):",
       "detail_total": "Total:",
       "detail_total": "Total:",
+      "detail_price_range": "Rango de precio:",
       "detail_package_total": "Total del paquete (Pix):",
       "detail_package_total": "Total del paquete (Pix):",
       "detail_pix_total": "Total en Pix:",
       "detail_pix_total": "Total en Pix:",
       "detail_credit_card_total": "Total en tarjeta de crédito:",
       "detail_credit_card_total": "Total en tarjeta de crédito:",
@@ -664,7 +673,8 @@
         "other": "Otro"
         "other": "Otro"
       },
       },
       "missing_coords": "Coordenadas no encontradas para esta dirección.",
       "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": {
     "help": {
       "title": "Ayuda",
       "title": "Ayuda",
@@ -825,6 +835,7 @@
     "other_provider_blocked": "Tu pedido ya tiene horarios con otro profesional. Finaliza o vacía el pedido actual antes de reservar con otro profesional.",
     "other_provider_blocked": "Tu pedido ya tiene horarios con otro profesional. Finaliza o vacía el pedido actual antes de reservar con otro profesional.",
     "local_cart_title": "Pedidos",
     "local_cart_title": "Pedidos",
     "orders_title": "Pedidos",
     "orders_title": "Pedidos",
+    "pending_custom_title": "Solicitudes en curso",
     "no_orders": "Ningún pedido creado.",
     "no_orders": "Ningún pedido creado.",
     "order_title": "Pedido #{id}",
     "order_title": "Pedido #{id}",
     "view_favorites": "ver favoritos",
     "view_favorites": "ver favoritos",

+ 16 - 5
src/i18n/locales/pt.json

@@ -3,6 +3,10 @@
     "confirm": "Confirmar",
     "confirm": "Confirmar",
     "expand": "expandir",
     "expand": "expandir",
     "collapse": "recolher",
     "collapse": "recolher",
+    "address_incomplete_banner": {
+      "title": "Complete o endereço do seu perfil!",
+      "cta": "Resolver agora"
+    },
     "actions": {
     "actions": {
       "save": "Salvar",
       "save": "Salvar",
       "cancel": "Cancelar",
       "cancel": "Cancelar",
@@ -437,12 +441,14 @@
       "my_schedules": "Minhas diárias"
       "my_schedules": "Minhas diárias"
     },
     },
     "today_schedules": {
     "today_schedules": {
+      "title": "Hoje",
       "start_with": "Início do serviço com",
       "start_with": "Início do serviço com",
-      "started_by": "Seriço serviço em andamento",
+      "not_started_label": "Início em breve com",
+      "started_by": "Serviço iniciado por",
       "finished_by": "Serviço concluído por",
       "finished_by": "Serviço concluído por",
       "code_label": "Código",
       "code_label": "Código",
       "in_progress": "em andamento",
       "in_progress": "em andamento",
-      "end_time_label": "Término às",
+      "end_time_label": "Termina em:",
       "rate_btn": "Avaliar",
       "rate_btn": "Avaliar",
       "help_btn": "ajuda",
       "help_btn": "ajuda",
       "cancelled": "Serviço cancelado"
       "cancelled": "Serviço cancelado"
@@ -495,7 +501,8 @@
       "btn_reschedule": "reagendar",
       "btn_reschedule": "reagendar",
       "empty_upcoming": "Nenhum serviço agendado",
       "empty_upcoming": "Nenhum serviço agendado",
       "empty_completed": "Nenhum serviço concluído",
       "empty_completed": "Nenhum serviço concluído",
-      "waiting_proposals": "Aguardando propostas"
+      "waiting_proposals": "Aguardando propostas",
+      "proposals_received_count": "{count} proposta(s) recebida(s)"
     },
     },
     "favorites": {
     "favorites": {
       "title": "Favoritos",
       "title": "Favoritos",
@@ -511,7 +518,8 @@
       "until_4h": "Até 4h",
       "until_4h": "Até 4h",
       "until_2h": "Até 2h",
       "until_2h": "Até 2h",
       "place_home": "Casa",
       "place_home": "Casa",
-      "no_price": "a combinar"
+      "no_price": "a combinar",
+      "empty_alert_text": "Ainda não há diaristas perto de você."
     },
     },
     "pending_schedules": {
     "pending_schedules": {
       "title": "Aguardando confirmação",
       "title": "Aguardando confirmação",
@@ -537,6 +545,7 @@
       "detail_service_fee_pix": "Taxa de serviço (Pix):",
       "detail_service_fee_pix": "Taxa de serviço (Pix):",
       "detail_service_fee_credit_card": "Taxa de serviço (cartão):",
       "detail_service_fee_credit_card": "Taxa de serviço (cartão):",
       "detail_total": "Total:",
       "detail_total": "Total:",
+      "detail_price_range": "Faixa de preço:",
       "detail_package_total": "Total do pacote (Pix):",
       "detail_package_total": "Total do pacote (Pix):",
       "detail_pix_total": "Total no Pix:",
       "detail_pix_total": "Total no Pix:",
       "detail_credit_card_total": "Total no cartão de crédito:",
       "detail_credit_card_total": "Total no cartão de crédito:",
@@ -667,7 +676,8 @@
         "other": "Outro"
         "other": "Outro"
       },
       },
       "missing_coords": "Coordenadas não encontradas para este endereço.",
       "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": {
     "help": {
       "title": "Ajuda",
       "title": "Ajuda",
@@ -836,6 +846,7 @@
     "other_provider_blocked": "Seu pedido já possui horários com outro profissional. Finalize ou esvazie o pedido atual antes de agendar com outro profissional.",
     "other_provider_blocked": "Seu pedido já possui horários com outro profissional. Finalize ou esvazie o pedido atual antes de agendar com outro profissional.",
     "local_cart_title": "Pedidos",
     "local_cart_title": "Pedidos",
     "orders_title": "Pedidos",
     "orders_title": "Pedidos",
+    "pending_custom_title": "Solicitações em andamento",
     "no_orders": "Nenhum pedido criado.",
     "no_orders": "Nenhum pedido criado.",
     "order_title": "Pedido #{id}",
     "order_title": "Pedido #{id}",
     "view_favorites": "ver favoritos",
     "view_favorites": "ver favoritos",

+ 1 - 23
src/pages/agenda/CalendarPage.vue

@@ -305,6 +305,7 @@
 <script setup>
 <script setup>
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
+import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 import { getClientCalendar } from "src/api/clientCalendar";
 import { getClientCalendar } from "src/api/clientCalendar";
 
 
 import {
 import {
@@ -331,29 +332,6 @@ const loading = ref(true);
 const upcomingSchedules = ref([]);
 const upcomingSchedules = ref([]);
 const completedSchedules = ref([]);
 const completedSchedules = ref([]);
 
 
-const parseLocalDate = (dateStr) => {
-  if (!dateStr) return null;
-
-  const iso = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
-  if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
-  const dmy = dateStr.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
-  if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
-  return null;
-};
-
-const formatWeekday = (dateStr) => {
-  const d = parseLocalDate(dateStr);
-  if (!d) return "";
-  const w = d.toLocaleDateString("pt-BR", { weekday: "long" });
-  return w.charAt(0).toUpperCase() + w.slice(1);
-};
-
-const formatDayMonth = (dateStr) => {
-  const d = parseLocalDate(dateStr);
-  if (!d) return "";
-  return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
-};
-
 const periodLabel = (periodType) => {
 const periodLabel = (periodType) => {
   const key = `period_types.${periodType}`;
   const key = `period_types.${periodType}`;
   const translated = t(key);
   const translated = t(key);

+ 20 - 6
src/pages/dashboard/DashboardPage.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <div class="dashboard-page bg-page">
+  <q-page class="dashboard-page bg-page">
     <template v-if="loading">
     <template v-if="loading">
       <div class="row items-center justify-center full-width bg-surface" style="height: 80vh">
       <div class="row items-center justify-center full-width bg-surface" style="height: 80vh">
         <q-spinner-dots color="primary" />
         <q-spinner-dots color="primary" />
@@ -13,7 +13,10 @@
         />
         />
         <DashboardRegistrationIncomplete v-if="!registrationComplete" />
         <DashboardRegistrationIncomplete v-if="!registrationComplete" />
         <DashboardSummaryInfos v-else :data="summaryInfos" />
         <DashboardSummaryInfos v-else :data="summaryInfos" />
-        <DashboardPaymentIncomplete v-if="!hasPaymentMethods" />
+        <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
         <DashboardPendingSchedules
           v-if="pendingServicePackages.length > 0"
           v-if="pendingServicePackages.length > 0"
           :data="pendingServicePackages"
           :data="pendingServicePackages"
@@ -29,15 +32,13 @@
         />
         />
         <DashboardTodaySchedules v-if="todaySchedules.length > 0" :data="todaySchedules" @rate="openRatingDialog" />
         <DashboardTodaySchedules v-if="todaySchedules.length > 0" :data="todaySchedules" @rate="openRatingDialog" />
         <DashboardScrollAreaSchedules />
         <DashboardScrollAreaSchedules />
-        <DashboardClientProposals v-if="clientProposals.length > 0" :data="clientProposals" @refresh-data="reloadDashboard" />
-        <DashboardPendingCustomSchedules v-if="customSchedulesNoProposals.length > 0" />
         <DashboardNextSchedules v-if="nextSchedules.length > 0" :data="nextSchedules" @view-details="openNextScheduleDialog" />
         <DashboardNextSchedules v-if="nextSchedules.length > 0" :data="nextSchedules" @view-details="openNextScheduleDialog" />
         <DashboardLastDoneSchedules v-if="lastDoneSchedules.length > 0" :data="lastDoneSchedules" />
         <DashboardLastDoneSchedules v-if="lastDoneSchedules.length > 0" :data="lastDoneSchedules" />
         <DashboardFavoriteProviders v-if="favoriteProviders.length > 0" :data="favoriteProviders" />
         <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>
       </q-pull-to-refresh>
     </template>
     </template>
-  </div>
+  </q-page>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
@@ -53,6 +54,7 @@ import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedu
 import DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
 import DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
 import DashboardFavoriteProviders from 'src/components/dashboard/DashboardFavoriteProviders.vue';
 import DashboardFavoriteProviders from 'src/components/dashboard/DashboardFavoriteProviders.vue';
 import DashboardProvidersClose from 'src/components/dashboard/DashboardProvidersClose.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 DashboardTodaySchedules from 'src/components/dashboard/DashboardTodaySchedules.vue';
 import FinalSuccesModal from '../schedules/components/FinalSuccesModal.vue';
 import FinalSuccesModal from '../schedules/components/FinalSuccesModal.vue';
 import DashboardPendingCustomSchedules from 'src/pages/dashboard/components/DashboardPendingCustomSchedules.vue';
 import DashboardPendingCustomSchedules from 'src/pages/dashboard/components/DashboardPendingCustomSchedules.vue';
@@ -62,6 +64,7 @@ import { onMounted, ref, computed } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import { LocalStorage, useQuasar } from 'quasar';
 import { LocalStorage, useQuasar } from 'quasar';
 import { dadosDashboard } from 'src/api/dashboard';
 import { dadosDashboard } from 'src/api/dashboard';
+import { updateMe } from 'src/api/user';
 import { userStore } from 'src/stores/user';
 import { userStore } from 'src/stores/user';
 import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
 import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
 import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
 import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
@@ -87,6 +90,8 @@ const todaySchedules = ref([]);
 const notifications = ref([]);
 const notifications = ref([]);
 const loading = ref(true);
 const loading = ref(true);
 const registrationComplete = computed(() => store.user?.registration_complete ?? true);
 const registrationComplete = computed(() => store.user?.registration_complete ?? true);
+const showPaymentBanner = ref(false);
+const hasLocation = ref(true);
 
 
 const successModalKey = ref(
 const successModalKey = ref(
   route.query.success ??
   route.query.success ??
@@ -184,6 +189,7 @@ const reloadDashboard = async (showLoader = true) => {
     todaySchedules.value = response.todaySchedules ?? [];
     todaySchedules.value = response.todaySchedules ?? [];
     notifications.value = response.notifications ?? [];
     notifications.value = response.notifications ?? [];
     hasPaymentMethods.value = response.has_payment_methods ?? true;
     hasPaymentMethods.value = response.has_payment_methods ?? true;
+    hasLocation.value = response.has_location ?? true;
   }
   }
   loading.value = false;
   loading.value = false;
 
 
@@ -239,6 +245,14 @@ const openRatingDialog = (schedule) => {
 onMounted(async () => {
 onMounted(async () => {
   await reloadDashboard();
   await reloadDashboard();
   maybeOpenSuccessModal();
   maybeOpenSuccessModal();
+
+  const isFirstAccess = store.user?.client?.first_access ?? false;
+  showPaymentBanner.value = isFirstAccess && !hasPaymentMethods.value;
+
+  if (isFirstAccess) {
+    updateMe({ first_access: false }).catch(() => {});
+    store.markFirstAccessSeen();
+  }
 });
 });
 </script>
 </script>
 
 

+ 50 - 75
src/pages/dashboard/components/DashboardClientProposals.vue

@@ -47,7 +47,7 @@
             <div class="row justify-between items-center text-text">
             <div class="row justify-between items-center text-text">
               <div>
               <div>
                 <div class="font12 fontmedium">
                 <div class="font12 fontmedium">
-                  {{ item.provider_name }}
+                  {{ getFirstName(item.provider_name) }}
 
 
                   <span class="font9 fontregular">
                   <span class="font9 fontregular">
                     {{
                     {{
@@ -56,27 +56,27 @@
                       })
                       })
                     }}
                     }}
                   </span>
                   </span>
+                  <span class="font10 fontmedium">
+                    <q-icon
+                      class="font9 fontregular"
+                      color="warning"
+                      name="star"
+                      size="12px"
+                    />
+
+                    <span>
+                      {{ item.average_rating }}
+                    </span>
+                  </span>
                 </div>
                 </div>
 
 
-                <span
+                <!-- <span
                   v-if="getProviderGenderLabel(item, t)"
                   v-if="getProviderGenderLabel(item, t)"
                   class="font9 fontregular text-grey-7"
                   class="font9 fontregular text-grey-7"
                 >
                 >
                   {{ getProviderGenderLabel(item, t) }}
                   {{ getProviderGenderLabel(item, t) }}
-                </span>
+                </span> -->
 
 
-                <span class="font10 fontmedium">
-                  <q-icon
-                    class="font9 fontregular"
-                    color="warning"
-                    name="star"
-                    size="12px"
-                  />
-
-                  <span>
-                    {{ item.average_rating }}
-                  </span>
-                </span>
               </div>
               </div>
             </div>
             </div>
 
 
@@ -129,7 +129,7 @@
               </span>
               </span>
             </div>
             </div>
 
 
-            <div class="actions">
+            <div class="actions q-pt-sm">
               <q-btn
               <q-btn
                 class="btn-reject"
                 class="btn-reject"
                 flat
                 flat
@@ -144,7 +144,7 @@
                 label="aceitar"
                 label="aceitar"
                 padding="4px 12px"
                 padding="4px 12px"
                 size="sm"
                 size="sm"
-                @click="() => handleAcceptProposal(item.id)"
+                @click="() => handleAcceptProposal(item)"
               />
               />
             </div>
             </div>
           </div>
           </div>
@@ -155,8 +155,9 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { acceptProposal, refuseProposal } from "src/api/customSchedules";
+import { refuseProposal } from "src/api/customSchedules";
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
+import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 import SchedulePaymentDialog from "src/components/dashboard/SchedulePaymentDialog.vue";
 import SchedulePaymentDialog from "src/components/dashboard/SchedulePaymentDialog.vue";
 
 
@@ -164,10 +165,11 @@ import {
   chooseprice,
   chooseprice,
   filterCurrency,
   filterCurrency,
   formatLabelByPeriodType,
   formatLabelByPeriodType,
+  getFirstName,
 } from "src/helpers/utils";
 } from "src/helpers/utils";
 
 
-import { getProviderGenderLabel } from "src/helpers/provider";
-import { useI18n } from "vue-i18n";
+// import { getProviderGenderLabel } from "src/helpers/provider";
+// import { useI18n } from "vue-i18n";
 
 
 const emit = defineEmits(["refreshData"]);
 const emit = defineEmits(["refreshData"]);
 
 
@@ -178,7 +180,7 @@ defineProps({
   },
   },
 });
 });
 
 
-const { t } = useI18n();
+// const { t } = useI18n();
 const $q = useQuasar();
 const $q = useQuasar();
 
 
 // const formatTime = (time) => {
 // const formatTime = (time) => {
@@ -203,56 +205,24 @@ const $q = useQuasar();
 //   return `${weekday}, ${day}-${month}`;
 //   return `${weekday}, ${day}-${month}`;
 // };
 // };
 
 
-const formatDayMonth = (iso) => {
-  if (!iso) return "";
-
-  return new Date(iso).toLocaleDateString("pt-BR", {
-    day: "2-digit",
-    month: "2-digit",
+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 formatWeekday = (iso) => {
-  if (!iso) return "";
-
-  const d = new Date(iso);
-
-  const w = d.toLocaleDateString("pt-BR", { weekday: "long" });
-
-  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 handleRefuseProposal = async (proposalId) => {
 const handleRefuseProposal = async (proposalId) => {
   // isLoading.value = true
   // isLoading.value = true
   try {
   try {
@@ -270,17 +240,23 @@ const handleRefuseProposal = async (proposalId) => {
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
 .scroll-wrapper {
 .scroll-wrapper {
-  overflow-x: auto;
-  scrollbar-width: none;
+  overflow: hidden;
 }
 }
 
 
 .scroll-track {
 .scroll-track {
   display: flex;
   display: flex;
   gap: 12px;
   gap: 12px;
-}
-
-.scroll-wrapper::-webkit-scrollbar {
-  display: none;
+  overflow-x: auto;
+  overscroll-behavior-x: contain;
+  scroll-snap-type: x proximity;
+  scrollbar-width: none;
+  &::-webkit-scrollbar {
+    display: none;
+  }
+  &::after {
+    content: "";
+    flex: 0 0 1px;
+  }
 }
 }
 
 
 .proposal-card {
 .proposal-card {
@@ -336,7 +312,6 @@ const handleRefuseProposal = async (proposalId) => {
   display: flex;
   display: flex;
   justify-content: flex-end;
   justify-content: flex-end;
   gap: 8px;
   gap: 8px;
-  margin-top: auto;
 }
 }
 
 
 .btn-reject {
 .btn-reject {

+ 9 - 1
src/pages/dashboard/components/DashboardPendingCustomSchedules.vue

@@ -1,5 +1,10 @@
 <template>
 <template>
-  <q-card :flat="false" bordered class="pending-request-card shadow-card">
+  <q-card
+    :flat="false"
+    bordered
+    class="pending-request-card shadow-card cursor-pointer"
+    @click="router.push({ name: 'ScheduleCartPage' })"
+  >
     <div class="request-wrapper">
     <div class="request-wrapper">
       <!-- informações -->
       <!-- informações -->
       <div class="request-info">
       <div class="request-info">
@@ -34,6 +39,9 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
+import { useRouter } from "vue-router";
+
+const router = useRouter();
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 140 - 68
src/pages/orders/ServicePackagePage.vue

@@ -20,74 +20,7 @@
       <div style="width: 36px"></div>
       <div style="width: 36px"></div>
     </div>
     </div>
 
 
-    <div
-      v-if="servicePackage.items.length === 0"
-      class="q-px-md q-pt-md"
-    >
-      <q-card
-        :flat="false"
-        class="empty-card bg-surface shadow-card card-border"
-      >
-        <q-card-section class="column items-center text-center q-pa-lg">
-          <q-icon
-            color="grey-5"
-            name="mdi-shopping-outline"
-            size="42px"
-          />
-
-          <span class="font14 fontbold text-text q-mt-sm">
-            {{ $t("service_package.empty_title") }}
-          </span>
-
-          <span class="font12 text-grey-7 q-mt-xs">
-            {{ $t("service_package.empty_message") }}
-          </span>
-
-          <q-btn
-            class="q-mt-md"
-            color="primary"
-            no-caps
-            padding="8px 18px"
-            rounded
-            unelevated
-            :label="$t('service_package.view_favorites')"
-            @click="router.push({ name: 'ProfilePage' })"
-          />
-        </q-card-section>
-      </q-card>
-
-      <q-card
-        v-if="servicePackage.lastCreatedGroup"
-        :flat="false"
-        class="created-card bg-surface shadow-card card-border q-mt-md"
-      >
-        <q-card-section class="q-pa-md">
-          <div class="row items-center no-wrap">
-            <q-icon
-              color="positive"
-              name="mdi-check-circle-outline"
-              size="24px"
-            />
-
-            <div class="column q-ml-sm">
-              <span class="font14 fontbold text-text">
-                {{ $t("service_package.last_request_title") }}
-              </span>
-
-              <span class="font12 text-grey-7">
-                {{
-                  $t("service_package.created_count", {
-                    count: servicePackage.lastCreatedGroup.schedule_ids.length,
-                  })
-                }}
-              </span>
-            </div>
-          </div>
-        </q-card-section>
-      </q-card>
-    </div>
-
-    <template v-else>
+    <template v-if="servicePackage.items.length > 0">
       <div class="q-px-md q-pt-md column q-gutter-y-sm">
       <div class="q-px-md q-pt-md column q-gutter-y-sm">
         <div class="service-package-discount-banner row items-center no-wrap q-pa-sm">
         <div class="service-package-discount-banner row items-center no-wrap q-pa-sm">
           <span class="font12 fontbold text-positive q-ml-sm">
           <span class="font12 fontbold text-positive q-ml-sm">
@@ -300,13 +233,106 @@
         </q-card>
         </q-card>
       </div>
       </div>
     </template>
     </template>
+
+    <DashboardClientProposals
+      v-if="schedulesProposals.length > 0"
+      :data="schedulesProposals"
+      class="q-pt-md"
+      @refresh-data="loadCustomRequests"
+    />
+
+    <div
+      v-if="pendingCustomRequests.length > 0"
+      class="q-px-md q-pt-md column q-gutter-y-sm"
+    >
+      <div class="section-title gradient-diarista q-mb-sm font16 fontbold">
+        {{ $t("service_package.pending_custom_title") }}
+      </div>
+
+      <CustomScheduleRequestCard
+        v-for="entry in pendingCustomRequests"
+        :key="entry.schedule.id"
+        :proposals-count="entry.proposalsCount"
+        :schedule="entry.schedule"
+      />
+    </div>
+
+    <div
+      v-if="servicePackage.items.length === 0 && !hasPendingCustomRequests"
+      class="q-px-md q-pt-md"
+    >
+      <q-card
+        :flat="false"
+        class="empty-card bg-surface shadow-card card-border"
+      >
+        <q-card-section class="column items-center text-center q-pa-lg">
+          <q-icon
+            color="grey-5"
+            name="mdi-shopping-outline"
+            size="42px"
+          />
+
+          <span class="font14 fontbold text-text q-mt-sm">
+            {{ $t("service_package.empty_title") }}
+          </span>
+
+          <span class="font12 text-grey-7 q-mt-xs">
+            {{ $t("service_package.empty_message") }}
+          </span>
+
+          <q-btn
+            class="q-mt-md"
+            color="primary"
+            no-caps
+            padding="8px 18px"
+            rounded
+            unelevated
+            :label="$t('service_package.view_favorites')"
+            @click="router.push({ name: 'ProfilePage' })"
+          />
+        </q-card-section>
+      </q-card>
+
+      <q-card
+        v-if="servicePackage.lastCreatedGroup"
+        :flat="false"
+        class="created-card bg-surface shadow-card card-border q-mt-md"
+      >
+        <q-card-section class="q-pa-md">
+          <div class="row items-center no-wrap">
+            <q-icon
+              color="positive"
+              name="mdi-check-circle-outline"
+              size="24px"
+            />
+
+            <div class="column q-ml-sm">
+              <span class="font14 fontbold text-text">
+                {{ $t("service_package.last_request_title") }}
+              </span>
+
+              <span class="font12 text-grey-7">
+                {{
+                  $t("service_package.created_count", {
+                    count: servicePackage.lastCreatedGroup.schedule_ids.length,
+                  })
+                }}
+              </span>
+            </div>
+          </div>
+        </q-card-section>
+      </q-card>
+    </div>
   </q-page>
   </q-page>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
 import { computed, onMounted, ref } from "vue";
 import { computed, onMounted, ref } from "vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
+import DashboardClientProposals from "src/pages/dashboard/components/DashboardClientProposals.vue";
+import CustomScheduleRequestCard from "src/pages/orders/components/CustomScheduleRequestCard.vue";
 import { createScheduleServicePackage } from "src/api/servicePackage";
 import { createScheduleServicePackage } from "src/api/servicePackage";
+import { dadosPedidosCliente } from "src/api/dashboard";
 import { getAddresses } from "src/api/address";
 import { getAddresses } from "src/api/address";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
@@ -327,6 +353,42 @@ const store = userStore();
 const addressLoading = ref(false);
 const addressLoading = ref(false);
 const primaryAddress = ref(null);
 const primaryAddress = ref(null);
 const submitting = ref(false);
 const submitting = ref(false);
+const customSchedulesNoProposals = ref([]);
+const schedulesProposals = ref([]);
+
+const pendingCustomRequests = computed(() => {
+  const groups = new Map();
+
+  for (const schedule of customSchedulesNoProposals.value) {
+    groups.set(schedule.id, { schedule, proposalsCount: 0 });
+  }
+
+  for (const item of schedulesProposals.value) {
+    const existing = groups.get(item.schedule_id);
+
+    if (existing) {
+      existing.proposalsCount += 1;
+
+      continue;
+    }
+
+    groups.set(item.schedule_id, {
+      schedule: {
+        id: item.schedule_id,
+        date: item.date,
+        start_time: item.start_time,
+        end_time: item.end_time,
+        address: item.address,
+        schedule_type: "custom",
+      },
+      proposalsCount: 1,
+    });
+  }
+
+  return [...groups.values()];
+});
+
+const hasPendingCustomRequests = computed(() => pendingCustomRequests.value.length > 0);
 
 
 const isValidFee = (fee) =>
 const isValidFee = (fee) =>
   fee !== null && fee !== undefined && Number.isFinite(Number(fee));
   fee !== null && fee !== undefined && Number.isFinite(Number(fee));
@@ -606,9 +668,19 @@ const submitServicePackage = async () => {
   }
   }
 };
 };
 
 
+const loadCustomRequests = async () => {
+  const response = await dadosPedidosCliente();
+
+  if (response) {
+    customSchedulesNoProposals.value = response.customSchedulesNoProposals ?? [];
+    schedulesProposals.value = response.schedulesProposals ?? [];
+  }
+};
+
 onMounted(() => {
 onMounted(() => {
   loadPrimaryAddress();
   loadPrimaryAddress();
   loadPlatformFees().catch(() => {});
   loadPlatformFees().catch(() => {});
+  loadCustomRequests();
 });
 });
 </script>
 </script>
 
 

+ 115 - 0
src/pages/orders/components/CustomScheduleRequestCard.vue

@@ -0,0 +1,115 @@
+<template>
+  <q-card :flat="false" class="request-card bg-surface shadow-card q-mb-sm">
+    <q-card-section class="q-pa-sm">
+      <div class="row no-wrap items-start q-gutter-x-sm">
+        <q-avatar size="44px" class="request-avatar">
+          <q-icon
+            :name="hasProposals ? 'mdi-format-list-bulleted' : 'mdi-clock-outline'"
+            size="20px"
+            color="white"
+          />
+        </q-avatar>
+
+        <div class="col column text-text">
+          <span class="font12 fontbold">
+            {{
+              hasProposals
+                ? $t("dashboard_client.agenda.proposals_received_count", { count: proposalsCount })
+                : $t("dashboard_client.agenda.waiting_proposals")
+            }}
+          </span>
+
+          <div class="row items-center no-wrap">
+            <span class="font10 fontbold">{{ formatWeekday(schedule.date) }}</span>
+
+            <span class="font10 fontmedium">{{ ", " + formatDayMonth(schedule.date) }}</span>
+          </div>
+
+          <span>
+            <span class="font10 fontmedium">
+              {{ $t("dashboard_client.next_schedules.from") }}
+            </span>
+
+            <span class="font10 fontbold q-ml-xs">
+              {{ schedule.start_time?.slice(0, 5) }}
+              {{ $t("dashboard_client.next_schedules.to") }}
+              {{ schedule.end_time?.slice(0, 5) }}
+            </span>
+          </span>
+        </div>
+
+        <div class="col-auto column items-end">
+          <q-chip
+            class="font10"
+            color="warning-bg"
+            dense
+            square
+            text-color="warning"
+            :label="$t('dashboard_client.agenda.status_pending')"
+          />
+        </div>
+      </div>
+
+      <div class="row items-center no-wrap q-mt-xs">
+        <span class="type-label text-primary font10 fontmedium">
+          {{ $t("dashboard_client.agenda.type_custom") }}
+        </span>
+
+        <q-space />
+
+        <q-btn
+          color="primary"
+          no-caps
+          padding="4px 8px"
+          rounded
+          size="sm"
+          :label="$t('dashboard_client.agenda.btn_view_details')"
+          @click="openDetailsDialog"
+        />
+      </div>
+    </q-card-section>
+  </q-card>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
+import { useQuasar } from "quasar";
+import NextSchedulesDetailsDialog from "src/components/dashboard/NextSchedulesDetailsDialog.vue";
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    required: true,
+  },
+  proposalsCount: {
+    type: Number,
+    default: 0,
+  },
+});
+
+const $q = useQuasar();
+
+const hasProposals = computed(() => props.proposalsCount > 0);
+
+const openDetailsDialog = () => {
+  $q.dialog({
+    component: NextSchedulesDetailsDialog,
+    componentProps: { schedule: props.schedule },
+  });
+};
+</script>
+
+<style scoped lang="scss">
+.request-card {
+  border-radius: 8px;
+}
+
+.request-avatar {
+  background: linear-gradient(135deg, #8b5cf6, #ec4899);
+}
+
+.type-label {
+  line-height: 1.2;
+}
+</style>

+ 4 - 4
src/pages/schedules/SobMedidaPage.vue

@@ -298,10 +298,10 @@ const saveFinalOrder = async (payloadFinal) => {
 
 
   await createCustomSchedule(payload)
   await createCustomSchedule(payload)
 
 
-  $q.notify({
-    type: 'positive',
-    message: t('sob_medida.success_message')
-  })
+  // $q.notify({
+  //   type: 'positive',
+  //   message: t('sob_medida.success_message')
+  // })
 
 
   router.push({ name: 'DashboardPage', query: { success: 'custom_schedule' } })
   router.push({ name: 'DashboardPage', query: { success: 'custom_schedule' } })
 }
 }

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

@@ -89,7 +89,10 @@
       />
       />
     </div>
     </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">
       <div class="dashboard-section-title font16 fontbold gradient-diarista">
         {{ $t("search_page.choose_provider") }}
         {{ $t("search_page.choose_provider") }}
       </div>
       </div>
@@ -129,6 +132,10 @@
       />
       />
     </div>
     </div>
 
 
+    <template v-else-if="!hasLocation">
+      <AddressIncompleteBanner @resolved="loadProviders" />
+    </template>
+
     <template v-else>
     <template v-else>
       <div
       <div
         v-if="sortedProviders.length === 0"
         v-if="sortedProviders.length === 0"
@@ -269,6 +276,7 @@ import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 import { useRouter } from "vue-router";
 import { useRouter } from "vue-router";
 
 
+import AddressIncompleteBanner from "src/components/shared/AddressIncompleteBanner.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 import SearchFilterDialog from "src/pages/search/components/SearchFilterDialog.vue";
 import SearchFilterDialog from "src/pages/search/components/SearchFilterDialog.vue";
 import ProfileInfoDialog from "src/components/profile/ProfileInfoDialog.vue";
 import ProfileInfoDialog from "src/components/profile/ProfileInfoDialog.vue";
@@ -281,6 +289,7 @@ const activeDate = ref(null);
 const activeSort = ref(null);
 const activeSort = ref(null);
 const allProviders = ref([]);
 const allProviders = ref([]);
 const currentPeriodType = ref(8);
 const currentPeriodType = ref(8);
+const hasLocation = ref(true);
 const loading = ref(true);
 const loading = ref(true);
 const searchName = ref("");
 const searchName = ref("");
 
 
@@ -316,6 +325,14 @@ const sortedProviders = computed(() => {
       return list.sort(
       return list.sort(
         (a, b) => Number(b[priceKey] ?? 0) - Number(a[priceKey] ?? 0),
         (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":
     case "rating_desc":
       return list.sort(
       return list.sort(
         (a, b) => Number(b.average_rating ?? 0) - Number(a.average_rating ?? 0),
         (a, b) => Number(b.average_rating ?? 0) - Number(a.average_rating ?? 0),
@@ -355,17 +372,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) => {
 const goToScheduling = (provider) => {
   $q.dialog({
   $q.dialog({
     component: SchedulingDialog,
     component: SchedulingDialog,
@@ -386,13 +392,16 @@ const loadProviders = async () => {
   loading.value = true;
   loading.value = true;
 
 
   try {
   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 {
   } catch {
     allProviders.value = [];
     allProviders.value = [];
+    hasLocation.value = true;
   } finally {
   } finally {
     loading.value = false;
     loading.value = false;
   }
   }

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

@@ -137,16 +137,8 @@ const sortGroups = computed(() => [
     key: "distance",
     key: "distance",
     label: t("search_filter.groups.distance"),
     label: t("search_filter.groups.distance"),
     options: [
     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") },
     ],
     ],
   },
   },
   {
   {

+ 5 - 0
src/stores/user.js

@@ -28,6 +28,10 @@ export const userStore = defineStore("user", () => {
     isAdmin.value = normalized?.type === "admin";
     isAdmin.value = normalized?.type === "admin";
   };
   };
 
 
+  const markFirstAccessSeen = () => {
+    if (user.value?.client) user.value.client.first_access = false;
+  };
+
   const resetUser = () => {
   const resetUser = () => {
     user.value = null;
     user.value = null;
     isAdmin.value = false;
     isAdmin.value = false;
@@ -45,6 +49,7 @@ export const userStore = defineStore("user", () => {
     setUser,
     setUser,
     resetUser,
     resetUser,
     fetchUser,
     fetchUser,
+    markFirstAccessSeen,
     accessToken,
     accessToken,
   };
   };
 });
 });