فهرست منبع

Merge branch 'development'

Gustavo Zanatta 23 ساعت پیش
والد
کامیت
843222fa3c
37فایلهای تغییر یافته به همراه1057 افزوده شده و 310 حذف شده
  1. 2 2
      package.json
  2. 1 1
      quasar.config.js
  3. 2 2
      src-capacitor/android/app/build.gradle
  4. 2 0
      src-capacitor/android/app/src/main/AndroidManifest.xml
  5. 1 1
      src-capacitor/android/variables.gradle
  6. 1 1
      src-capacitor/capacitor.config.json
  7. 1 1
      src-capacitor/package.json
  8. 45 9
      src/App.vue
  9. 5 0
      src/api/dashboard.js
  10. 0 11
      src/components/dashboard/DashboardFavoriteProviders.vue
  11. 213 27
      src/components/dashboard/DashboardPendingSchedules.vue
  12. 5 5
      src/components/dashboard/DashboardProvidersClose.vue
  13. 33 22
      src/components/dashboard/DashboardSummaryInfos.vue
  14. 57 14
      src/components/dashboard/DashboardTodaySchedules.vue
  15. 68 25
      src/components/dashboard/ScheduleAcceptedDialog.vue
  16. 29 1
      src/components/dashboard/ScheduleCancelDialog.vue
  17. 17 5
      src/components/dashboard/SchedulePaymentDialog.vue
  18. 14 0
      src/components/login/LoginStep4Panel.vue
  19. 2 60
      src/components/profile/ProfileAddressFormDialog.vue
  20. 4 1
      src/components/profile/ProfileHelpDialog.vue
  21. 118 54
      src/components/profile/ProfileInfoDialog.vue
  22. 0 20
      src/composables/useGeocodingApi.js
  23. 0 1
      src/helpers/utils.js
  24. 25 4
      src/i18n/locales/en.json
  25. 25 4
      src/i18n/locales/es.json
  26. 27 6
      src/i18n/locales/pt.json
  27. 6 1
      src/pages/LoginPage.vue
  28. 6 1
      src/pages/agenda/CalendarPage.vue
  29. 65 2
      src/pages/dashboard/DashboardPage.vue
  30. 26 6
      src/pages/dashboard/components/DashboardClientProposals.vue
  31. 19 1
      src/pages/location/AddressCompletionPage.vue
  32. 59 11
      src/pages/location/LocationMapPage.vue
  33. 30 0
      src/pages/orders/ServicePackagePage.vue
  34. 70 6
      src/pages/schedules/SobMedidaPage.vue
  35. 19 0
      src/pages/search/components/SchedulingDialog.vue
  36. 9 3
      src/pages/search/components/ServiceSelectionSheet.vue
  37. 51 2
      src/stores/servicePackage.js

+ 2 - 2
package.json

@@ -1,8 +1,8 @@
 {
 {
   "name": "diarista-cliente-app",
   "name": "diarista-cliente-app",
   "version": "0.0.1",
   "version": "0.0.1",
-  "description": "Aplicativo para o cliente do Diarista",
-  "productName": "Diarista App",
+  "description": "Aplicativo para o cliente do Diaria",
+  "productName": "Diaria App",
   "author": "zntt <zanattagg@gmail.com>",
   "author": "zntt <zanattagg@gmail.com>",
   "type": "module",
   "type": "module",
   "private": true,
   "private": true,

+ 1 - 1
quasar.config.js

@@ -181,7 +181,7 @@ export default defineConfig((ctx) => {
       // directives: [],
       // directives: [],
 
 
       // Quasar plugins
       // Quasar plugins
-      plugins: ["Dialog", "Notify", "Loading", "Cookies", "Dark"],
+      plugins: ["Dialog", "Notify", "Loading", "Cookies", "Dark", "LocalStorage"],
     },
     },
 
 
     // animations: 'all', // --- includes all animations
     // animations: 'all', // --- includes all animations

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

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

+ 2 - 0
src-capacitor/android/app/src/main/AndroidManifest.xml

@@ -46,6 +46,8 @@
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
 
 
     <uses-feature android:name="android.hardware.camera" android:required="false" />
     <uses-feature android:name="android.hardware.camera" android:required="false" />
 </manifest>
 </manifest>

+ 1 - 1
src-capacitor/android/variables.gradle

@@ -1,7 +1,7 @@
 ext {
 ext {
     minSdkVersion = 23
     minSdkVersion = 23
     compileSdkVersion = 36
     compileSdkVersion = 36
-    targetSdkVersion = 35
+    targetSdkVersion = 36
     androidxActivityVersion = '1.9.2'
     androidxActivityVersion = '1.9.2'
     androidxAppCompatVersion = '1.7.0'
     androidxAppCompatVersion = '1.7.0'
     androidxCoordinatorLayoutVersion = '1.2.0'
     androidxCoordinatorLayoutVersion = '1.2.0'

+ 1 - 1
src-capacitor/capacitor.config.json

@@ -1,6 +1,6 @@
 {
 {
   "appId": "br.com.diarista.client",
   "appId": "br.com.diarista.client",
-  "appName": "Diarista",
+  "appName": "Diaria",
   "webDir": "www",
   "webDir": "www",
   "plugins": {
   "plugins": {
     "Keyboard": {
     "Keyboard": {

+ 1 - 1
src-capacitor/package.json

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

+ 45 - 9
src/App.vue

@@ -17,33 +17,69 @@ defineOptions({
 const { locale } = useI18n();
 const { locale } = useI18n();
 
 
 const $q = useQuasar();
 const $q = useQuasar();
+
 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
 const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
   ? "dark"
   ? "dark"
   : "light";
   : "light";
 
 
+const updateStatusBar = async (isDark) => {
+  if (!Capacitor.isNativePlatform()) {
+    return;
+  }
+
+  await StatusBar.show();
+
+  await StatusBar.setBackgroundColor({
+    color: isDark ? "#121212" : "#FFFFFF",
+  });
+
+  await StatusBar.setStyle({
+    style: isDark ? Style.Light : Style.Dark,
+  });
+};
+
 watch(
 watch(
   () => $q.dark.isActive,
   () => $q.dark.isActive,
   async (value) => {
   async (value) => {
-    await Preferences.set({ key: "theme", value: value });
+    const theme = value ? "dark" : "light";
+
+    await Preferences.set({
+      key: "theme",
+      value: theme,
+    });
+
+    await updateStatusBar(value);
   },
   },
 );
 );
 
 
 watch(
 watch(
   () => locale.value,
   () => locale.value,
   async (value) => {
   async (value) => {
-    await Preferences.set({ key: "locale", value: value });
+    await Preferences.set({
+      key: "locale",
+      value,
+    });
   },
   },
 );
 );
 
 
 onBeforeMount(async () => {
 onBeforeMount(async () => {
-  const isNative = Capacitor.isNativePlatform();
-  if (isNative) await StatusBar.show();
-  const { value: themePref } = await Preferences.get({ key: "theme" });
+  const { value: themePref } = await Preferences.get({
+    key: "theme",
+  });
+
   const theme = themePref || systemTheme;
   const theme = themePref || systemTheme;
-  if (isNative) await StatusBar.setStyle({ style: Style.Dark });
-  const { value: localePref } = await Preferences.get({ key: "locale" });
+  const isDark = theme === "dark";
+
+  $q.dark.set(isDark);
+
+  await updateStatusBar(isDark);
+
+  const { value: localePref } = await Preferences.get({
+    key: "locale",
+  });
+
   const userLocale = localePref || window.navigator.language;
   const userLocale = localePref || window.navigator.language;
+
   console.log(theme, userLocale);
   console.log(theme, userLocale);
-  $q.dark.set(theme == "dark");
 });
 });
-</script>
+</script>

+ 5 - 0
src/api/dashboard.js

@@ -16,4 +16,9 @@ export const buscaPrestadores = async ({ name = '', date = '' } = {}) => {
   if (date) params.date = date;
   if (date) params.date = date;
   const { data } = await api.get('/prestadores-busca', { params });
   const { data } = await api.get('/prestadores-busca', { params });
   return data.payload;
   return data.payload;
+}
+
+export const getPerfilPrestador = async (providerId) => {
+  const { data } = await api.get(`/prestador-perfil/${providerId}`);
+  return data.payload;
 }
 }

+ 0 - 11
src/components/dashboard/DashboardFavoriteProviders.vue

@@ -38,13 +38,6 @@
                 {{ getFirstName(item.provider_name) || "Prestador" }}
                 {{ getFirstName(item.provider_name) || "Prestador" }}
               </span>
               </span>
 
 
-              <span
-                v-if="getProviderGenderLabel(item, t)"
-                class="font9 fontmedium text-grey-7"
-              >
-                {{ getProviderGenderLabel(item, t) }}
-              </span>
-
               <span class="text-fav-region font9 fontbold text-grey-7">
               <span class="text-fav-region font9 fontbold text-grey-7">
                 {{
                 {{
                   item.provider_district ??
                   item.provider_district ??
@@ -89,16 +82,12 @@
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
 import { getFirstName } from "src/helpers/utils";
 import { getFirstName } from "src/helpers/utils";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
-import { getProviderGenderLabel } from "src/helpers/provider";
-import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 
 
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 
 
 defineProps({ data: { default: () => [], type: Array } });
 defineProps({ data: { default: () => [], type: Array } });
 
 
-const { t } = useI18n();
-
 const $q = useQuasar();
 const $q = useQuasar();
 
 
 const goToScheduling = (provider) => {
 const goToScheduling = (provider) => {

+ 213 - 27
src/components/dashboard/DashboardPendingSchedules.vue

@@ -1,7 +1,11 @@
 <template>
 <template>
   <div class="q-mx-md q-mb-md">
   <div class="q-mx-md q-mb-md">
     <div class="scroll-wrapper">
     <div class="scroll-wrapper">
-      <div class="scroll-track">
+      <div
+        ref="trackRef"
+        class="scroll-track"
+        @scroll="onTrackScroll"
+      >
         <q-card
         <q-card
           v-for="item in data"
           v-for="item in data"
           :key="item.id"
           :key="item.id"
@@ -9,7 +13,7 @@
           class="pending-card card-border shadow-card bg-surface"
           class="pending-card card-border shadow-card bg-surface"
           @click="seeDetails(item)"
           @click="seeDetails(item)"
         >
         >
-          <q-card-section class="q-pa-md">
+          <q-card-section class="card-body q-pa-md">
             <div class="row no-wrap items-start q-mb-sm">
             <div class="row no-wrap items-start q-mb-sm">
               <q-avatar
               <q-avatar
                 class="q-mr-sm flex-shrink-0"
                 class="q-mr-sm flex-shrink-0"
@@ -61,7 +65,7 @@
                     size="13px"
                     size="13px"
                   />
                   />
 
 
-                  <span class="font9 fontmedium text-grey-5">
+                  <span class="font9 fontmedium text-grey-5 meta-text ellipsis">
                     {{ displayTime(item) }}
                     {{ displayTime(item) }}
                   </span>
                   </span>
                 </div>
                 </div>
@@ -95,11 +99,29 @@
             <div class="progress-track q-mb-md">
             <div class="progress-track q-mb-md">
               <div
               <div
                 class="progress-fill"
                 class="progress-fill"
-                :style="{ width: progressPercent(item.status) + '%' }"
+                :class="progressClass"
               />
               />
             </div>
             </div>
 
 
-            <div class="row items-center no-wrap">
+            <div class="schedule-info">
+  <div class="schedule-info-item">
+    <q-icon
+      name="mdi-calendar-outline"
+      size="14px"
+    />
+    <span>{{ displayDate(item) }}</span>
+  </div>
+
+  <div class="schedule-info-item">
+    <q-icon
+      name="mdi-clock-outline"
+      size="14px"
+    />
+    <span>{{ displayScheduleTime(item) }}</span>
+  </div>
+</div>
+
+            <div class="card-footer row items-center no-wrap">
               <q-btn
               <q-btn
                 v-if="type !== 'servicePackage'"
                 v-if="type !== 'servicePackage'"
                 class="q-mr-sm flex-shrink-0"
                 class="q-mr-sm flex-shrink-0"
@@ -109,7 +131,7 @@
                 no-caps
                 no-caps
                 size="xs"
                 size="xs"
                 :label="$t('dashboard_client.pending_schedules.cancel_btn')"
                 :label="$t('dashboard_client.pending_schedules.cancel_btn')"
-                @click="$emit('cancel', item)"
+                @click.stop="$emit('cancel', item)"
               />
               />
 
 
               <q-space />
               <q-space />
@@ -133,21 +155,49 @@
         </q-card>
         </q-card>
       </div>
       </div>
     </div>
     </div>
+
+    <div
+      v-if="data.length > 1"
+      class="row justify-center q-gutter-x-xs q-mt-sm"
+    >
+      <span
+        v-for="(item, index) in data"
+        :key="`dot-${item.id}`"
+        class="carousel-dot"
+        :class="{ 'carousel-dot--active': index === activeIndex }"
+      />
+    </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { computed } from "vue";
+import { computed, ref } from "vue";
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
 import { getFirstName } from "src/helpers/utils";
 import { getFirstName } from "src/helpers/utils";
+import { useI18n } from "vue-i18n";
 
 
 const props = defineProps({
 const props = defineProps({
   data: { type: Array, default: () => [] },
   data: { type: Array, default: () => [] },
   type: { type: String, default: 'schedule' },
   type: { type: String, default: 'schedule' },
+
+  progressClass: { type: String, default: '' },
 });
 });
 
 
 const emit = defineEmits(["view-details", "cancel"]);
 const emit = defineEmits(["view-details", "cancel"]);
 
 
+const { t } = useI18n();
+
+const trackRef = ref(null);
+const activeIndex = ref(0);
+
+const onTrackScroll = () => {
+  const track = trackRef.value;
+
+  if (!track || !track.clientWidth) return;
+
+  activeIndex.value = Math.round(track.scrollLeft / track.clientWidth);
+};
+
 const isServicePackage = computed(() => props.type === 'servicePackage');
 const isServicePackage = computed(() => props.type === 'servicePackage');
 
 
 const providerPhoto = (item) => {
 const providerPhoto = (item) => {
@@ -180,24 +230,54 @@ const displayAddress = (item) => {
   ].filter(Boolean).join(', ') || '—';
   ].filter(Boolean).join(', ') || '—';
 };
 };
 
 
+const scheduleCountLabel = (count) =>
+  t("dashboard_client.pending_schedules.schedule_count", { count });
+
 const displayTime = (item) => {
 const displayTime = (item) => {
   if (isServicePackage.value) {
   if (isServicePackage.value) {
-    return item.schedules?.length
-      ? `${item.schedules.length} agendamentos`
-      : '—';
+    return item.schedules?.length ? scheduleCountLabel(item.schedules.length) : "—";
   }
   }
-  return item.time_since_request;
+
+  if (!item.time_since_request) return "—";
+
+  const timeAgo = t("dashboard_client.pending_schedules.time_ago", {
+    time: item.time_since_request,
+  });
+
+  const packageCount = Number(item.service_package_items_count ?? 0);
+
+  return packageCount > 1
+    ? `${timeAgo} · ${scheduleCountLabel(packageCount)}`
+    : timeAgo;
 };
 };
 
 
-const statusProgressMap = {
-  pending: 20,
-  accepted: 40,
-  paid: 60,
-  started: 80,
-  finished: 100,
+const displayDate = (item) => {
+  const schedule = isServicePackage.value
+    ? item.schedules?.[0]
+    : item;
+
+  if (!schedule?.date) return "—";
+
+  const date = new Date(schedule.date);
+
+  return date.toLocaleDateString("pt-BR", {
+    day: "2-digit",
+    month: "2-digit",
+    year: "numeric",
+  });
 };
 };
 
 
-const progressPercent = (status) => statusProgressMap[status] ?? 20;
+const displayScheduleTime = (item) => {
+  const schedule = isServicePackage.value
+    ? item.schedules?.[0]
+    : item;
+
+  if (!schedule?.start_time || !schedule?.end_time) {
+    return "—";
+  }
+
+  return `${schedule.start_time.slice(0, 5)} às ${schedule.end_time.slice(0, 5)}`;
+};
 
 
 const seeDetails = (item) => {
 const seeDetails = (item) => {
   if (isServicePackage.value || item.status === "accepted") {
   if (isServicePackage.value || item.status === "accepted") {
@@ -216,26 +296,55 @@ const seeDetails = (item) => {
   gap: 12px;
   gap: 12px;
   overflow-x: auto;
   overflow-x: auto;
   overscroll-behavior-x: contain;
   overscroll-behavior-x: contain;
-  scroll-snap-type: x proximity;
+  scroll-snap-type: x mandatory;
   padding-bottom: 8px;
   padding-bottom: 8px;
   &::-webkit-scrollbar {
   &::-webkit-scrollbar {
     display: none;
     display: none;
   }
   }
-  &::after {
-    content: "";
-    flex: 0 0 1px;
-  }
+}
+
+.carousel-dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: #cbd5e1;
+  transition: background 0.3s ease, width 0.3s ease;
+}
+
+.carousel-dot--active {
+  width: 16px;
+  border-radius: 999px;
+  background: #8b5cf6;
 }
 }
 .pending-card {
 .pending-card {
-  min-width: 80%;
-  scroll-snap-align: start;
+  flex: 0 0 100%;
+  width: 100%;
+  min-height: 146px;
+  display: flex;
+  scroll-snap-align: center;
+}
+
+.card-body {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  width: 100%;
 }
 }
 
 
 .clock-badge-col {
 .clock-badge-col {
-  min-width: 52px;
+  flex: 0 0 68px;
+  width: 68px;
   align-items: center;
   align-items: center;
 }
 }
 
 
+.meta-text {
+  white-space: nowrap;
+}
+
+.card-footer {
+  min-height: 24px;
+}
+
 .clock-badge {
 .clock-badge {
   width: 24px;
   width: 24px;
   height: 24px;
   height: 24px;
@@ -253,14 +362,40 @@ const seeDetails = (item) => {
 .progress-track {
 .progress-track {
   width: 100%;
   width: 100%;
   height: 6px;
   height: 6px;
+  margin-top: auto;
   background: #e2e8f0;
   background: #e2e8f0;
   border-radius: 999px;
   border-radius: 999px;
   overflow: hidden;
   overflow: hidden;
 }
 }
 
 
+.schedule-info {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 4px;
+  width: 100%;
+  margin: 5px 0 4px;
+  padding: 0 2px;
+  color: #475569;
+  font-size: 10px;
+  font-weight: 500;
+}
+
+.schedule-info-item {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  white-space: nowrap;
+
+  .q-icon {
+    color: #64748b;
+  }
+}
+
 .progress-fill {
 .progress-fill {
   position: relative;
   position: relative;
   height: 100%;
   height: 100%;
+  width: 0;
   border-radius: inherit;
   border-radius: inherit;
 
 
   background: linear-gradient(
   background: linear-gradient(
@@ -270,7 +405,6 @@ const seeDetails = (item) => {
     #d94cff
     #d94cff
   );
   );
 
 
-  transition: width .4s ease;
   overflow: hidden;
   overflow: hidden;
 }
 }
 
 
@@ -302,4 +436,56 @@ const seeDetails = (item) => {
     left: 135%;
     left: 135%;
   }
   }
 }
 }
+
+.progress-fill--loop {
+  background: #ec4899;
+  animation: progressLoopFill 3s ease-in-out infinite;
+}
+
+.progress-fill--loop::after {
+  display: none;
+}
+
+@keyframes progressLoopFill {
+  0% {
+    width: 0;
+  }
+
+  80% {
+    width: 100%;
+  }
+
+  100% {
+    width: 100%;
+  }
+}
+
+.progress-fill--urgent {
+  width: 100%;
+  animation: progressUrgentPulse 1.2s ease-in-out infinite;
+}
+
+@keyframes progressUrgentPulse {
+  0%,
+  100% {
+    opacity: 1;
+  }
+
+  50% {
+    opacity: 0.2;
+  }
+}
+
+@media (prefers-reduced-motion: reduce) {
+  .progress-fill,
+  .progress-fill::after {
+    animation: none;
+  }
+
+  .progress-fill--loop,
+  .progress-fill--urgent {
+    width: 100%;
+    opacity: 1;
+  }
+}
 </style>
 </style>

+ 5 - 5
src/components/dashboard/DashboardProvidersClose.vue

@@ -10,11 +10,11 @@
           color="text-light"
           color="text-light"
           flat
           flat
           icon="mdi-chevron-left"
           icon="mdi-chevron-left"
-          size="sm"
+          size="md"
           @click="setPeriodTypePrevious"
           @click="setPeriodTypePrevious"
         />
         />
 
 
-        <span class="font10 fontbold">
+        <span class="font12 fontbold">
           {{ showCorrectLabels() }}
           {{ showCorrectLabels() }}
         </span>
         </span>
 
 
@@ -22,7 +22,7 @@
           color="text-light"
           color="text-light"
           flat
           flat
           icon="mdi-chevron-right"
           icon="mdi-chevron-right"
-          size="sm"
+          size="md"
           @click="setPeriodTypeNext"
           @click="setPeriodTypeNext"
         />
         />
       </div>
       </div>
@@ -132,7 +132,7 @@
                     rounded
                     rounded
                     size="sm"
                     size="sm"
                     unelevated
                     unelevated
-                    :label="$t('dashboard_client.providers_close.schedule')"
+                    :label="$t('dashboard_client.favorites.view_schedule')"
                     @click="goToScheduling(p)"
                     @click="goToScheduling(p)"
                   />
                   />
                 </div>
                 </div>
@@ -161,7 +161,7 @@ const { t } = useI18n();
 
 
 const $q = useQuasar();
 const $q = useQuasar();
 
 
-const currentPeriodType = ref(8);
+const currentPeriodType = ref(6);
 
 
 const periodTypeMap = ref({
 const periodTypeMap = ref({
   2: "daily_price_2h",
   2: "daily_price_2h",

+ 33 - 22
src/components/dashboard/DashboardSummaryInfos.vue

@@ -8,18 +8,14 @@
             <span v-else>{{ data?.name?.slice(0, 2).toUpperCase() ?? '??' }}</span>
             <span v-else>{{ data?.name?.slice(0, 2).toUpperCase() ?? '??' }}</span>
           </q-avatar>
           </q-avatar>
           <div class="column q-gutter-y-xs min-width-0">
           <div class="column q-gutter-y-xs min-width-0">
-  <span class="summary-greeting text-greeting font12 fontemedium">
-    {{ $t('dashboard_client.summary.welcome') }}
-  </span>
+            <span class="summary-greeting text-greeting font12 fontemedium">
+              {{ $t('dashboard_client.summary.welcome') }}
+            </span>
 
 
-  <span class="summary-name text-name text-primary font16 fontbold">
-    {{ getFirstName(data?.name) || $t('dashboard_client.summary.welcome') }}
-  </span>
-
-  <span class="summary-address font10 fontmedium text-text">
-    {{ formatAddress(data.address) }}
-  </span>
-</div>
+            <span class="summary-name text-name text-primary font16 fontbold">
+              {{ getFirstName(data?.name) || $t('dashboard_client.summary.welcome') }}
+            </span>
+          </div>
         </div>
         </div>
         <div class="column items-end q-gutter-y-xs col-auto">
         <div class="column items-end q-gutter-y-xs col-auto">
           <span class="summary-label font12 fontbold text-grey-6">{{ $t('dashboard_client.summary.my_schedules') }}</span>
           <span class="summary-label font12 fontbold text-grey-6">{{ $t('dashboard_client.summary.my_schedules') }}</span>
@@ -30,11 +26,28 @@
         </div>
         </div>
       </div>
       </div>
 
 
-      <div class="row items-center justify-between no-wrap summary-address-row">
-        <!-- <div class="summary-address font10 fontmedium text-text col">
+      <div
+        class="row justify-between q-mt-xs"
+        :class="expanded ? 'items-start' : 'items-center no-wrap'"
+      >
+        <div
+          class="summary-address text-text col font12 fontmedium"
+          :class="{ ellipsis: !expanded }"
+        >
           {{ formatAddress(data.address) }}
           {{ formatAddress(data.address) }}
-        </div> -->
-        <!-- <q-icon name="mdi-chevron-down" color="secondary" size="18px" class="col-auto" /> -->
+        </div>
+
+        <q-btn
+          :aria-label="$t(expanded ? 'common.collapse' : 'common.expand')"
+          :icon="expanded ? 'mdi-chevron-up' : 'mdi-chevron-down'"
+          class="col-auto expand-button"
+          color="secondary"
+          dense
+          flat
+          round
+          size="sm"
+          @click="expanded = !expanded"
+        />
       </div>
       </div>
     </q-card-section>
     </q-card-section>
   </q-card>
   </q-card>
@@ -42,8 +55,12 @@
 
 
 <script setup>
 <script setup>
 import { formatAddress, getFirstName } from 'src/helpers/utils';
 import { formatAddress, getFirstName } from 'src/helpers/utils';
+import { ref } from 'vue';
+
 defineProps({ data: { type: Object, default: () => null } });
 defineProps({ data: { type: Object, default: () => null } });
 
 
+const expanded = ref(false);
+
 const avatarStyle = {
 const avatarStyle = {
   background: 'linear-gradient(135deg, #ffd7e8 0%, #ff9acc 100%)',
   background: 'linear-gradient(135deg, #ffd7e8 0%, #ff9acc 100%)',
   color: '#7a154f',
   color: '#7a154f',
@@ -62,15 +79,9 @@ const avatarStyle = {
 }
 }
 .summary-address {
 .summary-address {
   color: #7c7c7c;
   color: #7c7c7c;
-  font-size: 10px;
   line-height: 14px;
   line-height: 14px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
   max-width: 100%;
   max-width: 100%;
 }
 }
 
 
-.min-width-0 {
-  min-width: 0;
-}
+.expand-button { margin: -6px -6px -6px 2px; }
 </style>
 </style>

+ 57 - 14
src/components/dashboard/DashboardTodaySchedules.vue

@@ -3,7 +3,7 @@
     <div class="scroll-wrapper">
     <div class="scroll-wrapper">
       <div class="scroll-track">
       <div class="scroll-track">
         <q-card
         <q-card
-          v-for="item in data"
+          v-for="item in visibleItems"
           :key="item.id"
           :key="item.id"
           :flat="false"
           :flat="false"
           class="today-card card-border shadow-card bg-surface"
           class="today-card card-border shadow-card bg-surface"
@@ -218,36 +218,69 @@
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
 import { getFirstName } from "src/helpers/utils";
 import { getFirstName } from "src/helpers/utils";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
+import { ref, onMounted, onBeforeUnmount, computed } from "vue";
 
 
 import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 
 
 
 
-defineProps({ data: { type: Array, default: () => [] } });
+const props = defineProps({
+  data: {
+    type: Array,
+    default: () => [],
+  },
+});
+
+
 
 
 const emit = defineEmits(["rate"]);
 const emit = defineEmits(["rate"]);
 
 
 const $q = useQuasar();
 const $q = useQuasar();
 
 
+const now = ref(Date.now());
+
+let timer = null;
+
 const cardState = (item) => {
 const cardState = (item) => {
-  switch (item.status) {
-    case "finished":
-      return "finished";
+  if (item.status === "cancelled") {
+    return "cancelled";
+  }
 
 
-    case "started":
-      return "in_progress";
+  if (item.client_reviewed) {
+    return "finished";
+  }
 
 
-    case "accepted":
-    case "paid":
-      return "awaiting_code";
+  if (
+    item.status === "accepted" ||
+    item.status === "paid"
+  ) {
+    return "awaiting_code";
+  }
 
 
-    case "cancelled":
-      return "cancelled";
+  if (
+    item.status === "started" ||
+    item.status === "finished"
+  ) {
+    const [hours, minutes] = (item.end_time || "23:59")
+      .slice(0, 5)
+      .split(":")
+      .map(Number);
 
 
-    default:
-      return "awaiting_code";
+    const endTime = new Date();
+
+    endTime.setHours(hours, minutes, 0, 0);
+
+    return now.value >= endTime.getTime()
+      ? "finished"
+      : "in_progress";
   }
   }
+
+  return "awaiting_code";
 };
 };
 
 
+const visibleItems = computed(() => {
+  return props.data.filter((item) => !item.client_reviewed);
+});
+
 const progressByState = (item) => {
 const progressByState = (item) => {
   const state = cardState(item);
   const state = cardState(item);
 
 
@@ -262,6 +295,16 @@ const progressByState = (item) => {
 const openHelp = () => {
 const openHelp = () => {
   $q.dialog({ component: ProfileHelpDialog });
   $q.dialog({ component: ProfileHelpDialog });
 };
 };
+
+onMounted(() => {
+  timer = setInterval(() => {
+    now.value = Date.now();
+  }, 1000);
+});
+
+onBeforeUnmount(() => {
+  clearInterval(timer);
+});
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 68 - 25
src/components/dashboard/ScheduleAcceptedDialog.vue

@@ -34,25 +34,48 @@
       <q-separator />
       <q-separator />
 
 
       <q-card-section class="q-py-md q-px-lg">
       <q-card-section class="q-py-md q-px-lg">
+        <div
+          v-if="activeSchedules.length > 1"
+          class="font13 fontbold text-primary q-mb-xs"
+        >
+          {{ $t("dashboard_client.pending_schedules.detail_schedules_title") }}
+        </div>
+
+        <div
+          v-for="schedule in activeSchedules"
+          :key="schedule.id"
+          class="detail-row schedule-row"
+        >
+          <span class="detail-value font13">
+            {{ formatScheduleDate(schedule) }}
+          </span>
+
+          <span class="detail-valued text-text font13">
+            {{ schedule.start_time?.slice(0, 5) }}
+            {{ $t("dashboard_client.next_schedules.to") }}
+            {{ schedule.end_time?.slice(0, 5) }}
+          </span>
+        </div>
+
+        <q-separator class="q-my-sm" />
+
         <div class="detail-row">
         <div class="detail-row">
           <span class="text-primary font14 fontmedium">
           <span class="text-primary font14 fontmedium">
-            {{ $t("dashboard_client.pending_schedules.detail_date") }}
+            {{ $t("dashboard_client.pending_schedules.detail_value") }}
           </span>
           </span>
 
 
-          <span class="detail-value">
-            {{ displayDate }}
+          <span class="detail-value font14">
+            {{ formatCurrency(baseAmount) }}
           </span>
           </span>
         </div>
         </div>
 
 
         <div class="detail-row">
         <div class="detail-row">
-          <span class="text-primary font14 fontmedium">
-            {{ $t("dashboard_client.pending_schedules.detail_time") }}
+          <span class="text-primary font13 fontmedium">
+            {{ $t("dashboard_client.pending_schedules.detail_service_fee_pix") }}
           </span>
           </span>
 
 
-          <span class="detail-valued text-text">
-            {{ displayStartTime }}
-            {{ $t("dashboard_client.next_schedules.to") }}
-            {{ displayEndTime }}
+          <span class="detail-value font13">
+            {{ formatCurrency(platformFee("pix")) }}
           </span>
           </span>
         </div>
         </div>
 
 
@@ -81,6 +104,20 @@
           </span>
           </span>
         </div>
         </div>
 
 
+        <div class="detail-row">
+          <span class="text-primary font13 fontmedium">
+            {{
+              $t(
+                "dashboard_client.pending_schedules.detail_service_fee_credit_card",
+              )
+            }}
+          </span>
+
+          <span class="detail-value font13">
+            {{ formatCurrency(platformFee("credit_card")) }}
+          </span>
+        </div>
+
         <div class="detail-row">
         <div class="detail-row">
           <span class="text-primary font14 fontmedium">
           <span class="text-primary font14 fontmedium">
             {{
             {{
@@ -155,15 +192,20 @@ const displayProviderName = computed(() => {
   return item.value.provider?.user?.name ?? item.value.provider_name ?? '';
   return item.value.provider?.user?.name ?? item.value.provider_name ?? '';
 });
 });
 
 
+const activeSchedules = computed(() =>
+  (item.value.schedules ?? []).filter(
+    (schedule) => !['cancelled', 'rejected'].includes(schedule.status),
+  ),
+);
+
 const displayDistrict = computed(() => {
 const displayDistrict = computed(() => {
-  return item.value.schedules?.[0]?.address?.district ?? item.value.address?.district ?? '';
+  return activeSchedules.value[0]?.address?.district ?? item.value.address?.district ?? '';
 });
 });
 
 
-const displayDate = computed(() => {
-  const src = item.value.schedules?.[0] ?? item.value;
-  if (!src) return '';
-  if (src.formatted_date) return src.formatted_date;
-  const raw = String(src.date || '');
+const formatScheduleDate = (schedule) => {
+  if (!schedule) return '';
+  if (schedule.formatted_date) return schedule.formatted_date;
+  const raw = String(schedule.date || '');
   const m = raw.match(/^(\d{4})-(\d{2})-(\d{2})/);
   const m = raw.match(/^(\d{4})-(\d{2})-(\d{2})/);
   if (!m) return raw;
   if (!m) return raw;
   const d = new Date(+m[1], +m[2] - 1, +m[3]);
   const d = new Date(+m[1], +m[2] - 1, +m[3]);
@@ -172,21 +214,22 @@ const displayDate = computed(() => {
     month: 'long',
     month: 'long',
     year: 'numeric',
     year: 'numeric',
   });
   });
-});
-
-const displayStartTime = computed(() => {
-  return item.value.schedules?.[0]?.start_time?.slice(0, 5) ?? '';
-});
-
-const displayEndTime = computed(() => {
-  return item.value.schedules?.[0]?.end_time?.slice(0, 5) ?? '';
-});
+};
 
 
 const avatarStyle = computed(
 const avatarStyle = computed(
   () => avatarColors[item.value.id % avatarColors.length],
   () => avatarColors[item.value.id % avatarColors.length],
 );
 );
 
 
-const baseAmount = computed(() => Number(item.value.total_amount) || 0);
+const baseAmount = computed(() => {
+  const total = Number(item.value.total_amount);
+
+  if (Number.isFinite(total) && total > 0) return total;
+
+  return activeSchedules.value.reduce(
+    (sum, schedule) => sum + (Number(schedule.total_amount) || 0),
+    0,
+  );
+});
 
 
 const creditCardTotal = computed(() =>
 const creditCardTotal = computed(() =>
   parseFloat((baseAmount.value + platformFee("credit_card")).toFixed(2)),
   parseFloat((baseAmount.value + platformFee("credit_card")).toFixed(2)),

+ 29 - 1
src/components/dashboard/ScheduleCancelDialog.vue

@@ -12,6 +12,15 @@
         </div>
         </div>
       </q-card-section>
       </q-card-section>
 
 
+      <q-card-section v-if="packageWarning" class="q-pt-none q-pb-sm q-px-lg">
+        <div class="package-warning-box row no-wrap q-gutter-x-sm q-pa-sm">
+          <q-icon name="mdi-package-variant" color="negative" size="22px" class="q-mt-xs flex-shrink-0" />
+          <span class="font13 fontbold text-negative">
+            {{ packageWarning }}
+          </span>
+        </div>
+      </q-card-section>
+
       <q-card-section class="q-pt-none q-pb-sm q-px-lg">
       <q-card-section class="q-pt-none q-pb-sm q-px-lg">
         <div class="font14 fontbold text-grey-8 q-mb-xs text-center">
         <div class="font14 fontbold text-grey-8 q-mb-xs text-center">
           {{ $t('provider.dashboard.cancel_schedule.reason_label') }}
           {{ $t('provider.dashboard.cancel_schedule.reason_label') }}
@@ -76,7 +85,7 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref } from 'vue'
+import { computed, ref } from 'vue'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
 import { cancelSchedule } from 'src/api/schedule'
 import { cancelSchedule } from 'src/api/schedule'
@@ -95,6 +104,20 @@ const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent()
 const cancelText = ref('')
 const cancelText = ref('')
 const loading = ref(false)
 const loading = ref(false)
 
 
+const packageWarning = computed(() => {
+  if (!props.schedule?.service_package_id) return null
+
+  const count = props.schedule?.service_package_items_count
+
+  if (count === undefined || count === null) {
+    return t('provider.dashboard.cancel_schedule.package_warning_generic')
+  }
+
+  return Number(count) > 1
+    ? t('provider.dashboard.cancel_schedule.package_warning', { count: Number(count) })
+    : null
+})
+
 const confirmCancel = async () => {
 const confirmCancel = async () => {
   if (!cancelText.value || cancelText.value.trim().length < 5) return
   if (!cancelText.value || cancelText.value.trim().length < 5) return
   loading.value = true
   loading.value = true
@@ -126,6 +149,11 @@ const confirmCancel = async () => {
   border-radius: 10px;
   border-radius: 10px;
 }
 }
 
 
+.package-warning-box {
+  background: #fdecec;
+  border-radius: 10px;
+}
+
 .btn-action {
 .btn-action {
   min-width: 110px;
   min-width: 110px;
 }
 }

+ 17 - 5
src/components/dashboard/SchedulePaymentDialog.vue

@@ -80,11 +80,13 @@
           </div>
           </div>
         </div>
         </div>
 
 
-        <div class="row items-center q-mb-lg">
-          <q-checkbox v-model="agreedToTerms" color="primary" keep-color />
-          <span class="terms-text">
-            {{ $t('payment.agree_prefix') }}
-            <span class="text-primary cursor-pointer text-underline">{{ $t('payment.terms_link') }}</span>
+        <div class="terms-text q-mb-lg">
+          {{ $t('payment.agree_prefix') }}
+          <span
+            class="text-primary cursor-pointer text-underline"
+            @click="openTerms"
+          >
+            {{ $t('payment.terms_link') }}
           </span>
           </span>
         </div>
         </div>
 
 
@@ -118,6 +120,7 @@ import { usePaymentPlatformFees } from 'src/composables/usePaymentPlatformFees'
 import ProfilePaymentAddDialog from 'src/components/profile/ProfilePaymentAddDialog.vue'
 import ProfilePaymentAddDialog from 'src/components/profile/ProfilePaymentAddDialog.vue'
 import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
 import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
 import SchedulePaymentProcessingDialog from './SchedulePaymentProcessingDialog.vue'
 import SchedulePaymentProcessingDialog from './SchedulePaymentProcessingDialog.vue'
+import { Browser } from "@capacitor/browser";
 
 
 const props = defineProps({
 const props = defineProps({
   servicePackage: {
   servicePackage: {
@@ -165,6 +168,15 @@ const addressFullText = computed(() => {
 
 
 const canConfirm = computed(() => selectedMethod.value !== null && agreedToTerms.value)
 const canConfirm = computed(() => selectedMethod.value !== null && agreedToTerms.value)
 
 
+
+//direcionamento para ttermos e serviços 
+const PRIVACY_POLICY_URL =
+  "https://politicas.softpar.inf.br/politicas/politicaDiaristaCliente.html";
+
+const openTerms = async () => {
+  await Browser.open({ url: PRIVACY_POLICY_URL });
+};
+
 const brandDisplay = (brand) => {
 const brandDisplay = (brand) => {
   if (!brand) return ''
   if (!brand) return ''
   const map = { visa: 'VISA', mastercard: 'Mastercard', elo: 'Elo', hipercard: 'Hipercard', diners: 'Diners', discover: 'Discover' }
   const map = { visa: 'VISA', mastercard: 'Mastercard', elo: 'Elo', hipercard: 'Hipercard', diners: 'Diners', discover: 'Discover' }

+ 14 - 0
src/components/login/LoginStep4Panel.vue

@@ -57,6 +57,8 @@
 import { ref } from "vue";
 import { ref } from "vue";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
+import { Capacitor } from "@capacitor/core";
+import { Keyboard } from "@capacitor/keyboard";
 
 
 import { useGeocodingApi } from "src/composables/useGeocodingApi";
 import { useGeocodingApi } from "src/composables/useGeocodingApi";
 import { useGeolocation } from "src/composables/useGeolocation";
 import { useGeolocation } from "src/composables/useGeolocation";
@@ -72,6 +74,16 @@ const cep = ref("");
 const loadingCep = ref(false);
 const loadingCep = ref(false);
 const loadingLocation = ref(false);
 const loadingLocation = ref(false);
 
 
+const closeKeyboard = async () => {
+  if (!Capacitor.isNativePlatform()) return;
+
+  try {
+    await Keyboard.hide();
+  } catch {
+    // 
+  }
+};
+
 const onCepChange = async (val) => {
 const onCepChange = async (val) => {
   const cleaned = val?.replace(/\D/g, "") ?? "";
   const cleaned = val?.replace(/\D/g, "") ?? "";
 
 
@@ -82,6 +94,8 @@ const onCepChange = async (val) => {
   loadingCep.value = true;
   loadingCep.value = true;
 
 
   try {
   try {
+    await closeKeyboard();
+
     const result = await geocodeCep(cleaned);
     const result = await geocodeCep(cleaned);
 
 
     if (!result) {
     if (!result) {

+ 2 - 60
src/components/profile/ProfileAddressFormDialog.vue

@@ -385,16 +385,6 @@ const openMapDialog = async () => {
 
 
   const hasAddress = form.address || form.zip_code;
   const hasAddress = form.address || form.zip_code;
 
 
-  $q.notify({
-    caption: `hasAddress: ${hasAddress}\naddress: ${form.address}\nzip_code: ${form.zip_code}\nlat: ${form.latitude}\nlng: ${form.longitude}`,
-    color: "indigo-9",
-    message: "[ADDR] openMapDialog iniciado",
-    multiLine: true,
-    position: "top",
-    textColor: "white",
-    timeout: 8000,
-  });
-
   if (hasAddress) {
   if (hasAddress) {
     geocodingCep.value = true;
     geocodingCep.value = true;
 
 
@@ -411,48 +401,12 @@ const openMapDialog = async () => {
       if (geo) {
       if (geo) {
         initialLat = geo.lat;
         initialLat = geo.lat;
         initialLng = geo.lng;
         initialLng = geo.lng;
-
-        $q.notify({
-          caption: `lat: ${geo.lat}\nlng: ${geo.lng}`,
-          color: "indigo-9",
-          message: "[ADDR] geocodeFullAddress OK",
-          multiLine: true,
-          position: "top",
-          textColor: "white",
-          timeout: 8000,
-        });
-      } else {
-        $q.notify({
-          color: "indigo-9",
-          message:
-            "[ADDR] geocodeFullAddress retornou null — usando coord default",
-          position: "top",
-          textColor: "white",
-          timeout: 6000,
-        });
       }
       }
-    } catch (err) {
-      $q.notify({
-        caption: `erro: ${err?.message ?? String(err)}`,
-        color: "indigo-9",
-        message: "[ADDR] geocodeFullAddress ERRO — usando coord default",
-        multiLine: true,
-        position: "top",
-        textColor: "white",
-        timeout: 8000,
-      });
+    } catch (error) {
+      console.error("Failed to geocode address", error);
     } finally {
     } finally {
       geocodingCep.value = false;
       geocodingCep.value = false;
     }
     }
-  } else {
-    $q.notify({
-      color: "indigo-9",
-      message:
-        "[ADDR] sem endereço preenchido — abrindo mapa com coord default",
-      position: "top",
-      textColor: "white",
-      timeout: 6000,
-    });
   }
   }
 
 
   const dialogProps =
   const dialogProps =
@@ -463,18 +417,6 @@ const openMapDialog = async () => {
         }
         }
       : {};
       : {};
 
 
-  $q.notify({
-    caption: `initialLat: ${
-      dialogProps.initialLat ?? "default"
-    }\ninitialLng: ${dialogProps.initialLng ?? "default"}`,
-    color: "indigo-9",
-    message: "[ADDR] abrindo LocationMapDialog",
-    multiLine: true,
-    position: "top",
-    textColor: "white",
-    timeout: 8000,
-  });
-
   showMapDialog(dialogProps);
   showMapDialog(dialogProps);
 };
 };
 
 

+ 4 - 1
src/components/profile/ProfileHelpDialog.vue

@@ -110,6 +110,7 @@
             input-class="text-text"
             input-class="text-text"
             :placeholder="$t('profile.help.message_placeholder')"
             :placeholder="$t('profile.help.message_placeholder')"
             :disable="loading"
             :disable="loading"
+            :maxlength="MESSAGE_MAX_LENGTH"
             @keyup.enter="sendMessage()"
             @keyup.enter="sendMessage()"
           />
           />
           <q-btn
           <q-btn
@@ -148,6 +149,8 @@ const $q = useQuasar();
 const { t } = useI18n();
 const { t } = useI18n();
 const store = chatbotStore();
 const store = chatbotStore();
 
 
+const MESSAGE_MAX_LENGTH = 4000;
+
 const messageInput = ref('');
 const messageInput = ref('');
 const loading = ref(false);
 const loading = ref(false);
 const messagesContainer = ref(null);
 const messagesContainer = ref(null);
@@ -192,7 +195,7 @@ const sendMessage = async (text) => {
 
 
   try {
   try {
     const history = store.messages
     const history = store.messages
-      .slice(-6, -1)
+      .slice(0, -1)
       .map(({ role, text: msgText }) => ({ role, text: msgText }));
       .map(({ role, text: msgText }) => ({ role, text: msgText }));
 
 
     const reply = await sendChatMessage({ message: content, history });
     const reply = await sendChatMessage({ message: content, history });

+ 118 - 54
src/components/profile/ProfileInfoDialog.vue

@@ -3,28 +3,28 @@
     ref="dialogRef"
     ref="dialogRef"
     @hide="onDialogHide"
     @hide="onDialogHide"
   >
   >
-    <q-card class="provider-dialog">
+    <q-card class="provider-dialog card-border shadow-card bg-surface text-text">
 
 
       <q-btn
       <q-btn
-        flat
-        round
+        class="close-btn"
+        color="grey-7"
         dense
         dense
+        flat
         icon="close"
         icon="close"
-        class="close-btn"
+        round
         @click="closeDialog"
         @click="closeDialog"
       />
       />
 
 
       <q-card-section class="provider-header">
       <q-card-section class="provider-header">
 
 
         <q-avatar
         <q-avatar
-          size="72px"
-          color="primary"
-          text-color="white"
           class="provider-avatar"
           class="provider-avatar"
+          size="72px"
+          :style="avatarColors[(info.provider_id ?? 0) % avatarColors.length]"
         >
         >
           <img
           <img
-            v-if="props.provider.provider_photo"
-            :src="props.provider.provider_photo"
+            v-if="photoUrl"
+            :src="photoUrl"
           >
           >
 
 
           <span
           <span
@@ -37,45 +37,55 @@
 
 
         <div class="provider-info">
         <div class="provider-info">
 
 
-          <div class="text-h6 text-weight-bold">
-            {{ props.provider.provider_name }}
+          <div class="font16 fontbold text-text">
+            {{ firstName }}
           </div>
           </div>
 
 
-          <div v-if="props.provider.age" class="text-grey-6 text-body2">
-            {{ `${props.provider.age}` }}
-            </div>
+          <div class="font10 fontmedium text-grey-7">
+            {{ info.district ?? "—" }}
+          </div>
+
+          <div
+            v-if="info.age"
+            class="font10 fontmedium text-grey-7"
+          >
+            <span>
+              {{ $t("scheduling_page.provider_info.age", { value: info.age }) }}
+            </span>
 
 
-          <div class="text-grey-6">
-            {{ props.provider.district }}
+            <span class="q-ml-md">
+              {{ $t("scheduling_page.provider_info.gender", { value: genderLabel }) }}
+            </span>
           </div>
           </div>
 
 
+
           <div class="provider-stats">
           <div class="provider-stats">
 
 
             <div class="row items-center">
             <div class="row items-center">
               <q-icon
               <q-icon
-                color="amber"
+                color="warning"
                 name="mdi-star"
                 name="mdi-star"
-                size="18px"
+                size="16px"
               />
               />
 
 
-              <span class="q-ml-xs">
-                {{ props.provider.average_rating }}
+              <span class="font10 fontmedium q-ml-xs">
+                {{ ratingLabel }}
               </span>
               </span>
 
 
-              <span class="text-grey q-ml-xs">
-                {{ `(${props.provider.total_reviews})` }}
+              <span class="font10 fontmedium text-grey-7 q-ml-xs">
+                {{ `(${info.total_reviews ?? 0})` }}
               </span>
               </span>
             </div>
             </div>
 
 
             <div class="row items-center">
             <div class="row items-center">
               <q-icon
               <q-icon
-                color="deep-purple"
+                color="secondary"
                 name="mdi-broom"
                 name="mdi-broom"
-                size="18px"
+                size="16px"
               />
               />
 
 
-              <span class="q-ml-xs">
-                {{ `(${props.provider.total_services})` }}
+              <span class="font10 fontmedium q-ml-xs">
+                {{ `(${info.total_services ?? 0})` }}
               </span>
               </span>
             </div>
             </div>
 
 
@@ -87,36 +97,57 @@
 
 
       <q-separator />
       <q-separator />
 
 
-      <!-- Especialidades -->
+      <div
+        v-if="loading"
+        class="row items-center justify-center q-py-md"
+      >
+        <q-spinner-dots
+          color="primary"
+          size="32px"
+        />
+      </div>
+
       <q-list
       <q-list
-        v-if="props.provider.specialities?.length"
-        dense
+        v-else-if="serviceTypes.length"
         class="q-pa-sm"
         class="q-pa-sm"
+        dense
       >
       >
         <q-item
         <q-item
-          v-for="speciality in props.provider.specialities"
-          :key="speciality.id"
+          v-for="serviceType in serviceTypes"
+          :key="serviceType.id"
         >
         >
           <q-item-section avatar>
           <q-item-section avatar>
             <q-icon
             <q-icon
-              name="mdi-check-circle"
-              color="positive"
+              color="primary"
+              name="mdi-check"
             />
             />
           </q-item-section>
           </q-item-section>
 
 
-          <q-item-section>
-            {{ speciality.description }}
+          <q-item-section class="font12 fontmedium text-text">
+            {{ serviceType.description }}
           </q-item-section>
           </q-item-section>
         </q-item>
         </q-item>
       </q-list>
       </q-list>
 
 
+      <div
+        v-else
+        class="text-center text-grey-6 font10 fontmedium q-pa-md"
+      >
+        {{ $t("scheduling_page.provider_info.no_service_types") }}
+      </div>
+
     </q-card>
     </q-card>
   </q-dialog>
   </q-dialog>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { computed } from "vue";
+import { computed, onMounted, ref } from "vue";
 import { useDialogPluginComponent } from "quasar";
 import { useDialogPluginComponent } from "quasar";
+import { avatarColors } from "src/helpers/avatarColors";
+import { getFirstName } from "src/helpers/utils";
+import { getPerfilPrestador } from "src/api/dashboard";
+import { getProfileMediaUrl } from "src/helpers/profileMedia";
+import { useI18n } from "vue-i18n";
 
 
 const props = defineProps({
 const props = defineProps({
   provider: {
   provider: {
@@ -132,21 +163,57 @@ const {
   onDialogHide,
   onDialogHide,
 } = useDialogPluginComponent();
 } = useDialogPluginComponent();
 
 
+const { t } = useI18n();
+
+const loading = ref(false);
+const info = ref({ ...props.provider });
+
 const closeDialog = () => {
 const closeDialog = () => {
   dialogRef.value.hide();
   dialogRef.value.hide();
 };
 };
 
 
-const initials = computed(() => {
-  if (!props.provider.provider_name) {
-    return "?";
-  }
+const firstName = computed(
+  () => getFirstName(info.value.provider_name) || "—"
+);
+
+const photoUrl = computed(() => getProfileMediaUrl(info.value));
+
+const serviceTypes = computed(() => info.value.service_types ?? []);
+
+const ratingLabel = computed(
+  () => Number(info.value.average_rating ?? 0).toFixed(1)
+);
+
+const genderLabel = computed(() => {
+  if (info.value.gender_label) return info.value.gender_label;
+
+  const gender = info.value.gender;
+  if (!gender) return null;
 
 
-  return props.provider.provider_name
-    .trim()
-    .split(" ")
-    .slice(0, 2)
-    .map(name => name[0].toUpperCase())
-    .join("");
+  const key = `genders.${gender}`;
+  const label = t(key);
+
+  return label === key ? null : label;
+});
+
+const initials = computed(
+  () => firstName.value.slice(0, 2).toUpperCase() || "?"
+);
+
+onMounted(async () => {
+  const providerId = props.provider?.provider_id ?? props.provider?.id;
+  if (!providerId) return;
+
+  loading.value = true;
+
+  try {
+    const payload = await getPerfilPrestador(providerId);
+    info.value = { ...props.provider, ...payload };
+  } catch (error) {
+    console.error(error);
+  } finally {
+    loading.value = false;
+  }
 });
 });
 </script>
 </script>
 
 
@@ -154,7 +221,6 @@ const initials = computed(() => {
 .provider-dialog {
 .provider-dialog {
   width: 380px;
   width: 380px;
   max-width: 95vw;
   max-width: 95vw;
-  border-radius: 14px;
   overflow: hidden;
   overflow: hidden;
 }
 }
 
 
@@ -162,7 +228,9 @@ const initials = computed(() => {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   gap: 18px;
   gap: 18px;
-  padding: 20px;
+  padding-top: 20px;
+  padding-right: 20px;
+  padding-left: 20px;
 }
 }
 
 
 .provider-avatar {
 .provider-avatar {
@@ -179,14 +247,10 @@ const initials = computed(() => {
   flex: 1;
   flex: 1;
 }
 }
 
 
-.provider-info .text-h6 {
-  line-height: 1.2;
-}
-
 .provider-stats {
 .provider-stats {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
-  gap: 6px;
+  gap: 10px;
   margin-top: 8px;
   margin-top: 8px;
 }
 }
 
 
@@ -200,4 +264,4 @@ const initials = computed(() => {
 .q-list .q-item {
 .q-list .q-item {
   min-height: 38px;
   min-height: 38px;
 }
 }
-</style>
+</style>

+ 0 - 20
src/composables/useGeocodingApi.js

@@ -1,15 +1,7 @@
 import axios from 'axios';
 import axios from 'axios';
-import { Notify } from 'quasar';
 
 
 const GEOCODING_URL = 'https://maps.googleapis.com/maps/api/geocode/json';
 const GEOCODING_URL = 'https://maps.googleapis.com/maps/api/geocode/json';
 
 
-// ── DEBUG via notificação ─────────────────────────────────────────
-const dbg = (title, data = {}) => {
-  const lines = Object.entries(data).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join('\n');
-  Notify.create({ color: 'teal-9', textColor: 'white', position: 'top', message: `[GEO] ${title}`, caption: lines || undefined, timeout: 8000, multiLine: true });
-};
-// ─────────────────────────────────────────────────────────────────
-
 const parseAddressComponents = (components) => {
 const parseAddressComponents = (components) => {
   const get = (type, nameType = 'long_name') =>
   const get = (type, nameType = 'long_name') =>
     components.find((c) => c.types.includes(type))?.[nameType] ?? '';
     components.find((c) => c.types.includes(type))?.[nameType] ?? '';
@@ -36,11 +28,8 @@ export const useGeocodingApi = () => {
     const apiKey = process.env.GOOGLE_MAPS_API_KEY;
     const apiKey = process.env.GOOGLE_MAPS_API_KEY;
     const usandoGeocoderNativo = !!window.google?.maps?.Geocoder;
     const usandoGeocoderNativo = !!window.google?.maps?.Geocoder;
 
 
-    dbg('geocodeCep', { cep: cleaned, via: usandoGeocoderNativo ? 'Geocoder nativo' : 'HTTP', apiKey: apiKey ? `${apiKey.substring(0, 8)}...` : 'UNDEFINED' });
-
     if (usandoGeocoderNativo) {
     if (usandoGeocoderNativo) {
       const results = await geocodeViaGoogleMaps({ address: `${cleaned}, Brazil` });
       const results = await geocodeViaGoogleMaps({ address: `${cleaned}, Brazil` });
-      dbg('geocodeCep nativo result', { total: results.length, encontrou: results.length > 0 });
       if (!results.length) return null;
       if (!results.length) return null;
       const result = results[0];
       const result = results[0];
       const parsed = parseAddressComponents(result.address_components);
       const parsed = parseAddressComponents(result.address_components);
@@ -54,7 +43,6 @@ export const useGeocodingApi = () => {
     const { data } = await axios.get(GEOCODING_URL, {
     const { data } = await axios.get(GEOCODING_URL, {
       params: { address: `${cleaned}, Brazil`, key: apiKey },
       params: { address: `${cleaned}, Brazil`, key: apiKey },
     });
     });
-    dbg('geocodeCep HTTP result', { status: data.status, total: data.results?.length ?? 0 });
     if (data.status !== 'OK' || !data.results.length) return null;
     if (data.status !== 'OK' || !data.results.length) return null;
     const result = data.results[0];
     const result = data.results[0];
     const { lat, lng } = result.geometry.location;
     const { lat, lng } = result.geometry.location;
@@ -65,11 +53,8 @@ export const useGeocodingApi = () => {
     const apiKey = process.env.GOOGLE_MAPS_API_KEY;
     const apiKey = process.env.GOOGLE_MAPS_API_KEY;
     const usandoGeocoderNativo = !!window.google?.maps?.Geocoder;
     const usandoGeocoderNativo = !!window.google?.maps?.Geocoder;
 
 
-    dbg('reverseGeocode chamado', { lat, lng, via: usandoGeocoderNativo ? 'Geocoder nativo' : 'HTTP', apiKey: apiKey ? `${apiKey.substring(0, 8)}...` : 'UNDEFINED' });
-
     if (usandoGeocoderNativo) {
     if (usandoGeocoderNativo) {
       const results = await geocodeViaGoogleMaps({ location: { lat, lng } });
       const results = await geocodeViaGoogleMaps({ location: { lat, lng } });
-      dbg('reverseGeocode nativo result', { total: results.length });
       if (!results.length) return null;
       if (!results.length) return null;
       return { lat, lng, ...parseAddressComponents(results[0].address_components) };
       return { lat, lng, ...parseAddressComponents(results[0].address_components) };
     }
     }
@@ -77,7 +62,6 @@ export const useGeocodingApi = () => {
     const { data } = await axios.get(GEOCODING_URL, {
     const { data } = await axios.get(GEOCODING_URL, {
       params: { latlng: `${lat},${lng}`, key: apiKey },
       params: { latlng: `${lat},${lng}`, key: apiKey },
     });
     });
-    dbg('reverseGeocode HTTP result', { status: data.status, total: data.results?.length ?? 0, errorMessage: data.error_message ?? null });
     if (data.status !== 'OK' || !data.results.length) return null;
     if (data.status !== 'OK' || !data.results.length) return null;
     const result = data.results[0];
     const result = data.results[0];
     return { lat, lng, ...parseAddressComponents(result.address_components) };
     return { lat, lng, ...parseAddressComponents(result.address_components) };
@@ -89,11 +73,8 @@ export const useGeocodingApi = () => {
     const apiKey = process.env.GOOGLE_MAPS_API_KEY;
     const apiKey = process.env.GOOGLE_MAPS_API_KEY;
     const usandoGeocoderNativo = !!window.google?.maps?.Geocoder;
     const usandoGeocoderNativo = !!window.google?.maps?.Geocoder;
 
 
-    dbg('geocodeFullAddress', { query, via: usandoGeocoderNativo ? 'Geocoder nativo' : 'HTTP', apiKey: apiKey ? `${apiKey.substring(0, 8)}...` : 'UNDEFINED' });
-
     if (usandoGeocoderNativo) {
     if (usandoGeocoderNativo) {
       const results = await geocodeViaGoogleMaps({ address: query });
       const results = await geocodeViaGoogleMaps({ address: query });
-      dbg('geocodeFullAddress nativo result', { total: results.length });
       if (!results.length) return null;
       if (!results.length) return null;
       const result = results[0];
       const result = results[0];
       return {
       return {
@@ -106,7 +87,6 @@ export const useGeocodingApi = () => {
     const { data } = await axios.get(GEOCODING_URL, {
     const { data } = await axios.get(GEOCODING_URL, {
       params: { address: query, key: apiKey },
       params: { address: query, key: apiKey },
     });
     });
-    dbg('geocodeFullAddress HTTP result', { status: data.status, total: data.results?.length ?? 0, errorMessage: data.error_message ?? null });
     if (data.status !== 'OK' || !data.results.length) return null;
     if (data.status !== 'OK' || !data.results.length) return null;
     const result = data.results[0];
     const result = data.results[0];
     const { lat, lng } = result.geometry.location;
     const { lat, lng } = result.geometry.location;

+ 0 - 1
src/helpers/utils.js

@@ -296,7 +296,6 @@ const formatAddress = (address) => {
   return parts.join(', ');
   return parts.join(', ');
 };
 };
 
 
-// Função para mostra apenas o primeiro nome (pode ser alterada para mostrar o primeiro e o último nome, caso seja necessário)
 const getFirstName = (fullName) => {
 const getFirstName = (fullName) => {
   if (!fullName) return '';
   if (!fullName) return '';
 
 

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

@@ -1,13 +1,15 @@
 {
 {
   "common": {
   "common": {
     "confirm": "Confirm",
     "confirm": "Confirm",
+    "expand": "expand",
+    "collapse": "collapse",
     "actions": {
     "actions": {
       "save": "Save",
       "save": "Save",
       "cancel": "Cancel",
       "cancel": "Cancel",
       "close": "Close",
       "close": "Close",
       "edit": "Edit",
       "edit": "Edit",
       "add": "Add",
       "add": "Add",
-      "select": "request",
+      "select": "select",
       "search": "Search",
       "search": "Search",
       "delete": "Delete",
       "delete": "Delete",
       "view": "View",
       "view": "View",
@@ -177,7 +179,8 @@
     "complement_required": "Please fill in the complement or check 'Address without complement'.",
     "complement_required": "Please fill in the complement or check 'Address without complement'.",
     "register_error": "Error completing registration. Please try again.",
     "register_error": "Error completing registration. Please try again.",
     "geocoding_failed": "Address not identified. Drag the pin to adjust the location.",
     "geocoding_failed": "Address not identified. Drag the pin to adjust the location.",
-    "geocoding_failed_short": "Drag the pin to identify the address"
+    "geocoding_failed_short": "Drag the pin to identify the address",
+    "map_unavailable": "Could not open the map. Please try again."
   },
   },
   "business": {
   "business": {
     "advertise": "Advertise",
     "advertise": "Advertise",
@@ -412,6 +415,8 @@
     "description_placeholder": "Hello, I would like a dedicated professional who will...",
     "description_placeholder": "Hello, I would like a dedicated professional who will...",
     "price_range_title": "Price range for 8 hours",
     "price_range_title": "Price range for 8 hours",
     "price_range_helper": "Select the full price range to receive housekeeper proposals.",
     "price_range_helper": "Select the full price range to receive housekeeper proposals.",
+    "price_preview_label": "{hours}h for",
+    "price_preview_value": "R$ {min} - {max}",
     "date_and_time": "Date and time",
     "date_and_time": "Date and time",
     "success_message": "Custom request saved successfully!",
     "success_message": "Custom request saved successfully!",
     "residential": "Residential",
     "residential": "Residential",
@@ -433,7 +438,7 @@
     },
     },
     "today_schedules": {
     "today_schedules": {
       "start_with": "Service starting with",
       "start_with": "Service starting with",
-      "started_by": "Service started by",
+      "started_by": "Service in progress",
       "finished_by": "Service completed by",
       "finished_by": "Service completed by",
       "code_label": "Code",
       "code_label": "Code",
       "in_progress": "in progress",
       "in_progress": "in progress",
@@ -511,6 +516,8 @@
     "pending_schedules": {
     "pending_schedules": {
       "title": "Awaiting",
       "title": "Awaiting",
       "requesting_with": "Requesting booking with",
       "requesting_with": "Requesting booking with",
+      "time_ago": "{time} ago",
+      "schedule_count": "{count} bookings",
       "pay_to_provider": "Make payment to",
       "pay_to_provider": "Make payment to",
       "no_provider": "Provider not defined",
       "no_provider": "Provider not defined",
       "cancel_btn": "cancel",
       "cancel_btn": "cancel",
@@ -526,6 +533,9 @@
       "detail_time": "Time:",
       "detail_time": "Time:",
       "detail_value": "Amount:",
       "detail_value": "Amount:",
       "detail_service_fee": "Total:",
       "detail_service_fee": "Total:",
+      "detail_schedules_title": "Dates and times",
+      "detail_service_fee_pix": "Service fee (Pix):",
+      "detail_service_fee_credit_card": "Service fee (card):",
       "detail_total": "Total:",
       "detail_total": "Total:",
       "detail_package_total": "Package total (Pix):",
       "detail_package_total": "Package total (Pix):",
       "detail_pix_total": "Pix total:",
       "detail_pix_total": "Pix total:",
@@ -738,6 +748,11 @@
   "scheduling_page": {
   "scheduling_page": {
     "title": "Scheduling",
     "title": "Scheduling",
     "about_provider": "About the professional",
     "about_provider": "About the professional",
+    "provider_info": {
+      "age": "{value} years old",
+      "gender": "Gender: {value}",
+      "no_service_types": "No service types informed."
+    },
     "schedule_service": "Schedule a service",
     "schedule_service": "Schedule a service",
     "favorite_added": "Professional added to favorites!",
     "favorite_added": "Professional added to favorites!",
     "favorite_removed": "Professional removed from favorites.",
     "favorite_removed": "Professional removed from favorites.",
@@ -761,6 +776,8 @@
       "pause_note_4h": "Includes a break of up to 10 minutes.",
       "pause_note_4h": "Includes a break of up to 10 minutes.",
       "slot_required": "Select a time slot to continue."
       "slot_required": "Select a time slot to continue."
     },
     },
+    "price_up_to": "up to {value}",
+    "cart": "cart",
     "service_types": {
     "service_types": {
       "integral": {
       "integral": {
         "label": "Full day",
         "label": "Full day",
@@ -806,6 +823,8 @@
     "empty_message": "Add time slots from favorite professionals to send a request.",
     "empty_message": "Add time slots from favorite professionals to send a request.",
     "add_to_cart": "Add to orders",
     "add_to_cart": "Add to orders",
     "add_success": "Time slot added to orders.",
     "add_success": "Time slot added to orders.",
+    "add_more": "add service",
+    "other_provider_blocked": "Your order already has time slots with another professional. Finish or empty the current order before booking with another professional.",
     "local_cart_title": "Orders",
     "local_cart_title": "Orders",
     "orders_title": "Orders",
     "orders_title": "Orders",
     "no_orders": "No orders created.",
     "no_orders": "No orders created.",
@@ -915,7 +934,9 @@
         "warning_fee": "Cancellations after this period generate a 50% refund as a compensation fee to the professional.",
         "warning_fee": "Cancellations after this period generate a 50% refund as a compensation fee to the professional.",
         "btn_cancel": "cancel",
         "btn_cancel": "cancel",
         "btn_back": "go back",
         "btn_back": "go back",
-        "btn_keep": "Cancel the service"
+        "btn_keep": "Cancel the service",
+        "package_warning": "This booking is part of a package with {count} bookings. Cancelling it will cancel ALL upcoming bookings in the package.",
+        "package_warning_generic": "This booking is part of a package. Cancelling it will cancel ALL upcoming bookings in the package."
       }
       }
     }
     }
   },
   },

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

@@ -1,13 +1,15 @@
 {
 {
   "common": {
   "common": {
     "confirm": "Confirmar",
     "confirm": "Confirmar",
+    "expand": "expandir",
+    "collapse": "contraer",
     "actions": {
     "actions": {
       "save": "Guardar",
       "save": "Guardar",
       "cancel": "Cancelar",
       "cancel": "Cancelar",
       "close": "Cerrar",
       "close": "Cerrar",
       "edit": "Editar",
       "edit": "Editar",
       "add": "Añadir",
       "add": "Añadir",
-      "select": "pedido",
+      "select": "seleccionar",
       "search": "Buscar",
       "search": "Buscar",
       "delete": "Eliminar",
       "delete": "Eliminar",
       "view": "Ver",
       "view": "Ver",
@@ -177,7 +179,8 @@
     "complement_required": "Complete el complemento o marque 'Dirección sin complemento'.",
     "complement_required": "Complete el complemento o marque 'Dirección sin complemento'.",
     "register_error": "Error al finalizar el registro. Inténtelo de nuevo.",
     "register_error": "Error al finalizar el registro. Inténtelo de nuevo.",
     "geocoding_failed": "Dirección no identificada. Arrastre el pin para ajustar la ubicación.",
     "geocoding_failed": "Dirección no identificada. Arrastre el pin para ajustar la ubicación.",
-    "geocoding_failed_short": "Arrastre el pin para identificar la dirección"
+    "geocoding_failed_short": "Arrastre el pin para identificar la dirección",
+    "map_unavailable": "No se pudo abrir el mapa. Inténtelo de nuevo."
   },
   },
   "business": {
   "business": {
     "advertise": "Anunciar",
     "advertise": "Anunciar",
@@ -412,6 +415,8 @@
     "description_placeholder": "Hola, deseo un profesional dedicado que hará...",
     "description_placeholder": "Hola, deseo un profesional dedicado que hará...",
     "price_range_title": "Rango de precio por 8 horas",
     "price_range_title": "Rango de precio por 8 horas",
     "price_range_helper": "Selecciona el rango completo de precio para recibir propuestas de profesionales.",
     "price_range_helper": "Selecciona el rango completo de precio para recibir propuestas de profesionales.",
+    "price_preview_label": "{hours}h por",
+    "price_preview_value": "R$ {min} - {max}",
     "date_and_time": "Fecha y hora",
     "date_and_time": "Fecha y hora",
     "success_message": "¡Solicitud personalizada guardada con éxito!",
     "success_message": "¡Solicitud personalizada guardada con éxito!",
     "residential": "Residencial",
     "residential": "Residencial",
@@ -433,7 +438,7 @@
     },
     },
     "today_schedules": {
     "today_schedules": {
       "start_with": "Servicio iniciado con",
       "start_with": "Servicio iniciado con",
-      "started_by": "Servicio iniciado por",
+      "started_by": "Servicio en curso",
       "finished_by": "Servicio concluido por",
       "finished_by": "Servicio concluido por",
       "code_label": "Código",
       "code_label": "Código",
       "in_progress": "en progreso",
       "in_progress": "en progreso",
@@ -508,6 +513,8 @@
     "pending_schedules": {
     "pending_schedules": {
       "title": "En espera de confirmación",
       "title": "En espera de confirmación",
       "requesting_with": "Solicitando reserva con",
       "requesting_with": "Solicitando reserva con",
+      "time_ago": "hace {time}",
+      "schedule_count": "{count} reservas",
       "pay_to_provider": "Realizar pago a",
       "pay_to_provider": "Realizar pago a",
       "no_provider": "Proveedor no definido",
       "no_provider": "Proveedor no definido",
       "cancel_btn": "cancelar",
       "cancel_btn": "cancelar",
@@ -523,6 +530,9 @@
       "detail_time": "Horario:",
       "detail_time": "Horario:",
       "detail_value": "Valor:",
       "detail_value": "Valor:",
       "detail_service_fee": "Total:",
       "detail_service_fee": "Total:",
+      "detail_schedules_title": "Fechas y horarios",
+      "detail_service_fee_pix": "Tarifa de servicio (Pix):",
+      "detail_service_fee_credit_card": "Tarifa de servicio (tarjeta):",
       "detail_total": "Total:",
       "detail_total": "Total:",
       "detail_package_total": "Total del paquete (Pix):",
       "detail_package_total": "Total del paquete (Pix):",
       "detail_pix_total": "Total en Pix:",
       "detail_pix_total": "Total en Pix:",
@@ -735,6 +745,11 @@
   "scheduling_page": {
   "scheduling_page": {
     "title": "Agendamiento",
     "title": "Agendamiento",
     "about_provider": "Sobre el profesional",
     "about_provider": "Sobre el profesional",
+    "provider_info": {
+      "age": "{value} años",
+      "gender": "Género: {value}",
+      "no_service_types": "Ningún tipo de servicio informado."
+    },
     "schedule_service": "Agendar un servicio",
     "schedule_service": "Agendar un servicio",
     "favorite_added": "¡Profesional agregado a favoritos!",
     "favorite_added": "¡Profesional agregado a favoritos!",
     "favorite_removed": "Profesional eliminado de favoritos.",
     "favorite_removed": "Profesional eliminado de favoritos.",
@@ -758,6 +773,8 @@
       "pause_note_4h": "Incluye pausa de hasta 10 minutos.",
       "pause_note_4h": "Incluye pausa de hasta 10 minutos.",
       "slot_required": "Seleccione un horario para continuar."
       "slot_required": "Seleccione un horario para continuar."
     },
     },
+    "price_up_to": "hasta {value}",
+    "cart": "carrinho",
     "service_types": {
     "service_types": {
       "integral": {
       "integral": {
         "label": "Integral",
         "label": "Integral",
@@ -803,6 +820,8 @@
     "empty_message": "Agregue horarios de profesionales favoritos para enviar una solicitud.",
     "empty_message": "Agregue horarios de profesionales favoritos para enviar una solicitud.",
     "add_to_cart": "Agregar a pedidos",
     "add_to_cart": "Agregar a pedidos",
     "add_success": "Horario agregado a pedidos.",
     "add_success": "Horario agregado a pedidos.",
+    "add_more": "agregar otro servicio",
+    "other_provider_blocked": "Tu pedido ya tiene horarios con otro profesional. Finaliza o vacía el pedido actual antes de reservar con otro profesional.",
     "local_cart_title": "Pedidos",
     "local_cart_title": "Pedidos",
     "orders_title": "Pedidos",
     "orders_title": "Pedidos",
     "no_orders": "Ningún pedido creado.",
     "no_orders": "Ningún pedido creado.",
@@ -912,7 +931,9 @@
         "warning_fee": "Las cancelaciones después de este período generan un reembolso del 50% del valor como compensación al profesional.",
         "warning_fee": "Las cancelaciones después de este período generan un reembolso del 50% del valor como compensación al profesional.",
         "btn_cancel": "cancelar",
         "btn_cancel": "cancelar",
         "btn_back": "volver",
         "btn_back": "volver",
-        "btn_keep": "Cancelar el servicio"
+        "btn_keep": "Cancelar el servicio",
+        "package_warning": "Esta reserva forma parte de un paquete con {count} reservas. Al cancelarla, se cancelarán TODAS las reservas futuras del paquete.",
+        "package_warning_generic": "Esta reserva forma parte de un paquete. Al cancelarla, se cancelarán TODAS las reservas futuras del paquete."
       }
       }
     }
     }
   },
   },

+ 27 - 6
src/i18n/locales/pt.json

@@ -1,13 +1,15 @@
 {
 {
   "common": {
   "common": {
     "confirm": "Confirmar",
     "confirm": "Confirmar",
+    "expand": "expandir",
+    "collapse": "recolher",
     "actions": {
     "actions": {
       "save": "Salvar",
       "save": "Salvar",
       "cancel": "Cancelar",
       "cancel": "Cancelar",
       "close": "Fechar",
       "close": "Fechar",
       "edit": "Editar",
       "edit": "Editar",
       "add": "Adicionar",
       "add": "Adicionar",
-      "select": "solicitar",
+      "select": "selecionar",
       "search": "Buscar",
       "search": "Buscar",
       "delete": "Excluir",
       "delete": "Excluir",
       "view": "Visualizar",
       "view": "Visualizar",
@@ -177,7 +179,8 @@
     "complement_required": "Informe o complemento ou marque 'Endereço sem complemento'.",
     "complement_required": "Informe o complemento ou marque 'Endereço sem complemento'.",
     "register_error": "Erro ao finalizar cadastro. Tente novamente.",
     "register_error": "Erro ao finalizar cadastro. Tente novamente.",
     "geocoding_failed": "Endereço não identificado. Arraste o pin para ajustar a localização.",
     "geocoding_failed": "Endereço não identificado. Arraste o pin para ajustar a localização.",
-    "geocoding_failed_short": "Arraste o pin para identificar o endereço"
+    "geocoding_failed_short": "Arraste o pin para identificar o endereço",
+    "map_unavailable": "Não foi possível abrir o mapa. Tente novamente."
   },
   },
   "business": {
   "business": {
     "advertise": "Anunciar",
     "advertise": "Anunciar",
@@ -412,6 +415,8 @@
     "description_placeholder": "Olá, desejo profissional dedicado que irá fazer...",
     "description_placeholder": "Olá, desejo profissional dedicado que irá fazer...",
     "price_range_title": "Faixa de preço por 8 horas",
     "price_range_title": "Faixa de preço por 8 horas",
     "price_range_helper": "Selecione a faixa de preço integral para receber propostas de diaristas.",
     "price_range_helper": "Selecione a faixa de preço integral para receber propostas de diaristas.",
+    "price_preview_label": "{hours}h por",
+    "price_preview_value": "R$ {min} - {max}",
     "date_and_time": "Data e hora",
     "date_and_time": "Data e hora",
     "success_message": "Pedido sob medida salvo com sucesso!",
     "success_message": "Pedido sob medida salvo com sucesso!",
     "residential": "Residencial",
     "residential": "Residencial",
@@ -433,7 +438,7 @@
     },
     },
     "today_schedules": {
     "today_schedules": {
       "start_with": "Início do serviço com",
       "start_with": "Início do serviço com",
-      "started_by": "Serviço iniciado por",
+      "started_by": "Seriço serviço em andamento",
       "finished_by": "Serviço concluído por",
       "finished_by": "Serviço concluído por",
       "code_label": "Código",
       "code_label": "Código",
       "in_progress": "em andamento",
       "in_progress": "em andamento",
@@ -510,7 +515,9 @@
     },
     },
     "pending_schedules": {
     "pending_schedules": {
       "title": "Aguardando confirmação",
       "title": "Aguardando confirmação",
-      "requesting_with": "Solicitando serviço com",
+      "requesting_with": "Solicitando agendamento com",
+      "time_ago": "{time} atrás",
+      "schedule_count": "{count} agendamentos",
       "pay_to_provider": "Realize o pagamento para",
       "pay_to_provider": "Realize o pagamento para",
       "no_provider": "Prestador não definido",
       "no_provider": "Prestador não definido",
       "cancel_btn": "cancelar",
       "cancel_btn": "cancelar",
@@ -526,6 +533,9 @@
       "detail_time": "Horário:",
       "detail_time": "Horário:",
       "detail_value": "Valor:",
       "detail_value": "Valor:",
       "detail_service_fee": "Total:",
       "detail_service_fee": "Total:",
+      "detail_schedules_title": "Datas e horários",
+      "detail_service_fee_pix": "Taxa de serviço (Pix):",
+      "detail_service_fee_credit_card": "Taxa de serviço (cartão):",
       "detail_total": "Total:",
       "detail_total": "Total:",
       "detail_package_total": "Total do pacote (Pix):",
       "detail_package_total": "Total do pacote (Pix):",
       "detail_pix_total": "Total no Pix:",
       "detail_pix_total": "Total no Pix:",
@@ -746,6 +756,11 @@
   "scheduling_page": {
   "scheduling_page": {
     "title": "Agendamento",
     "title": "Agendamento",
     "about_provider": "Sobre o profissional",
     "about_provider": "Sobre o profissional",
+    "provider_info": {
+      "age": "{value} anos",
+      "gender": "Gênero: {value}",
+      "no_service_types": "Nenhum tipo de serviço informado."
+    },
     "schedule_service": "Agende um serviço",
     "schedule_service": "Agende um serviço",
     "favorite_added": "Diarista adicionado aos favoritos!",
     "favorite_added": "Diarista adicionado aos favoritos!",
     "favorite_removed": "Diarista removido dos favoritos.",
     "favorite_removed": "Diarista removido dos favoritos.",
@@ -769,6 +784,8 @@
       "pause_note_4h": "Incluso pausa de até 10 minutos.",
       "pause_note_4h": "Incluso pausa de até 10 minutos.",
       "slot_required": "Selecione um horário para continuar."
       "slot_required": "Selecione um horário para continuar."
     },
     },
+    "price_up_to": "até {value}",
+    "cart": "carrinho",
     "service_types": {
     "service_types": {
       "integral": {
       "integral": {
         "label": "Integral",
         "label": "Integral",
@@ -814,6 +831,8 @@
     "empty_message": "Adicione horários de profissionais favoritos para enviar uma solicitação.",
     "empty_message": "Adicione horários de profissionais favoritos para enviar uma solicitação.",
     "add_to_cart": "Adicionar aos pedidos",
     "add_to_cart": "Adicionar aos pedidos",
     "add_success": "Horário adicionado aos pedidos.",
     "add_success": "Horário adicionado aos pedidos.",
+    "add_more": "adicionar serviço",
+    "other_provider_blocked": "Seu pedido já possui horários com outro profissional. Finalize ou esvazie o pedido atual antes de agendar com outro profissional.",
     "local_cart_title": "Pedidos",
     "local_cart_title": "Pedidos",
     "orders_title": "Pedidos",
     "orders_title": "Pedidos",
     "no_orders": "Nenhum pedido criado.",
     "no_orders": "Nenhum pedido criado.",
@@ -844,7 +863,7 @@
     "quantity": "Quantidade",
     "quantity": "Quantidade",
     "schedule_count": "{count} horário(s)",
     "schedule_count": "{count} horário(s)",
     "submit": "enviar solicitação",
     "submit": "enviar solicitação",
-    "close": "fechar pedidos",
+    "close": "confirmar pedido(s)",
     "closed_success": "Pedidos fechados. Aguarde a aprovação dos profissionais.",
     "closed_success": "Pedidos fechados. Aguarde a aprovação dos profissionais.",
     "same_provider_required": "Todos os horários devem ser do mesmo profissional.",
     "same_provider_required": "Todos os horários devem ser do mesmo profissional.",
     "unknown": "Sem informação",
     "unknown": "Sem informação",
@@ -923,7 +942,9 @@
         "warning_fee": "Cancelamentos após este período geram reembolso de 50% do valor, como taxa de compensação ao profissional.",
         "warning_fee": "Cancelamentos após este período geram reembolso de 50% do valor, como taxa de compensação ao profissional.",
         "btn_cancel": "cancelar",
         "btn_cancel": "cancelar",
         "btn_back": "Manter o pedido",
         "btn_back": "Manter o pedido",
-        "btn_keep": "Cancelar"
+        "btn_keep": "Cancelar",
+        "package_warning": "Este agendamento faz parte de um pacote com {count} agendamentos. Ao cancelar, TODOS os agendamentos futuros do pacote serão cancelados.",
+        "package_warning_generic": "Este agendamento faz parte de um pacote. Ao cancelar, TODOS os agendamentos futuros do pacote serão cancelados."
       }
       }
     }
     }
   },
   },

+ 6 - 1
src/pages/LoginPage.vue

@@ -291,9 +291,14 @@ const onSubmit = async () => {
   try {
   try {
     switch (steps.value) {
     switch (steps.value) {
       case 1: {
       case 1: {
-        await execute(() => sendCode(email.value, phone.value));
+        let response;
+
+        await execute(async () => {
+          response = await sendCode(email.value, phone.value);
+        });
 
 
         if (Object.keys(serverErrors.value).length === 0) {
         if (Object.keys(serverErrors.value).length === 0) {
+          isLogin.value = response?.data?.payload?.isLogin === true;
           steps.value = 2;
           steps.value = 2;
         }
         }
 
 

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

@@ -250,6 +250,7 @@
                 <q-space />
                 <q-space />
 
 
                 <q-rating
                 <q-rating
+                  v-if="item.client_reviewed || canReview(item)"
                   class="q-mr-sm"
                   class="q-mr-sm"
                   color="amber"
                   color="amber"
                   icon="mdi-star-outline"
                   icon="mdi-star-outline"
@@ -274,7 +275,7 @@
                 />
                 />
 
 
                 <q-btn
                 <q-btn
-                  v-else
+                  v-else-if="canReview(item)"
                   class="btn-rate"
                   class="btn-rate"
                   color="secondary"
                   color="secondary"
                   no-caps
                   no-caps
@@ -427,7 +428,11 @@ const openDetailsDialog = (schedule) => {
   });
   });
 };
 };
 
 
+const canReview = (item) => item.status !== "cancelled";
+
 const openRatingDialog = (schedule) => {
 const openRatingDialog = (schedule) => {
+  if (!canReview(schedule)) return;
+
   $q.dialog({
   $q.dialog({
     component: ScheduleRatingDialog,
     component: ScheduleRatingDialog,
     componentProps: { schedule },
     componentProps: { schedule },

+ 65 - 2
src/pages/dashboard/DashboardPage.vue

@@ -18,8 +18,15 @@
           v-if="pendingServicePackages.length > 0"
           v-if="pendingServicePackages.length > 0"
           :data="pendingServicePackages"
           :data="pendingServicePackages"
           :type="'servicePackage'"
           :type="'servicePackage'"
+          progress-class="progress-fill--urgent"
           @view-details="openServicePackagePaymentDialog"
           @view-details="openServicePackagePaymentDialog"
         />
         />
+        <DashboardPendingSchedules
+          v-if="pendingRequests.length > 0"
+          :data="pendingRequests"
+          progress-class="progress-fill--loop"
+          @cancel="openCancelRequestDialog"
+        />
         <DashboardTodaySchedules v-if="todaySchedules.length > 0" :data="todaySchedules" @rate="openRatingDialog" />
         <DashboardTodaySchedules v-if="todaySchedules.length > 0" :data="todaySchedules" @rate="openRatingDialog" />
         <DashboardScrollAreaSchedules />
         <DashboardScrollAreaSchedules />
         <DashboardClientProposals v-if="clientProposals.length > 0" :data="clientProposals" @refresh-data="reloadDashboard" />
         <DashboardClientProposals v-if="clientProposals.length > 0" :data="clientProposals" @refresh-data="reloadDashboard" />
@@ -40,6 +47,7 @@ import DashboardRegistrationIncomplete from 'src/components/dashboard/DashboardR
 import DashboardPaymentIncomplete from 'src/components/dashboard/DashboardPaymentIncomplete.vue';
 import DashboardPaymentIncomplete from 'src/components/dashboard/DashboardPaymentIncomplete.vue';
 import DashboardPendingSchedules from 'src/components/dashboard/DashboardPendingSchedules.vue';
 import DashboardPendingSchedules from 'src/components/dashboard/DashboardPendingSchedules.vue';
 import ScheduleAcceptedDialog from 'src/components/dashboard/ScheduleAcceptedDialog.vue';
 import ScheduleAcceptedDialog from 'src/components/dashboard/ScheduleAcceptedDialog.vue';
+import ScheduleCancelDialog from 'src/components/dashboard/ScheduleCancelDialog.vue';
 import DashboardScrollAreaSchedules from 'src/components/dashboard/DashboardScrollAreaSchedules.vue';
 import DashboardScrollAreaSchedules from 'src/components/dashboard/DashboardScrollAreaSchedules.vue';
 import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedules.vue';
 import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedules.vue';
 import DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
 import DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
@@ -51,7 +59,7 @@ import DashboardPendingCustomSchedules from 'src/pages/dashboard/components/Dash
 import DashboardClientProposals from 'src/pages/dashboard/components/DashboardClientProposals.vue';
 import DashboardClientProposals from 'src/pages/dashboard/components/DashboardClientProposals.vue';
 import { useRouter } from 'vue-router'
 import { useRouter } from 'vue-router'
 import { onMounted, ref, computed } from 'vue';
 import { onMounted, ref, computed } from 'vue';
-import { useQuasar } from 'quasar';
+import { LocalStorage, useQuasar } from 'quasar';
 import { dadosDashboard } from 'src/api/dashboard';
 import { dadosDashboard } from 'src/api/dashboard';
 import { userStore } from 'src/stores/user';
 import { userStore } from 'src/stores/user';
 import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
 import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
@@ -78,6 +86,30 @@ const loading = ref(true);
 const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSuccessModal'));
 const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSuccessModal'));
 const registrationComplete = computed(() => store.user?.registration_complete ?? true);
 const registrationComplete = computed(() => store.user?.registration_complete ?? true);
 
 
+const pendingRequests = computed(() => {
+  const seenPackages = new Set();
+
+  return pendingSchedules.value.filter((schedule) => {
+    if (schedule.status !== 'pending') return false;
+
+    if (!schedule.service_package_id) return true;
+
+    if (seenPackages.has(schedule.service_package_id)) return false;
+
+    seenPackages.add(schedule.service_package_id);
+
+    return true;
+  });
+});
+
+const openCancelRequestDialog = (schedule) => {
+  $q.dialog({
+    component: ScheduleCancelDialog,
+    componentProps: { schedule },
+  }).onDismiss(() => {
+    reloadDashboard(false);
+  });
+};
 
 
 const openServicePackagePaymentDialog = (servicePackage) => {
 const openServicePackagePaymentDialog = (servicePackage) => {
   $q.dialog({
   $q.dialog({
@@ -88,6 +120,35 @@ const openServicePackagePaymentDialog = (servicePackage) => {
   });
   });
 };
 };
 
 
+const AUTO_SHOWN_PACKAGES_KEY = "sp_payment_popup_shown_ids";
+let autoOpeningPackageDialog = false;
+
+const maybeAutoOpenServicePackagePayment = () => {
+  if (autoOpeningPackageDialog) return;
+
+  const shownIds = LocalStorage.getItem(AUTO_SHOWN_PACKAGES_KEY) ?? [];
+
+  const packageToShow = pendingServicePackages.value.find(
+    (servicePackage) => !shownIds.includes(servicePackage.id),
+  );
+
+  if (!packageToShow) return;
+
+  autoOpeningPackageDialog = true;
+  LocalStorage.set(AUTO_SHOWN_PACKAGES_KEY, [...shownIds, packageToShow.id]);
+
+  $q.dialog({
+    component: ScheduleAcceptedDialog,
+    componentProps: { servicePackage: packageToShow },
+  })
+    .onOk(() => {
+      reloadDashboard();
+    })
+    .onDismiss(() => {
+      autoOpeningPackageDialog = false;
+    });
+};
+
 const reloadDashboard = async (showLoader = true) => {
 const reloadDashboard = async (showLoader = true) => {
   if (showLoader) loading.value = true;
   if (showLoader) loading.value = true;
   const response = await dadosDashboard();
   const response = await dadosDashboard();
@@ -108,7 +169,7 @@ const reloadDashboard = async (showLoader = true) => {
   }
   }
   if( showSuccessModal.value == true) {
   if( showSuccessModal.value == true) {
     $q.dialog({
     $q.dialog({
-       component: FinalSuccesModal   
+       component: FinalSuccesModal
     }).onDismiss(() => {
     }).onDismiss(() => {
       showSuccessModal.value = false;
       showSuccessModal.value = false;
       router.replace({ path: router.currentRoute.value.path, query: {} });
       router.replace({ path: router.currentRoute.value.path, query: {} });
@@ -116,6 +177,8 @@ const reloadDashboard = async (showLoader = true) => {
   }
   }
 
 
   loading.value = false;
   loading.value = false;
+
+  maybeAutoOpenServicePackagePayment();
 };
 };
 
 
 const onRefresh = async (done) => {
 const onRefresh = async (done) => {

+ 26 - 6
src/pages/dashboard/components/DashboardClientProposals.vue

@@ -157,6 +157,8 @@
 <script setup>
 <script setup>
 import { acceptProposal, refuseProposal } from "src/api/customSchedules";
 import { acceptProposal, refuseProposal } from "src/api/customSchedules";
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
+import { useQuasar } from "quasar";
+import SchedulePaymentDialog from "src/components/dashboard/SchedulePaymentDialog.vue";
 
 
 import {
 import {
   chooseprice,
   chooseprice,
@@ -177,6 +179,7 @@ defineProps({
 });
 });
 
 
 const { t } = useI18n();
 const { t } = useI18n();
+const $q = useQuasar();
 
 
 // const formatTime = (time) => {
 // const formatTime = (time) => {
 //   if (!time) return '';
 //   if (!time) return '';
@@ -220,16 +223,33 @@ const formatWeekday = (iso) => {
 };
 };
 
 
 const handleAcceptProposal = async (proposalId) => {
 const handleAcceptProposal = async (proposalId) => {
-  // isLoading.value = true
   try {
   try {
-    await acceptProposal(proposalId);
+    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");
+    });
 
 
-    emit("refreshData");
-    // onDialogOK()
   } catch (error) {
   } catch (error) {
     console.log(error);
     console.log(error);
-  } finally {
-    // isLoading.value = false
   }
   }
 };
 };
 
 

+ 19 - 1
src/pages/location/AddressCompletionPage.vue

@@ -15,6 +15,23 @@
         ref="addressForm"
         ref="addressForm"
         class="address-completion-content"
         class="address-completion-content"
       >
       >
+        <div class="text-text">
+          <span class="font14 fontbold">{{ $t("auth.full_name") }}</span>
+        </div>
+
+        <q-input
+          v-model="form.name"
+          class="bg-surface q-mt-sm q-mb-md"
+          hide-bottom-space
+          input-class="text-text"
+          lazy-rules
+          no-error-icon
+          outlined
+          rounded
+          :placeholder="$t('auth.full_name')"
+          :rules="[inputRules.required]"
+        />
+
         <div class="text-text">
         <div class="text-text">
           <span class="font14 fontbold">{{ $t("common.terms.document") }}</span>
           <span class="font14 fontbold">{{ $t("common.terms.document") }}</span>
         </div>
         </div>
@@ -251,6 +268,7 @@ const addressForm = ref(null);
 const submitting = ref(false);
 const submitting = ref(false);
 
 
 const form = ref({
 const form = ref({
+  name: flowStore.name || "",
   document: flowStore.document || "",
   document: flowStore.document || "",
   number: "",
   number: "",
   no_complement: false,
   no_complement: false,
@@ -291,7 +309,7 @@ const handleConfirm = async () => {
       email: flowStore.email || undefined,
       email: flowStore.email || undefined,
       phone: form.value.phone,
       phone: form.value.phone,
       document: form.value.document,
       document: form.value.document,
-      name: flowStore.name || undefined,
+      name: form.value.name?.trim() || undefined,
       code: flowStore.code,
       code: flowStore.code,
       zip_code: flowStore.confirmedZipCode || undefined,
       zip_code: flowStore.confirmedZipCode || undefined,
       address: flowStore.confirmedAddress || undefined,
       address: flowStore.confirmedAddress || undefined,

+ 59 - 11
src/pages/location/LocationMapPage.vue

@@ -39,7 +39,7 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, onMounted, onUnmounted } from 'vue';
+import { ref, nextTick, onMounted, onUnmounted } from 'vue';
 import { useRouter } from 'vue-router';
 import { useRouter } from 'vue-router';
 import { useQuasar } from 'quasar';
 import { useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
@@ -63,7 +63,35 @@ const currentGeoData = ref(null);
 let googleMap = null;
 let googleMap = null;
 const markerId = ref(null);
 const markerId = ref(null);
 
 
+const transparentEls = [];
+
+const applyMapTransparency = () => {
+  [document.documentElement, document.body, document.getElementById('q-app')]
+    .filter(Boolean)
+    .forEach((el) => {
+      el.style.setProperty('background', 'transparent', 'important');
+      transparentEls.push(el);
+    });
+};
+
+const removeMapTransparency = () => {
+  transparentEls.forEach((el) => el.style.removeProperty('background'));
+  transparentEls.length = 0;
+};
+
+const waitForStableBounds = async (el) => {
+  let previous = -1;
+
+  for (let i = 0; i < 12; i += 1) {
+    const { height } = el.getBoundingClientRect();
+    if (height > 0 && height === previous) return;
+    previous = height;
+    await new Promise((resolve) => setTimeout(resolve, 50));
+  }
+};
+
 const handleBack = async () => {
 const handleBack = async () => {
+  removeMapTransparency();
   await destroyMap();
   await destroyMap();
   router.back();
   router.back();
 };
 };
@@ -71,6 +99,7 @@ const handleBack = async () => {
 const handleConfirm = async () => {
 const handleConfirm = async () => {
   if (!currentGeoData.value) return;
   if (!currentGeoData.value) return;
   flowStore.setConfirmedLocation(currentGeoData.value);
   flowStore.setConfirmedLocation(currentGeoData.value);
+  removeMapTransparency();
   await destroyMap();
   await destroyMap();
   router.push({ name: 'AddressCompletionPage' });
   router.push({ name: 'AddressCompletionPage' });
 };
 };
@@ -125,15 +154,32 @@ onMounted(async () => {
   currentLat.value = lat;
   currentLat.value = lat;
   currentLng.value = lng;
   currentLng.value = lng;
 
 
-  googleMap = await GoogleMap.create({
-    id: 'location-map',
-    element: mapRef.value,
-    apiKey: process.env.GOOGLE_MAPS_API_KEY,
-    config: {
-      center: { lat, lng },
-      zoom: 17,
-    },
-  });
+  applyMapTransparency();
+  await nextTick();
+  await waitForStableBounds(mapRef.value);
+
+  const apiKey = process.env.GOOGLE_MAPS_API_KEY;
+
+  if (!apiKey) {
+    $q.notify({ type: 'negative', message: t('auth.map_unavailable') });
+    return;
+  }
+
+  try {
+    googleMap = await GoogleMap.create({
+      id: 'location-map',
+      element: mapRef.value,
+      apiKey,
+      config: {
+        center: { lat, lng },
+        zoom: 17,
+      },
+    });
+  } catch (error) {
+    console.error('Failed to create map', error);
+    $q.notify({ type: 'negative', message: t('auth.map_unavailable') });
+    return;
+  }
 
 
   markerId.value = await googleMap.addMarker({
   markerId.value = await googleMap.addMarker({
     coordinate: { lat, lng },
     coordinate: { lat, lng },
@@ -159,6 +205,7 @@ onMounted(async () => {
 });
 });
 
 
 onUnmounted(async () => {
 onUnmounted(async () => {
+  removeMapTransparency();
   await destroyMap();
   await destroyMap();
 });
 });
 </script>
 </script>
@@ -169,12 +216,13 @@ onUnmounted(async () => {
   width: 100vw;
   width: 100vw;
   height: 100dvh;
   height: 100dvh;
   overflow: hidden;
   overflow: hidden;
-  background: #e5e3df;
+  background: transparent;
 }
 }
 
 
 .location-map-container {
 .location-map-container {
   width: 100%;
   width: 100%;
   height: 100%;
   height: 100%;
+  background: transparent;
 }
 }
 
 
 .location-map-back-btn {
 .location-map-back-btn {

+ 30 - 0
src/pages/orders/ServicePackagePage.vue

@@ -95,6 +95,18 @@
           </span>
           </span>
         </div>
         </div>
 
 
+        <q-btn
+          class="full-width"
+          color="primary"
+          icon="mdi-plus"
+          no-caps
+          outline
+          padding="8px 16px"
+          rounded
+          :label="$t('service_package.add_more')"
+          @click="openAddMoreDialog"
+        />
+
         <q-card
         <q-card
           v-for="(item, index) in servicePackage.items"
           v-for="(item, index) in servicePackage.items"
           :key="item.local_id"
           :key="item.local_id"
@@ -293,6 +305,7 @@
 
 
 <script setup>
 <script setup>
 import { computed, onMounted, ref } from "vue";
 import { computed, onMounted, ref } from "vue";
+import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 import { createScheduleServicePackage } from "src/api/servicePackage";
 import { createScheduleServicePackage } from "src/api/servicePackage";
 import { getAddresses } from "src/api/address";
 import { getAddresses } from "src/api/address";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
@@ -412,6 +425,23 @@ const hasSingleProvider = computed(() => {
   return providers.size <= 1;
   return providers.size <= 1;
 });
 });
 
 
+const openAddMoreDialog = () => {
+  const firstItem = servicePackage.items[0];
+
+  if (!firstItem) return;
+
+  const provider = servicePackage.provider ?? {
+    provider_id: firstItem.provider_id,
+    provider_name: firstItem.provider_name,
+    profile_media_url: firstItem.provider_photo,
+  };
+
+  $q.dialog({
+    component: SchedulingDialog,
+    componentProps: { provider },
+  });
+};
+
 const primaryAddressLabel = computed(() => {
 const primaryAddressLabel = computed(() => {
   if (addressLoading.value) return t("service_package.loading");
   if (addressLoading.value) return t("service_package.loading");
   if (!primaryAddress.value) return t("service_package.no_primary_address");
   if (!primaryAddress.value) return t("service_package.no_primary_address");

+ 70 - 6
src/pages/schedules/SobMedidaPage.vue

@@ -149,6 +149,25 @@
         <div class="range-helper">
         <div class="range-helper">
           {{ $t('sob_medida.price_range_helper') }}
           {{ $t('sob_medida.price_range_helper') }}
         </div>
         </div>
+
+        <div class="price-preview-grid">
+          <div
+            v-for="preview in pricePreviews"
+            :key="preview.periodType"
+            class="price-preview-col"
+          >
+            <div class="price-preview-label font12 fontbold">
+              {{ $t('sob_medida.price_preview_label', { hours: preview.periodType }) }}
+            </div>
+
+            <div class="price-preview-box font12">
+              {{ $t('sob_medida.price_preview_value', {
+                min: formatPriceAmount(preview.min),
+                max: formatPriceAmount(preview.max)
+              }) }}
+            </div>
+          </div>
+        </div>
       </q-card>
       </q-card>
 
 
       <div class="font16 fontbold section-title gradient-diarista">
       <div class="font16 fontbold section-title gradient-diarista">
@@ -183,7 +202,7 @@ import { getPublicServiceTypes } from 'src/api/serviceTypes'
 import { getPublicSpecialties } from 'src/api/specialties'
 import { getPublicSpecialties } from 'src/api/specialties'
 import {useI18n} from 'vue-i18n'
 import {useI18n} from 'vue-i18n'
 import { userStore } from 'src/stores/user'
 import { userStore } from 'src/stores/user'
-import { calculateDailyPrices } from 'src/helpers/utils'
+import { calculateDailyPrices, chooseprice } from 'src/helpers/utils'
 
 
 const $q = useQuasar()
 const $q = useQuasar()
 const router = useRouter()
 const router = useRouter()
@@ -220,6 +239,22 @@ const providerPrices = computed(() =>
   calculateDailyPrices(priceRange.value.max * quantity.value)
   calculateDailyPrices(priceRange.value.max * quantity.value)
 )
 )
 
 
+const PREVIEW_PERIOD_TYPES = Object.freeze(['6', '4', '2'])
+
+const pricePreviews = computed(() =>
+  PREVIEW_PERIOD_TYPES.map((periodType) => ({
+    periodType,
+    min: chooseprice(periodType, priceRange.value.min),
+    max: chooseprice(periodType, priceRange.value.max)
+  }))
+)
+
+const formatPriceAmount = (value) =>
+  Number(value ?? 0).toLocaleString('pt-BR', {
+    minimumFractionDigits: 2,
+    maximumFractionDigits: 2
+  })
+
 const minPosition = computed(() =>
 const minPosition = computed(() =>
   ((priceRange.value.min - PRICE_LIMITS.min) /
   ((priceRange.value.min - PRICE_LIMITS.min) /
     (PRICE_LIMITS.max - PRICE_LIMITS.min)) * 100
     (PRICE_LIMITS.max - PRICE_LIMITS.min)) * 100
@@ -235,7 +270,8 @@ const openServiceSelection = () => {
     component: ServiceSelectionSheet,
     component: ServiceSelectionSheet,
     componentProps: {
     componentProps: {
       provider: providerPrices.value,
       provider: providerPrices.value,
-      selectedDate: selectedDate.value
+      selectedDate: selectedDate.value,
+      scheduleType: 'custom'
     }
     }
   }).onOk((payload) => {
   }).onOk((payload) => {
     if (payload?.serviceType) {
     if (payload?.serviceType) {
@@ -511,6 +547,32 @@ onMounted(async () => {
   color: #6b6b6b;
   color: #6b6b6b;
 }
 }
 
 
+.price-preview-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 8px;
+  margin-top: 18px;
+}
+
+.price-preview-col {
+  min-width: 0;
+  text-align: center;
+}
+
+.price-preview-label {
+  margin-bottom: 6px;
+  color: #4a4a4a;
+}
+
+.price-preview-box {
+  padding: 8px 4px;
+  border: 1px solid #e2dcf0;
+  border-radius: 12px;
+  background: #fff;
+  line-height: 1.35;
+  color: #7a5cff;
+}
+
 .price-pin,
 .price-pin,
 .price-range :deep(.q-slider__thumb) {
 .price-range :deep(.q-slider__thumb) {
   transition:
   transition:
@@ -671,10 +733,12 @@ onMounted(async () => {
     }
     }
   }
   }
 
 
-  :deep(.q-date__view--months .q-btn),
-  :deep(.q-date__view--years .q-btn) {
-    color: #6366F1 !important;
-  }
+  :deep(.q-date__months .q-btn),
+:deep(.q-date__months .q-btn__content),
+:deep(.q-date__years .q-btn),
+:deep(.q-date__years .q-btn__content) {
+  color: #6366F1 !important;
+}
 }
 }
 
 
 
 

+ 19 - 0
src/pages/search/components/SchedulingDialog.vue

@@ -347,6 +347,11 @@ const getWeekStart = (dateStr) => {
   return d.toISOString().split("T")[0];
   return d.toISOString().split("T")[0];
 };
 };
 
 
+const cartHasAnotherProvider = () =>
+  servicePackage.items.length > 0 &&
+  Number(servicePackage.items[0].provider_id) !==
+    Number(props.provider.provider_id);
+
 const getServicePackageWeekCount = (selectedDate) => {
 const getServicePackageWeekCount = (selectedDate) => {
   const weekStart = getWeekStart(selectedDate);
   const weekStart = getWeekStart(selectedDate);
 
 
@@ -541,6 +546,7 @@ const onDateSelected = (val) => {
       partialBlocks,
       partialBlocks,
       provider: props.provider,
       provider: props.provider,
       selectedDate: val,
       selectedDate: val,
+      scheduleType: 'default'
     },
     },
   }).onOk(({ action, serviceType, date: date_, provider: prov }) => {
   }).onOk(({ action, serviceType, date: date_, provider: prov }) => {
     $q.dialog({
     $q.dialog({
@@ -553,6 +559,15 @@ const onDateSelected = (val) => {
       },
       },
     }).onOk((booking) => {
     }).onOk((booking) => {
       if (action === "servicePackage") {
       if (action === "servicePackage") {
+        if (cartHasAnotherProvider()) {
+          $q.notify({
+            message: t("service_package.other_provider_blocked"),
+            type: "warning",
+          });
+
+          return;
+        }
+
         servicePackage.addItem({
         servicePackage.addItem({
           date: normalizeDate(booking.date),
           date: normalizeDate(booking.date),
           end_time: formatHour(booking.slot.endHour),
           end_time: formatHour(booking.slot.endHour),
@@ -572,6 +587,10 @@ const onDateSelected = (val) => {
             booking.serviceType.basePrice ?? booking.serviceType.price,
             booking.serviceType.basePrice ?? booking.serviceType.price,
         });
         });
 
 
+        if (!servicePackage.provider) {
+          servicePackage.setProvider({ ...props.provider });
+        }
+
         $q.notify({
         $q.notify({
           message: t("service_package.add_success"),
           message: t("service_package.add_success"),
           type: "positive",
           type: "positive",

+ 9 - 3
src/pages/search/components/ServiceSelectionSheet.vue

@@ -58,7 +58,12 @@
               v-if="type.price"
               v-if="type.price"
                class="service-price"
                class="service-price"
             >
             >
-              {{ formatCurrency(type.price) }}
+              <div v-if="props.scheduleType == 'custom'">
+                {{ $t('scheduling_page.price_up_to', { value: formatCurrency(type.price) }) }}
+              </div>
+              <div v-else>
+                {{ formatCurrency(type.price) }}
+              </div>
             </div>
             </div>
 
 
           <div class="row no-wrap q-gutter-x-xs">
           <div class="row no-wrap q-gutter-x-xs">
@@ -69,7 +74,7 @@
               rounded
               rounded
               size="sm"
               size="sm"
               unelevated
               unelevated
-              :label="$t('common.actions.select')"
+              :label="$t('scheduling_page.cart')"
               @click="onDialogOK({ action: 'servicePackage', serviceType: type, date: selectedDate, provider })"
               @click="onDialogOK({ action: 'servicePackage', serviceType: type, date: selectedDate, provider })"
             />
             />
           </div>
           </div>
@@ -93,6 +98,7 @@ const props = defineProps({
   partialBlocks: { default: () => [], required: false, type: Array },
   partialBlocks: { default: () => [], required: false, type: Array },
   provider: { required: true, type: Object },
   provider: { required: true, type: Object },
   selectedDate: { required: true, type: String },
   selectedDate: { required: true, type: String },
+  scheduleType: { required: false, type: String, default: "default" },
 });
 });
 
 
 const $q = useQuasar();
 const $q = useQuasar();
@@ -188,7 +194,7 @@ const openInfo = (type) => {
   margin: 0 20px;
   margin: 0 20px;
   text-align: right;
   text-align: right;
 
 
-  font-size: 18px;
+  font-size: 14px;
   font-weight: 700;
   font-weight: 700;
   color: var(--q-text);
   color: var(--q-text);
 }
 }

+ 51 - 2
src/stores/servicePackage.js

@@ -1,21 +1,68 @@
 import { defineStore } from "pinia";
 import { defineStore } from "pinia";
-import { ref } from "vue";
+import { LocalStorage, date } from "quasar";
+import { ref, watch } from "vue";
+
+const STORAGE_KEY = "service_package_cart";
+
+const loadPersistedCart = () => {
+  const persisted = LocalStorage.getItem(STORAGE_KEY);
+
+  if (!persisted || !Array.isArray(persisted.items)) {
+    return { items: [], provider: null };
+  }
+
+  const today = date.formatDate(Date.now(), "YYYY-MM-DD");
+
+  const items = persisted.items.filter(
+    (item) => item && item.date && item.date >= today,
+  );
+
+  return {
+    items,
+    provider: items.length ? (persisted.provider ?? null) : null,
+  };
+};
 
 
 export const useServicePackageStore = defineStore("servicePackage", () => {
 export const useServicePackageStore = defineStore("servicePackage", () => {
-  const items = ref([]);
+  const persisted = loadPersistedCart();
+
+  const items = ref(persisted.items);
+
+  const provider = ref(persisted.provider);
 
 
   const lastCreatedGroup = ref(null);
   const lastCreatedGroup = ref(null);
 
 
+  watch(
+    [items, provider],
+    () => {
+      LocalStorage.set(STORAGE_KEY, {
+        items: items.value,
+        provider: provider.value,
+      });
+    },
+    { deep: true },
+  );
+
   const addItem = (item) => {
   const addItem = (item) => {
     items.value.push(item);
     items.value.push(item);
   };
   };
 
 
   const removeItem = (index) => {
   const removeItem = (index) => {
     items.value.splice(index, 1);
     items.value.splice(index, 1);
+
+    if (!items.value.length) {
+      provider.value = null;
+    }
+  };
+
+  const setProvider = (value) => {
+    provider.value = value ? { ...value } : null;
   };
   };
 
 
   const clear = () => {
   const clear = () => {
     items.value = [];
     items.value = [];
+    provider.value = null;
+    LocalStorage.remove(STORAGE_KEY);
   };
   };
 
 
   const setLastCreatedGroup = (group) => {
   const setLastCreatedGroup = (group) => {
@@ -24,9 +71,11 @@ export const useServicePackageStore = defineStore("servicePackage", () => {
 
 
   return {
   return {
     items,
     items,
+    provider,
     lastCreatedGroup,
     lastCreatedGroup,
     addItem,
     addItem,
     removeItem,
     removeItem,
+    setProvider,
     clear,
     clear,
     setLastCreatedGroup,
     setLastCreatedGroup,
   };
   };