26 Angajamente 5f4d43e8a5 ... c1ddf77e09

Autor SHA1 Permisiunea de a trimite mesaje. Dacă este dezactivată, utilizatorul nu va putea trimite nici un fel de mesaj Data
  Gustavo Zanatta c1ddf77e09 fix pushs 1 săptămână în urmă
  Gustavo Zanatta f7ca60b223 padronizacao indicador de multiplos cards 1 săptămână în urmă
  Gustavo Zanatta 3388f5fc40 retirando sombras 1 săptămână în urmă
  Gustavo Zanatta cec075de8a style: animacao no card 'solicitando sob medida' 1 săptămână în urmă
  Gustavo Zanatta b70687cd90 ajustes avaliacao: max 5 files, so permite anexar foto em avaliaca negativa, nao tava exibindo foto do avaliado, pré-seleciona favorito se ja era antes 1 săptămână în urmă
  Gustavo Zanatta f62cbb791b fix comentario na avaliacao 1 săptămână în urmă
  Gustavo Zanatta 39d77bd90e feat: redireciona e abre dealhes do agendamento no bloco servicos do dia 1 săptămână în urmă
  Gustavo Zanatta 5727d3f188 ajuste botao agendar dos favoritos e redirecionamento correto 1 săptămână în urmă
  Gustavo Zanatta 09a4b7fc4c ajuste layout botao 'tenho um codigo' 1 săptămână în urmă
  Gustavo Zanatta 2124d84aaf ajustes aba pedidos: troca labels, reordena componentes e troca botao de lugar 1 săptămână în urmă
  Gustavo Zanatta 22476a3e53 ajuste label dos botoes 1 săptămână în urmă
  Gustavo Zanatta 5bc4130c56 fix status em andamento e exibicao do codigo 1 săptămână în urmă
  Gustavo Zanatta 583068ba01 chip 'atencao diaria' no bloco de servicos do dia 1 săptămână în urmă
  Gustavo Zanatta 738a230e26 ajuste no compartilhamento de codigo unico para favorito 1 săptămână în urmă
  Gustavo Zanatta d4eca26a39 fix light dark 1 săptămână în urmă
  Gustavo Zanatta c8d7eb8135 feat: codigo unico para favorito 1 săptămână în urmă
  Gustavo Zanatta 8aa1f6a48f fix botoes cancelar 1 săptămână în urmă
  Gustavo Zanatta e045b0371b Merge branch 'development' of gogs.softpar.inf.br:Softpar/sfp_front_vue_diarista_cliente into development 2 săptămâni în urmă
  Gustavo Zanatta f1812beadf chore version 2.7 + aba pedidos com agendamentos 2 săptămâni în urmă
  Gustavo Mantovani c868e260df refactor: secao de pagamento 2 săptămâni în urmă
  Gustavo Zanatta 70f07f926a fix bloco servicos do dia 2 săptămâni în urmă
  Gustavo Zanatta 9b6a36a0e1 ajustes share 2 săptămâni în urmă
  Gustavo Mantovani 987a2dcb7e fix: apply payment status so atualizava quando status era paid, prendia a tela em um estado sem payment e sem as infos (pix) 2 săptămâni în urmă
  Gustavo Zanatta ca26f611e2 change default to 6h 2 săptămâni în urmă
  Gustavo Zanatta c396efd34a remove permission nao necessaria 2 săptămâni în urmă
  Gustavo Zanatta a37937f79a chore version 2.6 2 săptămâni în urmă
53 a modificat fișierele cu 2065 adăugiri și 851 ștergeri
  1. 2 2
      package-lock.json
  2. 1 1
      package.json
  3. 1 1
      quasar.config.js
  4. 2 2
      src-capacitor/android/app/build.gradle
  5. 1 0
      src-capacitor/android/app/capacitor.build.gradle
  6. 0 1
      src-capacitor/android/app/src/main/AndroidManifest.xml
  7. 3 0
      src-capacitor/android/app/src/main/res/values/styles.xml
  8. 3 0
      src-capacitor/android/capacitor.settings.gradle
  9. 2 1
      src-capacitor/capacitor.config.json
  10. 5 0
      src-capacitor/ios/App/Podfile
  11. 12 2
      src-capacitor/package-lock.json
  12. 2 1
      src-capacitor/package.json
  13. 6 30
      src/App.vue
  14. 9 0
      src/api/clientFavoriteProvider.js
  15. 33 46
      src/boot/push-notifications.js
  16. 16 2
      src/components/dashboard/DashboardFavoriteProviders.vue
  17. 43 39
      src/components/dashboard/DashboardHeaderBar.vue
  18. 16 2
      src/components/dashboard/DashboardLastDoneSchedules.vue
  19. 31 29
      src/components/dashboard/DashboardNextSchedules.vue
  20. 64 107
      src/components/dashboard/DashboardPendingSchedules.vue
  21. 24 30
      src/components/dashboard/DashboardProvidersClose.vue
  22. 16 1
      src/components/dashboard/DashboardScrollAreaSchedules.vue
  23. 37 10
      src/components/dashboard/DashboardTodaySchedules.vue
  24. 194 0
      src/components/dashboard/PackageScheduleDetailsDialog.vue
  25. 72 0
      src/components/defaults/DefaultCarouselIndicator.vue
  26. 4 2
      src/components/login/LoginStep1Panel.vue
  27. 162 0
      src/components/profile/ProfileFavoriteCodeDialog.vue
  28. 59 4
      src/components/profile/ProfileFavoritesDialog.vue
  29. 5 2
      src/composables/useAuth.js
  30. 121 0
      src/composables/useCarouselIndicator.js
  31. 120 0
      src/composables/usePushNotifications.js
  32. 14 0
      src/helpers/paymentPlatformFees.js
  33. 43 1
      src/helpers/scheduleDate.js
  34. 33 1
      src/helpers/utils.js
  35. 17 3
      src/i18n/locales/en.json
  36. 17 3
      src/i18n/locales/es.json
  37. 18 4
      src/i18n/locales/pt.json
  38. 75 58
      src/pages/agenda/CalendarPage.vue
  39. 118 71
      src/pages/dashboard/DashboardPage.vue
  40. 35 36
      src/pages/dashboard/components/DashboardClientProposals.vue
  41. 59 1
      src/pages/dashboard/components/DashboardPendingCustomSchedules.vue
  42. 20 43
      src/pages/dashboard/components/schedule/NextSchedulesDetailsDialog.vue
  43. 56 48
      src/pages/dashboard/components/schedule/ScheduleAcceptedDialog.vue
  44. 28 23
      src/pages/dashboard/components/schedule/ScheduleCancelDialog.vue
  45. 59 65
      src/pages/dashboard/components/schedule/SchedulePaymentDialog.vue
  46. 125 75
      src/pages/dashboard/components/schedule/SchedulePaymentPixDialog.vue
  47. 33 26
      src/pages/dashboard/components/schedule/SchedulePaymentProcessingDialog.vue
  48. 89 24
      src/pages/dashboard/components/schedule/ScheduleRatingDialog.vue
  49. 92 17
      src/pages/orders/ServicePackagePage.vue
  50. 11 5
      src/pages/orders/components/CustomScheduleRequestCard.vue
  51. 1 2
      src/pages/search/SearchPage.vue
  52. 23 2
      src/pages/search/components/SchedulingDialog.vue
  53. 33 28
      src/stores/payment.js

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
 {
   "name": "diarista-cliente-app",
   "name": "diarista-cliente-app",
-  "version": "2.5",
+  "version": "2.8",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "diarista-cliente-app",
       "name": "diarista-cliente-app",
-      "version": "2.5",
+      "version": "2.8",
       "dependencies": {
       "dependencies": {
         "@bufbuild/protobuf": "^2.5.1",
         "@bufbuild/protobuf": "^2.5.1",
         "@capacitor/browser": "^7.0.5",
         "@capacitor/browser": "^7.0.5",

+ 1 - 1
package.json

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

+ 1 - 1
quasar.config.js

@@ -169,7 +169,7 @@ export default defineConfig((ctx) => {
     framework: {
     framework: {
       lang: "pt-BR",
       lang: "pt-BR",
       config: {
       config: {
-        dark: "auto",
+        dark: false,
         notify: {
         notify: {
           position: "top-right",
           position: "top-right",
         },
         },

+ 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 16
-        versionName "2.5"
+        versionCode 19
+        versionName "2.8"
         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.

+ 1 - 0
src-capacitor/android/app/capacitor.build.gradle

@@ -18,6 +18,7 @@ dependencies {
     implementation project(':capacitor-keyboard')
     implementation project(':capacitor-keyboard')
     implementation project(':capacitor-preferences')
     implementation project(':capacitor-preferences')
     implementation project(':capacitor-push-notifications')
     implementation project(':capacitor-push-notifications')
+    implementation project(':capacitor-share')
     implementation project(':capacitor-splash-screen')
     implementation project(':capacitor-splash-screen')
     implementation project(':capacitor-status-bar')
     implementation project(':capacitor-status-bar')
 
 

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

@@ -46,7 +46,6 @@
     <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-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" />

+ 3 - 0
src-capacitor/android/app/src/main/res/values/styles.xml

@@ -13,6 +13,9 @@
         <item name="windowActionBar">false</item>
         <item name="windowActionBar">false</item>
         <item name="windowNoTitle">true</item>
         <item name="windowNoTitle">true</item>
         <item name="android:background">@null</item>
         <item name="android:background">@null</item>
+
+        <item name="android:windowLightStatusBar">true</item>
+        <item name="android:windowLightNavigationBar">true</item>
     </style>
     </style>
 
 
 
 

+ 3 - 0
src-capacitor/android/capacitor.settings.gradle

@@ -29,6 +29,9 @@ project(':capacitor-preferences').projectDir = new File('../node_modules/@capaci
 include ':capacitor-push-notifications'
 include ':capacitor-push-notifications'
 project(':capacitor-push-notifications').projectDir = new File('../node_modules/@capacitor/push-notifications/android')
 project(':capacitor-push-notifications').projectDir = new File('../node_modules/@capacitor/push-notifications/android')
 
 
+include ':capacitor-share'
+project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
+
 include ':capacitor-splash-screen'
 include ':capacitor-splash-screen'
 project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android')
 project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android')
 
 

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

@@ -8,7 +8,8 @@
       "resizeOnFullScreen": true
       "resizeOnFullScreen": true
     },
     },
     "StatusBar": {
     "StatusBar": {
-      "overlaysWebView": true
+      "overlaysWebView": true,
+      "style": "LIGHT"
     },
     },
     "SplashScreen": {
     "SplashScreen": {
       "launchAutoHide": false,
       "launchAutoHide": false,

+ 5 - 0
src-capacitor/ios/App/Podfile

@@ -13,10 +13,15 @@ def capacitor_pods
   pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
   pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
   pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
   pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
   pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
   pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
+  pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
   pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
   pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
+  pod 'CapacitorGeolocation', :path => '../../node_modules/@capacitor/geolocation'
+  pod 'CapacitorGoogleMaps', :path => '../../node_modules/@capacitor/google-maps'
   pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
   pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
   pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
   pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
   pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
   pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
+  pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
+  pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
   pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
   pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
 end
 end
 
 

+ 12 - 2
src-capacitor/package-lock.json

@@ -1,12 +1,12 @@
 {
 {
   "name": "diarista-cliente-app",
   "name": "diarista-cliente-app",
-  "version": "0.0.1",
+  "version": "2.8",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "diarista-cliente-app",
       "name": "diarista-cliente-app",
-      "version": "0.0.1",
+      "version": "2.8",
       "dependencies": {
       "dependencies": {
         "@capacitor/android": "^7.6.8",
         "@capacitor/android": "^7.6.8",
         "@capacitor/app": "^7.0.0",
         "@capacitor/app": "^7.0.0",
@@ -21,6 +21,7 @@
         "@capacitor/keyboard": "^7.0.1",
         "@capacitor/keyboard": "^7.0.1",
         "@capacitor/preferences": "^7.0.1",
         "@capacitor/preferences": "^7.0.1",
         "@capacitor/push-notifications": "^7.0.7",
         "@capacitor/push-notifications": "^7.0.7",
+        "@capacitor/share": "^7.0.4",
         "@capacitor/splash-screen": "^7.0.5",
         "@capacitor/splash-screen": "^7.0.5",
         "@capacitor/status-bar": "^7.0.1"
         "@capacitor/status-bar": "^7.0.1"
       }
       }
@@ -172,6 +173,15 @@
         "@capacitor/core": ">=7.0.0"
         "@capacitor/core": ">=7.0.0"
       }
       }
     },
     },
+    "node_modules/@capacitor/share": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/@capacitor/share/-/share-7.0.4.tgz",
+      "integrity": "sha512-wNXxmXUcChrtbZ5jQv8scFIbIGKo3rk6B7qpWySxfUhYJP5NANgAqQ9Z69m0k/zYsucdh66SBU7XW1ykd9aUug==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@capacitor/core": ">=7.0.0"
+      }
+    },
     "node_modules/@capacitor/splash-screen": {
     "node_modules/@capacitor/splash-screen": {
       "version": "7.0.5",
       "version": "7.0.5",
       "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-7.0.5.tgz",
       "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-7.0.5.tgz",

+ 2 - 1
src-capacitor/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "diarista-cliente-app",
   "name": "diarista-cliente-app",
-  "version": "2.5",
+  "version": "2.8",
   "description": "Aplicativo para o cliente do Diaria",
   "description": "Aplicativo para o cliente do Diaria",
   "author": "zntt <zanattagg@gmail.com>",
   "author": "zntt <zanattagg@gmail.com>",
   "private": true,
   "private": true,
@@ -18,6 +18,7 @@
     "@capacitor/keyboard": "^7.0.1",
     "@capacitor/keyboard": "^7.0.1",
     "@capacitor/preferences": "^7.0.1",
     "@capacitor/preferences": "^7.0.1",
     "@capacitor/push-notifications": "^7.0.7",
     "@capacitor/push-notifications": "^7.0.7",
+    "@capacitor/share": "^7.0.4",
     "@capacitor/splash-screen": "^7.0.5",
     "@capacitor/splash-screen": "^7.0.5",
     "@capacitor/status-bar": "^7.0.1"
     "@capacitor/status-bar": "^7.0.1"
   }
   }

+ 6 - 30
src/App.vue

@@ -18,11 +18,7 @@ const { locale } = useI18n();
 
 
 const $q = useQuasar();
 const $q = useQuasar();
 
 
-const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
-  ? "dark"
-  : "light";
-
-const updateStatusBar = async (isDark) => {
+const updateStatusBar = async () => {
   if (!Capacitor.isNativePlatform()) {
   if (!Capacitor.isNativePlatform()) {
     return;
     return;
   }
   }
@@ -30,24 +26,10 @@ const updateStatusBar = async (isDark) => {
   await StatusBar.show();
   await StatusBar.show();
 
 
   await StatusBar.setStyle({
   await StatusBar.setStyle({
-    style: isDark ? Style.Dark : Style.Light,
+    style: Style.Light,
   });
   });
 };
 };
 
 
-watch(
-  () => $q.dark.isActive,
-  async (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) => {
@@ -59,16 +41,9 @@ watch(
 );
 );
 
 
 onBeforeMount(async () => {
 onBeforeMount(async () => {
-  const { value: themePref } = await Preferences.get({
-    key: "theme",
-  });
+  $q.dark.set(false);
 
 
-  const theme = themePref || systemTheme;
-  const isDark = theme === "dark";
-
-  $q.dark.set(isDark);
-
-  await updateStatusBar(isDark);
+  await updateStatusBar();
 
 
   const { value: localePref } = await Preferences.get({
   const { value: localePref } = await Preferences.get({
     key: "locale",
     key: "locale",
@@ -76,6 +51,7 @@ onBeforeMount(async () => {
 
 
   const userLocale = localePref || window.navigator.language;
   const userLocale = localePref || window.navigator.language;
 
 
-  console.log(theme, userLocale);
+  console.log(userLocale);
 });
 });
+
 </script>
 </script>

+ 9 - 0
src/api/clientFavoriteProvider.js

@@ -19,3 +19,12 @@ export const deleteClientFavoriteProvider = async (id) => {
   const { data } = await api.delete(`/client/favorite-provider/${id}`);
   const { data } = await api.delete(`/client/favorite-provider/${id}`);
   return data.payload;
   return data.payload;
 }
 }
+
+export const createClientFavoriteProviderByCode = async (clientId, code) => {
+  const { data } = await api.post(
+    `/client/favorite-provider/by-code`,
+    { client_id: clientId, code },
+  );
+
+  return data.payload;
+};

+ 33 - 46
src/boot/push-notifications.js

@@ -1,65 +1,52 @@
 import { defineBoot } from "#q-app/wrappers";
 import { defineBoot } from "#q-app/wrappers";
 import { Capacitor } from "@capacitor/core";
 import { Capacitor } from "@capacitor/core";
+import { Notify } from "quasar";
 import { PushNotifications } from "@capacitor/push-notifications";
 import { PushNotifications } from "@capacitor/push-notifications";
-import { Preferences } from "@capacitor/preferences";
-import { registerDeviceToken } from "src/api/deviceToken";
+import { usePushNotifications } from "src/composables/usePushNotifications";
 
 
-const FCM_TOKEN_KEY = "pending_fcm_token";
-
-export const registerPendingFcmToken = async () => {
-  if (!Capacitor.isNativePlatform()) return;
-
-  const { value } = await Preferences.get({ key: FCM_TOKEN_KEY });
-  if (!value) return;
-
-  try {
-    const { token, platform } = JSON.parse(value);
-    await registerDeviceToken(token, platform);
-    await Preferences.remove({ key: FCM_TOKEN_KEY });
-  } catch {
-    // mantém o token pendente para tentar na próxima autenticação
-  }
-};
-
-const setupPushNotifications = async () => {
-  let permission = await PushNotifications.checkPermissions();
-
-  if (permission.receive === "prompt") {
-    permission = await PushNotifications.requestPermissions();
-  }
-
-  if (permission.receive !== "granted") {
-    return;
-  }
-
-  await PushNotifications.register();
-};
+const CHANNEL_ID = "diaria";
 
 
 export default defineBoot(async () => {
 export default defineBoot(async () => {
   if (!Capacitor.isNativePlatform()) {
   if (!Capacitor.isNativePlatform()) {
     return;
     return;
   }
   }
 
 
-  await PushNotifications.createChannel({
-    id: "default",
-    name: "Notificações",
-    description: "Canal padrão de notificações",
-    importance: 4, // HIGH
-    visibility: 1, // PUBLIC
-    vibration: true,
-  }).catch(() => {});
+  const { requestPermissionAndRegister, setDeviceToken, syncDeviceToken } =
+    usePushNotifications();
+
+  if (Capacitor.getPlatform() === "android") {
+    await PushNotifications.createChannel({
+      id: CHANNEL_ID,
+      name: "Notificações",
+      description: "Canal padrão de notificações",
+      importance: 4, // HIGH
+      visibility: 1, // PUBLIC
+      vibration: true,
+    }).catch(() => {});
+
+    await PushNotifications.deleteChannel({ id: "default" }).catch(() => {});
+  }
 
 
   PushNotifications.addListener("registration", async (token) => {
   PushNotifications.addListener("registration", async (token) => {
-    const platform = Capacitor.getPlatform();
-    await Preferences.set({
-      key: FCM_TOKEN_KEY,
-      value: JSON.stringify({ token: token.value, platform }),
-    });
+    await setDeviceToken(token.value);
+
+    await syncDeviceToken();
   });
   });
 
 
   PushNotifications.addListener("registrationError", () => {
   PushNotifications.addListener("registrationError", () => {
     // falha silenciosa
     // falha silenciosa
   });
   });
 
 
-  setupPushNotifications();
+  PushNotifications.addListener("pushNotificationReceived", (notification) => {
+    Notify.create({
+      message: notification.title || "Nova notificação",
+      caption: notification.body,
+      type: "info",
+      position: "top",
+      timeout: 6000,
+      actions: [{ icon: "close", color: "white", round: true }],
+    });
+  });
+
+  requestPermissionAndRegister();
 });
 });

+ 16 - 2
src/components/dashboard/DashboardFavoriteProviders.vue

@@ -6,7 +6,11 @@
       {{ $t("dashboard_client.favorites.title") }}
       {{ $t("dashboard_client.favorites.title") }}
     </div>
     </div>
 
 
-    <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.provider_id"
         :key="item.provider_id"
@@ -75,6 +79,11 @@
         </q-card-section>
         </q-card-section>
       </q-card>
       </q-card>
     </div>
     </div>
+
+    <DefaultCarouselIndicator
+      :active="activeIndex"
+      :count="pageCount"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -82,14 +91,19 @@
 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 { useCarouselIndicator } from "src/composables/useCarouselIndicator";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 
 
+import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 
 
-defineProps({ data: { default: () => [], type: Array } });
+const props = defineProps({ data: { default: () => [], type: Array } });
 
 
 const $q = useQuasar();
 const $q = useQuasar();
 
 
+const { activeIndex, onTrackScroll, pageCount, trackRef } =
+  useCarouselIndicator(() => props.data);
+
 const goToScheduling = (provider) => {
 const goToScheduling = (provider) => {
   $q.dialog({
   $q.dialog({
     component: SchedulingDialog,
     component: SchedulingDialog,

+ 43 - 39
src/components/dashboard/DashboardHeaderBar.vue

@@ -3,59 +3,60 @@
   <div class="dashboard-header shadow-card bg-white row items-center no-wrap q-px-md q-pb-sm">
   <div class="dashboard-header shadow-card bg-white row items-center no-wrap q-px-md q-pb-sm">
     <div class="col column q-gutter-y-xs">
     <div class="col column q-gutter-y-xs">
       <div class="row items-center q-gutter-x-xs">
       <div class="row items-center q-gutter-x-xs">
-        <q-icon name="mdi-star" color="warning" size="14px" />
-        <span class="dashboard-metric-value font10 fontregular">{{ data?.rating != null ? Number(data.rating).toFixed(1).replace('.', ',') : '-' }}</span>
+        <q-icon color="warning" name="mdi-star" size="14px" />
+
+        <span class="dashboard-metric-value font10 fontregular">{{ formatRating(data?.rating, '-') }}</span>
+
         <span class="dashboard-metric-meta font10 fontregular">({{ data?.total_ratings ?? 0 }})</span>
         <span class="dashboard-metric-meta font10 fontregular">({{ data?.total_ratings ?? 0 }})</span>
       </div>
       </div>
+
       <div class="row items-center q-gutter-x-xs">
       <div class="row items-center q-gutter-x-xs">
-        <q-icon name="mdi-broom" color="secondary" size="14px" />
+        <q-icon color="secondary" name="mdi-broom" size="14px" />
+
         <span class="dashboard-metric-value font10 fontregular">{{ data?.total_services ?? 0 }}</span>
         <span class="dashboard-metric-value font10 fontregular">{{ data?.total_services ?? 0 }}</span>
       </div>
       </div>
     </div>
     </div>
 
 
     <div class="col-auto row justify-center">
     <div class="col-auto row justify-center">
-      <img :src="LogoDiariaColorida" alt="Diária" class="dashboard-logo" />
+      <img alt="Diária" class="dashboard-logo" :src="LogoDiariaColorida" />
     </div>
     </div>
 
 
     <div class="col row justify-end items-center">
     <div class="col row justify-end items-center">
 
 
-  <q-btn
-    flat
-    round
-    dense
-    color="grey-7"
-    size="sm"
-    @click="goToNotifications"
-  >
-
-    <q-icon
-      name="mdi-bell-outline"
-      size="20px"
-    />
-
-    <q-badge
-      v-if="unreadNotifications > 0"
-      floating
-      rounded
-      color="pink"
-      class="notification-badge font10 fontregular"
-    >
-      {{ unreadNotifications }}
-    </q-badge>
-
-  </q-btn>
-
-</div>
+      <q-btn
+        color="grey-7"
+        dense
+        flat
+        round
+        size="sm"
+        @click="goToNotifications"
+      >
+        <q-icon
+          name="mdi-bell-outline"
+          size="20px"
+        />
+
+        <q-badge
+          v-if="unreadNotifications > 0"
+          class="notification-badge font10 fontregular"
+          color="pink"
+          floating
+          rounded
+        >
+          {{ unreadNotifications }}
+        </q-badge>
+      </q-btn>
+
+    </div>
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import LogoDiariaColorida from 'src/assets/logo_diaria_colorido_sem_texto.svg'
-
 import { computed } from 'vue'
 import { computed } from 'vue'
+import { formatRating } from 'src/helpers/utils'
 import { useRouter } from 'vue-router'
 import { useRouter } from 'vue-router'
 
 
-const router = useRouter()
+import LogoDiariaColorida from 'src/assets/logo_diaria_colorido_sem_texto.svg'
 
 
 const props = defineProps({
 const props = defineProps({
   data: {
   data: {
@@ -69,6 +70,8 @@ const props = defineProps({
   }
   }
 })
 })
 
 
+const router = useRouter()
+
 //vai para dashboard as notificações tem que ser mocada no backend
 //vai para dashboard as notificações tem que ser mocada no backend
 const unreadNotifications = computed(() => {
 const unreadNotifications = computed(() => {
   return props.notifications.filter((notification) => !notification.read).length
   return props.notifications.filter((notification) => !notification.read).length
@@ -76,12 +79,13 @@ const unreadNotifications = computed(() => {
 
 
 const goToNotifications = () => {
 const goToNotifications = () => {
   router.push({
   router.push({
-  name: 'NotificationsPage',
-  query: {
-    notifications: JSON.stringify(props.notifications)
-  }
-})
+    name: 'NotificationsPage',
+    query: {
+      notifications: JSON.stringify(props.notifications)
+    }
+  })
 }
 }
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 16 - 2
src/components/dashboard/DashboardLastDoneSchedules.vue

@@ -6,7 +6,11 @@
       {{ $t("dashboard_client.last_schedules.title") }}
       {{ $t("dashboard_client.last_schedules.title") }}
     </div>
     </div>
 
 
-    <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"
@@ -54,20 +58,30 @@
         </q-card-section>
         </q-card-section>
       </q-card>
       </q-card>
     </div>
     </div>
+
+    <DefaultCarouselIndicator
+      :active="activeIndex"
+      :count="pageCount"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
 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 { useCarouselIndicator } from "src/composables/useCarouselIndicator";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 
 
+import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 
 
-defineProps({ data: { type: Array, default: () => [] } });
+const props = defineProps({ data: { type: Array, default: () => [] } });
 
 
 const $q = useQuasar();
 const $q = useQuasar();
 
 
+const { activeIndex, onTrackScroll, pageCount, trackRef } =
+  useCarouselIndicator(() => props.data);
+
 const goToScheduling = (provider) => {
 const goToScheduling = (provider) => {
   $q.dialog({
   $q.dialog({
     component: SchedulingDialog,
     component: SchedulingDialog,

+ 31 - 29
src/components/dashboard/DashboardNextSchedules.vue

@@ -7,12 +7,16 @@
     </div>
     </div>
 
 
     <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"
-          :flat="false"
           class="schedule-card card-border shadow-card bg-surface"
           class="schedule-card card-border shadow-card bg-surface"
+          :flat="false"
         >
         >
           <q-card-section
           <q-card-section
             class="q-pa-md row col-12 no-wrap schedule-card-section"
             class="q-pa-md row col-12 no-wrap schedule-card-section"
@@ -25,8 +29,8 @@
                 >
                 >
                   <img
                   <img
                     v-if="item.provider_photo"
                     v-if="item.provider_photo"
-                    :src="item.provider_photo"
                     style="object-fit: cover; border-radius: 50%"
                     style="object-fit: cover; border-radius: 50%"
+                    :src="item.provider_photo"
                   />
                   />
 
 
                   <span v-else>
                   <span v-else>
@@ -56,7 +60,7 @@
                   >
                   >
                     {{
                     {{
                       getFirstName(item.provider_name) ||
                       getFirstName(item.provider_name) ||
-                      $t("dashboard_client.next_schedules.no_provider")
+                        $t("dashboard_client.next_schedules.no_provider")
                     }}
                     }}
                   </span>
                   </span>
                 </div>
                 </div>
@@ -112,7 +116,13 @@
             <div class="column text-text schedule-price">
             <div class="column text-text schedule-price">
               <div class="column schedule-price-info">
               <div class="column schedule-price-info">
                 <span class="text-price-main font16 fontbold">
                 <span class="text-price-main font16 fontbold">
-                  {{ formatScheduleTotal(item) }}
+                  {{
+                    formatScheduleTotal(
+                      item,
+                      platformFees,
+                      t("dashboard_client.next_schedules.to_combine"),
+                    )
+                  }}
                 </span>
                 </span>
 
 
                 <span
                 <span
@@ -140,34 +150,38 @@
         </q-card>
         </q-card>
       </div>
       </div>
     </div>
     </div>
+
+    <DefaultCarouselIndicator
+      :active="activeIndex"
+      :count="pageCount"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
 import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
+import { formatLabelByPeriodType, getFirstName } from "src/helpers/utils";
 
 
 import {
 import {
-  formatCurrency,
-  formatLabelByPeriodType,
-  getFirstName,
-} from "src/helpers/utils";
-
-import {
-  getSchedulePaymentType,
-  getScheduleTotalWithPlatformFee,
+  formatScheduleTotal,
 } from "src/helpers/paymentPlatformFees";
 } from "src/helpers/paymentPlatformFees";
 
 
 import { onMounted } from "vue";
 import { onMounted } from "vue";
+import { useCarouselIndicator } from "src/composables/useCarouselIndicator";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 
 
-defineProps({ data: { type: Array, default: () => [] } });
+import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
 
 
+const props = defineProps({ data: { type: Array, default: () => [] } });
 const emit = defineEmits(["view-details"]);
 const emit = defineEmits(["view-details"]);
 
 
-const { t } = useI18n();
+const { activeIndex, onTrackScroll, pageCount, trackRef } =
+  useCarouselIndicator(() => props.data);
+
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
+const { t } = useI18n();
 
 
 const addressIcon = (type) =>
 const addressIcon = (type) =>
   type === "home" ? "mdi-home-outline" : "mdi-office-building-outline";
   type === "home" ? "mdi-home-outline" : "mdi-office-building-outline";
@@ -183,23 +197,10 @@ const addressLabel = (type) => {
   return t("dashboard_client.next_schedules.place_unknown");
   return t("dashboard_client.next_schedules.place_unknown");
 };
 };
 
 
-const formatScheduleTotal = (item) => {
-  if (!item.total_amount || item.total_amount === "0.00") {
-    return t("dashboard_client.next_schedules.to_combine");
-  }
-
-  return formatCurrency(
-    getScheduleTotalWithPlatformFee(
-      item,
-      getSchedulePaymentType(item),
-      platformFees.value,
-    ),
-  );
-};
-
 onMounted(() => {
 onMounted(() => {
   loadPlatformFees().catch(() => {});
   loadPlatformFees().catch(() => {});
 });
 });
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
@@ -225,6 +226,7 @@ onMounted(() => {
 }
 }
 
 
 .schedule-card {
 .schedule-card {
+  scroll-snap-align: start;
   min-width: 90%;
   min-width: 90%;
 }
 }
 
 

+ 64 - 107
src/components/dashboard/DashboardPendingSchedules.vue

@@ -9,8 +9,8 @@
         <q-card
         <q-card
           v-for="item in data"
           v-for="item in data"
           :key="item.id"
           :key="item.id"
-          :flat="false"
           class="pending-card card-border shadow-card bg-surface"
           class="pending-card card-border shadow-card bg-surface"
+          :flat="false"
           @click="seeDetails(item)"
           @click="seeDetails(item)"
         >
         >
           <q-card-section class="card-body q-pa-md">
           <q-card-section class="card-body q-pa-md">
@@ -22,8 +22,8 @@
               >
               >
                 <img
                 <img
                   v-if="providerPhoto(item)"
                   v-if="providerPhoto(item)"
-                  :src="providerPhoto(item)"
                   style="object-fit: cover; border-radius: 50%"
                   style="object-fit: cover; border-radius: 50%"
+                  :src="providerPhoto(item)"
                 />
                 />
 
 
                 <span v-else>
                 <span v-else>
@@ -40,6 +40,7 @@
                       $t("dashboard_client.pending_schedules.pay_to_provider")
                       $t("dashboard_client.pending_schedules.pay_to_provider")
                     }}
                     }}
                   </span>
                   </span>
+
                   <span v-else-if="item.status == 'pending'">
                   <span v-else-if="item.status == 'pending'">
                     {{
                     {{
                       $t("dashboard_client.pending_schedules.requesting_with")
                       $t("dashboard_client.pending_schedules.requesting_with")
@@ -66,7 +67,7 @@
                   />
                   />
 
 
                   <span class="font9 fontmedium text-grey-5 meta-text ellipsis">
                   <span class="font9 fontmedium text-grey-5 meta-text ellipsis">
-                    {{ displayTime(item) }}
+                    {{ requestStatusText(item) }}
                   </span>
                   </span>
                 </div>
                 </div>
               </div>
               </div>
@@ -89,8 +90,8 @@
                     type === 'servicePackage'
                     type === 'servicePackage'
                       ? $t("dashboard_client.pending_schedules.status.accepted")
                       ? $t("dashboard_client.pending_schedules.status.accepted")
                       : $t(
                       : $t(
-                          `dashboard_client.pending_schedules.status.${item.status ?? "pending"}`,
-                        )
+                        `dashboard_client.pending_schedules.status.${item.status ?? "pending"}`,
+                      )
                   }}
                   }}
                 </span>
                 </span>
               </div>
               </div>
@@ -104,22 +105,31 @@
             </div>
             </div>
 
 
             <div class="schedule-info">
             <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-calendar-outline"
+                  size="14px"
+                />
 
 
-  <div class="schedule-info-item">
-    <q-icon
-      name="mdi-clock-outline"
-      size="14px"
-    />
-    <span>{{ displayScheduleTime(item) }}</span>
-  </div>
-</div>
+                <span>{{ formatNumericDate(scheduleFor(item)?.date) || "—" }}</span>
+              </div>
+
+              <div class="schedule-info-item">
+                <q-icon
+                  name="mdi-clock-outline"
+                  size="14px"
+                />
+
+                <span>
+                  {{
+                    formatTimeRange(
+                      scheduleFor(item)?.start_time,
+                      scheduleFor(item)?.end_time,
+                    ) || "—"
+                  }}
+                </span>
+              </div>
+            </div>
 
 
             <div class="card-footer row items-center no-wrap">
             <div class="card-footer row items-center no-wrap">
               <q-btn
               <q-btn
@@ -147,7 +157,7 @@
                 />
                 />
 
 
                 {{
                 {{
-                  displayAddress(item)
+                  formatAddress(addressFor(item)) || "—"
                 }}
                 }}
               </span>
               </span>
             </div>
             </div>
@@ -156,27 +166,25 @@
       </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>
+    <DefaultCarouselIndicator
+      :active="activeIndex"
+      :count="pageCount"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { computed, ref } from "vue";
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
-import { getFirstName } from "src/helpers/utils";
-import { parseLocalDate } from "src/helpers/scheduleDate";
+import { computed } from "vue";
+import { formatAddress, getFirstName } from "src/helpers/utils";
+import { formatNumericDate, formatTimeRange } from "src/helpers/scheduleDate";
+import { useCarouselIndicator } from "src/composables/useCarouselIndicator";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 
 
+import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
+
+const emit = defineEmits(["view-details", "cancel"]);
+
 const props = defineProps({
 const props = defineProps({
   data: { type: Array, default: () => [] },
   data: { type: Array, default: () => [] },
   type: { type: String, default: 'schedule' },
   type: { type: String, default: 'schedule' },
@@ -184,28 +192,25 @@ const props = defineProps({
   progressClass: { type: String, default: '' },
   progressClass: { type: String, default: '' },
 });
 });
 
 
-const emit = defineEmits(["view-details", "cancel"]);
-
 const { t } = useI18n();
 const { t } = useI18n();
 
 
-const trackRef = ref(null);
-const activeIndex = ref(0);
-
-const onTrackScroll = () => {
-  const track = trackRef.value;
+const { activeIndex, onTrackScroll, pageCount, trackRef } =
+  useCarouselIndicator(() => props.data);
 
 
-  if (!track || !track.clientWidth) return;
+const isServicePackage = computed(() => props.type === 'servicePackage');
 
 
-  activeIndex.value = Math.round(track.scrollLeft / track.clientWidth);
-};
+const addressFor = (item) => {
+  const address = isServicePackage.value
+    ? item.schedules?.[0]?.address
+    : item.address;
 
 
-const isServicePackage = computed(() => props.type === 'servicePackage');
+  if (!address) return null;
 
 
-const providerPhoto = (item) => {
-  if (isServicePackage.value) {
-    return item.provider?.profile_media?.url ?? item.provider_photo ?? null;
-  }
-  return item.provider_photo;
+  return {
+    address: address.address,
+    district: address.district,
+    number: address.number,
+  };
 };
 };
 
 
 const providerName = (item) => {
 const providerName = (item) => {
@@ -215,26 +220,17 @@ const providerName = (item) => {
   return item.provider_name ?? '';
   return item.provider_name ?? '';
 };
 };
 
 
-const displayAddress = (item) => {
+const providerPhoto = (item) => {
   if (isServicePackage.value) {
   if (isServicePackage.value) {
-    const firstSchedule = item.schedules?.[0];
-    const addr = firstSchedule?.address;
-    if (addr) {
-      return [addr.address, addr.number, addr.district].filter(Boolean).join(', ') || '—';
-    }
-    return '—';
+    return item.provider?.profile_media?.url ?? item.provider_photo ?? null;
   }
   }
-  return [
-    item.address?.address,
-    item.address?.number,
-    item.address?.district,
-  ].filter(Boolean).join(', ') || '—';
+  return item.provider_photo;
 };
 };
 
 
 const scheduleCountLabel = (count) =>
 const scheduleCountLabel = (count) =>
   t("dashboard_client.pending_schedules.schedule_count", { count });
   t("dashboard_client.pending_schedules.schedule_count", { count });
 
 
-const displayTime = (item) => {
+const requestStatusText = (item) => {
   if (isServicePackage.value) {
   if (isServicePackage.value) {
     return item.schedules?.length ? scheduleCountLabel(item.schedules.length) : "—";
     return item.schedules?.length ? scheduleCountLabel(item.schedules.length) : "—";
   }
   }
@@ -252,39 +248,13 @@ const displayTime = (item) => {
     : timeAgo;
     : timeAgo;
 };
 };
 
 
-const displayDate = (item) => {
-  const schedule = isServicePackage.value
-    ? item.schedules?.[0]
-    : item;
-
-  if (!schedule?.date) return "—";
-
-  const date = parseLocalDate(schedule.date);
-
-  return date.toLocaleDateString("pt-BR", {
-    day: "2-digit",
-    month: "2-digit",
-    year: "numeric",
-  });
-};
-
-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 scheduleFor = (item) =>
+  isServicePackage.value ? item.schedules?.[0] : item;
 
 
 const seeDetails = (item) => {
 const seeDetails = (item) => {
-  if (isServicePackage.value || item.status === "accepted") {
-    emit("view-details", item);
-  }
+  emit("view-details", item);
 };
 };
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
@@ -304,19 +274,6 @@ const seeDetails = (item) => {
   }
   }
 }
 }
 
 
-.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 {
   flex: 0 0 100%;
   flex: 0 0 100%;
   width: 100%;
   width: 100%;

+ 24 - 30
src/components/dashboard/DashboardProvidersClose.vue

@@ -57,8 +57,7 @@
       <q-card
       <q-card
         v-for="p in data"
         v-for="p in data"
         :key="p.provider_id"
         :key="p.provider_id"
-        :flat="false"
-        class="card-border bg-page text-text q-mb-sm"
+        class="bg-page text-text q-mb-sm"
       >
       >
         <q-card-section class="row no-wrap q-pa-sm">
         <q-card-section class="row no-wrap q-pa-sm">
           <div class="row no-wrap full-width">
           <div class="row no-wrap full-width">
@@ -69,8 +68,8 @@
               >
               >
                 <img
                 <img
                   v-if="p.provider_photo"
                   v-if="p.provider_photo"
-                  :src="p.provider_photo"
                   style="object-fit: cover; border-radius: 50%"
                   style="object-fit: cover; border-radius: 50%"
+                  :src="p.provider_photo"
                 />
                 />
 
 
                 <span v-else>
                 <span v-else>
@@ -100,7 +99,7 @@
                     <span class="text-provider-close-rating font9 fontmedium">
                     <span class="text-provider-close-rating font9 fontmedium">
                       {{
                       {{
                         p.average_rating != null
                         p.average_rating != null
-                          ? Number(p.average_rating).toFixed(1) +
+                          ? formatRating(p.average_rating) +
                             " (" +
                             " (" +
                             (p.total_reviews ?? 0) +
                             (p.total_reviews ?? 0) +
                             ")"
                             ")"
@@ -166,7 +165,14 @@
 
 
 <script setup>
 <script setup>
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
-import { formatCurrency, getFirstName } from "src/helpers/utils";
+
+import {
+  formatCurrency,
+  formatDistance,
+  formatRating,
+  getFirstName,
+} from "src/helpers/utils";
+
 import { ref } from "vue";
 import { ref } from "vue";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
@@ -175,9 +181,8 @@ import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 
 
 defineProps({ data: { type: Array, default: () => [] } });
 defineProps({ data: { type: Array, default: () => [] } });
 
 
-const { t } = useI18n();
-
 const $q = useQuasar();
 const $q = useQuasar();
+const { t } = useI18n();
 
 
 const currentPeriodType = ref(6);
 const currentPeriodType = ref(6);
 
 
@@ -188,15 +193,20 @@ const periodTypeMap = ref({
   8: "daily_price_8h",
   8: "daily_price_8h",
 });
 });
 
 
-const formatDistance = (distance) => {
-  if (distance === null || distance === undefined || distance === "")
-    return "—";
-
-  const numericDistance = Number(distance);
+//
+const goToScheduling = (provider) => {
+  $q.dialog({
+    component: SchedulingDialog,
+    componentProps: { provider },
+  });
+};
 
 
-  if (!Number.isFinite(numericDistance)) return "—";
+const setPeriodTypeNext = () => {
+  const nextPeriod = currentPeriodType.value + 2;
 
 
-  return `${numericDistance.toLocaleString("pt-BR", { maximumFractionDigits: 1 })} km`;
+  if (periodTypeMap.value[nextPeriod]) {
+    currentPeriodType.value = nextPeriod;
+  }
 };
 };
 
 
 const setPeriodTypePrevious = () => {
 const setPeriodTypePrevious = () => {
@@ -207,14 +217,6 @@ const setPeriodTypePrevious = () => {
   }
   }
 };
 };
 
 
-const setPeriodTypeNext = () => {
-  const nextPeriod = currentPeriodType.value + 2;
-
-  if (periodTypeMap.value[nextPeriod]) {
-    currentPeriodType.value = nextPeriod;
-  }
-};
-
 const showCorrectLabels = () => {
 const showCorrectLabels = () => {
   switch (currentPeriodType.value) {
   switch (currentPeriodType.value) {
     case 8:
     case 8:
@@ -240,14 +242,6 @@ const showCorrectValues = (p) => {
     : t("dashboard_client.providers_close.no_price");
     : t("dashboard_client.providers_close.no_price");
 };
 };
 
 
-//
-
-const goToScheduling = (provider) => {
-  $q.dialog({
-    component: SchedulingDialog,
-    componentProps: { provider },
-  });
-};
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 16 - 1
src/components/dashboard/DashboardScrollAreaSchedules.vue

@@ -1,6 +1,10 @@
 <template>
 <template>
   <section class="promo-scroll-wrapper q-mx-md q-mb-md">
   <section class="promo-scroll-wrapper q-mx-md q-mb-md">
-    <div class="promo-scroll">
+    <div
+      ref="trackRef"
+      class="promo-scroll"
+      @scroll="onTrackScroll"
+    >
       <div
       <div
         v-for="card in cards"
         v-for="card in cards"
         :key="card.id"
         :key="card.id"
@@ -11,11 +15,19 @@
         <img :src="card.image" :alt="card.alt" class="promo-card__img" />
         <img :src="card.image" :alt="card.alt" class="promo-card__img" />
       </div>
       </div>
     </div>
     </div>
+
+    <DefaultCarouselIndicator
+      :active="activeIndex"
+      :count="pageCount"
+    />
   </section>
   </section>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
+import { useCarouselIndicator } from 'src/composables/useCarouselIndicator';
 import { useRouter } from 'vue-router';
 import { useRouter } from 'vue-router';
+
+import DefaultCarouselIndicator from 'src/components/defaults/DefaultCarouselIndicator.vue';
 import Banner1 from 'src/assets/banner_1.svg';
 import Banner1 from 'src/assets/banner_1.svg';
 import Banner2 from 'src/assets/banner_2.svg';
 import Banner2 from 'src/assets/banner_2.svg';
 
 
@@ -25,6 +37,9 @@ const cards = [
   { id: 1, image: Banner1, alt: 'Diária sob medida', route: 'SobMedidaPage' },
   { id: 1, image: Banner1, alt: 'Diária sob medida', route: 'SobMedidaPage' },
   { id: 2, image: Banner2, alt: 'Escolha profissionais', route: 'SearchPage' },
   { id: 2, image: Banner2, alt: 'Escolha profissionais', route: 'SearchPage' },
 ];
 ];
+
+const { activeIndex, onTrackScroll, pageCount, trackRef } =
+  useCarouselIndicator(() => cards);
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 37 - 10
src/components/dashboard/DashboardTodaySchedules.vue

@@ -1,16 +1,27 @@
 <template>
 <template>
   <div class="q-mx-md q-mb-md">
   <div class="q-mx-md q-mb-md">
-    <div class="font16 fontbold gradient-diarista q-mb-sm">
-      {{ $t("dashboard_client.today_schedules.title") }}
+    <div class="row items-center no-wrap q-mb-sm">
+      <span class="font16 fontbold gradient-diarista">
+        {{ $t("dashboard_client.today_schedules.title") }}
+      </span>
+
+      <div class="attention-badge q-ml-sm">
+        {{ $t("dashboard_client.today_schedules.attention_badge") }}
+      </div>
     </div>
     </div>
 
 
     <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 visibleItems"
           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"
+          @click="emit('view-details', item)"
         >
         >
           <q-card-section class="q-pa-md">
           <q-card-section class="q-pa-md">
             <div class="row no-wrap items-start q-mb-xs">
             <div class="row no-wrap items-start q-mb-xs">
@@ -89,7 +100,7 @@
                   </div>
                   </div>
                 </template>
                 </template>
 
 
-                <template v-else-if="cardState(item) === 'awaiting_code' && hasStarted(item)">
+                <template v-else-if="(cardState(item) === 'awaiting_code') || !item.code_verified">
                   <div class="column items-center">
                   <div class="column items-center">
                     <span class="font9 fontbold text-primary q-mb-xs">
                     <span class="font9 fontbold text-primary q-mb-xs">
                       {{ $t("dashboard_client.today_schedules.code_label") }}
                       {{ $t("dashboard_client.today_schedules.code_label") }}
@@ -110,7 +121,6 @@
                         size="15px"
                         size="15px"
                       />
                       />
                     </div>
                     </div>
-
                     <span
                     <span
                       class="font10 fontregular badge-status-text text-primary q-mt-xs"
                       class="font10 fontregular badge-status-text text-primary q-mt-xs"
                     >
                     >
@@ -229,6 +239,11 @@
         </q-card>
         </q-card>
       </div>
       </div>
     </div>
     </div>
+
+    <DefaultCarouselIndicator
+      :active="activeIndex"
+      :count="pageCount"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -237,7 +252,9 @@ 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 { ref, onMounted, onBeforeUnmount, computed } from "vue";
+import { useCarouselIndicator } from "src/composables/useCarouselIndicator";
 
 
+import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
 import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 
 
 
 
@@ -250,7 +267,7 @@ const props = defineProps({
 
 
 
 
 
 
-const emit = defineEmits(["rate"]);
+const emit = defineEmits(["rate", "view-details"]);
 
 
 const $q = useQuasar();
 const $q = useQuasar();
 
 
@@ -267,10 +284,7 @@ const cardState = (item) => {
     return "finished";
     return "finished";
   }
   }
 
 
-  if (
-    item.status === "accepted" ||
-    item.status === "paid"
-  ) {
+  if (item.status === "paid") {
     return "awaiting_code";
     return "awaiting_code";
   }
   }
 
 
@@ -329,6 +343,9 @@ const visibleItems = computed(() => {
   return props.data.filter((item) => !item.client_reviewed);
   return props.data.filter((item) => !item.client_reviewed);
 });
 });
 
 
+const { activeIndex, onTrackScroll, pageCount, trackRef } =
+  useCarouselIndicator(() => visibleItems.value);
+
 const progressByState = (item) => {
 const progressByState = (item) => {
   const state = cardState(item);
   const state = cardState(item);
 
 
@@ -356,6 +373,16 @@ onBeforeUnmount(() => {
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
+.attention-badge {
+  background: #22c55e;
+  color: white;
+  font-size: 11px;
+  font-weight: 700;
+  border-radius: 999px;
+  padding: 1px 8px;
+  white-space: nowrap;
+}
+
 .scroll-wrapper {
 .scroll-wrapper {
   overflow: hidden;
   overflow: hidden;
 }
 }

+ 194 - 0
src/components/dashboard/PackageScheduleDetailsDialog.vue

@@ -0,0 +1,194 @@
+<template>
+  <q-dialog
+    ref="dialogRef"
+    @hide="onDialogHide"
+  >
+    <q-card
+      class="package-details-card bg-surface shadow-card"
+      :flat="false"
+    >
+      <div class="row justify-end q-pt-sm q-pr-sm">
+        <q-btn
+          color="grey-6"
+          dense
+          flat
+          icon="close"
+          round
+          size="sm"
+          @click="onDialogCancel"
+        />
+      </div>
+
+      <q-card-section class="column items-center q-pt-xs q-pb-sm">
+        <q-avatar
+          class="q-mb-sm"
+          size="72px"
+        >
+          <img
+            v-if="schedule.provider_photo"
+            :src="schedule.provider_photo"
+            style="object-fit: cover"
+          />
+
+          <span
+            v-else
+            class="full-width full-height flex flex-center"
+            style="font-size: 20px; border-radius: 50%"
+            :style="avatarColors[schedule.id % avatarColors.length]"
+          >
+            {{ providerInitials }}
+          </span>
+        </q-avatar>
+
+        <div class="text-text font16 fontbold">
+          {{ schedule.provider_name || "—" }}
+        </div>
+
+        <div class="text-primary font16 fontbold q-mt-xs">
+          {{ formatCurrency(packageTotal) }}
+        </div>
+
+        <div class="text-grey-6 font10">
+          {{ $t("service_package.services_total") }}
+        </div>
+
+        <div
+          v-if="packageItems.length > 1"
+          class="text-grey-6 font12 q-mt-xs"
+        >
+          {{ $t("service_package.package_count", { count: packageItems.length }) }}
+        </div>
+      </q-card-section>
+
+      <q-card-section class="q-pt-none q-pb-sm q-px-md text-text">
+        <div
+          v-for="item in packageItems"
+          :key="item.id"
+          class="package-item-row"
+        >
+          <div class="row items-center no-wrap justify-between">
+            <span class="fontbold font12">
+              {{ formatWeekday(item.date) + ", " + formatFullDate(item.date) }}
+            </span>
+
+            <span class="fontbold font12">
+              {{ formatCurrency(Number(item.total_amount) || 0) }}
+            </span>
+          </div>
+
+          <div class="row items-center no-wrap justify-between">
+            <span class="text-grey-6 font12">
+              {{ item.start_time?.slice(0, 5) }}
+              {{ $t("dashboard_client.next_schedules.to") }}
+              {{ item.end_time?.slice(0, 5) }}
+            </span>
+
+            <span class="text-grey-7 font12">
+              {{
+                item.offers_meal
+                  ? $t("service_package.with_meal")
+                  : $t("service_package.without_meal")
+              }}
+            </span>
+          </div>
+        </div>
+      </q-card-section>
+
+      <q-card-section class="column items-center q-pt-xs q-pb-sm text-text">
+        <div class="row items-start no-wrap self-start q-gutter-x-xs">
+          <q-icon
+            class="q-mt-xs"
+            color="primary"
+            name="mdi-map-marker"
+            size="20px"
+          />
+
+          <span class="font14 fontbold">
+            {{ formatAddress(schedule.address) || "—" }}
+          </span>
+        </div>
+      </q-card-section>
+
+      <q-separator />
+
+      <q-card-section class="q-py-md">
+        <q-btn
+          class="full-width"
+          color="primary"
+          no-caps
+          padding="8px 16px"
+          rounded
+          unelevated
+          :label="$t('common.actions.close')"
+          @click="onDialogCancel"
+        />
+      </q-card-section>
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { avatarColors } from "src/helpers/avatarColors";
+import { formatAddress, formatCurrency } from "src/helpers/utils";
+import { formatWeekday, parseLocalDate } from "src/helpers/scheduleDate";
+import { useDialogPluginComponent } from "quasar";
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    required: true,
+  },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogCancel, onDialogHide } = useDialogPluginComponent();
+
+const packageItems = computed(() => {
+  const items = props.schedule?.package_items;
+
+  return Array.isArray(items) && items.length ? items : [props.schedule];
+});
+
+const packageTotal = computed(() =>
+  packageItems.value.reduce(
+    (total, item) => total + (Number(item.total_amount) || 0),
+    0,
+  ),
+);
+
+const providerInitials = computed(
+  () => props.schedule.provider_name?.slice(0, 2).toUpperCase() ?? "??",
+);
+
+const formatFullDate = (value) => {
+  const date = parseLocalDate(value);
+
+  if (!date) return "";
+
+  return date.toLocaleDateString("pt-BR", {
+    day: "2-digit",
+    month: "2-digit",
+    year: "numeric",
+  });
+};
+</script>
+
+<style scoped lang="scss">
+.package-details-card {
+  width: 320px;
+  max-width: 92vw;
+  border-radius: 20px !important;
+  overflow: hidden;
+}
+
+.package-item-row {
+  border-bottom: 1px solid #ececf2;
+  padding: 6px 0;
+
+  &:last-child {
+    border-bottom: none;
+  }
+}
+</style>

+ 72 - 0
src/components/defaults/DefaultCarouselIndicator.vue

@@ -0,0 +1,72 @@
+<template>
+  <div
+    v-if="count > 1"
+    aria-hidden="true"
+    class="row justify-center q-gutter-x-xs q-mt-sm"
+  >
+    <span
+      v-for="index in visibleIndexes"
+      :key="`dot-${index}`"
+      class="carousel-dot"
+      :class="{
+        'carousel-dot--active': index === active,
+        'carousel-dot--edge': isEdge(index),
+      }"
+    />
+  </div>
+</template>
+
+<script setup>
+import { computed } from "vue";
+
+const props = defineProps({
+  active: { type: Number, default: 0 },
+  count: { type: Number, default: 0 },
+
+  max: { type: Number, default: 7 },
+});
+
+const visibleIndexes = computed(() => {
+  const size = Math.min(props.max, props.count);
+
+  const start = Math.min(
+    Math.max(props.active - Math.floor(size / 2), 0),
+    props.count - size,
+  );
+
+  return Array.from({ length: size }, (_, index) => start + index);
+});
+
+const isEdge = (index) => {
+  if (index === props.active) return false;
+
+  const indexes = visibleIndexes.value;
+
+  return (
+    (index === indexes[0] && index > 0) ||
+    (index === indexes[indexes.length - 1] && index < props.count - 1)
+  );
+};
+</script>
+
+<style scoped lang="scss">
+.carousel-dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: #cbd5e1;
+  transition: background 0.3s ease, width 0.3s ease, height 0.3s ease;
+}
+
+.carousel-dot--active {
+  width: 16px;
+  border-radius: 999px;
+  background: #8b5cf6;
+}
+
+.carousel-dot--edge {
+  width: 4px;
+  height: 4px;
+  align-self: center;
+}
+</style>

+ 4 - 2
src/components/login/LoginStep1Panel.vue

@@ -8,7 +8,8 @@
 
 
     <q-input
     <q-input
       v-model="email"
       v-model="email"
-      class="bg-surface q-mt-sm"
+      bg-color="surface"
+      class="q-mt-sm"
       hide-bottom-space
       hide-bottom-space
       input-class="font12 fontmedium text-text"
       input-class="font12 fontmedium text-text"
       lazy-rules
       lazy-rules
@@ -50,7 +51,8 @@
 
 
       <q-input
       <q-input
         v-model="phone"
         v-model="phone"
-        class="bg-surface q-mt-sm"
+        bg-color="surface"
+        class="q-mt-sm"
         hide-bottom-space
         hide-bottom-space
         input-class="text-text"
         input-class="text-text"
         lazy-rules
         lazy-rules

+ 162 - 0
src/components/profile/ProfileFavoriteCodeDialog.vue

@@ -0,0 +1,162 @@
+<template>
+  <q-dialog ref="dialogRef">
+    <div class="code-dialog-wrapper column items-center">
+      <q-card class="code-card bg-white">
+        <q-card-section class="q-pt-lg q-pb-none q-px-lg text-center">
+          <p class="code-title text-primary font16 fontbold">
+            {{ $t('profile.favorites.code_title') }}
+          </p>
+
+          <p class="code-subtitle text-grey-7 font12 fontregular q-mt-sm">
+            {{ $t('profile.favorites.code_subtitle') }}
+          </p>
+        </q-card-section>
+
+        <q-card-section class="q-px-lg q-pt-md q-pb-none">
+          <q-input
+            v-model="code"
+            autofocus
+            class="code-input"
+            input-class="text-center text-text font16 fontbold"
+            mask="XXXXXX"
+            outlined
+            :disable="loading"
+            :error="!!errorMessage"
+            :error-message="errorMessage"
+            :placeholder="$t('profile.favorites.code_placeholder')"
+            @keyup.enter="confirm"
+            @update:model-value="onCodeInput"
+          />
+        </q-card-section>
+
+        <q-card-actions class="row q-px-lg q-pb-lg q-gutter-x-sm justify-center">
+          <q-btn
+            class="col code-btn font12 fontbold"
+            color="grey-6"
+            no-caps
+            rounded
+            text-color="grey-2"
+            unelevated
+            :disable="loading"
+            :label="$t('profile.favorites.code_cancel')"
+            @click="onDialogCancel"
+          />
+
+          <q-btn
+            class="col code-btn font12 fontbold"
+            color="secondary"
+            no-caps
+            rounded
+            unelevated
+            :disable="!isValid"
+            :label="$t('profile.favorites.code_confirm')"
+            :loading="loading"
+            @click="confirm"
+          />
+        </q-card-actions>
+      </q-card>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue';
+import { useDialogPluginComponent } from 'quasar';
+import { useI18n } from 'vue-i18n';
+
+import { createClientFavoriteProviderByCode } from 'src/api/clientFavoriteProvider';
+import { userStore } from 'src/stores/user';
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+
+const { t } = useI18n();
+const store = userStore();
+
+const CODE_LENGTH = 6;
+
+const code = ref('');
+const errorMessage = ref('');
+const loading = ref(false);
+
+const isValid = computed(() => code.value.length === CODE_LENGTH && !loading.value);
+
+const onCodeInput = (value) => {
+  code.value = String(value ?? '').toUpperCase();
+
+  errorMessage.value = '';
+};
+
+const confirm = async () => {
+  if (!isValid.value) {
+    return;
+  }
+
+  const clientId = store.user?.client_id;
+
+  if (!clientId) {
+    errorMessage.value = t('profile.favorites.code_error');
+
+    return;
+  }
+
+  loading.value = true;
+
+  try {
+    const favorite = await createClientFavoriteProviderByCode(clientId, code.value);
+
+    onDialogOK(favorite);
+  } catch (error) {
+    errorMessage.value = error?.response?.status === 422
+      ? (error?.response?.data?.message ?? t('profile.favorites.code_error'))
+      : '';
+  } finally {
+    loading.value = false;
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.code-dialog-wrapper {
+  width: 320px;
+  max-width: 90vw;
+}
+
+.code-card {
+  width: 100%;
+  border-radius: 20px;
+  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
+}
+
+.code-title {
+  line-height: 1.4;
+  margin: 0;
+}
+
+.code-subtitle {
+  line-height: 1.4;
+  margin: 0;
+}
+
+.code-input :deep(input) {
+  letter-spacing: 4px;
+  text-transform: uppercase;
+}
+
+.code-input :deep(input::placeholder) {
+  color: #bbbbbb;
+  font-weight: 400;
+  letter-spacing: 4px;
+  opacity: 1;
+}
+
+.code-input :deep(input::selection) {
+  background: rgba(139, 92, 246, 0.2);
+  color: #555555;
+}
+
+.code-btn {
+  padding: 6px 12px;
+}
+</style>

+ 59 - 4
src/components/profile/ProfileFavoritesDialog.vue

@@ -74,6 +74,17 @@
           :label="$t('profile.favorites.search_btn')"
           :label="$t('profile.favorites.search_btn')"
           @click="goToSearch"
           @click="goToSearch"
         />
         />
+
+        <q-btn
+          class="full-width q-py-sm q-mt-sm"
+          color="primary"
+          no-caps
+          outline
+          padding="10px 16px"
+          rounded
+          :label="$t('profile.favorites.code_btn')"
+          @click="openCodeDialog"
+        />
       </div>
       </div>
 
 
       <div
       <div
@@ -85,6 +96,21 @@
             {{ $t("profile.favorites.indicate_title") }}
             {{ $t("profile.favorites.indicate_title") }}
           </p>
           </p>
 
 
+          <div class="row justify-center q-mb-md">
+            <q-btn
+              color="primary"
+              dense
+              outline
+              no-caps
+              padding="6px 16px"
+              rounded
+              size="sm"
+              icon="mdi-ticket-confirmation-outline"
+              :label="$t('profile.favorites.code_btn')"
+              @click="openCodeDialog"
+            />
+          </div>
+
           <div class="row items-center">
           <div class="row items-center">
             <div class="col column items-center">
             <div class="col column items-center">
               <q-avatar
               <q-avatar
@@ -327,7 +353,8 @@
                   size="sm"
                   size="sm"
                   style="background: #a63df7; color: white"
                   style="background: #a63df7; color: white"
                   unelevated
                   unelevated
-                  :label="$t('profile.favorites.schedule_btn')"
+                  :label="$t('dashboard_client.favorites.view_schedule')"
+                  @click="goToScheduling(item)"
                 />
                 />
               </div>
               </div>
             </div>
             </div>
@@ -358,7 +385,9 @@ import { useI18n } from "vue-i18n";
 import { useRouter } from "vue-router";
 import { useRouter } from "vue-router";
 import { userStore } from "src/stores/user";
 import { userStore } from "src/stores/user";
 
 
+import ProfileFavoriteCodeDialog from "./ProfileFavoriteCodeDialog.vue";
 import ProfileFavoriteRemoveDialog from "./ProfileFavoriteRemoveDialog.vue";
 import ProfileFavoriteRemoveDialog from "./ProfileFavoriteRemoveDialog.vue";
+import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 
 
 defineEmits([...useDialogPluginComponent.emits]);
 defineEmits([...useDialogPluginComponent.emits]);
 
 
@@ -370,10 +399,13 @@ const $q = useQuasar();
 const router = useRouter();
 const router = useRouter();
 const store = userStore();
 const store = userStore();
 
 
+const CLIENT_APP_STORE_URL =
+  "https://play.google.com/store/apps/details?id=br.com.diarista.client";
+
 const favorites = ref([]);
 const favorites = ref([]);
 const highlightIndex = ref(0);
 const highlightIndex = ref(0);
 const loading = ref(false);
 const loading = ref(false);
-const selectedHours = ref("8h");
+const selectedHours = ref("6h");
 
 
 const highlightedFavorite = computed(
 const highlightedFavorite = computed(
   () => favorites.value[highlightIndex.value] ?? null,
   () => favorites.value[highlightIndex.value] ?? null,
@@ -438,10 +470,23 @@ const nextSelectedHours = () => {
   }
   }
 };
 };
 
 
+const openCodeDialog = () => {
+  $q.dialog({
+    component: ProfileFavoriteCodeDialog,
+  }).onOk(async () => {
+    await loadFavorites();
+
+    highlightIndex.value = 0;
+  });
+};
+
 const openIndicateDialog = async (item) => {
 const openIndicateDialog = async (item) => {
   const message = t("profile.favorites.indicate_whatsapp_message", {
   const message = t("profile.favorites.indicate_whatsapp_message", {
     city: item?.city_name ?? "",
     city: item?.city_name ?? "",
     name: item?.provider_name ?? "",
     name: item?.provider_name ?? "",
+    code: item?.share_code ?? "",
+    link: CLIENT_APP_STORE_URL,
+    button: t("profile.favorites.code_btn"),
   });
   });
 
 
   try {
   try {
@@ -474,7 +519,7 @@ const prevSelectedHours = () => {
   }
   }
 };
 };
 
 
-onMounted(async () => {
+const loadFavorites = async () => {
   loading.value = true;
   loading.value = true;
 
 
   try {
   try {
@@ -488,7 +533,17 @@ onMounted(async () => {
   } finally {
   } finally {
     loading.value = false;
     loading.value = false;
   }
   }
-});
+};
+
+const goToScheduling = (provider) => {
+  $q.dialog({
+    component: SchedulingDialog,
+    componentProps: { provider },
+  });
+  dialogRef.value.hide();
+};
+
+onMounted(loadFavorites);
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 5 - 2
src/composables/useAuth.js

@@ -2,7 +2,7 @@ import api from "src/api";
 import { permissionStore } from "src/stores/permission";
 import { permissionStore } from "src/stores/permission";
 import { userStore } from "src/stores/user";
 import { userStore } from "src/stores/user";
 import { useAuthStorage } from "src/composables/useAuthStorage";
 import { useAuthStorage } from "src/composables/useAuthStorage";
-import { registerPendingFcmToken } from "src/boot/push-notifications";
+import { usePushNotifications } from "src/composables/usePushNotifications";
 
 
 let refreshPromise = null;
 let refreshPromise = null;
 
 
@@ -21,7 +21,7 @@ export const useAuth = () => {
     userStore().accessToken = access_token;
     userStore().accessToken = access_token;
     await setRefreshToken(refresh_token);
     await setRefreshToken(refresh_token);
     await permissionStore().fetchScopes();
     await permissionStore().fetchScopes();
-    await registerPendingFcmToken();
+    await usePushNotifications().syncDeviceToken();
   };
   };
 
 
   const login = async (email, password) => {
   const login = async (email, password) => {
@@ -42,6 +42,9 @@ export const useAuth = () => {
 
 
   const logout = async () => {
   const logout = async () => {
     try {
     try {
+      // Precisa acontecer antes do /logout, enquanto o token ainda é válido.
+      await usePushNotifications().unregisterDeviceToken();
+
       const response = await api.post("/logout");
       const response = await api.post("/logout");
       if (response.status === 200) {
       if (response.status === 200) {
         await clearAuthData();
         await clearAuthData();

+ 121 - 0
src/composables/useCarouselIndicator.js

@@ -0,0 +1,121 @@
+import { nextTick, onBeforeUnmount, onMounted, ref, unref, watch } from "vue";
+
+const ITEM_MODE_RATIO = 0.8;
+
+const closestCardIndex = (cards, scrollLeft) => {
+  const origin = cards[0].offsetLeft;
+
+  let closest = 0;
+  let closestDistance = Infinity;
+
+  cards.forEach((card, index) => {
+    const distance = Math.abs(card.offsetLeft - origin - scrollLeft);
+
+    if (distance < closestDistance) {
+      closestDistance = distance;
+      closest = index;
+    }
+  });
+
+  return closest;
+};
+
+/**
+ * @param {Ref<Array>|Function} source - The rendered items (ref or getter)
+ * @returns {{ trackRef: Ref, activeIndex: Ref<number>, pageCount: Ref<number>, onTrackScroll: Function }}
+ */
+export const useCarouselIndicator = (source) => {
+  const activeIndex = ref(0);
+  const pageCount = ref(0);
+  const trackRef = ref(null);
+
+  let resizeObserver = null;
+
+  const resolveItems = () => {
+    const value = typeof source === "function" ? source() : unref(source);
+
+    return Array.isArray(value) ? value : [];
+  };
+
+  const measure = () => {
+    const track = trackRef.value;
+
+    if (!track || !track.clientWidth) return;
+
+    const cards = Array.from(track.children).filter(
+      (element) => element.nodeType === Node.ELEMENT_NODE,
+    );
+
+    if (!cards.length) {
+      activeIndex.value = 0;
+      pageCount.value = 0;
+
+      return;
+    }
+
+    const isItemMode =
+      cards[0].offsetWidth >= track.clientWidth * ITEM_MODE_RATIO;
+
+    pageCount.value = isItemMode
+      ? cards.length
+      : Math.max(1, Math.ceil(track.scrollWidth / track.clientWidth));
+
+    if (pageCount.value <= 1) {
+      activeIndex.value = 0;
+
+      return;
+    }
+
+    const maxScroll = track.scrollWidth - track.clientWidth;
+
+    if (maxScroll > 0 && track.scrollLeft >= maxScroll - 1) {
+      activeIndex.value = pageCount.value - 1;
+
+      return;
+    }
+
+    const index = isItemMode
+      ? closestCardIndex(cards, track.scrollLeft)
+      : Math.round(track.scrollLeft / track.clientWidth);
+
+    activeIndex.value = Math.min(Math.max(index, 0), pageCount.value - 1);
+  };
+
+  const stopObserving = () => {
+    resizeObserver?.disconnect();
+    resizeObserver = null;
+  };
+
+  const startObserving = (track) => {
+    if (!track || typeof ResizeObserver === "undefined") return;
+
+    resizeObserver = new ResizeObserver(() => measure());
+    resizeObserver.observe(track);
+  };
+
+  watch(
+    trackRef,
+    (track) => {
+      stopObserving();
+
+      if (track) startObserving(track);
+    },
+    { flush: "post" },
+  );
+
+  watch(
+    () => resolveItems().length,
+    () => nextTick(measure),
+  );
+
+  onMounted(() => nextTick(measure));
+
+  onBeforeUnmount(stopObserving);
+
+  return {
+    activeIndex,
+    onTrackScroll: measure,
+    pageCount,
+    trackRef,
+  };
+};

+ 120 - 0
src/composables/usePushNotifications.js

@@ -0,0 +1,120 @@
+import { Capacitor } from "@capacitor/core";
+import { Preferences } from "@capacitor/preferences";
+import { PushNotifications } from "@capacitor/push-notifications";
+import { registerDeviceToken, removeDeviceToken } from "src/api/deviceToken";
+import { userStore } from "src/stores/user";
+
+// Token FCM persistido em disco: o listener "registration" quase sempre dispara
+// antes de existir sessão, e guardar só em memória faz quem instala e loga
+// depois de reabrir o app nunca registrar o dispositivo na API.
+const PUSH_TOKEN_KEY = "fcm_device_token";
+
+export const usePushNotifications = () => {
+  const isSupported = () => Capacitor.isNativePlatform();
+
+  const setDeviceToken = async (token) => {
+    if (!isSupported()) {
+      return;
+    }
+
+    await Preferences.set({
+      key: PUSH_TOKEN_KEY,
+      value: JSON.stringify({ token, platform: Capacitor.getPlatform() }),
+    });
+  };
+
+  const getDeviceToken = async () => {
+    if (!isSupported()) {
+      return null;
+    }
+
+    const { value } = await Preferences.get({ key: PUSH_TOKEN_KEY });
+
+    if (!value) {
+      return null;
+    }
+
+    try {
+      return JSON.parse(value);
+    } catch {
+      return null;
+    }
+  };
+
+  /**
+   * Envia o token do dispositivo para a API.
+   * Só faz sentido autenticado: sem access token o POST volta 401, o
+   * dispositivo fica sem push e o interceptor ainda tenta um refresh à toa.
+   * O token continua guardado depois do envio — o logout precisa dele.
+   */
+  const syncDeviceToken = async () => {
+    if (!isSupported() || !userStore().accessToken) {
+      return;
+    }
+
+    const stored = await getDeviceToken();
+
+    if (!stored?.token) {
+      return;
+    }
+
+    try {
+      await registerDeviceToken(stored.token, stored.platform);
+    } catch {
+      // falha silenciosa — o token segue guardado para a próxima tentativa
+    }
+  };
+
+  /**
+   * Desativa o token na API para o dispositivo parar de receber push
+   * depois que o usuário sai da conta.
+   */
+  const unregisterDeviceToken = async () => {
+    if (!isSupported() || !userStore().accessToken) {
+      return;
+    }
+
+    const stored = await getDeviceToken();
+
+    if (!stored?.token) {
+      return;
+    }
+
+    try {
+      await removeDeviceToken(stored.token);
+    } catch {
+      // falha silenciosa — não pode impedir o logout
+    }
+  };
+
+  /**
+   * Solicita a permissão de notificações e registra o app no FCM,
+   * o que dispara o listener "registration" configurado no boot.
+   */
+  const requestPermissionAndRegister = async () => {
+    if (!isSupported()) {
+      return;
+    }
+
+    let permission = await PushNotifications.checkPermissions();
+
+    if (permission.receive === "prompt") {
+      permission = await PushNotifications.requestPermissions();
+    }
+
+    if (permission.receive !== "granted") {
+      return;
+    }
+
+    await PushNotifications.register();
+  };
+
+  return {
+    isSupported,
+    requestPermissionAndRegister,
+    setDeviceToken,
+    getDeviceToken,
+    syncDeviceToken,
+    unregisterDeviceToken,
+  };
+};

+ 14 - 0
src/helpers/paymentPlatformFees.js

@@ -1,3 +1,5 @@
+import { formatCurrency } from './utils'
+
 export const scheduleUsesServicePackageDiscount = (item) => {
 export const scheduleUsesServicePackageDiscount = (item) => {
   const count = Number(
   const count = Number(
     item?.schedules?.[0]?.service_package_items_count
     item?.schedules?.[0]?.service_package_items_count
@@ -47,3 +49,15 @@ export const getScheduleTotalWithPlatformFee = (schedule, paymentType, platformF
 
 
   return parseFloat((base * (1 + (feeRate ?? 0))).toFixed(2))
   return parseFloat((base * (1 + (feeRate ?? 0))).toFixed(2))
 }
 }
+
+export const formatScheduleTotal = (schedule, platformFees, emptyLabel = '') => {
+  if (!schedule?.total_amount || schedule.total_amount === '0.00') return emptyLabel
+
+  return formatCurrency(
+    getScheduleTotalWithPlatformFee(
+      schedule,
+      getSchedulePaymentType(schedule),
+      platformFees,
+    ),
+  )
+}

+ 43 - 1
src/helpers/scheduleDate.js

@@ -8,6 +8,13 @@ const parseLocalDate = (dateStr) => {
   return null;
   return null;
 };
 };
 
 
+const parseUtcDate = (dateStr) => {
+  if (!dateStr) return null;
+
+  const hasTimezone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(dateStr);
+  return new Date(hasTimezone ? dateStr : `${dateStr}Z`);
+};
+
 const formatWeekday = (dateStr) => {
 const formatWeekday = (dateStr) => {
   const d = parseLocalDate(dateStr);
   const d = parseLocalDate(dateStr);
   if (!d) return "";
   if (!d) return "";
@@ -21,4 +28,39 @@ const formatDayMonth = (dateStr) => {
   return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
   return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
 };
 };
 
 
-export { parseLocalDate, formatWeekday, formatDayMonth };
+const formatLongDate = (dateStr) => {
+  const date = parseLocalDate(dateStr);
+  if (!date) return dateStr ?? "";
+
+  return date.toLocaleDateString("pt-BR", {
+    day: "2-digit",
+    month: "long",
+    year: "numeric",
+  });
+};
+
+const formatNumericDate = (dateStr) => {
+  const date = parseLocalDate(dateStr);
+  if (!date) return dateStr ?? "";
+
+  return date.toLocaleDateString("pt-BR", {
+    day: "2-digit",
+    month: "2-digit",
+    year: "numeric",
+  });
+};
+
+const formatTimeRange = (start, end, separator = "às") => {
+  if (!start || !end) return "";
+  return `${start.slice(0, 5)} ${separator} ${end.slice(0, 5)}`;
+};
+
+export {
+  formatDayMonth,
+  formatLongDate,
+  formatNumericDate,
+  formatTimeRange,
+  formatWeekday,
+  parseLocalDate,
+  parseUtcDate,
+};

+ 33 - 1
src/helpers/utils.js

@@ -296,6 +296,35 @@ const formatAddress = (address) => {
   return parts.join(', ');
   return parts.join(', ');
 };
 };
 
 
+const formatCardBrand = (brand) => {
+  if (!brand) return '';
+
+  const brands = {
+    diners: 'Diners',
+    discover: 'Discover',
+    elo: 'Elo',
+    hipercard: 'Hipercard',
+    mastercard: 'Mastercard',
+    visa: 'VISA',
+  };
+
+  return brands[brand] ?? brand.toUpperCase();
+};
+
+const formatDistance = (distance) => {
+  if (distance === null || distance === undefined || distance === '') return '—';
+
+  const value = Number(distance);
+  if (!Number.isFinite(value)) return '—';
+
+  return `${value.toLocaleString('pt-BR', { maximumFractionDigits: 1 })} km`;
+};
+
+const formatRating = (rating, fallback = '—') => {
+  if (rating === null || rating === undefined || rating === '') return fallback;
+  return Number(rating).toFixed(1).replace('.', ',');
+};
+
 const getFirstName = (fullName) => {
 const getFirstName = (fullName) => {
   if (!fullName) return '';
   if (!fullName) return '';
 
 
@@ -367,8 +396,11 @@ export {
   detectCardBrand,
   detectCardBrand,
   validateCardExpiration,
   validateCardExpiration,
   formatAddress,
   formatAddress,
+  formatCardBrand,
   getFirstName,
   getFirstName,
   calculateDailyPrices,
   calculateDailyPrices,
   chooseprice,
   chooseprice,
-  formatLabelByPeriodType
+  formatDistance,
+  formatLabelByPeriodType,
+  formatRating,
 };
 };

+ 17 - 3
src/i18n/locales/en.json

@@ -442,6 +442,7 @@
     },
     },
     "today_schedules": {
     "today_schedules": {
       "title": "Today",
       "title": "Today",
+      "attention_badge": "attention, daily service!",
       "start_with": "Service starting with",
       "start_with": "Service starting with",
       "not_started_label": "Starting soon with",
       "not_started_label": "Starting soon with",
       "started_by": "Service started by",
       "started_by": "Service started by",
@@ -477,7 +478,9 @@
       "custom": "custom",
       "custom": "custom",
       "age": "({age} years)",
       "age": "({age} years)",
       "distance": "Distance",
       "distance": "Distance",
-      "title": "proposals from service providers"
+      "title": "proposals from service providers",
+      "dismiss": "dismiss",
+      "accept": "accept"
     },
     },
     "last_schedules": {
     "last_schedules": {
       "title": "Last services",
       "title": "Last services",
@@ -626,13 +629,20 @@
       "search_btn": "search",
       "search_btn": "search",
       "indicate_title": "Refer a cleaner to a friend.",
       "indicate_title": "Refer a cleaner to a friend.",
       "indicate_btn": "Refer",
       "indicate_btn": "Refer",
-      "indicate_whatsapp_message": "Hi! I use the Diária app to hire cleaners, and I want to recommend a professional: {name} ({city}). You can download the Diária app and view their profile here 👇",
+      "indicate_whatsapp_message": "Hi! I use the Diária app to hire cleaners, and I want to recommend a professional: {name} ({city}).\n\n1. Download the Diária app here \n{link}\n\n2. In the app, open Profile > Favorites, tap \"{button}\" and enter the code:\n\n{code}\n\nThat's it, {name} will be saved to your favorites.",
       "providers_title": "Cleaners",
       "providers_title": "Cleaners",
       "until": "Until",
       "until": "Until",
       "schedule_btn": "book",
       "schedule_btn": "book",
       "remove_title": "Do you want to remove the cleaner from favorites?",
       "remove_title": "Do you want to remove the cleaner from favorites?",
       "remove_confirm": "Remove",
       "remove_confirm": "Remove",
-      "remove_cancel": "Cancel"
+      "remove_cancel": "Cancel",
+      "code_btn": "I have a code",
+      "code_title": "Add by code",
+      "code_subtitle": "Enter the 6-character code the cleaner shared with you.",
+      "code_placeholder": "ABC123",
+      "code_confirm": "Add to favorites",
+      "code_cancel": "Cancel",
+      "code_error": "Could not add a favorite with this code. Please try again."
     },
     },
     "address": {
     "address": {
       "title": "Addresses",
       "title": "Addresses",
@@ -838,6 +848,8 @@
     "local_cart_title": "Orders",
     "local_cart_title": "Orders",
     "orders_title": "Orders",
     "orders_title": "Orders",
     "pending_custom_title": "Requests in progress",
     "pending_custom_title": "Requests in progress",
+    "pending_default_title": "Requested bookings",
+    "awaiting_payment_title": "Awaiting payment",
     "no_orders": "No orders created.",
     "no_orders": "No orders created.",
     "order_title": "Order #{id}",
     "order_title": "Order #{id}",
     "view_favorites": "view favorites",
     "view_favorites": "view favorites",
@@ -856,6 +868,7 @@
     "pix_total": "Pix total",
     "pix_total": "Pix total",
     "credit_card_fee": "Card total",
     "credit_card_fee": "Card total",
     "credit_card_total": "Card total",
     "credit_card_total": "Card total",
+    "total": "Total",
     "pix_discount": "Pix discount: save R$ {value}",
     "pix_discount": "Pix discount: save R$ {value}",
     "discount_saved": "Orders total discount: R$ {value}",
     "discount_saved": "Orders total discount: R$ {value}",
     "discount_hint": "Add {count} more time slot(s) to get the orders discount.",
     "discount_hint": "Add {count} more time slot(s) to get the orders discount.",
@@ -865,6 +878,7 @@
     "address": "Address",
     "address": "Address",
     "quantity": "Quantity",
     "quantity": "Quantity",
     "schedule_count": "{count} time slot(s)",
     "schedule_count": "{count} time slot(s)",
+    "package_count": "Package with {count} services",
     "submit": "send request",
     "submit": "send request",
     "close": "close orders",
     "close": "close orders",
     "closed_success": "Orders closed. Wait for provider approval.",
     "closed_success": "Orders closed. Wait for provider approval.",

+ 17 - 3
src/i18n/locales/es.json

@@ -442,6 +442,7 @@
     },
     },
     "today_schedules": {
     "today_schedules": {
       "title": "Hoy",
       "title": "Hoy",
+      "attention_badge": "¡atención, diaria!",
       "start_with": "Servicio iniciado con",
       "start_with": "Servicio iniciado con",
       "not_started_label": "Comienza pronto con",
       "not_started_label": "Comienza pronto con",
       "started_by": "Servicio iniciado por",
       "started_by": "Servicio iniciado por",
@@ -474,7 +475,9 @@
       "custom": "a medida",
       "custom": "a medida",
       "age": "({edad} años)",
       "age": "({edad} años)",
       "distance": "Distancia",
       "distance": "Distancia",
-      "title": "Propuestas de proveedores de servicios"
+      "title": "Propuestas de proveedores de servicios",
+      "dismiss": "descartar",
+      "accept": "aceptar"
     },
     },
     "last_schedules": {
     "last_schedules": {
       "title": "Últimos servicios",
       "title": "Últimos servicios",
@@ -623,13 +626,20 @@
       "search_btn": "buscar",
       "search_btn": "buscar",
       "indicate_title": "Recomienda un limpiador a un amigo.",
       "indicate_title": "Recomienda un limpiador a un amigo.",
       "indicate_btn": "Recomendar",
       "indicate_btn": "Recomendar",
-      "indicate_whatsapp_message": "¡Hola! Uso la app Diária para contratar limpiadores y quiero recomendarte un profesional: {name} ({city}). Puedes descargar la app Diária y ver su perfil aquí 👇",
+      "indicate_whatsapp_message": "¡Hola! Uso la app Diária para contratar limpiadores y quiero recomendarte un profesional: {name} ({city}).\n\n1. Descarga la app Diária aquí \n{link}\n\n2. En la app, abre Perfil > Favoritos, toca \"{button}\" y escribe el código:\n\n{code}\n\nListo, {name} quedará guardado en tus favoritos.",
       "providers_title": "Limpiadores",
       "providers_title": "Limpiadores",
       "until": "hasta",
       "until": "hasta",
       "schedule_btn": "reservar",
       "schedule_btn": "reservar",
       "remove_title": "¿Deseas eliminar al limpiador de favoritos?",
       "remove_title": "¿Deseas eliminar al limpiador de favoritos?",
       "remove_confirm": "Eliminar",
       "remove_confirm": "Eliminar",
-      "remove_cancel": "Cancelar"
+      "remove_cancel": "Cancelar",
+      "code_btn": "Tengo un código",
+      "code_title": "Añadir por código",
+      "code_subtitle": "Escribe el código de 6 caracteres que el limpiador compartió contigo.",
+      "code_placeholder": "ABC123",
+      "code_confirm": "Añadir a favoritos",
+      "code_cancel": "Cancelar",
+      "code_error": "No se pudo añadir a favoritos con este código. Inténtalo de nuevo."
     },
     },
     "address": {
     "address": {
       "title": "Direcciones",
       "title": "Direcciones",
@@ -835,6 +845,8 @@
     "local_cart_title": "Pedidos",
     "local_cart_title": "Pedidos",
     "orders_title": "Pedidos",
     "orders_title": "Pedidos",
     "pending_custom_title": "Solicitudes en curso",
     "pending_custom_title": "Solicitudes en curso",
+    "pending_default_title": "Reservas solicitadas",
+    "awaiting_payment_title": "Esperando el pago",
     "no_orders": "Ningún pedido creado.",
     "no_orders": "Ningún pedido creado.",
     "order_title": "Pedido #{id}",
     "order_title": "Pedido #{id}",
     "view_favorites": "ver favoritos",
     "view_favorites": "ver favoritos",
@@ -853,6 +865,7 @@
     "pix_total": "Total con Pix",
     "pix_total": "Total con Pix",
     "credit_card_fee": "Total con tarjeta",
     "credit_card_fee": "Total con tarjeta",
     "credit_card_total": "Total con tarjeta",
     "credit_card_total": "Total con tarjeta",
+    "total": "Total",
     "pix_discount": "Descuento con Pix: ahorre R$ {value}",
     "pix_discount": "Descuento con Pix: ahorre R$ {value}",
     "discount_saved": "Descuento en el total de los pedidos: R$ {value}",
     "discount_saved": "Descuento en el total de los pedidos: R$ {value}",
     "discount_hint": "Agregue {count} horario(s) más para recibir descuento en los pedidos.",
     "discount_hint": "Agregue {count} horario(s) más para recibir descuento en los pedidos.",
@@ -862,6 +875,7 @@
     "address": "Dirección",
     "address": "Dirección",
     "quantity": "Cantidad",
     "quantity": "Cantidad",
     "schedule_count": "{count} horario(s)",
     "schedule_count": "{count} horario(s)",
+    "package_count": "Paquete con {count} servicios",
     "submit": "enviar solicitud",
     "submit": "enviar solicitud",
     "close": "cerrar pedidos",
     "close": "cerrar pedidos",
     "closed_success": "Pedidos cerrados. Espere la aprobación de los profesionales.",
     "closed_success": "Pedidos cerrados. Espere la aprobación de los profesionales.",

+ 18 - 4
src/i18n/locales/pt.json

@@ -442,6 +442,7 @@
     },
     },
     "today_schedules": {
     "today_schedules": {
       "title": "Hoje",
       "title": "Hoje",
+      "attention_badge": "atenção, diária!",
       "start_with": "Início do serviço com",
       "start_with": "Início do serviço com",
       "not_started_label": "Início em breve com",
       "not_started_label": "Início em breve com",
       "started_by": "Serviço iniciado por",
       "started_by": "Serviço iniciado por",
@@ -477,7 +478,9 @@
       "custom": "sob medida",
       "custom": "sob medida",
       "age": "({idade} anos)",
       "age": "({idade} anos)",
       "distance": "Distância",
       "distance": "Distância",
-      "title": "Propostas de prestadores"
+      "title": "Propostas de prestadores",
+      "dismiss": "dispensar",
+      "accept": "aceitar"
     },
     },
     "last_schedules": {
     "last_schedules": {
       "title": "Últimos serviços",
       "title": "Últimos serviços",
@@ -626,13 +629,20 @@
       "search_btn": "buscar",
       "search_btn": "buscar",
       "indicate_title": "Indique um diarista para um amigo.",
       "indicate_title": "Indique um diarista para um amigo.",
       "indicate_btn": "Indicar",
       "indicate_btn": "Indicar",
-      "indicate_whatsapp_message": "Oi! Eu uso o aplicativo Diária pra contratar diaristas, e quero te indicar um profissional: {name} ({city}). Você pode baixar o app Diária e ver o perfil dela por aqui 👇",
+      "indicate_whatsapp_message": "Oi! Eu uso o aplicativo Diária pra contratar diaristas, e quero te indicar um profissional: {name} ({city}).\n\n1. Baixe o app Diária aqui \n{link}\n\n2. No app, abra Perfil > Favoritos, toque em \"{button}\" e digite o código:\n\n{code}\n\nPronto, {name} já fica salvo nos seus favoritos.",
       "providers_title": "Diaristas",
       "providers_title": "Diaristas",
       "until": "até",
       "until": "até",
       "schedule_btn": "agendar",
       "schedule_btn": "agendar",
       "remove_title": "Deseja remover o diarista dos favoritos?",
       "remove_title": "Deseja remover o diarista dos favoritos?",
       "remove_confirm": "Remover",
       "remove_confirm": "Remover",
-      "remove_cancel": "Cancelar"
+      "remove_cancel": "Cancelar",
+      "code_btn": "Tenho um código",
+      "code_title": "Adicionar por código",
+      "code_subtitle": "Digite o código de 6 caracteres que o diarista compartilhou com você.",
+      "code_placeholder": "ABC123",
+      "code_confirm": "Favoritar",
+      "code_cancel": "Cancelar",
+      "code_error": "Não foi possível favoritar com esse código. Tente novamente."
     },
     },
     "address": {
     "address": {
       "title": "Endereços",
       "title": "Endereços",
@@ -845,7 +855,9 @@
     "other_provider_blocked": "Seu pedido já possui horários com outro profissional. Finalize ou esvazie o pedido atual antes de agendar com outro profissional.",
     "other_provider_blocked": "Seu pedido já possui horários com outro profissional. Finalize ou esvazie o pedido atual antes de agendar com outro profissional.",
     "local_cart_title": "Pedidos",
     "local_cart_title": "Pedidos",
     "orders_title": "Pedidos",
     "orders_title": "Pedidos",
-    "pending_custom_title": "Solicitações em andamento",
+    "pending_custom_title": "Solicitações Sob Medida em andamento",
+    "pending_default_title": "Agendamentos solicitados",
+    "awaiting_payment_title": "Aguardando pagamento",
     "no_orders": "Nenhum pedido criado.",
     "no_orders": "Nenhum pedido criado.",
     "order_title": "Pedido #{id}",
     "order_title": "Pedido #{id}",
     "view_favorites": "ver favoritos",
     "view_favorites": "ver favoritos",
@@ -864,6 +876,7 @@
     "pix_total": "Total com Pix",
     "pix_total": "Total com Pix",
     "credit_card_fee": "Total no cartão",
     "credit_card_fee": "Total no cartão",
     "credit_card_total": "Total no cartão",
     "credit_card_total": "Total no cartão",
+    "total" : "Total",
     "pix_discount": "Desconto no Pix: economize R$ {value}",
     "pix_discount": "Desconto no Pix: economize R$ {value}",
     "discount_saved": "Desconto no total dos pedidos: R$ {value}",
     "discount_saved": "Desconto no total dos pedidos: R$ {value}",
     "discount_hint": "Adicione mais {count} horário(s) para receber desconto nos pedidos.",
     "discount_hint": "Adicione mais {count} horário(s) para receber desconto nos pedidos.",
@@ -873,6 +886,7 @@
     "address": "Endereço",
     "address": "Endereço",
     "quantity": "Quantidade",
     "quantity": "Quantidade",
     "schedule_count": "{count} horário(s)",
     "schedule_count": "{count} horário(s)",
+    "package_count": "Pacote com {count} serviços",
     "submit": "enviar solicitação",
     "submit": "enviar solicitação",
     "close": "confirmar pedido(s)",
     "close": "confirmar pedido(s)",
     "closed_success": "Pedidos fechados. Aguarde a aprovação dos profissionais.",
     "closed_success": "Pedidos fechados. Aguarde a aprovação dos profissionais.",

+ 75 - 58
src/pages/agenda/CalendarPage.vue

@@ -41,16 +41,16 @@
           <q-card
           <q-card
             v-for="item in upcomingSchedules"
             v-for="item in upcomingSchedules"
             :key="item.id"
             :key="item.id"
-            :flat="false"
             class="calendar-card bg-surface shadow-card q-mb-sm"
             class="calendar-card bg-surface shadow-card q-mb-sm"
+            :flat="false"
           >
           >
             <q-card-section class="q-pa-sm">
             <q-card-section class="q-pa-sm">
               <div class="row no-wrap items-start q-gutter-x-sm">
               <div class="row no-wrap items-start q-gutter-x-sm">
                 <q-avatar size="44px">
                 <q-avatar size="44px">
                   <img
                   <img
                     v-if="item.provider_photo"
                     v-if="item.provider_photo"
-                    :src="item.provider_photo"
                     style="object-fit: cover"
                     style="object-fit: cover"
+                    :src="item.provider_photo"
                   />
                   />
 
 
                   <span
                   <span
@@ -71,7 +71,7 @@
                   <span class="font12 fontbold">
                   <span class="font12 fontbold">
                     {{
                     {{
                       getFirstName(item.provider_name) ||
                       getFirstName(item.provider_name) ||
-                      $t("dashboard_client.agenda.waiting_proposals")
+                        $t("dashboard_client.agenda.waiting_proposals")
                     }}
                     }}
                   </span>
                   </span>
 
 
@@ -161,16 +161,16 @@
           <q-card
           <q-card
             v-for="item in completedSchedules"
             v-for="item in completedSchedules"
             :key="item.id"
             :key="item.id"
-            :flat="false"
             class="calendar-card bg-surface shadow-card q-mb-sm"
             class="calendar-card bg-surface shadow-card q-mb-sm"
+            :flat="false"
           >
           >
             <q-card-section class="q-pa-sm">
             <q-card-section class="q-pa-sm">
               <div class="row no-wrap items-start q-gutter-x-sm">
               <div class="row no-wrap items-start q-gutter-x-sm">
                 <q-avatar size="44px">
                 <q-avatar size="44px">
                   <img
                   <img
                     v-if="item.provider_photo"
                     v-if="item.provider_photo"
-                    :src="item.provider_photo"
                     style="object-fit: cover"
                     style="object-fit: cover"
+                    :src="item.provider_photo"
                   />
                   />
 
 
                   <span
                   <span
@@ -191,7 +191,7 @@
                   <span class="font12 fontbold">
                   <span class="font12 fontbold">
                     {{
                     {{
                       getFirstName(item.provider_name) ||
                       getFirstName(item.provider_name) ||
-                      $t("dashboard_client.agenda.waiting_proposals")
+                        $t("dashboard_client.agenda.waiting_proposals")
                     }}
                     }}
                   </span>
                   </span>
 
 
@@ -313,33 +313,29 @@ import {
   getScheduleTotalWithPlatformFee,
   getScheduleTotalWithPlatformFee,
 } from "src/helpers/paymentPlatformFees";
 } from "src/helpers/paymentPlatformFees";
 
 
-import { onMounted, ref } from "vue";
+import { nextTick, onMounted, ref } from "vue";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
-import { useRouter } from "vue-router";
+import { useRoute, useRouter } from "vue-router";
 
 
-import NextSchedulesDetailsDialog from "src/components/dashboard/NextSchedulesDetailsDialog.vue";
-import ScheduleRatingDialog from "src/components/dashboard/ScheduleRatingDialog.vue";
+import NextSchedulesDetailsDialog from "src/pages/dashboard/components/schedule/NextSchedulesDetailsDialog.vue";
+import ScheduleRatingDialog from "src/pages/dashboard/components/schedule/ScheduleRatingDialog.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 
 
 const $q = useQuasar();
 const $q = useQuasar();
-const { t } = useI18n();
-const router = useRouter();
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
+const route = useRoute();
+const router = useRouter();
+const { t } = useI18n();
 
 
+const completedSchedules = ref([]);
 const loading = ref(true);
 const loading = ref(true);
 const upcomingSchedules = ref([]);
 const upcomingSchedules = ref([]);
-const completedSchedules = ref([]);
 
 
-const periodLabel = (periodType) => {
-  const key = `period_types.${periodType}`;
-  const translated = t(key);
-  return translated !== key ? translated : "";
-};
+const canReview = (item) => item.status !== "cancelled";
 
 
 //
 //
-
 const formatScheduleTotal = (item) =>
 const formatScheduleTotal = (item) =>
   formatCurrency(
   formatCurrency(
     getScheduleTotalWithPlatformFee(
     getScheduleTotalWithPlatformFee(
@@ -349,44 +345,7 @@ const formatScheduleTotal = (item) =>
     ),
     ),
   );
   );
 
 
-const statusLabel = (status) => {
-  const map = {
-    pending: t("dashboard_client.agenda.status_pending"),
-    accepted: t("dashboard_client.agenda.status_accepted"),
-    paid: t("dashboard_client.agenda.status_paid"),
-    started: t("dashboard_client.agenda.status_started"),
-    finished: t("dashboard_client.agenda.status_finished"),
-    cancelled: t("dashboard_client.agenda.status_cancelled"),
-  };
-  return map[status] ?? status;
-};
-
-const statusBgColor = (status) => {
-  const map = {
-    pending: "warning-bg",
-    accepted: "success-bg",
-    paid: "success-bg",
-    started: "info-bg",
-    finished: "neutral-bg",
-    cancelled: "secondary-bg",
-  };
-  return map[status] ?? "neutral-bg";
-};
-
-const statusTextColor = (status) => {
-  const map = {
-    pending: "warning",
-    accepted: "success",
-    paid: "success",
-    started: "info",
-    finished: "status-finished",
-    cancelled: "secondary",
-  };
-  return map[status] ?? "text";
-};
-
 //
 //
-
 const loadCalendar = async () => {
 const loadCalendar = async () => {
   const response = await getClientCalendar();
   const response = await getClientCalendar();
   if (response) {
   if (response) {
@@ -406,8 +365,6 @@ const openDetailsDialog = (schedule) => {
   });
   });
 };
 };
 
 
-const canReview = (item) => item.status !== "cancelled";
-
 const openRatingDialog = (schedule) => {
 const openRatingDialog = (schedule) => {
   if (!canReview(schedule)) return;
   if (!canReview(schedule)) return;
 
 
@@ -434,10 +391,70 @@ const openSchedulingDialog = (item) => {
   });
   });
 };
 };
 
 
+const periodLabel = (periodType) => {
+  const key = `period_types.${periodType}`;
+  const translated = t(key);
+  return translated !== key ? translated : "";
+};
+
+const statusBgColor = (status) => {
+  const map = {
+    pending: "warning-bg",
+    accepted: "success-bg",
+    paid: "success-bg",
+    started: "info-bg",
+    finished: "neutral-bg",
+    cancelled: "secondary-bg",
+  };
+  return map[status] ?? "neutral-bg";
+};
+
+const statusLabel = (status) => {
+  const map = {
+    pending: t("dashboard_client.agenda.status_pending"),
+    accepted: t("dashboard_client.agenda.status_accepted"),
+    paid: t("dashboard_client.agenda.status_paid"),
+    started: t("dashboard_client.agenda.status_started"),
+    finished: t("dashboard_client.agenda.status_finished"),
+    cancelled: t("dashboard_client.agenda.status_cancelled"),
+  };
+  return map[status] ?? status;
+};
+
+const statusTextColor = (status) => {
+  const map = {
+    pending: "warning",
+    accepted: "success",
+    paid: "success",
+    started: "info",
+    finished: "status-finished",
+    cancelled: "secondary",
+  };
+  return map[status] ?? "text";
+};
+
+const openScheduleFromQuery = async () => {
+  const scheduleId = Number(route.query.scheduleId);
+
+  if (!scheduleId) return;
+
+  await router.replace({ path: route.path, query: {} });
+  await nextTick();
+
+  const schedule = [...upcomingSchedules.value, ...completedSchedules.value].find(
+    (item) => item.id === scheduleId,
+  );
+
+  if (schedule) openDetailsDialog(schedule);
+};
+
 onMounted(async () => {
 onMounted(async () => {
   await Promise.all([loadCalendar(), loadPlatformFees().catch(() => {})]);
   await Promise.all([loadCalendar(), loadPlatformFees().catch(() => {})]);
   loading.value = false;
   loading.value = false;
+
+  await openScheduleFromQuery();
 });
 });
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 118 - 71
src/pages/dashboard/DashboardPage.vue

@@ -5,36 +5,57 @@
         <q-spinner-dots color="primary" />
         <q-spinner-dots color="primary" />
       </div>
       </div>
     </template>
     </template>
+
     <template v-else>
     <template v-else>
       <q-pull-to-refresh color="primary" @refresh="onRefresh">
       <q-pull-to-refresh color="primary" @refresh="onRefresh">
         <DashboardHeaderBar
         <DashboardHeaderBar
           :data="headerBar"
           :data="headerBar"
           :notifications="notifications"
           :notifications="notifications"
         />
         />
+
         <DashboardRegistrationIncomplete v-if="!registrationComplete" />
         <DashboardRegistrationIncomplete v-if="!registrationComplete" />
+
         <DashboardSummaryInfos v-else :data="summaryInfos" />
         <DashboardSummaryInfos v-else :data="summaryInfos" />
+
         <DashboardPaymentIncomplete v-if="showPaymentBanner" />
         <DashboardPaymentIncomplete v-if="showPaymentBanner" />
+
         <AddressIncompleteBanner v-if="!hasLocation" @resolved="reloadDashboard" />
         <AddressIncompleteBanner v-if="!hasLocation" @resolved="reloadDashboard" />
+
         <DashboardPendingCustomSchedules v-if="customSchedulesNoProposals.length > 0" />
         <DashboardPendingCustomSchedules v-if="customSchedulesNoProposals.length > 0" />
+
         <DashboardClientProposals v-if="clientProposals.length > 0" :data="clientProposals" @refresh-data="reloadDashboard" />
         <DashboardClientProposals v-if="clientProposals.length > 0" :data="clientProposals" @refresh-data="reloadDashboard" />
+
         <DashboardPendingSchedules
         <DashboardPendingSchedules
           v-if="pendingServicePackages.length > 0"
           v-if="pendingServicePackages.length > 0"
+          progress-class="progress-fill--urgent"
           :data="pendingServicePackages"
           :data="pendingServicePackages"
           :type="'servicePackage'"
           :type="'servicePackage'"
-          progress-class="progress-fill--urgent"
           @view-details="openServicePackagePaymentDialog"
           @view-details="openServicePackagePaymentDialog"
         />
         />
+
         <DashboardPendingSchedules
         <DashboardPendingSchedules
           v-if="pendingRequests.length > 0"
           v-if="pendingRequests.length > 0"
-          :data="pendingRequests"
           progress-class="progress-fill--loop"
           progress-class="progress-fill--loop"
+          :data="pendingRequests"
           @cancel="openCancelRequestDialog"
           @cancel="openCancelRequestDialog"
+          @view-details="openPackageDetailsDialog"
+        />
+
+        <DashboardTodaySchedules
+          v-if="todaySchedules.length > 0"
+          :data="todaySchedules"
+          @rate="openRatingDialog"
+          @view-details="openScheduleOnCalendar"
         />
         />
-        <DashboardTodaySchedules v-if="todaySchedules.length > 0" :data="todaySchedules" @rate="openRatingDialog" />
+
         <DashboardScrollAreaSchedules />
         <DashboardScrollAreaSchedules />
+
         <DashboardNextSchedules v-if="nextSchedules.length > 0" :data="nextSchedules" @view-details="openNextScheduleDialog" />
         <DashboardNextSchedules v-if="nextSchedules.length > 0" :data="nextSchedules" @view-details="openNextScheduleDialog" />
+
         <DashboardLastDoneSchedules v-if="lastDoneSchedules.length > 0" :data="lastDoneSchedules" />
         <DashboardLastDoneSchedules v-if="lastDoneSchedules.length > 0" :data="lastDoneSchedules" />
+
         <DashboardFavoriteProviders v-if="favoriteProviders.length > 0" :data="favoriteProviders" />
         <DashboardFavoriteProviders v-if="favoriteProviders.length > 0" :data="favoriteProviders" />
+
         <DashboardProvidersClose v-if="hasLocation" :data="providersClose" />
         <DashboardProvidersClose v-if="hasLocation" :data="providersClose" />
       </q-pull-to-refresh>
       </q-pull-to-refresh>
     </template>
     </template>
@@ -42,62 +63,93 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
+import { computed, onMounted, ref } from 'vue';
+import { dadosDashboard } from 'src/api/dashboard';
+import { LocalStorage, useQuasar } from 'quasar';
+import { updateMe } from 'src/api/user';
+import { useI18n } from 'vue-i18n';
+import { usePaymentStore } from 'src/stores/payment';
+import { useRoute, useRouter } from 'vue-router'
+import { userStore } from 'src/stores/user';
+
+import AddressIncompleteBanner from 'src/components/shared/AddressIncompleteBanner.vue';
+import DashboardClientProposals from 'src/pages/dashboard/components/DashboardClientProposals.vue';
+import DashboardFavoriteProviders from 'src/components/dashboard/DashboardFavoriteProviders.vue';
 import DashboardHeaderBar from 'src/components/dashboard/DashboardHeaderBar.vue';
 import DashboardHeaderBar from 'src/components/dashboard/DashboardHeaderBar.vue';
-import DashboardSummaryInfos from 'src/components/dashboard/DashboardSummaryInfos.vue';
-import DashboardRegistrationIncomplete from 'src/components/dashboard/DashboardRegistrationIncomplete.vue';
+import DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
+import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedules.vue';
 import DashboardPaymentIncomplete from 'src/components/dashboard/DashboardPaymentIncomplete.vue';
 import DashboardPaymentIncomplete from 'src/components/dashboard/DashboardPaymentIncomplete.vue';
+import DashboardPendingCustomSchedules from 'src/pages/dashboard/components/DashboardPendingCustomSchedules.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 ScheduleCancelDialog from 'src/components/dashboard/ScheduleCancelDialog.vue';
-import DashboardScrollAreaSchedules from 'src/components/dashboard/DashboardScrollAreaSchedules.vue';
-import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedules.vue';
-import DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
-import DashboardFavoriteProviders from 'src/components/dashboard/DashboardFavoriteProviders.vue';
+import PackageScheduleDetailsDialog from 'src/components/dashboard/PackageScheduleDetailsDialog.vue';
 import DashboardProvidersClose from 'src/components/dashboard/DashboardProvidersClose.vue';
 import DashboardProvidersClose from 'src/components/dashboard/DashboardProvidersClose.vue';
-import AddressIncompleteBanner from 'src/components/shared/AddressIncompleteBanner.vue';
+import DashboardRegistrationIncomplete from 'src/components/dashboard/DashboardRegistrationIncomplete.vue';
+import DashboardScrollAreaSchedules from 'src/components/dashboard/DashboardScrollAreaSchedules.vue';
+import DashboardSummaryInfos from 'src/components/dashboard/DashboardSummaryInfos.vue';
 import DashboardTodaySchedules from 'src/components/dashboard/DashboardTodaySchedules.vue';
 import DashboardTodaySchedules from 'src/components/dashboard/DashboardTodaySchedules.vue';
 import FinalSuccesModal from '../schedules/components/FinalSuccesModal.vue';
 import FinalSuccesModal from '../schedules/components/FinalSuccesModal.vue';
-import DashboardPendingCustomSchedules from 'src/pages/dashboard/components/DashboardPendingCustomSchedules.vue';
-import DashboardClientProposals from 'src/pages/dashboard/components/DashboardClientProposals.vue';
-import { useRoute, useRouter } from 'vue-router'
-import { onMounted, ref, computed } from 'vue';
-import { useI18n } from 'vue-i18n';
-import { LocalStorage, useQuasar } from 'quasar';
-import { dadosDashboard } from 'src/api/dashboard';
-import { updateMe } from 'src/api/user';
-import { userStore } from 'src/stores/user';
-import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
-import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
+import NextSchedulesDetailsDialog from 'src/pages/dashboard/components/schedule/NextSchedulesDetailsDialog.vue';
+import ScheduleAcceptedDialog from 'src/pages/dashboard/components/schedule/ScheduleAcceptedDialog.vue';
+import ScheduleCancelDialog from 'src/pages/dashboard/components/schedule/ScheduleCancelDialog.vue';
+import ScheduleRatingDialog from 'src/pages/dashboard/components/schedule/ScheduleRatingDialog.vue';
 
 
-const router = useRouter()
-const route = useRoute()
-const { t } = useI18n();
 const $q = useQuasar();
 const $q = useQuasar();
+const paymentStore = usePaymentStore();
+const route = useRoute()
+const router = useRouter()
 const store = userStore();
 const store = userStore();
+const { t } = useI18n();
 
 
+const clientProposals = ref([]);
+const customSchedulesNoProposals = ref([]);
+const favoriteProviders = ref([]);
+const hasLocation = ref(true);
 const hasPaymentMethods = ref(true);
 const hasPaymentMethods = ref(true);
 const headerBar = ref({});
 const headerBar = ref({});
-const summaryInfos = ref({});
+const lastDoneSchedules = ref([]);
+const loading = ref(true);
+const nextSchedules = ref([]);
+const notifications = ref([]);
 const pendingSchedules = ref([]);
 const pendingSchedules = ref([]);
 const pendingServicePackages = ref([]);
 const pendingServicePackages = ref([]);
-const nextSchedules = ref([]);
-const clientProposals = ref([]);
-const customSchedulesNoProposals = ref([]);
-const lastDoneSchedules = ref([]);
-const favoriteProviders = ref([]);
 const providersClose = ref([]);
 const providersClose = ref([]);
-const todaySchedules = ref([]);
-const notifications = ref([]);
-const loading = ref(true);
-const registrationComplete = computed(() => store.user?.registration_complete ?? true);
 const showPaymentBanner = ref(false);
 const showPaymentBanner = ref(false);
-const hasLocation = ref(true);
 
 
 const successModalKey = ref(
 const successModalKey = ref(
   route.query.success ??
   route.query.success ??
     (router.currentRoute.value.fullPath.includes('showSuccessModal') ? 'custom_schedule' : null),
     (router.currentRoute.value.fullPath.includes('showSuccessModal') ? 'custom_schedule' : null),
 );
 );
 
 
+const summaryInfos = ref({});
+const todaySchedules = ref([]);
+
+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 openPackageDetailsDialog = (schedule) => {
+  $q.dialog({
+    component: PackageScheduleDetailsDialog,
+    componentProps: { schedule },
+  });
+};
+
+const registrationComplete = computed(() => store.user?.registration_complete ?? true);
+
+const AUTO_SHOWN_PACKAGES_KEY = "sp_payment_popup_shown_ids";
+
 const successModalContents = {
 const successModalContents = {
   custom_schedule: null,
   custom_schedule: null,
   order_sent: () => ({
   order_sent: () => ({
@@ -109,21 +161,27 @@ const successModalContents = {
   }),
   }),
 };
 };
 
 
-const pendingRequests = computed(() => {
-  const seenPackages = new Set();
+let autoOpeningPackageDialog = false;
 
 
-  return pendingSchedules.value.filter((schedule) => {
-    if (schedule.status !== 'pending') return false;
+const maybeOpenSuccessModal = () => {
+  if (!successModalKey.value) return;
 
 
-    if (!schedule.service_package_id) return true;
+  if (!(successModalKey.value in successModalContents)) {
+    successModalKey.value = null;
+    return;
+  }
 
 
-    if (seenPackages.has(schedule.service_package_id)) return false;
+  const buildContent = successModalContents[successModalKey.value];
 
 
-    seenPackages.add(schedule.service_package_id);
+  successModalKey.value = null;
 
 
-    return true;
+  $q.dialog({
+    component: FinalSuccesModal,
+    componentProps: { content: buildContent ? buildContent() : null },
+  }).onDismiss(() => {
+    router.replace({ path: route.path, query: {} });
   });
   });
-});
+};
 
 
 const openCancelRequestDialog = (schedule) => {
 const openCancelRequestDialog = (schedule) => {
   $q.dialog({
   $q.dialog({
@@ -135,17 +193,17 @@ const openCancelRequestDialog = (schedule) => {
 };
 };
 
 
 const openServicePackagePaymentDialog = (servicePackage) => {
 const openServicePackagePaymentDialog = (servicePackage) => {
+  paymentStore.setPackage(servicePackage);
+
   $q.dialog({
   $q.dialog({
     component: ScheduleAcceptedDialog,
     component: ScheduleAcceptedDialog,
-    componentProps: { servicePackage }
   }).onOk(() => {
   }).onOk(() => {
     reloadDashboard();
     reloadDashboard();
+  }).onDismiss(() => {
+    paymentStore.clearPackage();
   });
   });
 };
 };
 
 
-const AUTO_SHOWN_PACKAGES_KEY = "sp_payment_popup_shown_ids";
-let autoOpeningPackageDialog = false;
-
 const maybeAutoOpenServicePackagePayment = () => {
 const maybeAutoOpenServicePackagePayment = () => {
   if (autoOpeningPackageDialog) return;
   if (autoOpeningPackageDialog) return;
 
 
@@ -159,16 +217,17 @@ const maybeAutoOpenServicePackagePayment = () => {
 
 
   autoOpeningPackageDialog = true;
   autoOpeningPackageDialog = true;
   LocalStorage.set(AUTO_SHOWN_PACKAGES_KEY, [...shownIds, packageToShow.id]);
   LocalStorage.set(AUTO_SHOWN_PACKAGES_KEY, [...shownIds, packageToShow.id]);
+  paymentStore.setPackage(packageToShow);
 
 
   $q.dialog({
   $q.dialog({
     component: ScheduleAcceptedDialog,
     component: ScheduleAcceptedDialog,
-    componentProps: { servicePackage: packageToShow },
   })
   })
     .onOk(() => {
     .onOk(() => {
       reloadDashboard();
       reloadDashboard();
     })
     })
     .onDismiss(() => {
     .onDismiss(() => {
       autoOpeningPackageDialog = false;
       autoOpeningPackageDialog = false;
+      paymentStore.clearPackage();
     });
     });
 };
 };
 
 
@@ -196,26 +255,6 @@ const reloadDashboard = async (showLoader = true) => {
   maybeAutoOpenServicePackagePayment();
   maybeAutoOpenServicePackagePayment();
 };
 };
 
 
-const maybeOpenSuccessModal = () => {
-  if (!successModalKey.value) return;
-
-  if (!(successModalKey.value in successModalContents)) {
-    successModalKey.value = null;
-    return;
-  }
-
-  const buildContent = successModalContents[successModalKey.value];
-
-  successModalKey.value = null;
-
-  $q.dialog({
-    component: FinalSuccesModal,
-    componentProps: { content: buildContent ? buildContent() : null },
-  }).onDismiss(() => {
-    router.replace({ path: route.path, query: {} });
-  });
-};
-
 const onRefresh = async (done) => {
 const onRefresh = async (done) => {
   try {
   try {
     await reloadDashboard(false);
     await reloadDashboard(false);
@@ -233,6 +272,13 @@ const openNextScheduleDialog = (schedule) => {
   });
   });
 };
 };
 
 
+const openScheduleOnCalendar = (schedule) => {
+  router.push({
+    name: 'CalendarPage',
+    query: { scheduleId: schedule.id },
+  });
+};
+
 const openRatingDialog = (schedule) => {
 const openRatingDialog = (schedule) => {
   $q.dialog({
   $q.dialog({
     component: ScheduleRatingDialog,
     component: ScheduleRatingDialog,
@@ -254,6 +300,7 @@ onMounted(async () => {
     store.markFirstAccessSeen();
     store.markFirstAccessSeen();
   }
   }
 });
 });
+
 </script>
 </script>
 
 
 <style scoped>
 <style scoped>

+ 35 - 36
src/pages/dashboard/components/DashboardClientProposals.vue

@@ -6,12 +6,16 @@
       {{ $t("dashboard_client.client_proposals.title") }}
       {{ $t("dashboard_client.client_proposals.title") }}
     </div>
     </div>
 
 
-    <div class="scroll-track q-pb-md q-px-md text-text">
+    <div
+      ref="trackRef"
+      class="scroll-track q-pb-md q-px-md text-text"
+      @scroll="onTrackScroll"
+    >
       <q-card
       <q-card
         v-for="item in data"
         v-for="item in data"
         :key="item.id"
         :key="item.id"
-        :flat="false"
         class="proposal-card shadow-card"
         class="proposal-card shadow-card"
+        :flat="false"
       >
       >
         <div class="row no-wrap">
         <div class="row no-wrap">
           <div class="column">
           <div class="column">
@@ -21,8 +25,8 @@
             >
             >
               <img
               <img
                 v-if="item.provider_photo"
                 v-if="item.provider_photo"
-                :src="item.provider_photo"
                 style="object-fit: cover; border-radius: 50%"
                 style="object-fit: cover; border-radius: 50%"
+                :src="item.provider_photo"
               />
               />
 
 
               <span v-else>
               <span v-else>
@@ -56,6 +60,7 @@
                       })
                       })
                     }}
                     }}
                   </span>
                   </span>
+
                   <span class="font10 fontmedium">
                   <span class="font10 fontmedium">
                     <q-icon
                     <q-icon
                       class="font9 fontregular"
                       class="font9 fontregular"
@@ -104,7 +109,6 @@
               </div>
               </div>
             </div>
             </div>
 
 
-            <!-- PREÇO -->
             <div class="price text-text">
             <div class="price text-text">
               <span class="font9 fontmedium distance">
               <span class="font9 fontmedium distance">
                 <span class="fontbold font10">
                 <span class="fontbold font10">
@@ -133,7 +137,7 @@
               <q-btn
               <q-btn
                 class="btn-reject"
                 class="btn-reject"
                 flat
                 flat
-                label="recusar"
+                :label="$t('dashboard_client.client_proposals.dismiss')"
                 padding="4px 12px"
                 padding="4px 12px"
                 size="sm"
                 size="sm"
                 @click="() => handleRefuseProposal(item.id)"
                 @click="() => handleRefuseProposal(item.id)"
@@ -141,7 +145,7 @@
 
 
               <q-btn
               <q-btn
                 class="btn-accept"
                 class="btn-accept"
-                label="aceitar"
+                :label="$t('dashboard_client.client_proposals.accept')"
                 padding="4px 12px"
                 padding="4px 12px"
                 size="sm"
                 size="sm"
                 @click="() => handleAcceptProposal(item)"
                 @click="() => handleAcceptProposal(item)"
@@ -151,15 +155,16 @@
         </div>
         </div>
       </q-card>
       </q-card>
     </div>
     </div>
+
+    <DefaultCarouselIndicator
+      :active="activeIndex"
+      :count="pageCount"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { refuseProposal } from "src/api/customSchedules";
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
-import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
-import { useQuasar } from "quasar";
-import SchedulePaymentDialog from "src/components/dashboard/SchedulePaymentDialog.vue";
 
 
 import {
 import {
   chooseprice,
   chooseprice,
@@ -168,42 +173,32 @@ import {
   getFirstName,
   getFirstName,
 } from "src/helpers/utils";
 } from "src/helpers/utils";
 
 
-// import { getProviderGenderLabel } from "src/helpers/provider";
-// import { useI18n } from "vue-i18n";
+import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
+import { refuseProposal } from "src/api/customSchedules";
+import { usePaymentStore } from "src/stores/payment";
+import { useCarouselIndicator } from "src/composables/useCarouselIndicator";
+import { useQuasar } from "quasar";
 
 
-const emit = defineEmits(["refreshData"]);
+import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
+import SchedulePaymentDialog from "./schedule/SchedulePaymentDialog.vue";
 
 
-defineProps({
+const props = defineProps({
   data: {
   data: {
     type: Array,
     type: Array,
     default: () => [],
     default: () => [],
   },
   },
 });
 });
 
 
+// import { getProviderGenderLabel } from "src/helpers/provider";
+// import { useI18n } from "vue-i18n";
+const emit = defineEmits(["refreshData"]);
+
 // const { t } = useI18n();
 // const { t } = useI18n();
 const $q = useQuasar();
 const $q = useQuasar();
+const paymentStore = usePaymentStore();
 
 
-// const formatTime = (time) => {
-//   if (!time) return '';
-
-//   const [hour, minute] = time.split(':');
-//   return `${hour}:${minute}`;
-// };
-
-// const formatDate = (date) => {
-//   if (!date) return '';
-
-//   const d = new Date(date);
-
-//   const weekday = d.toLocaleDateString('pt-BR', {
-//     weekday: 'long'
-//   });
-
-//   const day = String(d.getDate()).padStart(2, '0');
-//   const month = String(d.getMonth() + 1).padStart(2, '0');
-
-//   return `${weekday}, ${day}-${month}`;
-// };
+const { activeIndex, onTrackScroll, pageCount, trackRef } =
+  useCarouselIndicator(() => props.data);
 
 
 const handleAcceptProposal = (item) => {
 const handleAcceptProposal = (item) => {
   const servicePackage = {
   const servicePackage = {
@@ -212,14 +207,17 @@ const handleAcceptProposal = (item) => {
     address: item.address ?? null,
     address: item.address ?? null,
   };
   };
 
 
+  paymentStore.setPackage(servicePackage);
+
   $q.dialog({
   $q.dialog({
     component: SchedulePaymentDialog,
     component: SchedulePaymentDialog,
     componentProps: {
     componentProps: {
-      servicePackage,
       targetType: "schedule_proposal",
       targetType: "schedule_proposal",
     },
     },
   }).onOk(() => {
   }).onOk(() => {
     emit("refreshData");
     emit("refreshData");
+  }).onDismiss(() => {
+    paymentStore.clearPackage();
   });
   });
 };
 };
 
 
@@ -236,6 +234,7 @@ const handleRefuseProposal = async (proposalId) => {
     // isLoading.value = false
     // isLoading.value = false
   }
   }
 };
 };
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

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

@@ -92,9 +92,67 @@ const router = useRouter();
 }
 }
 
 
 .progress-fill {
 .progress-fill {
-  width: 35%;
+  position: relative;
+  width: 0;
   height: 100%;
   height: 100%;
+  border-radius: inherit;
+  overflow: hidden;
   background: linear-gradient(90deg, #ff72df, #8f5cff);
   background: linear-gradient(90deg, #ff72df, #8f5cff);
+  animation: progressLoopFill 3s ease-in-out infinite;
+}
+
+.progress-fill::after {
+  content: "";
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: -35%;
+  width: 35%;
+
+  background: linear-gradient(
+    90deg,
+    transparent,
+    rgba(255, 255, 255, 0.45),
+    transparent
+  );
+
+  transform: skewX(-20deg);
+  animation: progressShine 2s linear infinite;
+}
+
+@keyframes progressLoopFill {
+  0% {
+    width: 0;
+  }
+
+  80% {
+    width: 100%;
+  }
+
+  100% {
+    width: 100%;
+  }
+}
+
+@keyframes progressShine {
+  from {
+    left: -35%;
+  }
+
+  to {
+    left: 135%;
+  }
+}
+
+@media (prefers-reduced-motion: reduce) {
+  .progress-fill,
+  .progress-fill::after {
+    animation: none;
+  }
+
+  .progress-fill {
+    width: 100%;
+  }
 }
 }
 
 
 .clock-badge {
 .clock-badge {

+ 20 - 43
src/components/dashboard/NextSchedulesDetailsDialog.vue → src/pages/dashboard/components/schedule/NextSchedulesDetailsDialog.vue

@@ -32,10 +32,10 @@
           >
           >
             {{
             {{
               "(" +
               "(" +
-              providerAge +
-              " " +
-              $t("dashboard_client.next_schedules.provider_age_unit") +
-              ")"
+                providerAge +
+                " " +
+                $t("dashboard_client.next_schedules.provider_age_unit") +
+                ")"
             }}
             }}
           </span>
           </span>
         </div>
         </div>
@@ -184,6 +184,7 @@
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
 import { computed, onMounted, ref } from "vue";
 import { computed, onMounted, ref } from "vue";
 import { formatAddress, formatCurrency, getFirstName } from "src/helpers/utils";
 import { formatAddress, formatCurrency, getFirstName } from "src/helpers/utils";
+import { formatLongDate } from "src/helpers/scheduleDate";
 import { getScheduleClienteDetails } from "src/api/dashboard";
 import { getScheduleClienteDetails } from "src/api/dashboard";
 
 
 import {
 import {
@@ -198,18 +199,17 @@ import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 import ScheduleCancelDialog from "./ScheduleCancelDialog.vue";
 import ScheduleCancelDialog from "./ScheduleCancelDialog.vue";
 
 
+defineEmits([...useDialogPluginComponent.emits]);
+
 const props = defineProps({
 const props = defineProps({
   schedule: { type: Object, required: true },
   schedule: { type: Object, required: true },
 });
 });
 
 
-defineEmits([...useDialogPluginComponent.emits]);
-
-const { t } = useI18n();
 const $q = useQuasar();
 const $q = useQuasar();
-
-const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
-
+const { dialogRef, onDialogHide, onDialogCancel, onDialogOK } =
+  useDialogPluginComponent();
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
+const { t } = useI18n();
 
 
 const details = ref(null);
 const details = ref(null);
 const loadingDetails = ref(true);
 const loadingDetails = ref(true);
@@ -218,34 +218,17 @@ const avatarStyle = computed(
   () => avatarColors[props.schedule.id % avatarColors.length],
   () => avatarColors[props.schedule.id % avatarColors.length],
 );
 );
 
 
-const parseLocalDate = (dateStr) => {
-  if (!dateStr) return null;
-
-  const s = String(dateStr);
-
-  const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
-
-  if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
-
-  const dmy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
-
-  if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
-
-  return null;
-};
-
 const fullDateLabel = computed(() => {
 const fullDateLabel = computed(() => {
-  if (props.schedule.formatted_date) return props.schedule.formatted_date;
+  return props.schedule.formatted_date || formatLongDate(props.schedule.date);
+});
 
 
-  const d = parseLocalDate(props.schedule.date);
+const priceRangeLabel = computed(() => {
+  const minPrice = details.value?.min_price;
+  const maxPrice = details.value?.max_price;
 
 
-  if (!d) return props.schedule.date ?? "";
+  if (minPrice == null || maxPrice == null) return null;
 
 
-  return d.toLocaleDateString("pt-BR", {
-    day: "2-digit",
-    month: "long",
-    year: "numeric",
-  });
+  return `${formatCurrency(minPrice)} - ${formatCurrency(maxPrice)}`;
 });
 });
 
 
 const providerAge = computed(() => {
 const providerAge = computed(() => {
@@ -287,19 +270,12 @@ const total = computed(() =>
   ),
   ),
 );
 );
 
 
-const priceRangeLabel = computed(() => {
-  const minPrice = details.value?.min_price;
-  const maxPrice = details.value?.max_price;
-
-  if (minPrice == null || maxPrice == null) return null;
-
-  return `${formatCurrency(minPrice)} - ${formatCurrency(maxPrice)}`;
-});
-
 const openCancelDialog = () => {
 const openCancelDialog = () => {
   $q.dialog({
   $q.dialog({
     component: ScheduleCancelDialog,
     component: ScheduleCancelDialog,
     componentProps: { schedule: props.schedule },
     componentProps: { schedule: props.schedule },
+  }).onOk(() => {
+    onDialogOK({ action: "cancelled", id: props.schedule.id });
   });
   });
 };
 };
 
 
@@ -318,6 +294,7 @@ onMounted(async () => {
     loadingDetails.value = false;
     loadingDetails.value = false;
   }
   }
 });
 });
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 56 - 48
src/components/dashboard/ScheduleAcceptedDialog.vue → src/pages/dashboard/components/schedule/ScheduleAcceptedDialog.vue

@@ -15,8 +15,8 @@
         >
         >
           <img
           <img
             v-if="getProfileMediaUrl(item)"
             v-if="getProfileMediaUrl(item)"
-            :src="getProfileMediaUrl(item)"
             style="object-fit: cover"
             style="object-fit: cover"
+            :src="getProfileMediaUrl(item)"
           />
           />
 
 
           <span v-else>
           <span v-else>
@@ -55,7 +55,7 @@
           class="detail-row schedule-row"
           class="detail-row schedule-row"
         >
         >
           <span class="detail-value font13">
           <span class="detail-value font13">
-            {{ formatScheduleDate(schedule) }}
+            {{ schedule.formatted_date || formatLongDate(schedule.date) }}
           </span>
           </span>
 
 
           <span class="detail-valued text-text font13">
           <span class="detail-valued text-text font13">
@@ -90,8 +90,8 @@
         <div class="detail-row">
         <div class="detail-row">
           <span class="text-primary font14 fontmedium">
           <span class="text-primary font14 fontmedium">
             {{ hasServicePackageDiscount
             {{ hasServicePackageDiscount
-                ? $t("dashboard_client.pending_schedules.detail_package_total")
-                : $t("dashboard_client.pending_schedules.detail_pix_total") }}
+              ? $t("dashboard_client.pending_schedules.detail_package_total")
+              : $t("dashboard_client.pending_schedules.detail_pix_total") }}
           </span>
           </span>
 
 
           <span class="total-value font14 fontbold">
           <span class="total-value font14 fontbold">
@@ -156,7 +156,7 @@
           no-caps
           no-caps
           padding="4px 12px"
           padding="4px 12px"
           :label="$t('dashboard_client.pending_schedules.btn_cancel')"
           :label="$t('dashboard_client.pending_schedules.btn_cancel')"
-          @click="onDialogHide"
+          @click="onCancelOrder"
         />
         />
       </q-card-section>
       </q-card-section>
     </q-card>
     </q-card>
@@ -167,6 +167,7 @@
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
 import { computed, onMounted } from "vue";
 import { computed, onMounted } from "vue";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
+import { formatLongDate } from "src/helpers/scheduleDate";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
 
 
 import {
 import {
@@ -178,28 +179,18 @@ import { useDialogPluginComponent, useQuasar } from "quasar";
 import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 import { usePaymentStore } from "src/stores/payment";
 import { usePaymentStore } from "src/stores/payment";
 
 
+import ScheduleCancelDialog from "./ScheduleCancelDialog.vue";
 import SchedulePaymentDialog from "./SchedulePaymentDialog.vue";
 import SchedulePaymentDialog from "./SchedulePaymentDialog.vue";
 import SchedulePaymentPixDialog from "./SchedulePaymentPixDialog.vue";
 import SchedulePaymentPixDialog from "./SchedulePaymentPixDialog.vue";
 
 
-const props = defineProps({
-  servicePackage: { type: Object, required: true },
-});
-
 defineEmits([...useDialogPluginComponent.emits]);
 defineEmits([...useDialogPluginComponent.emits]);
 
 
-const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
-
 const $q = useQuasar();
 const $q = useQuasar();
-
+const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 const paymentStore = usePaymentStore();
 const paymentStore = usePaymentStore();
-
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
 
 
-const item = computed(() => props.servicePackage);
-
-const displayProviderName = computed(() => {
-  return item.value.provider?.user?.name ?? item.value.provider_name ?? '';
-});
+const item = computed(() => paymentStore.currentPackage);
 
 
 const activeSchedules = computed(() =>
 const activeSchedules = computed(() =>
   (item.value.schedules ?? []).filter(
   (item.value.schedules ?? []).filter(
@@ -207,24 +198,6 @@ const activeSchedules = computed(() =>
   ),
   ),
 );
 );
 
 
-const displayDistrict = computed(() => {
-  return activeSchedules.value[0]?.address?.district ?? item.value.address?.district ?? '';
-});
-
-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})/);
-  if (!m) return raw;
-  const d = new Date(+m[1], +m[2] - 1, +m[3]);
-  return d.toLocaleDateString('pt-BR', {
-    day: '2-digit',
-    month: 'long',
-    year: 'numeric',
-  });
-};
-
 const avatarStyle = computed(
 const avatarStyle = computed(
   () => avatarColors[item.value.id % avatarColors.length],
   () => avatarColors[item.value.id % avatarColors.length],
 );
 );
@@ -240,21 +213,18 @@ const baseAmount = computed(() => {
   );
   );
 });
 });
 
 
-const creditCardTotal = computed(() =>
-  parseFloat((baseAmount.value + platformFee("credit_card")).toFixed(2)),
-);
+const displayDistrict = computed(() => {
+  return activeSchedules.value[0]?.address?.district ?? item.value.address?.district ?? '';
+});
+
+const displayProviderName = computed(() => {
+  return item.value.provider?.user?.name ?? item.value.provider_name ?? '';
+});
 
 
 const hasServicePackageDiscount = computed(() =>
 const hasServicePackageDiscount = computed(() =>
   scheduleUsesServicePackageDiscount(item.value),
   scheduleUsesServicePackageDiscount(item.value),
 );
 );
 
 
-const pixDiscount = computed(() =>
-  Math.max(0, parseFloat((creditCardTotal.value - pixTotal.value).toFixed(2))),
-);
-const pixTotal = computed(() =>
-  parseFloat((baseAmount.value + platformFee("pix")).toFixed(2)),
-);
-
 const platformFee = (paymentType) => {
 const platformFee = (paymentType) => {
   const feeRate = getSchedulePlatformFeeRate(
   const feeRate = getSchedulePlatformFeeRate(
     item.value,
     item.value,
@@ -265,8 +235,20 @@ const platformFee = (paymentType) => {
   return parseFloat((baseAmount.value * (feeRate ?? 0)).toFixed(2));
   return parseFloat((baseAmount.value * (feeRate ?? 0)).toFixed(2));
 };
 };
 
 
+const creditCardTotal = computed(() =>
+  parseFloat((baseAmount.value + platformFee("credit_card")).toFixed(2)),
+);
+
+const pixTotal = computed(() =>
+  parseFloat((baseAmount.value + platformFee("pix")).toFixed(2)),
+);
+
+const pixDiscount = computed(() =>
+  Math.max(0, parseFloat((creditCardTotal.value - pixTotal.value).toFixed(2))),
+);
+
 const onGoToPayment = () => {
 const onGoToPayment = () => {
-  const validPixPayment = paymentStore.getValidPixPaymentForServicePackage(item.value.id);
+  const validPixPayment = paymentStore.getPix(`service_package:${item.value.id}`);
   const hasValidPixPayment = !!validPixPayment;
   const hasValidPixPayment = !!validPixPayment;
 
 
   $q.dialog({
   $q.dialog({
@@ -274,7 +256,6 @@ const onGoToPayment = () => {
       ? SchedulePaymentPixDialog
       ? SchedulePaymentPixDialog
       : SchedulePaymentDialog,
       : SchedulePaymentDialog,
     componentProps: {
     componentProps: {
-      servicePackage: item.value,
       ...(hasValidPixPayment ? { total: pixTotal.value } : {}),
       ...(hasValidPixPayment ? { total: pixTotal.value } : {}),
     },
     },
   }).onOk(() => {
   }).onOk(() => {
@@ -282,9 +263,36 @@ const onGoToPayment = () => {
   });
   });
 };
 };
 
 
+const onCancelOrder = () => {
+  const scheduleToCancel = activeSchedules.value[0];
+
+  if (!scheduleToCancel) {
+    onDialogHide();
+
+    return;
+  }
+
+  $q.dialog({
+    component: ScheduleCancelDialog,
+    componentProps: {
+      schedule: {
+        ...scheduleToCancel,
+        service_package_id:
+          scheduleToCancel.service_package_id ?? item.value.id,
+        service_package_items_count:
+          scheduleToCancel.service_package_items_count ??
+          activeSchedules.value.length,
+      },
+    },
+  }).onOk(() => {
+    onDialogOK();
+  });
+};
+
 onMounted(() => {
 onMounted(() => {
   loadPlatformFees().catch(() => {});
   loadPlatformFees().catch(() => {});
 });
 });
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 28 - 23
src/components/dashboard/ScheduleCancelDialog.vue → src/pages/dashboard/components/schedule/ScheduleCancelDialog.vue

@@ -1,9 +1,8 @@
 <template>
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
   <q-dialog ref="dialogRef" @hide="onDialogHide">
     <q-card class="cancel-dialog-card bg-surface shadow-card" :flat="false">
     <q-card class="cancel-dialog-card bg-surface shadow-card" :flat="false">
-
       <div class="row justify-end q-pt-sm q-pr-sm">
       <div class="row justify-end q-pt-sm q-pr-sm">
-        <q-btn flat round dense icon="close" color="grey-6" size="sm" @click="onDialogCancel" />
+        <q-btn color="grey-6" dense flat icon="close" round size="sm" @click="onDialogCancel" />
       </div>
       </div>
 
 
       <q-card-section class="q-pt-none q-pb-sm q-px-lg text-center">
       <q-card-section class="q-pt-none q-pb-sm q-px-lg text-center">
@@ -14,7 +13,8 @@
 
 
       <q-card-section v-if="packageWarning" class="q-pt-none q-pb-sm q-px-lg">
       <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">
         <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" />
+          <q-icon class="q-mt-xs flex-shrink-0" color="negative" name="mdi-package-variant" size="22px" />
+
           <span class="font13 fontbold text-negative">
           <span class="font13 fontbold text-negative">
             {{ packageWarning }}
             {{ packageWarning }}
           </span>
           </span>
@@ -25,30 +25,34 @@
         <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') }}
         </div>
         </div>
+
         <q-input
         <q-input
           v-model="cancelText"
           v-model="cancelText"
-          type="textarea"
-          outlined
-          dense
-          :placeholder="$t('provider.dashboard.cancel_schedule.reason_placeholder')"
-          rows="4"
           color="secondary"
           color="secondary"
+          dense
+          hide-bottom-space
           input-class="text-black"
           input-class="text-black"
+          outlined
+          rows="4"
+          type="textarea"
+          :placeholder="$t('provider.dashboard.cancel_schedule.reason_placeholder')"
           :rules="[val => (val && val.trim().length >= 5) || ' ']"
           :rules="[val => (val && val.trim().length >= 5) || ' ']"
-          hide-bottom-space
         />
         />
       </q-card-section>
       </q-card-section>
 
 
       <q-card-section class="q-pt-xs q-pb-md q-px-lg">
       <q-card-section class="q-pt-xs q-pb-md q-px-lg">
         <div class="warning-box row no-wrap q-gutter-x-sm q-pa-sm">
         <div class="warning-box row no-wrap q-gutter-x-sm q-pa-sm">
-          <q-icon name="mdi-alert-outline" color="secondary" size="22px" class="q-mt-xs flex-shrink-0" />
+          <q-icon class="q-mt-xs flex-shrink-0" color="secondary" name="mdi-alert-outline" size="22px" />
+
           <div class="text-primary">
           <div class="text-primary">
             <span class="font14 fontbold">
             <span class="font14 fontbold">
               {{ $t('provider.dashboard.cancel_schedule.warning_title') }}
               {{ $t('provider.dashboard.cancel_schedule.warning_title') }}
             </span>
             </span>
+
             <span class="">
             <span class="">
               {{ ' ' + $t('provider.dashboard.cancel_schedule.warning_free') }}
               {{ ' ' + $t('provider.dashboard.cancel_schedule.warning_free') }}
             </span>
             </span>
+
             <div class="q-mt-sm">
             <div class="q-mt-sm">
               {{ $t('provider.dashboard.cancel_schedule.warning_fee') }}
               {{ $t('provider.dashboard.cancel_schedule.warning_fee') }}
             </div>
             </div>
@@ -59,36 +63,36 @@
       <q-card-section class="q-pt-none q-pb-lg q-px-lg">
       <q-card-section class="q-pt-none q-pb-lg q-px-lg">
         <div class="row justify-center q-gutter-x-md">
         <div class="row justify-center q-gutter-x-md">
           <q-btn
           <q-btn
-            unelevated
-            rounded
-            no-caps
             class="btn-action bg-grey-6 text-grey-1"
             class="btn-action bg-grey-6 text-grey-1"
-            :loading="loading"
+            no-caps
+            rounded
+            unelevated
             :disable="!cancelText || cancelText.trim().length < 5"
             :disable="!cancelText || cancelText.trim().length < 5"
             :label="$t('provider.dashboard.cancel_schedule.btn_keep')"
             :label="$t('provider.dashboard.cancel_schedule.btn_keep')"
+            :loading="loading"
             @click="confirmCancel"
             @click="confirmCancel"
           />
           />
+
           <q-btn
           <q-btn
-            unelevated
-            rounded
-            no-caps
-            color="secondary"
             class="btn-action"
             class="btn-action"
+            color="secondary"
+            no-caps
+            rounded
+            unelevated
             :label="$t('provider.dashboard.cancel_schedule.btn_back')"
             :label="$t('provider.dashboard.cancel_schedule.btn_back')"
             @click="onDialogCancel"
             @click="onDialogCancel"
           />
           />
         </div>
         </div>
       </q-card-section>
       </q-card-section>
-
     </q-card>
     </q-card>
   </q-dialog>
   </q-dialog>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
+import { cancelSchedule } from 'src/api/schedule'
 import { computed, 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'
 
 
 const props = defineProps({
 const props = defineProps({
   schedule: {
   schedule: {
@@ -97,9 +101,9 @@ const props = defineProps({
   }
   }
 })
 })
 
 
-const { t } = useI18n()
 const $q = useQuasar()
 const $q = useQuasar()
-const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent()
+const { dialogRef, onDialogHide, onDialogCancel, onDialogOK } = useDialogPluginComponent()
+const { t } = useI18n()
 
 
 const cancelText = ref('')
 const cancelText = ref('')
 const loading = ref(false)
 const loading = ref(false)
@@ -123,13 +127,14 @@ const confirmCancel = async () => {
   loading.value = true
   loading.value = true
   try {
   try {
     await cancelSchedule(props.schedule.id, cancelText.value.trim())
     await cancelSchedule(props.schedule.id, cancelText.value.trim())
-    dialogRef.value.hide()
+    onDialogOK()
   } catch {
   } catch {
     $q.notify({ message: t('http.errors.failed'), color: 'negative' })
     $q.notify({ message: t('http.errors.failed'), color: 'negative' })
   } finally {
   } finally {
     loading.value = false
     loading.value = false
   }
   }
 }
 }
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 59 - 65
src/components/dashboard/SchedulePaymentDialog.vue → src/pages/dashboard/components/schedule/SchedulePaymentDialog.vue

@@ -1,38 +1,44 @@
 <template>
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down" @hide="onDialogHide">
+  <q-dialog ref="dialogRef" maximized persistent transition-hide="slide-down" transition-show="slide-up" @hide="onDialogHide">
     <div class="bg-page full-height column">
     <div class="bg-page full-height column">
-
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
-        <q-btn class="header-back-btn" icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
+        <q-btn class="header-back-btn" color="primary" dense flat icon="mdi-chevron-left" round @click="onDialogCancel" />
+
         <q-space />
         <q-space />
+
         <span class="font16 fontbold gradient-diarista">
         <span class="font16 fontbold gradient-diarista">
           {{ $t('payment.title') }}
           {{ $t('payment.title') }}
         </span>
         </span>
+
         <q-space />
         <q-space />
+
         <div style="width: 32px" />
         <div style="width: 32px" />
       </div>
       </div>
 
 
       <div class="col overflow-auto q-px-md q-pt-lg q-pb-xl">
       <div class="col overflow-auto q-px-md q-pt-lg q-pb-xl">
-
         <div class="q-mb-sm text-text font14 fontbold">{{ $t('payment.schedule_address') }}</div>
         <div class="q-mb-sm text-text font14 fontbold">{{ $t('payment.schedule_address') }}</div>
+
         <div class="address-box row items-center no-wrap q-mb-lg">
         <div class="address-box row items-center no-wrap q-mb-lg">
           <div class="col">
           <div class="col">
             <div class="address-type-label fontbold">{{ addressTypeLabel }}</div>
             <div class="address-type-label fontbold">{{ addressTypeLabel }}</div>
+
             <div class="address-full-text text-grey-7">{{ addressFullText }}</div>
             <div class="address-full-text text-grey-7">{{ addressFullText }}</div>
           </div>
           </div>
-          <q-icon name="mdi-chevron-down" color="grey-5" size="22px" />
+
+          <q-icon color="grey-5" name="mdi-chevron-down" size="22px" />
         </div>
         </div>
 
 
         <div class="text-text q-mb-sm font14 fontbold">{{ $t('payment.pay_with') }}</div>
         <div class="text-text q-mb-sm font14 fontbold">{{ $t('payment.pay_with') }}</div>
-        <div class="row q-gutter-sm q-mb-sm">
 
 
+        <div class="row q-gutter-sm q-mb-sm">
           <div
           <div
             class="payment-option-card col column items-center justify-center q-pa-md cursor-pointer"
             class="payment-option-card col column items-center justify-center q-pa-md cursor-pointer"
             :class="{ 'payment-option-selected': selectedMethod === 'pix' }"
             :class="{ 'payment-option-selected': selectedMethod === 'pix' }"
             @click="selectedMethod = 'pix'"
             @click="selectedMethod = 'pix'"
           >
           >
             <span class="payment-option-title">{{ $t('payment.pix') }}</span>
             <span class="payment-option-title">{{ $t('payment.pix') }}</span>
-            <q-icon name="mdi-cash-fast" size="32px" color="teal" class="q-mt-xs" />
+
+            <q-icon class="q-mt-xs" color="teal" name="mdi-cash-fast" size="32px" />
           </div>
           </div>
 
 
           <div
           <div
@@ -41,10 +47,11 @@
             @click="openAddCard"
             @click="openAddCard"
           >
           >
             <span class="payment-option-title">{{ $t('payment.add_card') }}</span>
             <span class="payment-option-title">{{ $t('payment.add_card') }}</span>
-            <q-icon name="mdi-plus-circle-outline" size="22px" color="grey-5" class="q-mt-xs" />
+
+            <q-icon class="q-mt-xs" color="grey-5" name="mdi-plus-circle-outline" size="22px" />
+
             <span class="payment-option-sub">{{ $t('payment.credit_debit') }}</span>
             <span class="payment-option-sub">{{ $t('payment.credit_debit') }}</span>
           </div>
           </div>
-
         </div>
         </div>
 
 
         <div v-if="loadingCards" class="flex flex-center q-py-md">
         <div v-if="loadingCards" class="flex flex-center q-py-md">
@@ -61,11 +68,15 @@
           >
           >
             <div class="col column">
             <div class="col column">
               <span class="card-titular-label">{{ $t('payment.card_holder') }}</span>
               <span class="card-titular-label">{{ $t('payment.card_holder') }}</span>
+
               <span class="card-holder-name text-text">{{ card.holder_name }}</span>
               <span class="card-holder-name text-text">{{ card.holder_name }}</span>
             </div>
             </div>
+
             <div class="column items-end">
             <div class="column items-end">
-              <span class="card-brand-text">{{ brandDisplay(card.brand) }}</span>
+              <span class="card-brand-text">{{ formatCardBrand(card.brand) }}</span>
+
               <span class="card-last-four">{{ '**** **** **** ' + card.last_four_digits }}</span>
               <span class="card-last-four">{{ '**** **** **** ' + card.last_four_digits }}</span>
+
               <span class="card-expiry-text">{{ card.expiration }}</span>
               <span class="card-expiry-text">{{ card.expiration }}</span>
             </div>
             </div>
           </div>
           </div>
@@ -76,6 +87,7 @@
         <div class="payment-summary q-mb-lg">
         <div class="payment-summary q-mb-lg">
           <div class="row items-center justify-between">
           <div class="row items-center justify-between">
             <span class="summary-total-label">{{ $t('dashboard_client.pending_schedules.detail_total') }}</span>
             <span class="summary-total-label">{{ $t('dashboard_client.pending_schedules.detail_total') }}</span>
+
             <span class="summary-total-value">{{ formatCurrency(selectedTotal) }}</span>
             <span class="summary-total-value">{{ formatCurrency(selectedTotal) }}</span>
           </div>
           </div>
         </div>
         </div>
@@ -91,101 +103,80 @@
         </div>
         </div>
 
 
         <q-btn
         <q-btn
-          unelevated
-          rounded
+          class="full-width"
+          color="primary"
           no-caps
           no-caps
           padding="8px 12px"
           padding="8px 12px"
-          color="primary"
-          class="full-width"
-          :label="$t('payment.confirm_btn')"
+          rounded
+          unelevated
           :disable="!canConfirm"
           :disable="!canConfirm"
+          :label="$t('payment.confirm_btn')"
           @click="onConfirm"
           @click="onConfirm"
         />
         />
-
       </div>
       </div>
     </div>
     </div>
   </q-dialog>
   </q-dialog>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, computed, onMounted } from 'vue'
-import { useDialogPluginComponent, useQuasar } from 'quasar'
-import { useI18n } from 'vue-i18n'
-import { userStore } from 'src/stores/user'
-import { usePaymentStore } from 'src/stores/payment'
+import { Browser } from "@capacitor/browser";
+import { computed, onMounted, ref } from 'vue'
+import { formatAddress, formatCardBrand, formatCurrency } from 'src/helpers/utils'
 import { getClientPaymentMethods } from 'src/api/clientPaymentMethod'
 import { getClientPaymentMethods } from 'src/api/clientPaymentMethod'
-import { formatCurrency } from 'src/helpers/utils'
 import { getScheduleTotalWithPlatformFee } from 'src/helpers/paymentPlatformFees'
 import { getScheduleTotalWithPlatformFee } from 'src/helpers/paymentPlatformFees'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
 import { usePaymentPlatformFees } from 'src/composables/usePaymentPlatformFees'
 import { usePaymentPlatformFees } from 'src/composables/usePaymentPlatformFees'
+import { usePaymentStore } from 'src/stores/payment'
+import { userStore } from 'src/stores/user'
+
 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";
+
+defineEmits([...useDialogPluginComponent.emits])
 
 
 const props = defineProps({
 const props = defineProps({
-  servicePackage: {
-    type: Object,
-    required: true,
-  },
   targetType: {
   targetType: {
     type: String,
     type: String,
     default: 'service_package',
     default: 'service_package',
   },
   },
 })
 })
 
 
-defineEmits([...useDialogPluginComponent.emits])
-
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
 const $q = useQuasar()
-const { t } = useI18n()
-const store = userStore()
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const paymentStore = usePaymentStore()
 const paymentStore = usePaymentStore()
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees()
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees()
+const store = userStore()
+const { t } = useI18n()
 
 
-const item = computed(() => props.servicePackage)
-
-const selectedMethod = ref(null)
-const paymentMethods = ref([])
 const loadingCards = ref(false)
 const loadingCards = ref(false)
+const paymentMethods = ref([])
+const selectedMethod = ref(null)
 
 
-const selectedPaymentType = computed(() => selectedMethod.value === 'pix' ? 'pix' : 'credit_card')
-const selectedTotal = computed(() => getScheduleTotalWithPlatformFee(item.value, selectedPaymentType.value, platformFees.value))
+const canConfirm = computed(() => selectedMethod.value !== null)
+const item = computed(() => paymentStore.currentPackage)
+const address = computed(() => item.value.schedules?.[0]?.address ?? item.value.address)
 
 
-const getAddress = () => {
-  return item.value.schedules?.[0]?.address ?? item.value.address ?? null;
-};
+const addressFullText = computed(() => formatAddress({
+  address: address.value?.address,
+  district: address.value?.district,
+  number: address.value?.number,
+}))
 
 
 const addressTypeLabel = computed(() => {
 const addressTypeLabel = computed(() => {
-  const addr = getAddress();
-  const type = addr?.address_type;
+  const type = address.value?.address_type;
   if (!type) return '';
   if (!type) return '';
   return t(`profile.address.type.${type}`, type);
   return t(`profile.address.type.${type}`, type);
 });
 });
 
 
-const addressFullText = computed(() => {
-  const a = getAddress();
-  if (!a) return '';
-  const parts = [a.address, a.number, a.district].filter(Boolean);
-  return parts.join(', ');
-});
-
-const canConfirm = computed(() => selectedMethod.value !== null)
-
+const selectedPaymentType = computed(() => selectedMethod.value === 'pix' ? 'pix' : 'credit_card')
+const selectedTotal = computed(() => getScheduleTotalWithPlatformFee(item.value, selectedPaymentType.value, platformFees.value))
 
 
 //direcionamento para ttermos e serviços 
 //direcionamento para ttermos e serviços 
 const PRIVACY_POLICY_URL =
 const PRIVACY_POLICY_URL =
   "https://politicas.softpar.inf.br/politicas/politicaDiaristaCliente.html";
   "https://politicas.softpar.inf.br/politicas/politicaDiaristaCliente.html";
 
 
-const openTerms = async () => {
-  await Browser.open({ url: PRIVACY_POLICY_URL });
-};
-
-const brandDisplay = (brand) => {
-  if (!brand) return ''
-  const map = { visa: 'VISA', mastercard: 'Mastercard', elo: 'Elo', hipercard: 'Hipercard', diners: 'Diners', discover: 'Discover' }
-  return map[brand] ?? brand.toUpperCase()
-}
-
 const loadCards = async () => {
 const loadCards = async () => {
   loadingCards.value = true
   loadingCards.value = true
   try {
   try {
@@ -216,7 +207,6 @@ const openPixPayment = () => {
   $q.dialog({
   $q.dialog({
     component: SchedulePaymentPixDialog,
     component: SchedulePaymentPixDialog,
     componentProps: {
     componentProps: {
-      servicePackage: item.value,
       total: selectedTotal.value,
       total: selectedTotal.value,
       targetType: props.targetType,
       targetType: props.targetType,
     },
     },
@@ -235,7 +225,6 @@ const onConfirm = () => {
   $q.dialog({
   $q.dialog({
     component: SchedulePaymentProcessingDialog,
     component: SchedulePaymentProcessingDialog,
     componentProps: {
     componentProps: {
-      servicePackage: item.value,
       clientPaymentMethodId,
       clientPaymentMethodId,
       total: selectedTotal.value,
       total: selectedTotal.value,
       targetType: props.targetType,
       targetType: props.targetType,
@@ -245,10 +234,14 @@ const onConfirm = () => {
   })
   })
 }
 }
 
 
+const openTerms = async () => {
+  await Browser.open({ url: PRIVACY_POLICY_URL });
+};
+
 onMounted(() => {
 onMounted(() => {
   loadPlatformFees().catch(() => {})
   loadPlatformFees().catch(() => {})
 
 
-  const existingPix = paymentStore.getValidPixPaymentForServicePackage(`${props.targetType}:${item.value.id}`)
+  const existingPix = paymentStore.getPix(`${props.targetType}:${item.value.id}`)
 
 
   if (existingPix) {
   if (existingPix) {
     openPixPayment()
     openPixPayment()
@@ -257,6 +250,7 @@ onMounted(() => {
 
 
   loadCards()
   loadCards()
 })
 })
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 125 - 75
src/components/dashboard/SchedulePaymentPixDialog.vue → src/pages/dashboard/components/schedule/SchedulePaymentPixDialog.vue

@@ -1,33 +1,43 @@
 <template>
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down" @hide="onDialogHide">
+  <q-dialog ref="dialogRef" maximized persistent transition-hide="slide-down" transition-show="slide-up" @hide="onDialogHide">
     <div class="bg-page full-height column">
     <div class="bg-page full-height column">
-
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
-        <q-btn class="header-back-btn" icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
+        <q-btn class="header-back-btn" color="primary" dense flat icon="mdi-chevron-left" round @click="onDialogCancel" />
+
         <q-space />
         <q-space />
+
         <span class="font16 fontbold gradient-diarista">
         <span class="font16 fontbold gradient-diarista">
           {{ $t('payment.pix_title') }}
           {{ $t('payment.pix_title') }}
         </span>
         </span>
+
         <q-space />
         <q-space />
+
         <div style="width: 32px" />
         <div style="width: 32px" />
       </div>
       </div>
 
 
       <div v-if="success" class="col column items-center justify-center q-px-xl">
       <div v-if="success" class="col column items-center justify-center q-px-xl">
         <q-btn
         <q-btn
-          flat round icon="mdi-close" color="grey-5"
           class="self-end q-mb-md"
           class="self-end q-mb-md"
+          color="grey-5"
+          flat
+          icon="mdi-close"
+          round
           @click="onDialogOK"
           @click="onDialogOK"
         />
         />
+
         <div class="success-icon-wrapper q-mb-lg">
         <div class="success-icon-wrapper q-mb-lg">
-          <q-icon name="mdi-check-circle" size="100px" color="primary" />
+          <q-icon color="primary" name="mdi-check-circle" size="100px" />
         </div>
         </div>
+
         <div class="success-title text-primary text-center q-mb-sm">
         <div class="success-title text-primary text-center q-mb-sm">
           {{ $t('payment.success_title') }}
           {{ $t('payment.success_title') }}
         </div>
         </div>
-        <i18n-t keypath="payment.success_message" tag="div" class="success-message text-grey-6 text-center">
+
+        <i18n-t class="success-message text-grey-6 text-center" keypath="payment.success_message" tag="div">
           <template #nextServices>
           <template #nextServices>
             <strong class="text-text">{{ $t('payment.success_next_services') }}</strong>
             <strong class="text-text">{{ $t('payment.success_next_services') }}</strong>
           </template>
           </template>
+
           <template #agenda>
           <template #agenda>
             <strong class="text-text">{{ $t('payment.success_agenda') }}</strong>
             <strong class="text-text">{{ $t('payment.success_agenda') }}</strong>
           </template>
           </template>
@@ -35,71 +45,79 @@
       </div>
       </div>
 
 
       <div v-else-if="processing" class="col column items-center justify-center q-px-xl">
       <div v-else-if="processing" class="col column items-center justify-center q-px-xl">
-        <q-spinner-oval color="primary" size="72px" class="q-mb-lg" />
+        <q-spinner-oval class="q-mb-lg" color="primary" size="72px" />
+
         <div class="processing-title text-primary text-center q-mb-sm">
         <div class="processing-title text-primary text-center q-mb-sm">
           {{ $t('payment.processing_title') }}
           {{ $t('payment.processing_title') }}
         </div>
         </div>
+
         <div class="processing-message text-grey-6 text-center">
         <div class="processing-message text-grey-6 text-center">
           {{ $t('payment.processing_message') }}
           {{ $t('payment.processing_message') }}
         </div>
         </div>
       </div>
       </div>
 
 
       <div v-else class="pix-payment-content col scroll q-px-lg q-pt-lg q-pb-xl column">
       <div v-else class="pix-payment-content col scroll q-px-lg q-pt-lg q-pb-xl column">
-
-        <div class="row items-center justify-between q-mb-sm">
+        <div class="pix-summary-row row items-center justify-between q-mb-sm">
           <span class="pix-label font14 fontbold">{{ $t('payment.pix_total') }}</span>
           <span class="pix-label font14 fontbold">{{ $t('payment.pix_total') }}</span>
+
           <span class="text-primary font14 fontbold">{{ formatCurrency(total) }}</span>
           <span class="text-primary font14 fontbold">{{ formatCurrency(total) }}</span>
         </div>
         </div>
+
         <q-separator />
         <q-separator />
 
 
-        <div class="row items-center justify-between q-mt-sm q-mb-lg">
+        <div class="pix-summary-row row items-center justify-between q-mt-sm q-mb-lg">
           <span class="pix-label font14 fontbold">{{ $t('payment.pix_expires') }}</span>
           <span class="pix-label font14 fontbold">{{ $t('payment.pix_expires') }}</span>
+
           <span class="text-primary font14 fontbold">{{ countdown }}</span>
           <span class="text-primary font14 fontbold">{{ countdown }}</span>
         </div>
         </div>
 
 
         <div class="flex flex-center q-mb-md">
         <div class="flex flex-center q-mb-md">
-          <q-icon name="mdi-cash-fast" size="48px" color="teal" />
+          <q-icon color="teal" name="mdi-cash-fast" size="48px" />
         </div>
         </div>
 
 
         <div class="flex flex-center q-mb-md">
         <div class="flex flex-center q-mb-md">
           <q-img
           <q-img
             v-if="pixQrCodeUrl"
             v-if="pixQrCodeUrl"
-            :src="pixQrCodeUrl"
-            width="180px"
-            height="180px"
-            fit="contain"
             class="qrcode-wrapper"
             class="qrcode-wrapper"
+            fit="contain"
+            height="180px"
+            width="180px"
+            :src="pixQrCodeUrl"
           />
           />
+
           <div v-else class="qrcode-wrapper column items-center justify-center">
           <div v-else class="qrcode-wrapper column items-center justify-center">
-            <q-icon name="mdi-qrcode" size="80px" color="grey-6" />
+            <q-icon color="grey-6" name="mdi-qrcode" size="80px" />
           </div>
           </div>
         </div>
         </div>
 
 
         <div class="pix-code-text q-mb-md">{{ pixCode || 'Código Pix indisponível.' }}</div>
         <div class="pix-code-text q-mb-md">{{ pixCode || 'Código Pix indisponível.' }}</div>
 
 
         <q-btn
         <q-btn
-          unelevated
-          rounded
-          no-caps
+          class="full-width q-mb-lg"
           color="primary"
           color="primary"
+          no-caps
           padding="4px 12px"
           padding="4px 12px"
-          class="full-width q-mb-lg"
+          rounded
+          unelevated
           :label="$t('payment.pix_copy_btn')"
           :label="$t('payment.pix_copy_btn')"
           :loading="processing"
           :loading="processing"
           @click="copyCode"
           @click="copyCode"
         />
         />
 
 
-        <i18n-t keypath="payment.pix_instructions" tag="p" class="pix-instructions-text font14 q-mb-sm">
+        <i18n-t class="pix-instructions-text font14 q-mb-sm" keypath="payment.pix_instructions" tag="p">
           <template #copyCode>
           <template #copyCode>
             <span class="fontbold">{{ $t('payment.pix_copy_code') }}</span>
             <span class="fontbold">{{ $t('payment.pix_copy_code') }}</span>
           </template>
           </template>
+
           <template #pasteCode>
           <template #pasteCode>
             <span class="fontbold">{{ $t('payment.pix_paste_code') }}</span>
             <span class="fontbold">{{ $t('payment.pix_paste_code') }}</span>
           </template>
           </template>
+
           <template #finalize>
           <template #finalize>
             <span class="fontbold">{{ $t('payment.pix_finalize') }}</span>
             <span class="fontbold">{{ $t('payment.pix_finalize') }}</span>
           </template>
           </template>
         </i18n-t>
         </i18n-t>
+
         <p class="pix-instructions-text font14">{{ $t('payment.pix_email_note') }}</p>
         <p class="pix-instructions-text font14">{{ $t('payment.pix_email_note') }}</p>
       </div>
       </div>
     </div>
     </div>
@@ -107,39 +125,44 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { computed, ref, onMounted, onUnmounted } from 'vue'
-import { useDialogPluginComponent, useQuasar, copyToClipboard } from 'quasar'
+import { computed, onMounted, onUnmounted, ref } from 'vue'
+import { copyToClipboard, useDialogPluginComponent, useQuasar } from 'quasar'
 import { formatCurrency } from 'src/helpers/utils'
 import { formatCurrency } from 'src/helpers/utils'
-import { getServicePackagePix, payServicePackage, getScheduleProposalPix, payScheduleProposal } from 'src/api/payment'
+import { getScheduleProposalPix, getServicePackagePix, payScheduleProposal, payServicePackage } from 'src/api/payment'
+import { parseUtcDate } from 'src/helpers/scheduleDate'
 import { usePaymentStore } from 'src/stores/payment'
 import { usePaymentStore } from 'src/stores/payment'
 
 
+defineEmits([...useDialogPluginComponent.emits])
+
 const props = defineProps({
 const props = defineProps({
-  servicePackage: { type: Object, required: true },
   total: { type: Number, required: true },
   total: { type: Number, required: true },
   targetType: { type: String, default: 'service_package' },
   targetType: { type: String, default: 'service_package' },
 })
 })
 
 
-defineEmits([...useDialogPluginComponent.emits])
-
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
 const $q = useQuasar()
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const paymentStore = usePaymentStore()
 const paymentStore = usePaymentStore()
 
 
-const pay = props.targetType === 'schedule_proposal' ? payScheduleProposal : payServicePackage
-const getPix = props.targetType === 'schedule_proposal' ? getScheduleProposalPix : getServicePackagePix
-
-const item = computed(() => props.servicePackage)
-const itemId = computed(() => item.value.id)
-const cacheKey = computed(() => `${props.targetType}:${itemId.value}`)
-
+const countdown = ref('')
 const payment = ref(null)
 const payment = ref(null)
-const success = ref(false)
 const processing = ref(true)
 const processing = ref(true)
+const success = ref(false)
+const totalSeconds = ref(20 * 60)
 
 
+const item = computed(() => paymentStore.currentPackage)
+const itemId = computed(() => item.value.id)
+const cacheKey = computed(() => `${props.targetType}:${itemId.value}`)
 const pixData = computed(() => payment.value?.pix ?? {})
 const pixData = computed(() => payment.value?.pix ?? {})
 const pixCode = computed(() => pixData.value?.qr_code ?? '')
 const pixCode = computed(() => pixData.value?.qr_code ?? '')
-const pixQrCodeUrl = computed(() => pixData.value?.qr_code_url ?? '')
 const pixExpiresAt = computed(() => pixData.value?.expires_at ?? payment.value?.expires_at ?? null)
 const pixExpiresAt = computed(() => pixData.value?.expires_at ?? payment.value?.expires_at ?? null)
+const pixQrCodeUrl = computed(() => pixData.value?.qr_code_url ?? '')
+
+const fetchPix = props.targetType === 'schedule_proposal' ? getScheduleProposalPix : getServicePackagePix
+const pay = props.targetType === 'schedule_proposal' ? payScheduleProposal : payServicePackage
+
+let countdownTimer = null
+let pollingInFlight = false
+let pollingTimer = null
 
 
 const copyCode = async () => {
 const copyCode = async () => {
   try {
   try {
@@ -157,46 +180,58 @@ const copyCode = async () => {
 
 
 }
 }
 
 
-const totalSeconds = ref(20 * 60)
-const countdown = ref('')
-let countdownTimer = null
-let pollingTimer = null
-let pollingInFlight = false
-
 const stopPolling = () => {
 const stopPolling = () => {
   clearInterval(pollingTimer)
   clearInterval(pollingTimer)
   pollingTimer = null
   pollingTimer = null
 }
 }
 
 
 const applyPaymentStatus = (nextPayment) => {
 const applyPaymentStatus = (nextPayment) => {
- if (nextPayment.status === 'paid') {
-  processing.value = false
+  payment.value = nextPayment
 
 
-  $q.notify({
-    type: 'positive',
-    message: 'Pagamento confirmado!',
-    position: 'top',
-  })
+  if (nextPayment.status === 'paid') {
+    processing.value = false
 
 
-  paymentStore.clearPixPaymentForServicePackage(cacheKey.value)
-  stopPolling()
+    $q.notify({
+      type: 'positive',
+      message: 'Pagamento confirmado!',
+      position: 'top',
+    })
 
 
-  onDialogOK()
+    paymentStore.clearPix(cacheKey.value)
+    stopPolling()
 
 
-  return
-}
+    onDialogOK()
+
+    return
+  }
 
 
 
 
   if (['failed', 'cancelled'].includes(nextPayment.status)) {
   if (['failed', 'cancelled'].includes(nextPayment.status)) {
     processing.value = false
     processing.value = false
-    paymentStore.clearPixPaymentForServicePackage(cacheKey.value)
+    paymentStore.clearPix(cacheKey.value)
     stopPolling()
     stopPolling()
     $q.notify({ type: 'negative', message: nextPayment.failure_message || 'Pagamento Pix não confirmado.' })
     $q.notify({ type: 'negative', message: nextPayment.failure_message || 'Pagamento Pix não confirmado.' })
     onDialogCancel()
     onDialogCancel()
     return
     return
   }
   }
 
 
-  paymentStore.setPixPaymentForServicePackage(cacheKey.value, nextPayment)
+  paymentStore.setPix(cacheKey.value, nextPayment)
+}
+
+const updateCountdown = () => {
+  if (pixExpiresAt.value) {
+    totalSeconds.value = Math.max(0, Math.floor((parseUtcDate(pixExpiresAt.value).getTime() - Date.now()) / 1000))
+  }
+
+  const m = Math.floor(totalSeconds.value / 60)
+  const s = totalSeconds.value % 60
+  countdown.value = `${m} min, ${String(s).padStart(2, '0')} seg`
+  if (!pixExpiresAt.value && totalSeconds.value > 0) totalSeconds.value--
+
+  if (pixExpiresAt.value && totalSeconds.value <= 0) {
+    paymentStore.clearPix(cacheKey.value)
+    stopPolling()
+  }
 }
 }
 
 
 const checkPaymentStatus = async () => {
 const checkPaymentStatus = async () => {
@@ -204,7 +239,7 @@ const checkPaymentStatus = async () => {
 
 
   pollingInFlight = true
   pollingInFlight = true
   try {
   try {
-    const result = await getPix(itemId.value)
+    const result = await fetchPix(itemId.value)
     applyPaymentStatus(result)
     applyPaymentStatus(result)
     updateCountdown()
     updateCountdown()
   } catch (e) {
   } catch (e) {
@@ -221,28 +256,12 @@ const startPolling = () => {
   pollingTimer = setInterval(checkPaymentStatus, 5000)
   pollingTimer = setInterval(checkPaymentStatus, 5000)
 }
 }
 
 
-const updateCountdown = () => {
-  if (pixExpiresAt.value) {
-    totalSeconds.value = Math.max(0, Math.floor((new Date(pixExpiresAt.value).getTime() - Date.now()) / 1000))
-  }
-
-  const m = Math.floor(totalSeconds.value / 60)
-  const s = totalSeconds.value % 60
-  countdown.value = `${m} min, ${String(s).padStart(2, '0')} seg`
-  if (!pixExpiresAt.value && totalSeconds.value > 0) totalSeconds.value--
-
-  if (pixExpiresAt.value && totalSeconds.value <= 0) {
-    paymentStore.clearPixPaymentForServicePackage(cacheKey.value)
-    stopPolling()
-  }
-}
-
 onMounted(async () => {
 onMounted(async () => {
   updateCountdown()
   updateCountdown()
   countdownTimer = setInterval(updateCountdown, 1000)
   countdownTimer = setInterval(updateCountdown, 1000)
 
 
   try {
   try {
-    const cachedPayment = paymentStore.getValidPixPaymentForServicePackage(cacheKey.value)
+    const cachedPayment = paymentStore.getPix(cacheKey.value)
 
 
     if (cachedPayment) {
     if (cachedPayment) {
       applyPaymentStatus(cachedPayment)
       applyPaymentStatus(cachedPayment)
@@ -273,6 +292,7 @@ onUnmounted(() => {
   clearInterval(countdownTimer)
   clearInterval(countdownTimer)
   stopPolling()
   stopPolling()
 })
 })
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
@@ -284,6 +304,29 @@ onUnmounted(() => {
   color: #3a3a4a;
   color: #3a3a4a;
 }
 }
 
 
+.pix-payment-content {
+  box-sizing: border-box;
+  min-width: 0;
+  width: 100%;
+  overflow-x: hidden;
+}
+
+.pix-summary-row {
+  min-width: 0;
+  width: 100%;
+  gap: 8px;
+}
+
+.pix-summary-row > * {
+  min-width: 0;
+  overflow-wrap: anywhere;
+}
+
+.pix-summary-row > :last-child {
+  margin-left: auto;
+  text-align: right;
+}
+
 .qrcode-wrapper {
 .qrcode-wrapper {
   background: #fff;
   background: #fff;
   border: 1px solid #e0e0e0;
   border: 1px solid #e0e0e0;
@@ -292,7 +335,11 @@ onUnmounted(() => {
 }
 }
 
 
 .pix-code-text {
 .pix-code-text {
+  align-self: stretch;
+  box-sizing: border-box;
   color: #5a5a6a;
   color: #5a5a6a;
+  max-width: 100%;
+  min-width: 0;
   text-align: center;
   text-align: center;
   word-break: break-all;
   word-break: break-all;
   line-height: 1.5;
   line-height: 1.5;
@@ -304,13 +351,16 @@ onUnmounted(() => {
 
 
 .pix-instructions-text {
 .pix-instructions-text {
   align-self: stretch;
   align-self: stretch;
+  box-sizing: border-box;
   line-height: 1.5;
   line-height: 1.5;
   color: #3a3a4a;
   color: #3a3a4a;
   margin-left: 0;
   margin-left: 0;
   margin-right: 0;
   margin-right: 0;
   max-width: 100%;
   max-width: 100%;
+  min-width: 0;
   overflow-wrap: anywhere;
   overflow-wrap: anywhere;
   text-align: left;
   text-align: left;
+  white-space: normal;
   width: 100%;
   width: 100%;
 }
 }
 
 

+ 33 - 26
src/components/dashboard/SchedulePaymentProcessingDialog.vue → src/pages/dashboard/components/schedule/SchedulePaymentProcessingDialog.vue

@@ -1,12 +1,13 @@
 <template>
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="fade" transition-hide="fade" @hide="onDialogHide">
+  <q-dialog ref="dialogRef" maximized persistent transition-hide="fade" transition-show="fade" @hide="onDialogHide">
     <div class="bg-surface full-height column items-center justify-center q-px-xl">
     <div class="bg-surface full-height column items-center justify-center q-px-xl">
-
       <template v-if="!success">
       <template v-if="!success">
-        <q-spinner-oval color="primary" size="72px" class="q-mb-lg" />
+        <q-spinner-oval class="q-mb-lg" color="primary" size="72px" />
+
         <div class="processing-title text-primary text-center q-mb-sm">
         <div class="processing-title text-primary text-center q-mb-sm">
           {{ $t('payment.processing_title') }}
           {{ $t('payment.processing_title') }}
         </div>
         </div>
+
         <div class="processing-message text-grey-6 text-center">
         <div class="processing-message text-grey-6 text-center">
           {{ $t('payment.processing_message') }}
           {{ $t('payment.processing_message') }}
         </div>
         </div>
@@ -14,58 +15,63 @@
 
 
       <template v-else>
       <template v-else>
         <q-btn
         <q-btn
+          class="self-end q-mb-md"
+          color="grey-5"
           flat
           flat
-          round
           icon="mdi-close"
           icon="mdi-close"
-          color="grey-5"
-          class="self-end q-mb-md"
+          round
           @click="onDialogOK"
           @click="onDialogOK"
         />
         />
+
         <img
         <img
-          :src="LogoDiariaSucesso"
           alt="mascote"
           alt="mascote"
           class="success-mascot q-mb-lg"
           class="success-mascot q-mb-lg"
+          :src="LogoDiariaSucesso"
         />
         />
+
         <div class="success-title font16 fontbold text-primary text-center q-mb-sm">
         <div class="success-title font16 fontbold text-primary text-center q-mb-sm">
           {{ $t('payment.success_title') }}
           {{ $t('payment.success_title') }}
         </div>
         </div>
-        <i18n-t keypath="payment.success_message" tag="div" class="success-message text-grey-6 text-center font14">
+
+        <i18n-t class="success-message text-grey-6 text-center font14" keypath="payment.success_message" tag="div">
           <template #nextServices>
           <template #nextServices>
             <strong class="text-text">{{ $t('payment.success_next_services') }}</strong>
             <strong class="text-text">{{ $t('payment.success_next_services') }}</strong>
           </template>
           </template>
+
           <template #agenda>
           <template #agenda>
             <strong class="text-text">{{ $t('payment.success_agenda') }}</strong>
             <strong class="text-text">{{ $t('payment.success_agenda') }}</strong>
           </template>
           </template>
         </i18n-t>
         </i18n-t>
       </template>
       </template>
-
     </div>
     </div>
   </q-dialog>
   </q-dialog>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, computed, onMounted } from 'vue'
+import { computed, onMounted, ref } from 'vue'
+import { payScheduleProposal, payServicePackage } from 'src/api/payment'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { usePaymentStore } from 'src/stores/payment'
+
 import LogoDiariaSucesso from 'src/assets/diarinho-success-payment.svg';
 import LogoDiariaSucesso from 'src/assets/diarinho-success-payment.svg';
-import { payServicePackage, payScheduleProposal } from 'src/api/payment'
+
+defineEmits([...useDialogPluginComponent.emits])
 
 
 const props = defineProps({
 const props = defineProps({
-  servicePackage: { type: Object, required: true },
   clientPaymentMethodId: { type: Number, required: true },
   clientPaymentMethodId: { type: Number, required: true },
   total: { type: Number, default: null },
   total: { type: Number, default: null },
   targetType: { type: String, default: 'service_package' },
   targetType: { type: String, default: 'service_package' },
 })
 })
 
 
-defineEmits([...useDialogPluginComponent.emits])
-
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
 const $q = useQuasar()
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
+const paymentStore = usePaymentStore()
 
 
-const pay = props.targetType === 'schedule_proposal' ? payScheduleProposal : payServicePackage
+const success = ref(false)
 
 
-const item = computed(() => props.servicePackage)
+const item = computed(() => paymentStore.currentPackage)
 
 
-const success = ref(false)
+const pay = props.targetType === 'schedule_proposal' ? payScheduleProposal : payServicePackage
 
 
 onMounted(async () => {
 onMounted(async () => {
   try {
   try {
@@ -75,16 +81,16 @@ onMounted(async () => {
     })
     })
 
 
     if (payment.status !== 'paid') {
     if (payment.status !== 'paid') {
-        throw new Error(payment.failure_message || 'Pagamento não confirmado.')
-      }
+      throw new Error(payment.failure_message || 'Pagamento não confirmado.')
+    }
 
 
-      $q.notify({
-        type: 'positive',
-        message: 'Pagamento confirmado!',
-        position: 'top',
-      })
+    $q.notify({
+      type: 'positive',
+      message: 'Pagamento confirmado!',
+      position: 'top',
+    })
 
 
-      onDialogOK()
+    onDialogOK()
       
       
   } catch (e) {
   } catch (e) {
     console.error('Erro ao pagar:', e)
     console.error('Erro ao pagar:', e)
@@ -96,6 +102,7 @@ onMounted(async () => {
     onDialogCancel()
     onDialogCancel()
   }
   }
 })
 })
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 89 - 24
src/components/dashboard/ScheduleRatingDialog.vue → src/pages/dashboard/components/schedule/ScheduleRatingDialog.vue

@@ -24,7 +24,14 @@
           class="q-mb-sm"
           class="q-mb-sm"
           size="64px"
           size="64px"
         >
         >
+          <img
+            v-if="photoUrl"
+            style="object-fit: cover"
+            :src="photoUrl"
+          />
+
           <span
           <span
+            v-else
             class="full-width full-height flex flex-center"
             class="full-width full-height flex flex-center"
             style="border-radius: 50%"
             style="border-radius: 50%"
             :style="avatarStyle"
             :style="avatarStyle"
@@ -86,8 +93,8 @@
           <div
           <div
             v-for="tag in tags"
             v-for="tag in tags"
             :key="tag.id"
             :key="tag.id"
-            :class="{ 'tag-pill--selected': selectedTagIds.includes(tag.id) }"
             class="tag-pill"
             class="tag-pill"
+            :class="{ 'tag-pill--selected': selectedTagIds.includes(tag.id) }"
             @click="toggleTag(tag.id)"
             @click="toggleTag(tag.id)"
           >
           >
             {{ tag.description }}
             {{ tag.description }}
@@ -112,7 +119,10 @@
         />
         />
       </q-card-section>
       </q-card-section>
 
 
-      <q-card-section class="q-pt-xs q-pb-xs q-px-lg">
+      <q-card-section
+        v-if="isNegative"
+        class="q-pt-xs q-pb-xs q-px-lg"
+      >
         <input
         <input
           ref="photoInputRef"
           ref="photoInputRef"
           accept="image/jpeg,image/png,image/webp"
           accept="image/jpeg,image/png,image/webp"
@@ -142,7 +152,7 @@
           </div>
           </div>
 
 
           <div
           <div
-            v-if="photos.length < 5"
+            v-if="photos.length < MAX_PHOTOS"
             class="photo-add"
             class="photo-add"
             @click="photoInputRef.click()"
             @click="photoInputRef.click()"
           >
           >
@@ -206,13 +216,20 @@
 import { avatarColors } from "src/helpers/avatarColors";
 import { avatarColors } from "src/helpers/avatarColors";
 import { computed, onMounted, ref } from "vue";
 import { computed, onMounted, ref } from "vue";
 import { createReview, getImprovementTypes } from "src/api/review";
 import { createReview, getImprovementTypes } from "src/api/review";
+import {
+  deleteClientFavoriteProvider,
+  getClientFavoriteProviders,
+} from "src/api/clientFavoriteProvider";
 import { getFirstName } from "src/helpers/utils";
 import { getFirstName } from "src/helpers/utils";
+import { getProfileMediaUrl } from "src/helpers/profileMedia";
 import { useDialogPluginComponent, useQuasar } from "quasar";
 import { useDialogPluginComponent, useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { userStore } from "src/stores/user";
 import { userStore } from "src/stores/user";
 
 
 import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 
 
+defineEmits([...useDialogPluginComponent.emits]);
+
 const props = defineProps({
 const props = defineProps({
   schedule: {
   schedule: {
     type: Object,
     type: Object,
@@ -220,25 +237,26 @@ const props = defineProps({
   },
   },
 });
 });
 
 
-defineEmits([...useDialogPluginComponent.emits]);
-
+const $q = useQuasar();
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
   useDialogPluginComponent();
-
-const { t } = useI18n();
-const $q = useQuasar();
 const store = userStore();
 const store = userStore();
+const { t } = useI18n();
+
+const MAX_PHOTOS = 5;
 
 
-const stars = ref(0);
-const selectedTagIds = ref([]);
-const comment = ref(null);
 const checkboxValue = ref(false);
 const checkboxValue = ref(false);
-const tags = ref([]);
-const loadingTags = ref(false);
+const comment = ref(null);
+const favoriteId = ref(null);
+const isFavoriteProvider = ref(false);
 const loading = ref(false);
 const loading = ref(false);
-const photos = ref([]);
-const photoPreviews = ref([]);
+const loadingTags = ref(false);
 const photoInputRef = ref(null);
 const photoInputRef = ref(null);
+const photoPreviews = ref([]);
+const photos = ref([]);
+const selectedTagIds = ref([]);
+const stars = ref(0);
+const tags = ref([]);
 
 
 const avatarStyle = computed(() => {
 const avatarStyle = computed(() => {
   const c = avatarColors[props.schedule.provider_id % avatarColors.length];
   const c = avatarColors[props.schedule.provider_id % avatarColors.length];
@@ -246,6 +264,8 @@ const avatarStyle = computed(() => {
   return { background: c.background, color: c.color };
   return { background: c.background, color: c.color };
 });
 });
 
 
+const photoUrl = computed(() => getProfileMediaUrl(props.schedule));
+
 const initials = computed(() => {
 const initials = computed(() => {
   const firstName = getFirstName(props.schedule.provider_name);
   const firstName = getFirstName(props.schedule.provider_name);
 
 
@@ -258,7 +278,7 @@ const isPositive = computed(() => stars.value >= 3);
 const onPhotosSelected = (event) => {
 const onPhotosSelected = (event) => {
   const files = Array.from(event.target.files);
   const files = Array.from(event.target.files);
 
 
-  const remaining = 5 - photos.value.length;
+  const remaining = MAX_PHOTOS - photos.value.length;
 
 
   files.slice(0, remaining).forEach((file) => {
   files.slice(0, remaining).forEach((file) => {
     photos.value.push(file);
     photos.value.push(file);
@@ -270,25 +290,48 @@ const onPhotosSelected = (event) => {
 
 
 const onStarsChange = () => {
 const onStarsChange = () => {
   selectedTagIds.value = [];
   selectedTagIds.value = [];
-  checkboxValue.value = false;
+  checkboxValue.value = isPositive.value && isFavoriteProvider.value;
+
+  if (!isNegative.value) clearPhotos();
 };
 };
 
 
 const openHelp = () => {
 const openHelp = () => {
   $q.dialog({ component: ProfileHelpDialog });
   $q.dialog({ component: ProfileHelpDialog });
 };
 };
 
 
+const clearPhotos = () => {
+  photoPreviews.value.forEach((preview) => URL.revokeObjectURL(preview));
+  photos.value = [];
+  photoPreviews.value = [];
+};
+
+const loadFavoriteState = async () => {
+  const clientId = store.user?.client?.id;
+  if (!clientId) return;
+
+  try {
+    const favorites = await getClientFavoriteProviders(clientId);
+
+    const match = (favorites ?? []).find(
+      (f) => Number(f.provider_id) === Number(props.schedule.provider_id),
+    );
+
+    isFavoriteProvider.value = !!match;
+    favoriteId.value = match?.id ?? null;
+
+    if (isPositive.value && isFavoriteProvider.value) checkboxValue.value = true;
+  } catch {
+    isFavoriteProvider.value = false;
+    favoriteId.value = null;
+  }
+};
+
 const removePhoto = (idx) => {
 const removePhoto = (idx) => {
   URL.revokeObjectURL(photoPreviews.value[idx]);
   URL.revokeObjectURL(photoPreviews.value[idx]);
   photos.value.splice(idx, 1);
   photos.value.splice(idx, 1);
   photoPreviews.value.splice(idx, 1);
   photoPreviews.value.splice(idx, 1);
 };
 };
 
 
-const toggleTag = (id) => {
-  const idx = selectedTagIds.value.indexOf(id);
-  if (idx === -1) selectedTagIds.value.push(id);
-  else selectedTagIds.value.splice(idx, 1);
-};
-
 const submit = async () => {
 const submit = async () => {
   if (stars.value === 0) return;
   if (stars.value === 0) return;
 
 
@@ -305,9 +348,22 @@ const submit = async () => {
       block_provider: isNegative.value && checkboxValue.value,
       block_provider: isNegative.value && checkboxValue.value,
       block_client: false,
       block_client: false,
       favorite_provider: isPositive.value && checkboxValue.value,
       favorite_provider: isPositive.value && checkboxValue.value,
-      photos: photos.value,
+      photos: isNegative.value ? photos.value : [],
     });
     });
 
 
+    if (
+      isPositive.value &&
+      !checkboxValue.value &&
+      isFavoriteProvider.value &&
+      favoriteId.value
+    ) {
+      try {
+        await deleteClientFavoriteProvider(favoriteId.value);
+      } catch {
+        // manter a avaliação enviada mesmo se a remoção do favorito falhar
+      }
+    }
+
     onDialogOK(true);
     onDialogOK(true);
   } catch (error) {
   } catch (error) {
     const status = error?.response?.status;
     const status = error?.response?.status;
@@ -326,7 +382,15 @@ const submit = async () => {
   }
   }
 };
 };
 
 
+const toggleTag = (id) => {
+  const idx = selectedTagIds.value.indexOf(id);
+  if (idx === -1) selectedTagIds.value.push(id);
+  else selectedTagIds.value.splice(idx, 1);
+};
+
 onMounted(async () => {
 onMounted(async () => {
+  loadFavoriteState();
+
   loadingTags.value = true;
   loadingTags.value = true;
 
 
   try {
   try {
@@ -339,6 +403,7 @@ onMounted(async () => {
     loadingTags.value = false;
     loadingTags.value = false;
   }
   }
 });
 });
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 92 - 17
src/pages/orders/ServicePackagePage.vue

@@ -28,18 +28,6 @@
           </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"
@@ -112,7 +100,7 @@
               <div class="item-prices q-mt-sm">
               <div class="item-prices q-mt-sm">
                 <div class="row items-center no-wrap">
                 <div class="row items-center no-wrap">
                   <span class="font11 fontbold text-primary">
                   <span class="font11 fontbold text-primary">
-                    {{ $t("service_package.credit_card_total") }}
+                    {{ $t("service_package.total") }}
                   </span>
                   </span>
 
 
                   <q-space />
                   <q-space />
@@ -134,8 +122,20 @@
             />
             />
           </q-card-section>
           </q-card-section>
         </q-card>
         </q-card>
+        <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"
+        />
       </div>
       </div>
 
 
+
       <div class="q-px-md q-pt-md">
       <div class="q-px-md q-pt-md">
         <q-card
         <q-card
           :flat="false"
           :flat="false"
@@ -234,11 +234,27 @@
       </div>
       </div>
     </template>
     </template>
 
 
+    <div
+      v-if="pendingServicePackages.length > 0"
+      class="q-pt-md"
+    >
+      <div class="section-title gradient-diarista q-px-md q-mb-sm font16 fontbold">
+        {{ $t("service_package.awaiting_payment_title") }}
+      </div>
+
+      <DashboardPendingSchedules
+        :data="pendingServicePackages"
+        progress-class="progress-fill--urgent"
+        type="servicePackage"
+        @view-details="openServicePackagePaymentDialog"
+      />
+    </div>
+
     <DashboardClientProposals
     <DashboardClientProposals
       v-if="schedulesProposals.length > 0"
       v-if="schedulesProposals.length > 0"
       :data="schedulesProposals"
       :data="schedulesProposals"
       class="q-pt-md"
       class="q-pt-md"
-      @refresh-data="loadCustomRequests"
+      @refresh-data="loadOrders"
     />
     />
 
 
     <div
     <div
@@ -254,11 +270,28 @@
         :key="entry.schedule.id"
         :key="entry.schedule.id"
         :proposals-count="entry.proposalsCount"
         :proposals-count="entry.proposalsCount"
         :schedule="entry.schedule"
         :schedule="entry.schedule"
+        @cancelled="loadOrders"
+      />
+    </div>
+
+    <div
+      v-if="pendingSchedules.length > 0"
+      class="q-pt-md"
+    >
+      <div class="section-title gradient-diarista q-px-md q-mb-sm font16 fontbold">
+        {{ $t("service_package.pending_default_title") }}
+      </div>
+
+      <DashboardPendingSchedules
+        :data="pendingSchedules"
+        progress-class="progress-fill--urgent"
+        @cancel="openCancelRequestDialog"
+        @view-details="openPackageDetailsDialog"
       />
       />
     </div>
     </div>
 
 
     <div
     <div
-      v-if="servicePackage.items.length === 0 && !hasPendingCustomRequests"
+      v-if="servicePackage.items.length === 0 && !hasPendingCustomRequests && !hasPendingDefaultSchedules"
       class="q-px-md q-pt-md"
       class="q-px-md q-pt-md"
     >
     >
       <q-card
       <q-card
@@ -330,13 +363,18 @@
 import { computed, onMounted, ref } from "vue";
 import { computed, onMounted, ref } from "vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 import DashboardClientProposals from "src/pages/dashboard/components/DashboardClientProposals.vue";
 import DashboardClientProposals from "src/pages/dashboard/components/DashboardClientProposals.vue";
+import DashboardPendingSchedules from "src/components/dashboard/DashboardPendingSchedules.vue";
 import CustomScheduleRequestCard from "src/pages/orders/components/CustomScheduleRequestCard.vue";
 import CustomScheduleRequestCard from "src/pages/orders/components/CustomScheduleRequestCard.vue";
+import PackageScheduleDetailsDialog from "src/components/dashboard/PackageScheduleDetailsDialog.vue";
+import ScheduleAcceptedDialog from "src/pages/dashboard/components/schedule/ScheduleAcceptedDialog.vue";
+import ScheduleCancelDialog from "src/pages/dashboard/components/schedule/ScheduleCancelDialog.vue";
 import { createScheduleServicePackage } from "src/api/servicePackage";
 import { createScheduleServicePackage } from "src/api/servicePackage";
 import { dadosPedidosCliente } from "src/api/dashboard";
 import { dadosPedidosCliente } from "src/api/dashboard";
 import { getAddresses } from "src/api/address";
 import { getAddresses } from "src/api/address";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
+import { usePaymentStore } from "src/stores/payment";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 import { useRouter } from "vue-router";
 import { useRouter } from "vue-router";
 import { userStore } from "src/stores/user";
 import { userStore } from "src/stores/user";
@@ -347,6 +385,7 @@ const router = useRouter();
 const $q = useQuasar();
 const $q = useQuasar();
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
 
 
+const paymentStore = usePaymentStore();
 const servicePackage = useServicePackageStore();
 const servicePackage = useServicePackageStore();
 const store = userStore();
 const store = userStore();
 
 
@@ -355,6 +394,8 @@ const primaryAddress = ref(null);
 const submitting = ref(false);
 const submitting = ref(false);
 const customSchedulesNoProposals = ref([]);
 const customSchedulesNoProposals = ref([]);
 const schedulesProposals = ref([]);
 const schedulesProposals = ref([]);
+const pendingSchedules = ref([]);
+const pendingServicePackages = ref([]);
 
 
 const pendingCustomRequests = computed(() => {
 const pendingCustomRequests = computed(() => {
   const groups = new Map();
   const groups = new Map();
@@ -390,6 +431,38 @@ const pendingCustomRequests = computed(() => {
 
 
 const hasPendingCustomRequests = computed(() => pendingCustomRequests.value.length > 0);
 const hasPendingCustomRequests = computed(() => pendingCustomRequests.value.length > 0);
 
 
+const hasPendingDefaultSchedules = computed(
+  () => pendingSchedules.value.length > 0 || pendingServicePackages.value.length > 0,
+);
+
+const openServicePackagePaymentDialog = (packageToPay) => {
+  paymentStore.setPackage(packageToPay);
+
+  $q.dialog({
+    component: ScheduleAcceptedDialog,
+  }).onOk(() => {
+    loadOrders();
+  }).onDismiss(() => {
+    paymentStore.clearPackage();
+  });
+};
+
+const openPackageDetailsDialog = (schedule) => {
+  $q.dialog({
+    component: PackageScheduleDetailsDialog,
+    componentProps: { schedule },
+  });
+};
+
+const openCancelRequestDialog = (schedule) => {
+  $q.dialog({
+    component: ScheduleCancelDialog,
+    componentProps: { schedule },
+  }).onDismiss(() => {
+    loadOrders();
+  });
+};
+
 const isValidFee = (fee) =>
 const isValidFee = (fee) =>
   fee !== null && fee !== undefined && Number.isFinite(Number(fee));
   fee !== null && fee !== undefined && Number.isFinite(Number(fee));
 
 
@@ -668,19 +741,21 @@ const submitServicePackage = async () => {
   }
   }
 };
 };
 
 
-const loadCustomRequests = async () => {
+const loadOrders = async () => {
   const response = await dadosPedidosCliente();
   const response = await dadosPedidosCliente();
 
 
   if (response) {
   if (response) {
     customSchedulesNoProposals.value = response.customSchedulesNoProposals ?? [];
     customSchedulesNoProposals.value = response.customSchedulesNoProposals ?? [];
     schedulesProposals.value = response.schedulesProposals ?? [];
     schedulesProposals.value = response.schedulesProposals ?? [];
+    pendingSchedules.value = response.pendingSchedules ?? [];
+    pendingServicePackages.value = response.pendingServicePackages ?? [];
   }
   }
 };
 };
 
 
 onMounted(() => {
 onMounted(() => {
   loadPrimaryAddress();
   loadPrimaryAddress();
   loadPlatformFees().catch(() => {});
   loadPlatformFees().catch(() => {});
-  loadCustomRequests();
+  loadOrders();
 });
 });
 </script>
 </script>
 
 

+ 11 - 5
src/pages/orders/components/CustomScheduleRequestCard.vue

@@ -1,12 +1,12 @@
 <template>
 <template>
-  <q-card :flat="false" class="request-card bg-surface shadow-card q-mb-sm">
+  <q-card class="request-card bg-surface shadow-card q-mb-sm" :flat="false">
     <q-card-section class="q-pa-sm">
     <q-card-section class="q-pa-sm">
       <div class="row no-wrap items-start q-gutter-x-sm">
       <div class="row no-wrap items-start q-gutter-x-sm">
-        <q-avatar size="44px" class="request-avatar">
+        <q-avatar class="request-avatar" size="44px">
           <q-icon
           <q-icon
-            :name="hasProposals ? 'mdi-format-list-bulleted' : 'mdi-clock-outline'"
-            size="20px"
             color="white"
             color="white"
+            size="20px"
+            :name="hasProposals ? 'mdi-format-list-bulleted' : 'mdi-clock-outline'"
           />
           />
         </q-avatar>
         </q-avatar>
 
 
@@ -75,7 +75,8 @@
 import { computed } from "vue";
 import { computed } from "vue";
 import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
-import NextSchedulesDetailsDialog from "src/components/dashboard/NextSchedulesDetailsDialog.vue";
+
+import NextSchedulesDetailsDialog from "src/pages/dashboard/components/schedule/NextSchedulesDetailsDialog.vue";
 
 
 const props = defineProps({
 const props = defineProps({
   schedule: {
   schedule: {
@@ -88,6 +89,8 @@ const props = defineProps({
   },
   },
 });
 });
 
 
+const emit = defineEmits(["cancelled"]);
+
 const $q = useQuasar();
 const $q = useQuasar();
 
 
 const hasProposals = computed(() => props.proposalsCount > 0);
 const hasProposals = computed(() => props.proposalsCount > 0);
@@ -96,8 +99,11 @@ const openDetailsDialog = () => {
   $q.dialog({
   $q.dialog({
     component: NextSchedulesDetailsDialog,
     component: NextSchedulesDetailsDialog,
     componentProps: { schedule: props.schedule },
     componentProps: { schedule: props.schedule },
+  }).onOk(({ action } = {}) => {
+    if (action === "cancelled") emit("cancelled");
   });
   });
 };
 };
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 1 - 2
src/pages/search/SearchPage.vue

@@ -151,7 +151,6 @@
         <q-card
         <q-card
           v-for="p in sortedProviders"
           v-for="p in sortedProviders"
           :key="p.provider_id"
           :key="p.provider_id"
-          :flat="false"
           class="card-border bg-page text-text q-mb-sm"
           class="card-border bg-page text-text q-mb-sm"
           @click="openProviderInfo(p)"
           @click="openProviderInfo(p)"
         >
         >
@@ -288,7 +287,7 @@ const $q = useQuasar();
 const activeDate = ref(null);
 const activeDate = ref(null);
 const activeSort = ref(null);
 const activeSort = ref(null);
 const allProviders = ref([]);
 const allProviders = ref([]);
-const currentPeriodType = ref(8);
+const currentPeriodType = ref(6);
 const hasLocation = ref(true);
 const hasLocation = ref(true);
 const loading = ref(true);
 const loading = ref(true);
 const searchName = ref("");
 const searchName = ref("");

+ 23 - 2
src/pages/search/components/SchedulingDialog.vue

@@ -202,7 +202,9 @@
 
 
           <div
           <div
             v-else
             v-else
+            ref="reviewsTrackRef"
             class="reviews-scroll"
             class="reviews-scroll"
+            @scroll="onReviewsScroll"
           >
           >
             <q-card
             <q-card
               v-for="review in reviews"
               v-for="review in reviews"
@@ -248,12 +250,20 @@
                   </div>
                   </div>
                 </div>
                 </div>
 
 
-                <div class="font9 fontregular text-text review-comment">
-                  {{ review.comment ?? "--------" }}
+                <div
+                  v-if="review.comment"
+                  class="font9 fontregular text-text review-comment"
+                >
+                  {{ review.comment }}
                 </div>
                 </div>
               </q-card-section>
               </q-card-section>
             </q-card>
             </q-card>
           </div>
           </div>
+
+          <DefaultCarouselIndicator
+            :active="reviewsActiveIndex"
+            :count="reviewsPageCount"
+          />
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
@@ -282,6 +292,7 @@ import {
 } from "src/api/providerAvailability";
 } from "src/api/providerAvailability";
 
 
 import { getProviderReceivedReviews } from "src/api/review";
 import { getProviderReceivedReviews } from "src/api/review";
+import { useCarouselIndicator } from "src/composables/useCarouselIndicator";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { useRouter } from "vue-router";
 import { useRouter } from "vue-router";
 import { userStore } from "src/stores/user";
 import { userStore } from "src/stores/user";
@@ -291,6 +302,7 @@ import ServicePackageSummaryDialog from "./ServicePackageSummaryDialog.vue";
 import ServiceSelectionSheet from "./ServiceSelectionSheet.vue";
 import ServiceSelectionSheet from "./ServiceSelectionSheet.vue";
 import ServiceTimeSelectionDialog from "./ServiceTimeSelectionDialog.vue";
 import ServiceTimeSelectionDialog from "./ServiceTimeSelectionDialog.vue";
 import ProfileInfoDialog from "src/components/profile/ProfileInfoDialog.vue";
 import ProfileInfoDialog from "src/components/profile/ProfileInfoDialog.vue";
+import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
 
 
 defineEmits([...useDialogPluginComponent.emits]);
 defineEmits([...useDialogPluginComponent.emits]);
 
 
@@ -321,6 +333,13 @@ const providerClientBlocks = ref({
   fully_blocked_weeks: [],
   fully_blocked_weeks: [],
 });
 });
 const reviews = ref([]);
 const reviews = ref([]);
+
+const {
+  activeIndex: reviewsActiveIndex,
+  onTrackScroll: onReviewsScroll,
+  pageCount: reviewsPageCount,
+  trackRef: reviewsTrackRef,
+} = useCarouselIndicator(() => reviews.value);
 const selectedDate = ref(null);
 const selectedDate = ref(null);
 const workingDays = ref([]);
 const workingDays = ref([]);
 
 
@@ -841,6 +860,7 @@ onMounted(() => {
   display: flex;
   display: flex;
   flex-direction: row;
   flex-direction: row;
   overflow-x: auto;
   overflow-x: auto;
+  scroll-snap-type: x proximity;
   -webkit-overflow-scrolling: touch;
   -webkit-overflow-scrolling: touch;
   scrollbar-width: none;
   scrollbar-width: none;
   padding-bottom: 8px;
   padding-bottom: 8px;
@@ -851,6 +871,7 @@ onMounted(() => {
 
 
 .review-card {
 .review-card {
   flex-shrink: 0;
   flex-shrink: 0;
+  scroll-snap-align: start;
   min-width: 220px;
   min-width: 220px;
   max-width: 240px;
   max-width: 240px;
   border-radius: 12px;
   border-radius: 12px;

+ 33 - 28
src/stores/payment.js

@@ -1,49 +1,54 @@
 import { defineStore } from 'pinia';
 import { defineStore } from 'pinia';
 import { ref } from 'vue';
 import { ref } from 'vue';
+import { parseUtcDate } from 'src/helpers/scheduleDate';
 
 
-const getPixExpiresAt = (payment) => payment?.pix?.expires_at ?? payment?.expires_at ?? null;
+export const usePaymentStore = defineStore('payment', () => {
+  const currentPackage = ref(null);
+  const pixById = ref({});
 
 
-const isFutureDate = (date) => {
-  if (!date) return false;
-  const time = new Date(date).getTime();
-  return Number.isFinite(time) && time > Date.now();
-};
+  const clearPackage = () => {
+    currentPackage.value = null;
+  };
 
 
-export const usePaymentStore = defineStore('payment', () => {
-  const pixPaymentsByServicePackageId = ref({});
+  const clearPix = (id) => {
+    if (!id || !pixById.value[id]) return;
 
 
-  const getValidPixPaymentForServicePackage = (servicePackageId) => {
-    const payment = pixPaymentsByServicePackageId.value[servicePackageId] ?? null;
+    const pix = { ...pixById.value };
+    delete pix[id];
+    pixById.value = pix;
+  };
+
+  const getPix = (id) => {
+    const payment = pixById.value[id] ?? null;
     if (!payment) return null;
     if (!payment) return null;
 
 
-    if (!isFutureDate(getPixExpiresAt(payment))) {
-      clearPixPaymentForServicePackage(servicePackageId);
+    const expiresAt = payment?.pix?.expires_at ?? payment?.expires_at;
+    const expiry = parseUtcDate(expiresAt)?.getTime();
+
+    if (!expiresAt || !Number.isFinite(expiry) || expiry <= Date.now()) {
+      clearPix(id);
       return null;
       return null;
     }
     }
 
 
     return payment;
     return payment;
   };
   };
 
 
-  const setPixPaymentForServicePackage = (servicePackageId, payment) => {
-    if (!servicePackageId || !payment) return;
-    pixPaymentsByServicePackageId.value = {
-      ...pixPaymentsByServicePackageId.value,
-      [servicePackageId]: payment,
-    };
+  const setPackage = (item) => {
+    currentPackage.value = item;
   };
   };
 
 
-  const clearPixPaymentForServicePackage = (servicePackageId) => {
-    if (!servicePackageId || !pixPaymentsByServicePackageId.value[servicePackageId]) return;
-
-    const payments = { ...pixPaymentsByServicePackageId.value };
-    delete payments[servicePackageId];
-    pixPaymentsByServicePackageId.value = payments;
+  const setPix = (id, payment) => {
+    if (!id || !payment) return;
+    pixById.value = { ...pixById.value, [id]: payment };
   };
   };
 
 
   return {
   return {
-    pixPaymentsByServicePackageId,
-    getValidPixPaymentForServicePackage,
-    setPixPaymentForServicePackage,
-    clearPixPaymentForServicePackage,
+    clearPackage,
+    clearPix,
+    currentPackage,
+    getPix,
+    pixById,
+    setPackage,
+    setPix,
   };
   };
 });
 });