Sfoglia il codice sorgente

Merge branch 'development', remote-tracking branch 'origin' into fix-diaria-kay-ajustes-cliente

kayo henrique 2 settimane fa
parent
commit
6c2bbe0ad1
44 ha cambiato i file con 1035 aggiunte e 466 eliminazioni
  1. 1 1
      package.json
  2. 5 0
      quasar.config.js
  3. 2 2
      src-capacitor/android/app/build.gradle
  4. 1 1
      src-capacitor/package.json
  5. 1 5
      src/App.vue
  6. 7 2
      src/api/clientFavoriteProvider.js
  7. 5 0
      src/api/dashboard.js
  8. 10 0
      src/api/payment.js
  9. 3 1
      src/api/servicePackage.js
  10. 1 1
      src/boot/axios.js
  11. 1 19
      src/components/dashboard/DashboardNextSchedules.vue
  12. 2 1
      src/components/dashboard/DashboardPendingSchedules.vue
  13. 53 17
      src/components/dashboard/DashboardProvidersClose.vue
  14. 52 4
      src/components/dashboard/DashboardTodaySchedules.vue
  15. 22 0
      src/components/dashboard/NextSchedulesDetailsDialog.vue
  16. 10 1
      src/components/dashboard/ScheduleAcceptedDialog.vue
  17. 8 3
      src/components/dashboard/SchedulePaymentDialog.vue
  18. 27 16
      src/components/dashboard/SchedulePaymentPixDialog.vue
  19. 15 5
      src/components/dashboard/SchedulePaymentProcessingDialog.vue
  20. 6 25
      src/components/login/LoginStep3Panel.vue
  21. 9 3
      src/components/profile/ProfileAddressFormDialog.vue
  22. 65 0
      src/components/shared/AddressIncompleteBanner.vue
  23. 0 29
      src/css/app.scss
  24. 24 0
      src/helpers/scheduleDate.js
  25. 26 9
      src/i18n/locales/en.json
  26. 26 9
      src/i18n/locales/es.json
  27. 26 9
      src/i18n/locales/pt.json
  28. 2 2
      src/layouts/MainLayout.vue
  29. 9 3
      src/pages/LoginPage.vue
  30. 1 23
      src/pages/agenda/CalendarPage.vue
  31. 61 17
      src/pages/dashboard/DashboardPage.vue
  32. 50 75
      src/pages/dashboard/components/DashboardClientProposals.vue
  33. 9 1
      src/pages/dashboard/components/DashboardPendingCustomSchedules.vue
  34. 7 4
      src/pages/location/AddressCompletionPage.vue
  35. 142 68
      src/pages/orders/ServicePackagePage.vue
  36. 115 0
      src/pages/orders/components/CustomScheduleRequestCard.vue
  37. 65 11
      src/pages/profile/ProfileEditDialog.vue
  38. 6 0
      src/pages/profile/ProfilePage.vue
  39. 15 34
      src/pages/schedules/SobMedidaPage.vue
  40. 22 5
      src/pages/schedules/components/FinalSuccesModal.vue
  41. 38 26
      src/pages/search/SearchPage.vue
  42. 78 24
      src/pages/search/components/SchedulingDialog.vue
  43. 2 10
      src/pages/search/components/SearchFilterDialog.vue
  44. 5 0
      src/stores/user.js

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "diarista-cliente-app",
-  "version": "0.0.1",
+  "version": "2.5",
   "description": "Aplicativo para o cliente do Diaria",
   "productName": "Diaria App",
   "author": "zntt <zanattagg@gmail.com>",

+ 5 - 0
quasar.config.js

@@ -14,6 +14,10 @@ const envFiles = {
   prod:    ".env.app.prod",
 };
 
+const appVersion = JSON.parse(
+  readFileSync(resolve(process.cwd(), "package.json"), "utf8"),
+).version;
+
 const loadAppEnv = (ctx) => {
   const appEnv  = process.env.APP_ENV || (ctx.dev ? "dev" : "prod");
   const envFile = envFiles[appEnv];
@@ -26,6 +30,7 @@ const loadAppEnv = (ctx) => {
 
   return {
     APP_ENV:             appEnv,
+    APP_VERSION:         appVersion,
     API_URL:             fileEnv.API_URL,
     PASSWORD:            fileEnv.PASSWORD,
     WEBSOCKET_API:       fileEnv.WEBSOCKET_API,

+ 2 - 2
src-capacitor/android/app/build.gradle

@@ -13,8 +13,8 @@ android {
         applicationId "br.com.diarista.client"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
-        versionCode 12
-        versionName "2.1"
+        versionCode 16
+        versionName "2.5"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         aaptOptions {
              // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

+ 1 - 1
src-capacitor/package.json

@@ -1,6 +1,6 @@
 {
   "name": "diarista-cliente-app",
-  "version": "0.0.1",
+  "version": "2.5",
   "description": "Aplicativo para o cliente do Diaria",
   "author": "zntt <zanattagg@gmail.com>",
   "private": true,

+ 1 - 5
src/App.vue

@@ -29,12 +29,8 @@ const updateStatusBar = async (isDark) => {
 
   await StatusBar.show();
 
-  await StatusBar.setBackgroundColor({
-    color: isDark ? "#121212" : "#FFFFFF",
-  });
-
   await StatusBar.setStyle({
-    style: isDark ? Style.Light : Style.Dark,
+    style: isDark ? Style.Dark : Style.Light,
   });
 };
 

+ 7 - 2
src/api/clientFavoriteProvider.js

@@ -6,9 +6,14 @@ export const getClientFavoriteProviders = async (clientId) => {
 }
 
 export const createClientFavoriteProvider = async (info) => {
-  const { data } = await api.post(`/client/favorite-provider`, info);
+  const { data } = await api.post(`/client/favorite-provider`,info,
+    {
+      skipSuccessNotify: true,
+    }
+  );
+
   return data.payload;
-}
+};
 
 export const deleteClientFavoriteProvider = async (id) => {
   const { data } = await api.delete(`/client/favorite-provider/${id}`);

+ 5 - 0
src/api/dashboard.js

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

+ 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;

+ 3 - 1
src/api/servicePackage.js

@@ -21,7 +21,9 @@ export const createServicePackage = async (payload) => {
 };
 
 export const createMyServicePackage = async (payload) => {
-  const { data } = await api.post("/service-packages/me", payload);
+  const { data } = await api.post("/service-packages/me", payload, {
+    skipSuccessNotify: true,
+  });
   return data.payload;
 };
 

+ 1 - 1
src/boot/axios.js

@@ -101,7 +101,7 @@ const errorInterceptor = async (error, router) => {
 };
 
 const successInterceptor = (response) => {
-  if (response.data.message) {
+  if (response.data.message && !response.config?.skipSuccessNotify) {
     Notify.create({
       message: response.data.message,
       type: "positive",

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

@@ -145,6 +145,7 @@
 
 <script setup>
 import { avatarColors } from "src/helpers/avatarColors";
+import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 
 import {
   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(() => {
   loadPlatformFees().catch(() => {});
 });

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

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

+ 53 - 17
src/components/dashboard/DashboardProvidersClose.vue

@@ -5,30 +5,55 @@
         {{ $t("dashboard_client.providers_close.title") }}
       </div>
 
-      <div class="text-text-light">
+      <div
+        v-if="data.length"
+        class="row items-center no-wrap text-text"
+      >
         <q-btn
-          color="text-light"
+          color="text"
+          dense
           flat
           icon="mdi-chevron-left"
-          size="md"
+          round
+          size="sm"
           @click="setPeriodTypePrevious"
         />
 
-        <span class="font12 fontbold">
+        <span class="font10 fontbold">
           {{ showCorrectLabels() }}
         </span>
 
         <q-btn
-          color="text-light"
+          color="text"
+          dense
           flat
           icon="mdi-chevron-right"
-          size="md"
+          round
+          size="sm"
           @click="setPeriodTypeNext"
         />
       </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"
@@ -60,13 +85,6 @@
                   {{ getFirstName(p.provider_name) }}
                 </span>
 
-                <span
-                  v-if="getProviderGenderLabel(p, t)"
-                  class="font9 fontmedium text-grey-7"
-                >
-                  {{ getProviderGenderLabel(p, t) }}
-                </span>
-
                 <span class="text-provider-close-region font9 fontmedium">
                   {{ p.district }}
                 </span>
@@ -126,9 +144,10 @@
 
                 <div class="full-width">
                   <q-btn
+                    class="full-width"
                     color="primary"
                     no-caps
-                    padding="3px 12px"
+                    padding="1px 5px"
                     rounded
                     size="sm"
                     unelevated
@@ -148,7 +167,6 @@
 <script setup>
 import { avatarColors } from "src/helpers/avatarColors";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
-import { getProviderGenderLabel } from "src/helpers/provider";
 import { ref } from "vue";
 import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
@@ -232,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>

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

@@ -1,5 +1,9 @@
 <template>
   <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-track">
         <q-card
@@ -31,7 +35,11 @@
 
               <div class="col-7 column no-wrap overflow-hidden justify-center">
                 <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") }}
                   </template>
 
@@ -71,7 +79,17 @@
               <div
                 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">
                     <span class="font9 fontbold text-primary q-mb-xs">
                       {{ $t("dashboard_client.today_schedules.code_label") }}
@@ -117,7 +135,7 @@
 
                   <q-btn
                     v-else
-                    class="rate-btn"
+                    class="rate-btn font10 fontmedium"
                     no-caps
                     unelevated
                     @click.stop="emit('rate', item)"
@@ -201,7 +219,7 @@
                     {{ $t("dashboard_client.today_schedules.end_time_label") }}
 
                     <span class="gradient-diarista">
-                      {{ item.end_time?.slice(0, 5) }}
+                      {{ calculateTimeUntilEnd(item.end_time) }}
                     </span>
                   </span>
                 </template>
@@ -277,6 +295,36 @@ const cardState = (item) => {
   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(() => {
   return props.data.filter((item) => !item.client_reviewed);
 });

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

@@ -117,6 +117,19 @@
           </span>
         </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">
           <span class="detail-label text-primary q-pr-sm">
             {{ $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 = () => {
   $q.dialog({
     component: ScheduleCancelDialog,

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

@@ -13,7 +13,15 @@
           size="80px"
           :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>
 
         <div class="font16 fontbold provider-name">
@@ -159,6 +167,7 @@
 import { avatarColors } from "src/helpers/avatarColors";
 import { computed, onMounted } from "vue";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
+import { getProfileMediaUrl } from "src/helpers/profileMedia";
 
 import {
   getSchedulePlatformFeeRate,

+ 8 - 3
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])
@@ -141,7 +145,6 @@ const { platformFees, loadPlatformFees } = usePaymentPlatformFees()
 const item = computed(() => props.servicePackage)
 
 const selectedMethod = ref(null)
-const agreedToTerms = ref(false)
 const paymentMethods = ref([])
 const loadingCards = ref(false)
 
@@ -166,7 +169,7 @@ const addressFullText = computed(() => {
   return parts.join(', ');
 });
 
-const canConfirm = computed(() => selectedMethod.value !== null && agreedToTerms.value)
+const canConfirm = computed(() => selectedMethod.value !== null)
 
 
 //direcionamento para ttermos e serviços 
@@ -215,6 +218,7 @@ const openPixPayment = () => {
     componentProps: {
       servicePackage: item.value,
       total: selectedTotal.value,
+      targetType: props.targetType,
     },
   }).onOk(() => {
     onDialogOK()
@@ -234,6 +238,7 @@ const onConfirm = () => {
       servicePackage: item.value,
       clientPaymentMethodId,
       total: selectedTotal.value,
+      targetType: props.targetType,
     },
   }).onOk(() => {
     onDialogOK()
@@ -243,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()

+ 27 - 16
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)
@@ -164,28 +169,34 @@ const stopPolling = () => {
 }
 
 const applyPaymentStatus = (nextPayment) => {
-  if (!nextPayment) return
+ if (nextPayment.status === 'paid') {
+  processing.value = false
 
-  payment.value = nextPayment
+  $q.notify({
+    type: 'positive',
+    message: 'Pagamento confirmado!',
+    position: 'top',
+  })
+
+  paymentStore.clearPixPaymentForServicePackage(cacheKey.value)
+  stopPolling()
+
+  onDialogOK()
+
+  return
+}
 
-  if (nextPayment.status === 'paid') {
-    success.value = true
-    processing.value = false
-    paymentStore.clearPixPaymentForServicePackage(itemId.value)
-    stopPolling()
-    return
-  }
 
   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 () => {
@@ -193,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) {
@@ -221,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()
   }
 }
@@ -231,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)
@@ -240,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()

+ 15 - 5
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,22 +61,31 @@ 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,
     })
 
     if (payment.status !== 'paid') {
-      throw new Error(payment.failure_message || 'Pagamento não confirmado.')
-    }
+        throw new Error(payment.failure_message || 'Pagamento não confirmado.')
+      }
+
+      $q.notify({
+        type: 'positive',
+        message: 'Pagamento confirmado!',
+        position: 'top',
+      })
 
-    success.value = true
+      onDialogOK()
+      
   } catch (e) {
     console.error('Erro ao pagar:', e)
 

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

@@ -28,7 +28,7 @@
     <div>
       <div class="text-text">
         <span class="font14 fontbold">
-          {{ $t("common.terms.document") }}
+          {{ $t("common.terms.cpf") }}
         </span>
       </div>
 
@@ -39,13 +39,16 @@
         hide-bottom-space
         input-class="text-text"
         lazy-rules
+        mask="###.###.###-##"
         no-error-icon
         outlined
+        placeholder="000.000.000-00"
         rounded
+        type="tel"
+        unmasked-value
         :error="!!serverErrors.document"
         :error-message="serverErrors.document"
-        :placeholder="$t('common.terms.document')"
-        :rules="[inputRules.cpfOrCnpj]"
+        :rules="[inputRules.required, inputRules.cpf]"
         @update:model-value="clearServerError('document')"
       />
     </div>
@@ -286,28 +289,6 @@
       />
     </div>
 
-    <div>
-      <div class="text-text">
-        <span class="font14 fontbold">
-          {{ $t("auth.address_instructions") }}
-        </span>
-      </div>
-
-      <q-input
-        v-model="form.instructions"
-        autogrow
-        bg-color="surface"
-        class="q-mt-sm q-mb-md"
-        hide-bottom-space
-        input-class="text-text"
-        lazy-rules
-        no-error-icon
-        outlined
-        rounded
-        rows="3"
-        type="textarea"
-      />
-    </div>
 
     <div>
       <div class="row q-gutter-sm q-mt-xs">

+ 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>

+ 0 - 29
src/css/app.scss

@@ -1,35 +1,6 @@
 @use "sass:map";
 @use "src/css/quasar.variables.scss";
 
-:root {
-  --system-bar-bg: #757575;
-}
-
-body::before,
-body::after {
-  content: "";
-  position: fixed;
-  left: 0;
-  right: 0;
-  background: var(--system-bar-bg);
-  pointer-events: none;
-  z-index: 10000;
-}
-
-body::before {
-  top: 0;
-  height: env(safe-area-inset-top, 0px);
-}
-
-body::after {
-  bottom: 0;
-  height: env(safe-area-inset-bottom, 0px);
-}
-
-.system-bar-spacer {
-  background: var(--system-bar-bg) !important;
-}
-
 .q-page {
   font-family: "Inter", sans-serif;
 }

+ 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 };

+ 26 - 9
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",
@@ -170,7 +174,7 @@
     "back_to_register": "Back to registration",
     "cep_not_found": "ZIP code not found. Please check and try again.",
     "location_permission_denied": "Location permission denied. Please enable it in device settings.",
-    "location_error": "Could not get your location. Please try again.",
+    "location_error": "Turn on your location",
     "confirm_location": "confirm location",
     "confirm_address": "confirm address",
     "complement": "Complement",
@@ -437,12 +441,14 @@
       "my_schedules": "My schedules"
     },
     "today_schedules": {
+      "title": "Today",
       "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",
       "code_label": "Code",
       "in_progress": "in progress",
-      "end_time_label": "Ends at",
+      "end_time_label": "Ends in:",
       "rate_btn": "Rate",
       "help_btn": "help",
       "cancelled": "Service cancelled"
@@ -495,7 +501,8 @@
       "btn_reschedule": "reschedule",
       "empty_upcoming": "No scheduled services",
       "empty_completed": "No completed services",
-      "waiting_proposals": "Waiting for proposals"
+      "waiting_proposals": "Waiting for proposals",
+      "proposals_received_count": "{count} proposal(s) received"
     },
     "favorites": {
       "title": "Favorites",
@@ -511,7 +518,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",
@@ -537,6 +545,7 @@
       "detail_service_fee_pix": "Service fee (Pix):",
       "detail_service_fee_credit_card": "Service fee (card):",
       "detail_total": "Total:",
+      "detail_price_range": "Price range:",
       "detail_package_total": "Package total (Pix):",
       "detail_pix_total": "Pix total:",
       "detail_credit_card_total": "Credit card total:",
@@ -573,13 +582,13 @@
     "placeholder_email": "Enter your email",
     "phone": "Phone",
     "placeholder_phone": "(11) 99999-9999",
-    "document": "Document",
-    "placeholder_document": "Enter your document",
     "update": "Update",
     "language": "Language",
     "lang_pt": "PT-br",
     "lang_en": "EN-us",
     "lang_es": "ES-es",
+    "notifications": "Notifications",
+    "push_notifications": "Receive notifications",
     "payments": {
       "title": "Payments",
       "description": "My cards",
@@ -667,7 +676,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",
@@ -760,6 +770,7 @@
     "reviews_title": "Reviews",
     "see_all": "see all",
     "no_reviews": "No reviews found.",
+    "provider_unavailable": "This provider is not available for scheduling right now.",
     "unknown_client": "Client",
     "select_service": "Select service",
     "no_slots_available": "No time slots available for this day.",
@@ -777,7 +788,7 @@
       "slot_required": "Select a time slot to continue."
     },
     "price_up_to": "up to {value}",
-    "cart": "cart",
+    "cart": "orders",
     "service_types": {
       "integral": {
         "label": "Full day",
@@ -827,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.",
     "local_cart_title": "Orders",
     "orders_title": "Orders",
+    "pending_custom_title": "Requests in progress",
     "no_orders": "No orders created.",
     "order_title": "Order #{id}",
     "view_favorites": "view favorites",
@@ -857,6 +869,11 @@
     "submit": "send request",
     "close": "close orders",
     "closed_success": "Orders closed. Wait for provider approval.",
+    "sent_title_line1": "YOUR ORDER WAS SENT",
+    "sent_title_line2": "TO THE PROVIDER!",
+    "sent_subtitle_before": "Wait for the ",
+    "sent_subtitle_highlight": "provider confirmation",
+    "sent_subtitle_after": " to make the payment and complete your booking.",
     "same_provider_required": "All time slots must be from the same provider.",
     "unknown": "No information",
     "multiple_providers": "{count} professionals",

+ 26 - 9
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",
@@ -170,7 +174,7 @@
     "back_to_register": "Volver al registro",
     "cep_not_found": "Código postal no encontrado. Verifique e intente nuevamente.",
     "location_permission_denied": "Permiso de ubicación denegado. Habilítelo en la configuración del dispositivo.",
-    "location_error": "No se pudo obtener tu ubicación. Inténtalo de nuevo.",
+    "location_error": "Activa tu ubicación.",
     "confirm_location": "confirmar ubicación",
     "confirm_address": "confirmar dirección",
     "complement": "Complemento",
@@ -437,12 +441,14 @@
       "my_schedules": "Mis jornadas"
     },
     "today_schedules": {
+      "title": "Hoy",
       "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",
       "code_label": "Código",
       "in_progress": "en progreso",
-      "end_time_label": "Término a las",
+      "end_time_label": "Termina en:",
       "rate_btn": "Avaliar",
       "help_btn": "ayuda",
       "cancelled": "Servicio cancelado"
@@ -492,7 +498,8 @@
       "btn_reschedule": "reprogramar",
       "empty_upcoming": "Sin servicios programados",
       "empty_completed": "Sin servicios completados",
-      "waiting_proposals": "Esperando propuestas"
+      "waiting_proposals": "Esperando propuestas",
+      "proposals_received_count": "{count} propuesta(s) recibida(s)"
     },
     "favorites": {
       "title": "Favoritos",
@@ -508,7 +515,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",
@@ -534,6 +542,7 @@
       "detail_service_fee_pix": "Tarifa de servicio (Pix):",
       "detail_service_fee_credit_card": "Tarifa de servicio (tarjeta):",
       "detail_total": "Total:",
+      "detail_price_range": "Rango de precio:",
       "detail_package_total": "Total del paquete (Pix):",
       "detail_pix_total": "Total en Pix:",
       "detail_credit_card_total": "Total en tarjeta de crédito:",
@@ -570,13 +579,13 @@
     "placeholder_email": "Ingrese su correo electrónico",
     "phone": "Teléfono",
     "placeholder_phone": "(11) 99999-9999",
-    "document": "Documento",
-    "placeholder_document": "Ingrese su documento",
     "update": "Actualizar",
     "language": "Idioma",
     "lang_pt": "PT-br",
     "lang_en": "EN-us",
     "lang_es": "ES-es",
+    "notifications": "Notificaciones",
+    "push_notifications": "Recibir notificaciones",
     "payments": {
       "title": "Pagos",
       "description": "Mis tarjetas",
@@ -664,7 +673,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",
@@ -757,6 +767,7 @@
     "reviews_title": "Reseñas",
     "see_all": "ver todas",
     "no_reviews": "No se encontraron reseñas.",
+    "provider_unavailable": "Este prestador no está disponible para agendar en este momento.",
     "unknown_client": "Cliente",
     "select_service": "Seleccionar servicio",
     "no_slots_available": "No hay horarios disponibles para este día.",
@@ -774,7 +785,7 @@
       "slot_required": "Seleccione un horario para continuar."
     },
     "price_up_to": "hasta {value}",
-    "cart": "carrinho",
+    "cart": "Pedidos",
     "service_types": {
       "integral": {
         "label": "Integral",
@@ -824,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.",
     "local_cart_title": "Pedidos",
     "orders_title": "Pedidos",
+    "pending_custom_title": "Solicitudes en curso",
     "no_orders": "Ningún pedido creado.",
     "order_title": "Pedido #{id}",
     "view_favorites": "ver favoritos",
@@ -854,6 +866,11 @@
     "submit": "enviar solicitud",
     "close": "cerrar pedidos",
     "closed_success": "Pedidos cerrados. Espere la aprobación de los profesionales.",
+    "sent_title_line1": "¡TU PEDIDO FUE ENVIADO",
+    "sent_title_line2": "AL PROFESIONAL!",
+    "sent_subtitle_before": "Espere la ",
+    "sent_subtitle_highlight": "confirmación del profesional",
+    "sent_subtitle_after": " para realizar el pago y concluir su reserva.",
     "same_provider_required": "Todos los horarios deben ser del mismo profesional.",
     "unknown": "Sin información",
     "multiple_providers": "{count} profesionales",

+ 26 - 9
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",
@@ -170,7 +174,7 @@
     "back_to_register": "Voltar para o cadastro",
     "cep_not_found": "CEP não encontrado. Verifique e tente novamente.",
     "location_permission_denied": "Permissão de localização negada. Habilite nas configurações do dispositivo.",
-    "location_error": "Não foi possível obter sua localização. Tente novamente.",
+    "location_error": "Ative a sua localização",
     "confirm_location": "confirmar localização",
     "confirm_address": "confirmar endereço",
     "complement": "Complemento",
@@ -437,12 +441,14 @@
       "my_schedules": "Minhas diárias"
     },
     "today_schedules": {
+      "title": "Hoje",
       "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",
       "code_label": "Código",
       "in_progress": "em andamento",
-      "end_time_label": "Término às",
+      "end_time_label": "Termina em:",
       "rate_btn": "Avaliar",
       "help_btn": "ajuda",
       "cancelled": "Serviço cancelado"
@@ -495,7 +501,8 @@
       "btn_reschedule": "reagendar",
       "empty_upcoming": "Nenhum serviço agendado",
       "empty_completed": "Nenhum serviço concluído",
-      "waiting_proposals": "Aguardando propostas"
+      "waiting_proposals": "Aguardando propostas",
+      "proposals_received_count": "{count} proposta(s) recebida(s)"
     },
     "favorites": {
       "title": "Favoritos",
@@ -511,7 +518,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",
@@ -537,6 +545,7 @@
       "detail_service_fee_pix": "Taxa de serviço (Pix):",
       "detail_service_fee_credit_card": "Taxa de serviço (cartão):",
       "detail_total": "Total:",
+      "detail_price_range": "Faixa de preço:",
       "detail_package_total": "Total do pacote (Pix):",
       "detail_pix_total": "Total no Pix:",
       "detail_credit_card_total": "Total no cartão de crédito:",
@@ -573,13 +582,13 @@
     "placeholder_email": "Digite seu e-mail",
     "phone": "Telefone",
     "placeholder_phone": "(11) 99999-9999",
-    "document": "Documento",
-    "placeholder_document": "Digite seu documento",
     "update": "Atualizar",
     "language": "Idioma",
     "lang_pt": "PT-br",
     "lang_en": "EN-us",
     "lang_es": "ES-es",
+    "notifications": "Notificações",
+    "push_notifications": "Receber notificações",
     "payments": {
       "title": "Pagamentos",
       "description": "Meus cartões",
@@ -667,7 +676,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",
@@ -768,6 +778,7 @@
     "reviews_title": "Avaliações",
     "see_all": "ver todas",
     "no_reviews": "Nenhuma avaliação encontrada.",
+    "provider_unavailable": "Este prestador não está disponível para agendamento no momento.",
     "unknown_client": "Cliente",
     "select_service": "Selecionar serviço",
     "no_slots_available": "Não há horários disponíveis para este dia.",
@@ -785,7 +796,7 @@
       "slot_required": "Selecione um horário para continuar."
     },
     "price_up_to": "até {value}",
-    "cart": "carrinho",
+    "cart": "Pedidos",
     "service_types": {
       "integral": {
         "label": "Integral",
@@ -835,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.",
     "local_cart_title": "Pedidos",
     "orders_title": "Pedidos",
+    "pending_custom_title": "Solicitações em andamento",
     "no_orders": "Nenhum pedido criado.",
     "order_title": "Pedido #{id}",
     "view_favorites": "ver favoritos",
@@ -865,6 +877,11 @@
     "submit": "enviar solicitação",
     "close": "confirmar pedido(s)",
     "closed_success": "Pedidos fechados. Aguarde a aprovação dos profissionais.",
+    "sent_title_line1": "SEU PEDIDO FOI ENVIADO",
+    "sent_title_line2": "PARA O DIARISTA!",
+    "sent_subtitle_before": "Aguarde a ",
+    "sent_subtitle_highlight": "confirmação do profissional",
+    "sent_subtitle_after": " para efetuar o pagamento e concluir o seu agendamento.",
     "same_provider_required": "Todos os horários devem ser do mesmo profissional.",
     "unknown": "Sem informação",
     "multiple_providers": "{count} profissionais",

+ 2 - 2
src/layouts/MainLayout.vue

@@ -5,7 +5,7 @@
   >
     <q-header
       v-if="$q.screen.lt.sm"
-      class="system-bar-spacer"
+      class="bg-surface"
       :style="{ height: `env(safe-area-inset-top)` }"
     >
     </q-header>
@@ -69,7 +69,7 @@
         </router-link>
       </nav>
 
-      <div class="system-bar-spacer provider-bottom-nav-safe-area"></div>
+      <div class="provider-bottom-nav-safe-area"></div>
     </q-footer>
   </q-layout>
 </template>

+ 9 - 3
src/pages/LoginPage.vue

@@ -607,13 +607,19 @@ onMounted(() => {
 }
 
 .register-later-row {
+  position: absolute;
+  top: 12px;
+  right: 16px;
+  z-index: 10;
+
   display: flex;
   justify-content: flex-end;
-  margin-top: -14px;
-  padding-bottom: 18px;
 }
 
 .register-later-btn {
-  justify-content: flex-end;
+  color: #8a8a8a !important;
+  font-size: 12px;
+  padding: 0;
+  min-height: 24px;
 }
 </style>

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

@@ -305,6 +305,7 @@
 <script setup>
 import { avatarColors } from "src/helpers/avatarColors";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
+import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 import { getClientCalendar } from "src/api/clientCalendar";
 
 import {
@@ -331,29 +332,6 @@ const loading = ref(true);
 const upcomingSchedules = 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 key = `period_types.${periodType}`;
   const translated = t(key);

+ 61 - 17
src/pages/dashboard/DashboardPage.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="dashboard-page bg-page">
+  <q-page class="dashboard-page bg-page">
     <template v-if="loading">
       <div class="row items-center justify-center full-width bg-surface" style="height: 80vh">
         <q-spinner-dots color="primary" />
@@ -13,7 +13,10 @@
         />
         <DashboardRegistrationIncomplete v-if="!registrationComplete" />
         <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
           v-if="pendingServicePackages.length > 0"
           :data="pendingServicePackages"
@@ -29,15 +32,13 @@
         />
         <DashboardTodaySchedules v-if="todaySchedules.length > 0" :data="todaySchedules" @rate="openRatingDialog" />
         <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" />
         <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>
-  </div>
+  </q-page>
 </template>
 
 <script setup>
@@ -53,19 +54,24 @@ 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';
 import DashboardClientProposals from 'src/pages/dashboard/components/DashboardClientProposals.vue';
-import { useRouter } from 'vue-router'
+import { useRoute, useRouter } from 'vue-router'
 import { onMounted, ref, computed } from 'vue';
+import { useI18n } from 'vue-i18n';
 import { LocalStorage, useQuasar } from 'quasar';
 import { dadosDashboard } from 'src/api/dashboard';
+import { updateMe } from 'src/api/user';
 import { userStore } from 'src/stores/user';
 import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
 import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
 
 const router = useRouter()
+const route = useRoute()
+const { t } = useI18n();
 const $q = useQuasar();
 const store = userStore();
 
@@ -83,8 +89,25 @@ const providersClose = ref([]);
 const todaySchedules = ref([]);
 const notifications = ref([]);
 const loading = ref(true);
-const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSuccessModal'));
 const registrationComplete = computed(() => store.user?.registration_complete ?? true);
+const showPaymentBanner = ref(false);
+const hasLocation = ref(true);
+
+const successModalKey = ref(
+  route.query.success ??
+    (router.currentRoute.value.fullPath.includes('showSuccessModal') ? 'custom_schedule' : null),
+);
+
+const successModalContents = {
+  custom_schedule: null,
+  order_sent: () => ({
+    titleLine1: t('service_package.sent_title_line1'),
+    titleLine2: t('service_package.sent_title_line2'),
+    subtitleBefore: t('service_package.sent_subtitle_before'),
+    subtitleHighlight: t('service_package.sent_subtitle_highlight'),
+    subtitleAfter: t('service_package.sent_subtitle_after'),
+  }),
+};
 
 const pendingRequests = computed(() => {
   const seenPackages = new Set();
@@ -166,21 +189,33 @@ 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;
   }
-  if( showSuccessModal.value == true) {
-    $q.dialog({
-       component: FinalSuccesModal
-    }).onDismiss(() => {
-      showSuccessModal.value = false;
-      router.replace({ path: router.currentRoute.value.path, query: {} });
-    })
-  }
-
   loading.value = false;
 
   maybeAutoOpenServicePackagePayment();
 };
 
+const maybeOpenSuccessModal = () => {
+  if (!successModalKey.value) return;
+
+  if (!(successModalKey.value in successModalContents)) {
+    successModalKey.value = null;
+    return;
+  }
+
+  const buildContent = successModalContents[successModalKey.value];
+
+  successModalKey.value = null;
+
+  $q.dialog({
+    component: FinalSuccesModal,
+    componentProps: { content: buildContent ? buildContent() : null },
+  }).onDismiss(() => {
+    router.replace({ path: route.path, query: {} });
+  });
+};
+
 const onRefresh = async (done) => {
   try {
     await reloadDashboard(false);
@@ -209,6 +244,15 @@ const openRatingDialog = (schedule) => {
 
 onMounted(async () => {
   await reloadDashboard();
+  maybeOpenSuccessModal();
+
+  const isFirstAccess = store.user?.client?.first_access ?? false;
+  showPaymentBanner.value = isFirstAccess && !hasPaymentMethods.value;
+
+  if (isFirstAccess) {
+    updateMe({ first_access: false }).catch(() => {});
+    store.markFirstAccessSeen();
+  }
 });
 </script>
 

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

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

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

@@ -1,5 +1,10 @@
 <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">
       <!-- informações -->
       <div class="request-info">
@@ -34,6 +39,9 @@
 </template>
 
 <script setup>
+import { useRouter } from "vue-router";
+
+const router = useRouter();
 </script>
 
 <style scoped lang="scss">

+ 7 - 4
src/pages/location/AddressCompletionPage.vue

@@ -33,7 +33,7 @@
         />
 
         <div class="text-text">
-          <span class="font14 fontbold">{{ $t("common.terms.document") }}</span>
+          <span class="font14 fontbold">{{ $t("common.terms.cpf") }}</span>
         </div>
 
         <q-input
@@ -42,11 +42,14 @@
           hide-bottom-space
           input-class="text-text"
           lazy-rules
+          mask="###.###.###-##"
           no-error-icon
           outlined
+          placeholder="000.000.000-00"
           rounded
-          :placeholder="$t('common.terms.document')"
-          :rules="[inputRules.cpfOrCnpj]"
+          type="tel"
+          unmasked-value
+          :rules="[inputRules.required, inputRules.cpf]"
         />
 
         <div class="text-text">
@@ -269,7 +272,7 @@ const submitting = ref(false);
 
 const form = ref({
   name: flowStore.name || "",
-  document: flowStore.document || "",
+  document: (flowStore.document || "").replace(/\D/g, ""),
   number: "",
   no_complement: false,
   complement: "",

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

@@ -20,74 +20,7 @@
       <div style="width: 36px"></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="service-package-discount-banner row items-center no-wrap q-pa-sm">
           <span class="font12 fontbold text-positive q-ml-sm">
@@ -300,13 +233,106 @@
         </q-card>
       </div>
     </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>
 </template>
 
 <script setup>
 import { computed, onMounted, ref } from "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 { dadosPedidosCliente } from "src/api/dashboard";
 import { getAddresses } from "src/api/address";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
 import { useI18n } from "vue-i18n";
@@ -327,6 +353,42 @@ const store = userStore();
 const addressLoading = ref(false);
 const primaryAddress = ref(null);
 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) =>
   fee !== null && fee !== undefined && Number.isFinite(Number(fee));
@@ -592,6 +654,8 @@ const submitServicePackage = async () => {
       type: "positive",
       message: t("service_package.closed_success"),
     });
+
+    router.push({ name: "DashboardPage", query: { success: "order_sent" } });
   } catch (error) {
     const message =
       error?.response?.data?.message ??
@@ -604,9 +668,19 @@ const submitServicePackage = async () => {
   }
 };
 
+const loadCustomRequests = async () => {
+  const response = await dadosPedidosCliente();
+
+  if (response) {
+    customSchedulesNoProposals.value = response.customSchedulesNoProposals ?? [];
+    schedulesProposals.value = response.schedulesProposals ?? [];
+  }
+};
+
 onMounted(() => {
   loadPrimaryAddress();
   loadPlatformFees().catch(() => {});
+  loadCustomRequests();
 });
 </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>

+ 65 - 11
src/pages/profile/ProfileEditDialog.vue

@@ -146,17 +146,24 @@
 
               <div>
                 <div class="text-grey-8 q-mb-sm font12 fontbold">
-                  {{ $t("profile.document") }}
+                  {{ $t("common.terms.cpf") }}
                 </div>
 
                 <q-input
                   v-model="form.document"
                   dense
+                  hide-bottom-space
                   input-class="text-text"
+                  lazy-rules
+                  mask="###.###.###-##"
+                  no-error-icon
                   outlined
+                  placeholder="000.000.000-00"
+                  type="tel"
+                  unmasked-value
                   :error="!!serverErrors.document"
                   :error-message="serverErrors.document"
-                  :placeholder="$t('profile.placeholder_document')"
+                  :rules="[inputRules.cpf]"
                   @update:model-value="serverErrors.document = null"
                 />
               </div>
@@ -198,6 +205,23 @@
                   />
                 </div>
               </div>
+
+              <div>
+                <div class="text-grey-8 q-mb-sm font12 fontbold">
+                  {{ $t("profile.notifications") }}
+                </div>
+
+                <div class="notification-card">
+                  <span class="font14 text-text">
+                    {{ $t("profile.push_notifications") }}
+                  </span>
+
+                  <q-toggle
+                    v-model="form.push_notifications_enabled"
+                    color="primary"
+                  />
+                </div>
+              </div>
             </div>
 
             <q-space />
@@ -241,8 +265,14 @@ const props = defineProps({
 
 const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
 
-const { form, hasUpdatedFields, setUpdateFormAsOriginal } =
-  useFormUpdateTracker({ name: "", email: "", phone: "", document: "" });
+const { form, getUpdatedFields, hasUpdatedFields, setUpdateFormAsOriginal } =
+  useFormUpdateTracker({
+    name: "",
+    email: "",
+    phone: "",
+    document: "",
+    push_notifications_enabled: false,
+  });
 
 const {
   loading: submitting,
@@ -307,12 +337,11 @@ const onSubmit = async () => {
 
   if (!valid) return;
 
-  const payload = {
-    name: form.name,
-    email: form.email,
-    phone: form.phone,
-    document: form.document || null,
-  };
+  const payload = { ...getUpdatedFields.value };
+
+  if ("document" in payload) {
+    payload.document = payload.document || null;
+  }
 
   if (avatarFile.value) {
     payload.avatar_base64 = await fileToBase64(avatarFile.value);
@@ -328,7 +357,8 @@ onMounted(async () => {
     form.name = data.name || "";
     form.email = data.email || "";
     form.phone = data.phone || "";
-    form.document = data.client_document || "";
+    form.document = (data.client_document || "").replace(/\D/g, "");
+    form.push_notifications_enabled = data.push_notifications_enabled ?? false;
 
     avatarPreview.value = getProfileMediaUrl(data);
 
@@ -352,4 +382,28 @@ onMounted(async () => {
 .hidden {
   display: none;
 }
+
+.notification-card {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+
+  min-height: 56px;
+  padding: 0 16px;
+
+  border: 1px solid #e0e0e0;
+  border-radius: 8px;
+
+  background: #fff;
+}
+
+.notification-card :deep(.q-toggle__inner--falsy) {
+  color: rgba(0, 0, 0, 0.62);
+
+  .q-toggle__thumb:after {
+    box-shadow:
+      0 0 0 1px #bdbdbd,
+      0 2px 2px 0 rgba(0, 0, 0, 0.14);
+  }
+}
 </style>

+ 6 - 0
src/pages/profile/ProfilePage.vue

@@ -263,6 +263,10 @@
           size="md"
         />
       </div>
+
+      <div class="text-center text-grey-6 font12 fontregular q-mt-lg q-mb-sm">
+        v{{ appVersion }}
+      </div>
     </div>
   </q-page>
 </template>
@@ -284,6 +288,8 @@ import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 import ProfilePaymentsDialog from "src/components/profile/ProfilePaymentsDialog.vue";
 import ProfilePrivacyDialog from "src/components/profile/ProfilePrivacyDialog.vue";
 
+const appVersion = process.env.APP_VERSION;
+
 const router = useRouter();
 const $q = useQuasar();
 const { logout } = useAuth();

+ 15 - 34
src/pages/schedules/SobMedidaPage.vue

@@ -97,24 +97,6 @@
             />
           </div>
         </div>
-
-        <div class="field-label text-center gradient-diarista">
-          {{ $t('sob_medida.description_label') }}
-          <span class="optional">
-            {{ $t('sob_medida.optional') }}
-          </span>
-        </div>
-
-        <q-input
-          v-model="description"
-          type="textarea"
-          outlined
-          bg-color="white"
-          color="dark"
-          input-class="text-black"
-          class="description-box"
-          :placeholder="$t('sob_medida.description_placeholder')"
-        />
       </q-card>
 
       <div class="section-title gradient-diarista font16 fontbold">
@@ -125,6 +107,7 @@
         <div class="range-container">
           <div
             class="price-pin"
+            :class="{ 'price-pin--min': minPosition === 0 }"
             :style="{ left: minPosition + '%' }"
           >
             <span>{{ priceRange.min }}</span>
@@ -132,6 +115,7 @@
 
           <div
             class="price-pin"
+            :class="{ 'price-pin--max': maxPosition === 100 }"
             :style="{ left: maxPosition + '%' }"
           >
             <span>{{ priceRange.max }}</span>
@@ -216,7 +200,6 @@ const address = ref(null)
 const selectedServiceType = ref(null)
 const selectedSpecialties = ref([])
 const selectedOption = ref('home')
-const description = ref('')
 const selectedDate = ref(null)
 const quantity = ref(1)
 
@@ -307,7 +290,6 @@ const saveFinalOrder = async (payloadFinal) => {
     end_time: `${endHour}:00`,
     address_type: selectedOption.value,
     service_type_id: selectedServiceType.value,
-    description: description.value,
     min_price: priceRange.value.min,
     max_price: priceRange.value.max,
     offers_meal: payloadFinal.meal === 'offer',
@@ -316,12 +298,12 @@ const saveFinalOrder = async (payloadFinal) => {
 
   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('/#showSuccessModal')
+  router.push({ name: 'DashboardPage', query: { success: 'custom_schedule' } })
 }
 
 const increaseQuantity = () => {
@@ -442,15 +424,6 @@ onMounted(async () => {
   justify-content: flex-start;
 }
 
-.description-box {
-  margin-top: 8px;
-}
-
-.description-box :deep(textarea) {
-  min-height: 90px;
-  resize: none;
-}
-
 .compact-card {
   padding: 28px 22px;
   background: #fafafa;
@@ -500,6 +473,14 @@ onMounted(async () => {
   z-index: 5;
 }
 
+.price-pin--min {
+  transform: translateX(0) rotate(-45deg);
+}
+
+.price-pin--max {
+  transform: translateX(-100%) rotate(-45deg);
+}
+
 .price-pin span {
   transform: rotate(45deg);
   color: #fff;

+ 22 - 5
src/pages/schedules/components/FinalSuccesModal.vue

@@ -44,17 +44,17 @@
 
         <!-- titulo -->
         <div class="success-title gradient-diarista q-mt-md font16 fontbold">
-          {{ $t('custom_schedule.success_title_line1') }}<br>
-          {{ $t('custom_schedule.success_title_line2') }}
+          {{ titleLine1 }}<br>
+          {{ titleLine2 }}
         </div>
 
         <!-- subtitulo -->
         <div class="success-subtitle q-mt-md">
-          {{ $t('custom_schedule.success_subtitle_before') }}
+          {{ subtitleBefore }}
 
-          <strong>{{ $t('custom_schedule.success_subtitle_highlight') }}</strong>
+          <strong>{{ subtitleHighlight }}</strong>
 
-          {{ $t('custom_schedule.success_subtitle_after') }}
+          {{ subtitleAfter }}
         </div>
       </q-card-section>
     </q-card>
@@ -63,14 +63,31 @@
 
 <script setup>
 import { useDialogPluginComponent } from 'quasar'
+import { computed } from 'vue'
+import { useI18n } from 'vue-i18n'
+
+const props = defineProps({
+  content: {
+    type: Object,
+    default: null
+  }
+})
 
 defineEmits([...useDialogPluginComponent.emits])
 
+const { t } = useI18n()
+
 const {
   dialogRef,
   onDialogHide
 } = useDialogPluginComponent()
 
+const titleLine1 = computed(() => props.content?.titleLine1 ?? t('custom_schedule.success_title_line1'))
+const titleLine2 = computed(() => props.content?.titleLine2 ?? t('custom_schedule.success_title_line2'))
+const subtitleBefore = computed(() => props.content?.subtitleBefore ?? t('custom_schedule.success_subtitle_before'))
+const subtitleHighlight = computed(() => props.content?.subtitleHighlight ?? t('custom_schedule.success_subtitle_highlight'))
+const subtitleAfter = computed(() => props.content?.subtitleAfter ?? t('custom_schedule.success_subtitle_after'))
+
 const handleClose = () => {
   dialogRef.value?.hide()
 }

+ 38 - 26
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"
@@ -146,6 +153,7 @@
           :key="p.provider_id"
           :flat="false"
           class="card-border bg-page text-text q-mb-sm"
+          @click="openProviderInfo(p)"
         >
           <q-card-section class="row no-wrap q-pa-sm">
             <div class="row no-wrap full-width">
@@ -178,13 +186,6 @@
                     }}
                   </span>
 
-                  <span
-                    v-if="getProviderGenderLabel(p, t)"
-                    class="font9 fontmedium text-grey-7"
-                  >
-                    {{ getProviderGenderLabel(p, t) }}
-                  </span>
-
                   <span
                     class="text-provider-close-region font9 fontbold text-grey-7"
                    >
@@ -253,7 +254,7 @@
                       size="sm"
                       unelevated
                       :label="$t('search_page.schedule_btn')"
-                      @click="goToScheduling(p)"
+                      @click.stop="goToScheduling(p)"
                     />
                   </div>
                 </div>
@@ -271,13 +272,14 @@ import { avatarColors } from "src/helpers/avatarColors";
 import { buscaPrestadores } from "src/api/dashboard";
 import { computed, onMounted, ref } from "vue";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
-import { getProviderGenderLabel } from "src/helpers/provider";
 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";
+import ProfileInfoDialog from "src/components/profile/ProfileInfoDialog.vue";
 
 const { t } = useI18n();
 const router = useRouter();
@@ -287,6 +289,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 +325,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 +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) => {
   $q.dialog({
     component: SchedulingDialog,
@@ -379,17 +379,29 @@ const goToScheduling = (provider) => {
   });
 };
 
+const openProviderInfo = (provider) => {
+  $q.dialog({
+    component: ProfileInfoDialog,
+    componentProps: {
+      provider,
+    },
+  });
+};
+
 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;
   }

+ 78 - 24
src/pages/search/components/SchedulingDialog.vue

@@ -43,18 +43,20 @@
                 <div class="col-2">
                   <q-avatar
                     :style="
-                      avatarColors[provider.provider_id % avatarColors.length]
+                      avatarColors[
+                        providerInfo.provider_id % avatarColors.length
+                      ]
                     "
                   >
                     <img
-                      v-if="getProfileMediaUrl(provider)"
+                      v-if="getProfileMediaUrl(providerInfo)"
                       style="object-fit: cover; border-radius: 50%"
-                      :src="getProfileMediaUrl(provider)"
+                      :src="getProfileMediaUrl(providerInfo)"
                     />
 
                     <span v-else>
                       {{
-                        getFirstName(provider.provider_name)
+                        getFirstName(providerInfo.provider_name)
                           ?.slice(0, 2)
                           .toUpperCase() ?? "??"
                       }}
@@ -64,11 +66,11 @@
 
                 <div class="col-3 column">
                   <div class="font12 fontbold text-text">
-                    {{ getFirstName(provider?.provider_name) ?? "—" }}
+                    {{ getFirstName(providerInfo?.provider_name) ?? "—" }}
                   </div>
 
                   <div class="font10 fontmedium text-grey-7">
-                    {{ provider?.district ?? "—" }}
+                    {{ providerDistrict ?? "—" }}
                   </div>
                 </div>
 
@@ -83,11 +85,11 @@
 
                       <span class="font9 fontmedium q-ml-xs">
                         {{
-                          (provider?.average_rating != null
-                            ? Number(provider.average_rating).toFixed(1)
+                          (providerInfo?.average_rating != null
+                            ? Number(providerInfo.average_rating).toFixed(1)
                             : "") +
                           " (" +
-                          (provider?.total_reviews ?? 0) +
+                          (providerInfo?.total_reviews ?? 0) +
                           ")"
                         }}
                       </span>
@@ -101,7 +103,7 @@
                       />
 
                       <span class="font9 fontmedium q-ml-xs">
-                        {{ provider?.total_services ?? 0 }}
+                        {{ providerInfo?.total_services ?? 0 }}
                       </span>
                     </div>
                   </div>
@@ -140,7 +142,7 @@
           </div>
 
           <div
-            v-if="loadingAvailability"
+            v-if="loadingAvailability || loadingProvider"
             class="row items-center justify-center q-py-lg"
           >
             <q-spinner-dots
@@ -271,6 +273,7 @@ import {
 import { date, useDialogPluginComponent, useQuasar } from "quasar";
 import { getClientProviderBlocks } from "src/api/schedule";
 import { getFirstName } from "src/helpers/utils";
+import { getPerfilPrestador } from "src/api/dashboard";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
 
 import {
@@ -303,12 +306,15 @@ const { dialogRef } = useDialogPluginComponent();
 const servicePackage = useServicePackageStore();
 const store = userStore();
 
+const providerInfo = ref({ ...props.provider });
+
 const blockedDays = ref([]);
 const bookings = ref([]);
 const favoriteId = ref(null);
 const favoriteLoading = ref(false);
 const isFavorite = ref(false);
 const loadingAvailability = ref(true);
+const loadingProvider = ref(true);
 const loadingReviews = ref(true);
 const providerClientBlocks = ref({
   existing_schedules: [],
@@ -322,6 +328,10 @@ const availableWeekDays = computed(() => [
   ...new Set(workingDays.value.map((wd) => wd.day)),
 ]);
 
+const providerDistrict = computed(
+  () => providerInfo.value?.district ?? providerInfo.value?.provider_district,
+);
+
 const blockedDateSet = computed(
   () =>
     new Set(
@@ -350,14 +360,14 @@ const getWeekStart = (dateStr) => {
 const cartHasAnotherProvider = () =>
   servicePackage.items.length > 0 &&
   Number(servicePackage.items[0].provider_id) !==
-    Number(props.provider.provider_id);
+    Number(providerInfo.value.provider_id);
 
 const getServicePackageWeekCount = (selectedDate) => {
   const weekStart = getWeekStart(selectedDate);
 
   return servicePackage.items.filter(
     (item) =>
-      Number(item.provider_id) === Number(props.provider.provider_id) &&
+      Number(item.provider_id) === Number(providerInfo.value.provider_id) &&
       getWeekStart(item.date) === weekStart,
   ).length;
 };
@@ -420,10 +430,10 @@ const loadAvailability = async () => {
     };
 
     const [wd, bd, clientBlocks] = await Promise.all([
-      getProviderWorkingDays(props.provider.provider_id),
-      getProviderBlockedDays(props.provider.provider_id),
+      getProviderWorkingDays(providerInfo.value.provider_id),
+      getProviderBlockedDays(providerInfo.value.provider_id),
       clientId
-        ? getClientProviderBlocks(clientId, props.provider.provider_id)
+        ? getClientProviderBlocks(clientId, providerInfo.value.provider_id)
         : Promise.resolve(defaultClientBlocks),
     ]);
 
@@ -446,6 +456,44 @@ const loadAvailability = async () => {
   }
 };
 
+const loadProviderProfile = async () => {
+  const providerId = props.provider?.provider_id ?? props.provider?.id;
+
+  if (!providerId) {
+    loadingProvider.value = false;
+
+    return;
+  }
+
+  loadingProvider.value = true;
+
+  try {
+    const payload = await getPerfilPrestador(providerId);
+
+    providerInfo.value = { ...props.provider, ...payload };
+  } catch {
+    providerInfo.value = { ...props.provider };
+  } finally {
+    loadingProvider.value = false;
+  }
+
+  const hasAnyPrice = [
+    "daily_price_8h",
+    "daily_price_6h",
+    "daily_price_4h",
+    "daily_price_2h",
+  ].some((key) => providerInfo.value?.[key] != null);
+
+  if (!hasAnyPrice) {
+    $q.notify({
+      message: t("scheduling_page.provider_unavailable"),
+      type: "warning",
+    });
+
+    dialogRef.value?.hide();
+  }
+};
+
 const loadFavoriteState = async () => {
   const clientId = store.user?.client?.id;
   if (!clientId) return;
@@ -453,7 +501,7 @@ const loadFavoriteState = async () => {
   try {
     const favorites = await getClientFavoriteProviders(clientId);
     const match = (favorites ?? []).find(
-      (f) => Number(f.provider_id) === Number(props.provider.provider_id),
+      (f) => Number(f.provider_id) === Number(providerInfo.value.provider_id),
     );
 
     isFavorite.value = !!match;
@@ -468,7 +516,7 @@ const loadReviews = async () => {
   loadingReviews.value = true;
 
   try {
-    const all = await getProviderReceivedReviews(props.provider.provider_id);
+    const all = await getProviderReceivedReviews(providerInfo.value.provider_id);
 
     reviews.value = (all ?? []).slice(0, 10);
   } catch {
@@ -515,7 +563,7 @@ const onDateSelected = (val) => {
   const servicePackageBookingBlocks = servicePackage.items
     .filter(
       (item) =>
-        Number(item.provider_id) === Number(props.provider.provider_id) &&
+        Number(item.provider_id) === Number(providerInfo.value.provider_id) &&
         normalizeDate(item.date) === valFormatted,
     )
     .map((item) => ({
@@ -544,7 +592,7 @@ const onDateSelected = (val) => {
     component: ServiceSelectionSheet,
     componentProps: {
       partialBlocks,
-      provider: props.provider,
+      provider: providerInfo.value,
       selectedDate: val,
       scheduleType: 'default'
     },
@@ -563,6 +611,11 @@ const onDateSelected = (val) => {
           $q.notify({
             message: t("service_package.other_provider_blocked"),
             type: "warning",
+            timeout: 10000,
+            progress: true,
+            actions: [
+              { icon: "close", color: "white", round: true, dense: true },
+            ],
           });
 
           return;
@@ -588,7 +641,7 @@ const onDateSelected = (val) => {
         });
 
         if (!servicePackage.provider) {
-          servicePackage.setProvider({ ...props.provider });
+          servicePackage.setProvider({ ...providerInfo.value });
         }
 
         $q.notify({
@@ -607,7 +660,7 @@ const onDateSelected = (val) => {
 
       $q.dialog({
         component: ServicePackageSummaryDialog,
-        componentProps: { initialBooking: booking, provider: props.provider },
+        componentProps: { initialBooking: booking, provider: providerInfo.value },
       }).onOk(() => {
         dialogRef.value.hide();
       });
@@ -619,7 +672,7 @@ const openProviderInfo = () => {
   $q.dialog({
     component: ProfileInfoDialog,
     componentProps: {
-      provider: props.provider,
+      provider: providerInfo.value,
     },
   });
 };
@@ -655,7 +708,7 @@ const toggleFavorite = async () => {
     } else {
       const created = await createClientFavoriteProvider({
         client_id: clientId,
-        provider_id: props.provider.provider_id,
+        provider_id: providerInfo.value.provider_id,
       });
 
       favoriteId.value = created?.id ?? null;
@@ -677,6 +730,7 @@ const toggleFavorite = async () => {
 };
 
 onMounted(() => {
+  loadProviderProfile();
   loadAvailability();
   loadFavoriteState();
   loadReviews();

+ 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") },
     ],
   },
   {

+ 5 - 0
src/stores/user.js

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