Эх сурвалжийг харах

Merge branch 'development'

Gustavo Mantovani 19 цаг өмнө
parent
commit
1971ca3349
71 өөрчлөгдсөн 6469 нэмэгдсэн , 3682 устгасан
  1. 50 78
      package-lock.json
  2. 6 5
      package.json
  3. 61 81
      src-capacitor/package-lock.json
  4. 8 8
      src-capacitor/package.json
  5. 2 2
      src/App.vue
  6. 0 53
      src/api/cart.js
  7. 4 4
      src/api/payment.js
  8. 53 0
      src/api/servicePackage.js
  9. 7 0
      src/api/services.js
  10. 8 0
      src/boot/axios.js
  11. 82 23
      src/components/dashboard/DashboardFavoriteProviders.vue
  12. 49 20
      src/components/dashboard/DashboardLastDoneSchedules.vue
  13. 109 46
      src/components/dashboard/DashboardNextSchedules.vue
  14. 198 48
      src/components/dashboard/DashboardPendingSchedules.vue
  15. 142 62
      src/components/dashboard/DashboardProvidersClose.vue
  16. 196 69
      src/components/dashboard/DashboardTodaySchedules.vue
  17. 142 93
      src/components/dashboard/NextSchedulesDetailsDialog.vue
  18. 124 79
      src/components/dashboard/ScheduleAcceptedDialog.vue
  19. 31 15
      src/components/dashboard/SchedulePaymentDialog.vue
  20. 21 11
      src/components/dashboard/SchedulePaymentPixDialog.vue
  21. 12 6
      src/components/dashboard/SchedulePaymentProcessingDialog.vue
  22. 223 133
      src/components/dashboard/ScheduleRatingDialog.vue
  23. 77 43
      src/components/defaults/DefaultInputDatePicker.vue
  24. 0 29
      src/components/layout/ScheduleCartMenu.vue
  25. 36 0
      src/components/layout/ServicePackageMenu.vue
  26. 57 31
      src/components/login/LoginStep1Panel.vue
  27. 13 1
      src/components/login/LoginStep2Panel.vue
  28. 120 48
      src/components/login/LoginStep3Panel.vue
  29. 9 9
      src/components/profile/ProfileAddressDialog.vue
  30. 3 2
      src/components/profile/ProfileAddressFormDialog.vue
  31. 340 133
      src/components/profile/ProfileFavoritesDialog.vue
  32. 1 1
      src/components/profile/ProfileHelpDialog.vue
  33. 203 0
      src/components/profile/ProfileInfoDialog.vue
  34. 18 8
      src/components/profile/ProfilePaymentAddDialog.vue
  35. 1 1
      src/components/profile/ProfilePaymentsDialog.vue
  36. 123 62
      src/components/profile/ProfilePrivacyDialog.vue
  37. 1 1
      src/components/profile/SupportRequestDialog.vue
  38. 83 78
      src/composables/useInputRules.js
  39. 11 9
      src/composables/usePaymentPlatformFees.js
  40. 46 34
      src/composables/useScroll.js
  41. 36 1
      src/css/app.scss
  42. 11 0
      src/helpers/avatarColors.js
  43. 19 4
      src/helpers/paymentPlatformFees.js
  44. 9 0
      src/helpers/profileMedia.js
  45. 25 0
      src/helpers/provider.js
  46. 28 17
      src/i18n/locales/en.json
  47. 30 18
      src/i18n/locales/es.json
  48. 29 18
      src/i18n/locales/pt.json
  49. 64 38
      src/layouts/MainLayout.vue
  50. 279 140
      src/pages/LoginPage.vue
  51. 226 114
      src/pages/agenda/CalendarPage.vue
  52. 9 18
      src/pages/dashboard/DashboardPage.vue
  53. 126 63
      src/pages/dashboard/components/DashboardClientProposals.vue
  54. 178 87
      src/pages/location/AddressCompletionPage.vue
  55. 618 0
      src/pages/orders/ServicePackagePage.vue
  56. 45 29
      src/pages/profile/ProfileEditDialog.vue
  57. 272 102
      src/pages/profile/ProfilePage.vue
  58. 0 529
      src/pages/schedule-cart/ScheduleCartPage.vue
  59. 2 2
      src/pages/schedules/SobMedidaPage.vue
  60. 297 125
      src/pages/search/SearchPage.vue
  61. 0 560
      src/pages/search/components/OrderSummaryDialog.vue
  62. 337 203
      src/pages/search/components/SchedulingDialog.vue
  63. 139 88
      src/pages/search/components/SearchFilterDialog.vue
  64. 690 0
      src/pages/search/components/ServicePackageSummaryDialog.vue
  65. 101 69
      src/pages/search/components/ServiceSelectionSheet.vue
  66. 126 64
      src/pages/search/components/ServiceTimeSelectionDialog.vue
  67. 23 10
      src/pages/search/components/ServiceTypeInfoDialog.vue
  68. 4 4
      src/router/routes/servicePackage.route.js
  69. 18 18
      src/stores/payment.js
  70. 57 34
      src/stores/registrationFlow.js
  71. 1 1
      src/stores/servicePackage.js

+ 50 - 78
package-lock.json

@@ -9,12 +9,13 @@
       "version": "0.0.1",
       "dependencies": {
         "@bufbuild/protobuf": "^2.5.1",
-        "@capacitor/browser": "^8.0.3",
+        "@capacitor/browser": "^7.0.5",
         "@capacitor/camera": "^7.0.5",
+        "@capacitor/core": "^7.6.8",
         "@capacitor/device": "^7.0.1",
-        "@capacitor/geolocation": "^8.2.0",
-        "@capacitor/google-maps": "^8.0.1",
-        "@capacitor/push-notifications": "^8.1.1",
+        "@capacitor/geolocation": "^7.1.8",
+        "@capacitor/google-maps": "^7.2.2",
+        "@capacitor/push-notifications": "^7.0.7",
         "@quasar/cli": "^2.5.0",
         "@quasar/extras": "^1.17.0",
         "axios": "^1.9.0",
@@ -34,7 +35,7 @@
       "devDependencies": {
         "@bufbuild/buf": "^1.54.0",
         "@bufbuild/protoc-gen-es": "^2.5.1",
-        "@capacitor/cli": "^8.3.3",
+        "@capacitor/cli": "^7.6.8",
         "@capacitor/keyboard": "^7.0.1",
         "@capacitor/preferences": "^7.0.1",
         "@capacitor/status-bar": "^7.0.1",
@@ -318,12 +319,12 @@
       }
     },
     "node_modules/@capacitor/browser": {
-      "version": "8.0.3",
-      "resolved": "https://registry.npmjs.org/@capacitor/browser/-/browser-8.0.3.tgz",
-      "integrity": "sha512-WJWPHEPbweiFoHYmVlCbZf5yrqJ2Rchx2Xvbmd+3Lf+Zkpq3nXBThThY2CF69lYEg1NINGF9BcHThIOEU1gZlQ==",
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/@capacitor/browser/-/browser-7.0.5.tgz",
+      "integrity": "sha512-YF/kC1m1e6c81X/agtAnYItVlqiQ/nXLG+Q3QXNDuiMoRynqa3Guu8ZaV9kWvqQcHoE2w18xz7WGpPOuh+R1fA==",
       "license": "MIT",
       "peerDependencies": {
-        "@capacitor/core": ">=8.0.0"
+        "@capacitor/core": ">=7.0.0"
       }
     },
     "node_modules/@capacitor/camera": {
@@ -336,9 +337,9 @@
       }
     },
     "node_modules/@capacitor/cli": {
-      "version": "8.3.3",
-      "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.3.3.tgz",
-      "integrity": "sha512-FHebL02KEyU5vs+Os5s1yZuE8QT3FzxoO4nZLywGk7Ny957E6gOujKouGKsnKYq01eAWWJGGV/Fv04rY27tSsw==",
+      "version": "7.6.8",
+      "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.6.8.tgz",
+      "integrity": "sha512-sJYLoSS1wVHjDPwRc0grtNZcQZnkQc7TJKoVZdHL2HI5TPCq+27WBrMAo0mXvQQSH2axPYCz5cyRrcqqakOvxQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -365,7 +366,7 @@
         "capacitor": "bin/capacitor"
       },
       "engines": {
-        "node": ">=22.0.0"
+        "node": ">=20.0.0"
       }
     },
     "node_modules/@capacitor/cli/node_modules/commander": {
@@ -436,11 +437,10 @@
       }
     },
     "node_modules/@capacitor/core": {
-      "version": "8.3.1",
-      "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.1.tgz",
-      "integrity": "sha512-UF8ItlHguU1Z6GXfPTeT2gakf+ctNI8pAS1kwSBQlsJMlfD4OPoto/SmKnOxKCQvnF4WRcdWeg6C0zREUNaAQg==",
+      "version": "7.6.8",
+      "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.6.8.tgz",
+      "integrity": "sha512-x1xfxvNcTSnj6OoYaAB0n0a0HRvi5f2YjcVnMLxpMhbHDgGDAzzN9z8cnPCYV+SKTvqGF1hezHm82Op32XaAGg==",
       "license": "MIT",
-      "peer": true,
       "dependencies": {
         "tslib": "^2.1.0"
       }
@@ -455,29 +455,29 @@
       }
     },
     "node_modules/@capacitor/geolocation": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-8.2.0.tgz",
-      "integrity": "sha512-N29QcoIPmme0xSxRkm7+3hjoHp6mBAOarxecvtCCZKyOBeKiJsFUq981cezg2XWBa6fhCXJMCCjQPngKK/dIag==",
+      "version": "7.1.8",
+      "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-7.1.8.tgz",
+      "integrity": "sha512-x/rJieD2N2zlGvXFRD3ru4ArJWTwXAIYmyhfo5zg4F1DKGos7mCCOvIOSVMdfQmt14SCsmJeO9Qe8EtDGNGCrw==",
       "license": "MIT",
       "dependencies": {
-        "@capacitor/synapse": "^1.0.4"
+        "@capacitor/synapse": "^1.0.3"
       },
       "peerDependencies": {
-        "@capacitor/core": ">=8.0.0"
+        "@capacitor/core": ">=7.0.0"
       }
     },
     "node_modules/@capacitor/google-maps": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/@capacitor/google-maps/-/google-maps-8.0.1.tgz",
-      "integrity": "sha512-XSOyanbtOeO5KrSfoJOpbcSW4EXvfdxp+SUy6umdIGFeR2WJKNnPC6isXn+zjJgJH8kblT2X4fF0dxhZV8wLBg==",
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/@capacitor/google-maps/-/google-maps-7.2.2.tgz",
+      "integrity": "sha512-BCVXHc+q6i2W3J4e5zM2PApt2eNpgcAiMsvaA6WJYE/GpGiIp6BI4lWsDOGbwZ4dYrUXG8mEozEcQTHZC88Ryw==",
       "license": "MIT",
       "dependencies": {
-        "@googlemaps/js-api-loader": "^2.0.2",
-        "@googlemaps/markerclusterer": "^2.6.2",
-        "@types/google.maps": "^3.58.1"
+        "@googlemaps/js-api-loader": "~1.16.8",
+        "@googlemaps/markerclusterer": "~2.5.3",
+        "@types/google.maps": "~3.58.1"
       },
       "peerDependencies": {
-        "@capacitor/core": ">=8.0.0"
+        "@capacitor/core": ">=7.0.0"
       }
     },
     "node_modules/@capacitor/keyboard": {
@@ -501,12 +501,12 @@
       }
     },
     "node_modules/@capacitor/push-notifications": {
-      "version": "8.1.1",
-      "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-8.1.1.tgz",
-      "integrity": "sha512-WqzjPKIbYbARMN+GC0XMAJcxJpUUzqgzS/Ny8RODLrro38pQhm3GXYwX2Mwd+LZlLY39rGImkCkrKyQSNfuikA==",
+      "version": "7.0.7",
+      "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-7.0.7.tgz",
+      "integrity": "sha512-TgFsPV4uOez0RtFKsWPz3XCrLxc9th+UyAkillUY9vL6ezVKT35te9Sn4vGR0jeU2U0KUNU7Dt6oSCALnSiEKg==",
       "license": "MIT",
       "peerDependencies": {
-        "@capacitor/core": ">=8.0.0"
+        "@capacitor/core": ">=7.0.0"
       }
     },
     "node_modules/@capacitor/status-bar": {
@@ -1164,22 +1164,18 @@
       }
     },
     "node_modules/@googlemaps/js-api-loader": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-2.0.2.tgz",
-      "integrity": "sha512-bKVuTqatS8Jven5aFqVB7rCHF1VFEzpzyi0ruzO0GUR+A7m9oMqMgtnmpANj7kMYEvvhty8Fk7TnJ1MKjWHu+Q==",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "@types/google.maps": "^3.53.1"
-      }
+      "version": "1.16.10",
+      "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.10.tgz",
+      "integrity": "sha512-c2erv2k7P2ilYzMmtYcMgAR21AULosQuUHJbStnrvRk2dG93k5cqptDrh9A8p+ZNlyhiqEOgHW7N9PAizdUM7Q==",
+      "license": "Apache-2.0"
     },
     "node_modules/@googlemaps/markerclusterer": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.6.2.tgz",
-      "integrity": "sha512-U6uVhq8iWhiIckA89sgRu8OK35mjd6/3CuoZKWakKEf0QmRRWpatlsPb3kqXkoWSmbcZkopRiI4dnW6DQSd7bQ==",
+      "version": "2.5.3",
+      "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.3.tgz",
+      "integrity": "sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==",
       "license": "Apache-2.0",
       "dependencies": {
-        "@types/supercluster": "^7.1.3",
-        "fast-equals": "^5.2.2",
+        "fast-deep-equal": "^3.1.3",
         "supercluster": "^8.0.1"
       }
     },
@@ -2796,16 +2792,10 @@
         "@types/node": "*"
       }
     },
-    "node_modules/@types/geojson": {
-      "version": "7946.0.16",
-      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
-      "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
-      "license": "MIT"
-    },
     "node_modules/@types/google.maps": {
-      "version": "3.64.0",
-      "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.64.0.tgz",
-      "integrity": "sha512-dN0H6tB4lgLQLovcbPXFYYOEV41TpyyJghzb5jrzjB96FZmjeOghevVdC+BMGd6YqyCqXaggyEtqRXLRjzCBZA==",
+      "version": "3.58.1",
+      "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz",
+      "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==",
       "license": "MIT"
     },
     "node_modules/@types/har-format": {
@@ -2913,15 +2903,6 @@
       "dev": true,
       "license": "MIT"
     },
-    "node_modules/@types/supercluster": {
-      "version": "7.1.3",
-      "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
-      "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/geojson": "*"
-      }
-    },
     "node_modules/@typescript-eslint/project-service": {
       "version": "8.37.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.37.0.tgz",
@@ -4039,9 +4020,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001727",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
-      "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
+      "version": "1.0.30001806",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
+      "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
       "dev": true,
       "funding": [
         {
@@ -5672,15 +5653,6 @@
       "dev": true,
       "license": "Apache-2.0"
     },
-    "node_modules/fast-equals": {
-      "version": "5.4.0",
-      "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
-      "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=6.0.0"
-      }
-    },
     "node_modules/fast-fifo": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
@@ -6886,9 +6858,9 @@
       }
     },
     "node_modules/kdbush": {
-      "version": "4.0.2",
-      "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
-      "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
+      "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
       "license": "ISC"
     },
     "node_modules/keyv": {

+ 6 - 5
package.json

@@ -23,12 +23,13 @@
   },
   "dependencies": {
     "@bufbuild/protobuf": "^2.5.1",
-    "@capacitor/browser": "^8.0.3",
+    "@capacitor/browser": "^7.0.5",
     "@capacitor/camera": "^7.0.5",
+    "@capacitor/core": "^7.6.8",
     "@capacitor/device": "^7.0.1",
-    "@capacitor/geolocation": "^8.2.0",
-    "@capacitor/google-maps": "^8.0.1",
-    "@capacitor/push-notifications": "^8.1.1",
+    "@capacitor/geolocation": "^7.1.8",
+    "@capacitor/google-maps": "^7.2.2",
+    "@capacitor/push-notifications": "^7.0.7",
     "@quasar/cli": "^2.5.0",
     "@quasar/extras": "^1.17.0",
     "axios": "^1.9.0",
@@ -48,7 +49,7 @@
   "devDependencies": {
     "@bufbuild/buf": "^1.54.0",
     "@bufbuild/protoc-gen-es": "^2.5.1",
-    "@capacitor/cli": "^8.3.3",
+    "@capacitor/cli": "^7.6.8",
     "@capacitor/keyboard": "^7.0.1",
     "@capacitor/preferences": "^7.0.1",
     "@capacitor/status-bar": "^7.0.1",

+ 61 - 81
src-capacitor/package-lock.json

@@ -8,29 +8,30 @@
       "name": "diarista-cliente-app",
       "version": "0.0.1",
       "dependencies": {
-        "@capacitor/android": "^7.2.0",
+        "@capacitor/android": "^7.6.8",
         "@capacitor/app": "^7.0.0",
-        "@capacitor/browser": "^8.0.3",
+        "@capacitor/browser": "^7.0.5",
         "@capacitor/camera": "^7.0.5",
-        "@capacitor/cli": "^8.3.3",
-        "@capacitor/core": "^7.0.0",
+        "@capacitor/cli": "^7.6.8",
+        "@capacitor/core": "^7.6.8",
         "@capacitor/device": "^7.0.1",
-        "@capacitor/geolocation": "^8.2.0",
-        "@capacitor/google-maps": "^8.0.1",
-        "@capacitor/ios": "^7.2.0",
+        "@capacitor/geolocation": "^7.1.8",
+        "@capacitor/google-maps": "^7.2.2",
+        "@capacitor/ios": "^7.6.8",
         "@capacitor/keyboard": "^7.0.1",
         "@capacitor/preferences": "^7.0.1",
-        "@capacitor/push-notifications": "^8.1.1",
+        "@capacitor/push-notifications": "^7.0.7",
         "@capacitor/splash-screen": "^7.0.5",
         "@capacitor/status-bar": "^7.0.1"
       }
     },
     "node_modules/@capacitor/android": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-7.2.0.tgz",
-      "integrity": "sha512-zdhEy3jZPG5Toe/pGzKtDgIiBGywjaoEuQWnGVjBYPlSAEUtAhpZ2At7V0SCb26yluAuzrAUV0Ue+LQeEtHwFQ==",
+      "version": "7.6.8",
+      "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-7.6.8.tgz",
+      "integrity": "sha512-N5LXe1ls+TA0mq+RhtyDdP1JISzAAI+OuLGL3G7OWQxfQQOZYkiEjEJMYqMV6JR421IqpRsYj08JOfC0PFC3NA==",
+      "license": "MIT",
       "peerDependencies": {
-        "@capacitor/core": "^7.2.0"
+        "@capacitor/core": "^7.6.0"
       }
     },
     "node_modules/@capacitor/app": {
@@ -43,12 +44,12 @@
       }
     },
     "node_modules/@capacitor/browser": {
-      "version": "8.0.3",
-      "resolved": "https://registry.npmjs.org/@capacitor/browser/-/browser-8.0.3.tgz",
-      "integrity": "sha512-WJWPHEPbweiFoHYmVlCbZf5yrqJ2Rchx2Xvbmd+3Lf+Zkpq3nXBThThY2CF69lYEg1NINGF9BcHThIOEU1gZlQ==",
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/@capacitor/browser/-/browser-7.0.5.tgz",
+      "integrity": "sha512-YF/kC1m1e6c81X/agtAnYItVlqiQ/nXLG+Q3QXNDuiMoRynqa3Guu8ZaV9kWvqQcHoE2w18xz7WGpPOuh+R1fA==",
       "license": "MIT",
       "peerDependencies": {
-        "@capacitor/core": ">=8.0.0"
+        "@capacitor/core": ">=7.0.0"
       }
     },
     "node_modules/@capacitor/camera": {
@@ -61,9 +62,9 @@
       }
     },
     "node_modules/@capacitor/cli": {
-      "version": "8.4.1",
-      "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.4.1.tgz",
-      "integrity": "sha512-t7F2s7fFHCq113xgrggrmK6ctV0/8E5YfLNVLfPHp4GCTDO+tly9fZvWPf2/sOI8lMm18dLT43qbXLRTz/OZgw==",
+      "version": "7.6.8",
+      "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.6.8.tgz",
+      "integrity": "sha512-sJYLoSS1wVHjDPwRc0grtNZcQZnkQc7TJKoVZdHL2HI5TPCq+27WBrMAo0mXvQQSH2axPYCz5cyRrcqqakOvxQ==",
       "license": "MIT",
       "dependencies": {
         "@ionic/cli-framework-output": "^2.2.8",
@@ -89,13 +90,13 @@
         "capacitor": "bin/capacitor"
       },
       "engines": {
-        "node": ">=22.0.0"
+        "node": ">=20.0.0"
       }
     },
     "node_modules/@capacitor/core": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.2.0.tgz",
-      "integrity": "sha512-2zCnA6RJeZ9ec4470o8QMZEQTWpekw9FNoqm5TLc10jeCrhvHVI8MPgxdZVc3mOdFlyieYu4AS1fNxSqbS57Pw==",
+      "version": "7.6.8",
+      "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.6.8.tgz",
+      "integrity": "sha512-x1xfxvNcTSnj6OoYaAB0n0a0HRvi5f2YjcVnMLxpMhbHDgGDAzzN9z8cnPCYV+SKTvqGF1hezHm82Op32XaAGg==",
       "license": "MIT",
       "dependencies": {
         "tslib": "^2.1.0"
@@ -110,37 +111,38 @@
       }
     },
     "node_modules/@capacitor/geolocation": {
-      "version": "8.2.0",
-      "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-8.2.0.tgz",
-      "integrity": "sha512-N29QcoIPmme0xSxRkm7+3hjoHp6mBAOarxecvtCCZKyOBeKiJsFUq981cezg2XWBa6fhCXJMCCjQPngKK/dIag==",
+      "version": "7.1.8",
+      "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-7.1.8.tgz",
+      "integrity": "sha512-x/rJieD2N2zlGvXFRD3ru4ArJWTwXAIYmyhfo5zg4F1DKGos7mCCOvIOSVMdfQmt14SCsmJeO9Qe8EtDGNGCrw==",
       "license": "MIT",
       "dependencies": {
-        "@capacitor/synapse": "^1.0.4"
+        "@capacitor/synapse": "^1.0.3"
       },
       "peerDependencies": {
-        "@capacitor/core": ">=8.0.0"
+        "@capacitor/core": ">=7.0.0"
       }
     },
     "node_modules/@capacitor/google-maps": {
-      "version": "8.0.1",
-      "resolved": "https://registry.npmjs.org/@capacitor/google-maps/-/google-maps-8.0.1.tgz",
-      "integrity": "sha512-XSOyanbtOeO5KrSfoJOpbcSW4EXvfdxp+SUy6umdIGFeR2WJKNnPC6isXn+zjJgJH8kblT2X4fF0dxhZV8wLBg==",
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/@capacitor/google-maps/-/google-maps-7.2.2.tgz",
+      "integrity": "sha512-BCVXHc+q6i2W3J4e5zM2PApt2eNpgcAiMsvaA6WJYE/GpGiIp6BI4lWsDOGbwZ4dYrUXG8mEozEcQTHZC88Ryw==",
       "license": "MIT",
       "dependencies": {
-        "@googlemaps/js-api-loader": "^2.0.2",
-        "@googlemaps/markerclusterer": "^2.6.2",
-        "@types/google.maps": "^3.58.1"
+        "@googlemaps/js-api-loader": "~1.16.8",
+        "@googlemaps/markerclusterer": "~2.5.3",
+        "@types/google.maps": "~3.58.1"
       },
       "peerDependencies": {
-        "@capacitor/core": ">=8.0.0"
+        "@capacitor/core": ">=7.0.0"
       }
     },
     "node_modules/@capacitor/ios": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-7.2.0.tgz",
-      "integrity": "sha512-MQgRZcXZpbpjN83bjkGrzQd7s3XeHBZplmWf38/msF/siMGJKLrXNmNzmmPIWA5Xpi/aH6UoJFk1wXuU2U+zMg==",
+      "version": "7.6.8",
+      "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-7.6.8.tgz",
+      "integrity": "sha512-TAM1FdI1Cfl8e/wLBo7x5m61V/J1fMEpyxU2psZzHzsa436k2pwEME6CQoBRYdw6Q6gud0jZSPwOVrNn+MI7Ag==",
+      "license": "MIT",
       "peerDependencies": {
-        "@capacitor/core": "^7.2.0"
+        "@capacitor/core": "^7.6.0"
       }
     },
     "node_modules/@capacitor/keyboard": {
@@ -162,12 +164,12 @@
       }
     },
     "node_modules/@capacitor/push-notifications": {
-      "version": "8.1.1",
-      "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-8.1.1.tgz",
-      "integrity": "sha512-WqzjPKIbYbARMN+GC0XMAJcxJpUUzqgzS/Ny8RODLrro38pQhm3GXYwX2Mwd+LZlLY39rGImkCkrKyQSNfuikA==",
+      "version": "7.0.7",
+      "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-7.0.7.tgz",
+      "integrity": "sha512-TgFsPV4uOez0RtFKsWPz3XCrLxc9th+UyAkillUY9vL6ezVKT35te9Sn4vGR0jeU2U0KUNU7Dt6oSCALnSiEKg==",
       "license": "MIT",
       "peerDependencies": {
-        "@capacitor/core": ">=8.0.0"
+        "@capacitor/core": ">=7.0.0"
       }
     },
     "node_modules/@capacitor/splash-screen": {
@@ -195,22 +197,18 @@
       "license": "ISC"
     },
     "node_modules/@googlemaps/js-api-loader": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-2.1.1.tgz",
-      "integrity": "sha512-yUpAwksbHrlZIWD49JmveNSfBG4oAK0AwMknfSaPMnP5N7UT8oFRVCqwjGb1XQovi//7KLbPQKZpbofiLGzpDw==",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "@types/google.maps": "^3.53.1"
-      }
+      "version": "1.16.10",
+      "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.10.tgz",
+      "integrity": "sha512-c2erv2k7P2ilYzMmtYcMgAR21AULosQuUHJbStnrvRk2dG93k5cqptDrh9A8p+ZNlyhiqEOgHW7N9PAizdUM7Q==",
+      "license": "Apache-2.0"
     },
     "node_modules/@googlemaps/markerclusterer": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.6.2.tgz",
-      "integrity": "sha512-U6uVhq8iWhiIckA89sgRu8OK35mjd6/3CuoZKWakKEf0QmRRWpatlsPb3kqXkoWSmbcZkopRiI4dnW6DQSd7bQ==",
+      "version": "2.5.3",
+      "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.3.tgz",
+      "integrity": "sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==",
       "license": "Apache-2.0",
       "dependencies": {
-        "@types/supercluster": "^7.1.3",
-        "fast-equals": "^5.2.2",
+        "fast-deep-equal": "^3.1.3",
         "supercluster": "^8.0.1"
       }
     },
@@ -470,16 +468,10 @@
         "@types/node": "*"
       }
     },
-    "node_modules/@types/geojson": {
-      "version": "7946.0.16",
-      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
-      "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
-      "license": "MIT"
-    },
     "node_modules/@types/google.maps": {
-      "version": "3.65.2",
-      "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.65.2.tgz",
-      "integrity": "sha512-e52bmOhGCQSNabFpL48iQlwJybq6rfns8NUVJ20MR7CdPlHQ2RmSCnPbJfrUYJfogrE4OiHQTZ4LXpop+eer1w==",
+      "version": "3.58.1",
+      "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz",
+      "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==",
       "license": "MIT"
     },
     "node_modules/@types/node": {
@@ -497,15 +489,6 @@
       "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==",
       "license": "MIT"
     },
-    "node_modules/@types/supercluster": {
-      "version": "7.1.3",
-      "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
-      "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
-      "license": "MIT",
-      "dependencies": {
-        "@types/geojson": "*"
-      }
-    },
     "node_modules/@xmldom/xmldom": {
       "version": "0.9.10",
       "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
@@ -731,14 +714,11 @@
         "node": ">=6"
       }
     },
-    "node_modules/fast-equals": {
-      "version": "5.4.0",
-      "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
-      "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=6.0.0"
-      }
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "license": "MIT"
     },
     "node_modules/fd-slicer": {
       "version": "1.1.0",

+ 8 - 8
src-capacitor/package.json

@@ -5,19 +5,19 @@
   "author": "zntt <zanattagg@gmail.com>",
   "private": true,
   "dependencies": {
-    "@capacitor/android": "^7.2.0",
+    "@capacitor/android": "^7.6.8",
     "@capacitor/app": "^7.0.0",
-    "@capacitor/browser": "^8.0.3",
+    "@capacitor/browser": "^7.0.5",
     "@capacitor/camera": "^7.0.5",
-    "@capacitor/cli": "^8.3.3",
-    "@capacitor/core": "^7.0.0",
+    "@capacitor/cli": "^7.6.8",
+    "@capacitor/core": "^7.6.8",
     "@capacitor/device": "^7.0.1",
-    "@capacitor/geolocation": "^8.2.0",
-    "@capacitor/google-maps": "^8.0.1",
-    "@capacitor/ios": "^7.2.0",
+    "@capacitor/geolocation": "^7.1.8",
+    "@capacitor/google-maps": "^7.2.2",
+    "@capacitor/ios": "^7.6.8",
     "@capacitor/keyboard": "^7.0.1",
     "@capacitor/preferences": "^7.0.1",
-    "@capacitor/push-notifications": "^8.1.1",
+    "@capacitor/push-notifications": "^7.0.7",
     "@capacitor/splash-screen": "^7.0.5",
     "@capacitor/status-bar": "^7.0.1"
   }

+ 2 - 2
src/App.vue

@@ -4,7 +4,7 @@
 
 <script setup>
 import { useQuasar } from "quasar";
-import { StatusBar } from "@capacitor/status-bar";
+import { StatusBar, Style } from "@capacitor/status-bar";
 import { Capacitor } from "@capacitor/core";
 import { watch, onBeforeMount } from "vue";
 import { useI18n } from "vue-i18n";
@@ -40,7 +40,7 @@ onBeforeMount(async () => {
   if (isNative) await StatusBar.show();
   const { value: themePref } = await Preferences.get({ key: "theme" });
   const theme = themePref || systemTheme;
-  if (isNative) await StatusBar.setStyle({ style: theme.toUpperCase() });
+  if (isNative) await StatusBar.setStyle({ style: Style.Dark });
   const { value: localePref } = await Preferences.get({ key: "locale" });
   const userLocale = localePref || window.navigator.language;
   console.log(theme, userLocale);

+ 0 - 53
src/api/cart.js

@@ -1,53 +0,0 @@
-import api from "src/api";
-
-export const getCarts = async () => {
-  const { data } = await api.get("/cart");
-  return data.payload;
-};
-
-export const getMyCarts = async () => {
-  const { data } = await api.get("/cart/me");
-  return data.payload;
-};
-
-export const getCart = async (cartId) => {
-  const { data } = await api.get(`/cart/${cartId}`);
-  return data.payload;
-};
-
-export const createCart = async (payload) => {
-  const { data } = await api.post("/cart", payload);
-  return data.payload;
-};
-
-export const createMyCart = async (payload) => {
-  const { data } = await api.post("/cart/me", payload);
-  return data.payload;
-};
-
-export const createScheduleCart = createMyCart;
-
-export const updateCart = async (cartId, payload) => {
-  const { data } = await api.put(`/cart/${cartId}`, payload);
-  return data.payload;
-};
-
-export const deleteCart = async (cartId) => {
-  const { data } = await api.delete(`/cart/${cartId}`);
-  return data.payload;
-};
-
-export const createCartItem = async (payload) => {
-  const { data } = await api.post("/cart-item", payload);
-  return data.payload;
-};
-
-export const updateCartItem = async (cartItemId, payload) => {
-  const { data } = await api.put(`/cart-item/${cartItemId}`, payload);
-  return data.payload;
-};
-
-export const deleteCartItem = async (cartItemId) => {
-  const { data } = await api.delete(`/cart-item/${cartItemId}`);
-  return data.payload;
-};

+ 4 - 4
src/api/payment.js

@@ -1,12 +1,12 @@
 import api from 'src/api';
 
-export const paySchedule = async (scheduleId, payload) => {
-  const { data } = await api.post(`/payment/schedule/${scheduleId}/pay`, payload);
+export const payServicePackage = async (servicePackageId, payload) => {
+  const { data } = await api.post(`/payment/service-package/${servicePackageId}/pay`, payload);
   return data.payload;
 };
 
-export const getSchedulePixPayment = async (scheduleId) => {
-  const { data } = await api.get(`/payment/schedule/${scheduleId}/pix`);
+export const getServicePackagePix = async (servicePackageId) => {
+  const { data } = await api.get(`/payment/service-package/${servicePackageId}/pix`);
   return data.payload;
 };
 

+ 53 - 0
src/api/servicePackage.js

@@ -0,0 +1,53 @@
+import api from "src/api";
+
+export const getServicePackages = async () => {
+  const { data } = await api.get("/service-packages");
+  return data.payload;
+};
+
+export const getMyServicePackages = async () => {
+  const { data } = await api.get("/service-packages/me");
+  return data.payload;
+};
+
+export const getServicePackage = async (servicePackageId) => {
+  const { data } = await api.get(`/service-packages/${servicePackageId}`);
+  return data.payload;
+};
+
+export const createServicePackage = async (payload) => {
+  const { data } = await api.post("/service-packages", payload);
+  return data.payload;
+};
+
+export const createMyServicePackage = async (payload) => {
+  const { data } = await api.post("/service-packages/me", payload);
+  return data.payload;
+};
+
+export const createScheduleServicePackage = createMyServicePackage;
+
+export const updateServicePackage = async (servicePackageId, payload) => {
+  const { data } = await api.put(`/service-packages/${servicePackageId}`, payload);
+  return data.payload;
+};
+
+export const deleteServicePackage = async (servicePackageId) => {
+  const { data } = await api.delete(`/service-packages/${servicePackageId}`);
+  return data.payload;
+};
+
+export const createServicePackageItem = async (payload) => {
+  const { data } = await api.post("/service-package-items", payload);
+  return data.payload;
+};
+
+export const updateServicePackageItem = async (servicePackageItemId, payload) => {
+  const { data } = await api.put(`/service-package-items/${servicePackageItemId}`, payload);
+  return data.payload;
+};
+
+export const deleteServicePackageItem = async (servicePackageItemId) => {
+  const { data } = await api.delete(`/service-package-items/${servicePackageItemId}`);
+  return data.payload;
+};

+ 7 - 0
src/api/services.js

@@ -0,0 +1,7 @@
+import api from "src/api";
+
+export const getServicesConfig = async () => {
+  const { data } = await api.get("/services/config");
+
+  return data.payload;
+};

+ 8 - 0
src/boot/axios.js

@@ -46,6 +46,14 @@ const errorInterceptor = async (error, router) => {
   const user_store = userStore();
 
   if (error.response?.status === 422) {
+    if (error.response?.data?.error === "missing_payment_data") {
+      Notify.create({
+        message: error.response.data.message,
+        type: "negative",
+      });
+      router.push({ name: "ProfilePage" });
+    }
+
     return Promise.reject(error);
   }
 

+ 82 - 23
src/components/dashboard/DashboardFavoriteProviders.vue

@@ -1,34 +1,78 @@
 <template>
   <div class="q-mx-md q-mb-md">
-    <div class="dashboard-section-title gradient-diarista q-mb-sm font16 fontbold">{{ $t('dashboard_client.favorites.title') }}</div>
+    <div
+      class="dashboard-section-title gradient-diarista q-mb-sm font16 fontbold"
+    >
+      {{ $t("dashboard_client.favorites.title") }}
+    </div>
 
     <div class="scroll-track">
       <q-card
         v-for="item in data"
         :key="item.provider_id"
-        class="fav-card shadow-card bg-surface card-border"
         :flat="false"
+        class="fav-card shadow-card bg-surface card-border"
       >
         <q-card-section class="q-pa-sm column text-text">
           <div class="row items-start no-wrap">
             <div class="col-3 q-my-auto">
-              <q-avatar :style="avatarColors[item.provider_id % avatarColors.length]" size="46px" class="">
-                <img v-if="item.provider_photo" :src="item.provider_photo" style="object-fit:cover;border-radius:50%;" />
-                <span v-else>{{ item.provider_name?.slice(0,1).toUpperCase() ?? '—' }}</span>
+              <q-avatar
+                class=""
+                size="46px"
+                :style="avatarColors[item.provider_id % avatarColors.length]"
+              >
+                <img
+                  v-if="getProfileMediaUrl(item)"
+                  class="profile-image"
+                  :src="getProfileMediaUrl(item)"
+                />
+
+                <span v-else>
+                  {{ item.provider_name?.slice(0, 1).toUpperCase() ?? "—" }}
+                </span>
               </q-avatar>
             </div>
+
             <div class="col-5 column q-gutter-y-xs q-my-auto">
-              <span class="text-fav-name font12 fontmedium">{{ getFirstName(item.provider_name) || 'Prestador' }}</span>
-              <span class="text-fav-region font9 fontbold  text-grey-7">{{ item.provider_district ?? $t('dashboard_client.last_schedules.no_address') }}</span>
-              <div v-if="item.average_rating != null" class="row items-center q-gutter-x-xs">
-                <q-icon name="mdi-star" color="warning" size="xs" />
-                <span class="text-fav-name font12 fontregular">{{ Number(item.average_rating).toFixed(1) }}</span>
+              <span class="text-fav-name font12 fontmedium">
+                {{ getFirstName(item.provider_name) || "Prestador" }}
+              </span>
+
+              <span
+                v-if="getProviderGenderLabel(item, t)"
+                class="font9 fontmedium text-grey-7"
+              >
+                {{ getProviderGenderLabel(item, t) }}
+              </span>
+
+              <span class="text-fav-region font9 fontbold text-grey-7">
+                {{
+                  item.provider_district ??
+                  $t("dashboard_client.last_schedules.no_address")
+                }}
+              </span>
+
+              <div
+                v-if="item.average_rating != null"
+                class="row items-center q-gutter-x-xs"
+              >
+                <q-icon
+                  color="warning"
+                  name="mdi-star"
+                  size="xs"
+                />
+
+                <span class="text-fav-name font12 fontregular">
+                  {{ Number(item.average_rating).toFixed(1) }}
+                </span>
               </div>
             </div>
+
             <div class="col-4 column q-mt-auto">
               <q-btn
-                rounded color="primary"
+                color="primary"
                 padding="1px 5px"
+                rounded
                 size="sm"
                 :label="$t('dashboard_client.favorites.view_schedule')"
                 @click="goToScheduling(item)"
@@ -42,19 +86,20 @@
 </template>
 
 <script setup>
-import { useQuasar } from 'quasar';
-import SchedulingDialog from 'src/pages/search/components/SchedulingDialog.vue';
-import { getFirstName } from 'src/helpers/utils';
+import { avatarColors } from "src/helpers/avatarColors";
+import { getFirstName } from "src/helpers/utils";
+import { getProfileMediaUrl } from "src/helpers/profileMedia";
+import { getProviderGenderLabel } from "src/helpers/provider";
+import { useI18n } from "vue-i18n";
+import { useQuasar } from "quasar";
+
+import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 
-defineProps({ data: { type: Array, default: () => [] } });
+defineProps({ data: { default: () => [], type: Array } });
+
+const { t } = useI18n();
 
 const $q = useQuasar();
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-];
 
 const goToScheduling = (provider) => {
   $q.dialog({
@@ -73,12 +118,26 @@ const goToScheduling = (provider) => {
   overscroll-behavior-x: contain;
   scroll-snap-type: x proximity;
   padding-bottom: 8px;
-  &::-webkit-scrollbar { display: none; }
-  &::after { content: ''; flex: 0 0 1px; }
+
+  &::-webkit-scrollbar {
+    display: none;
+  }
+
+  &::after {
+    content: "";
+    flex: 0 0 1px;
+  }
 }
+
 .fav-card {
   flex: 0 0 200px;
   scroll-snap-align: start;
   min-width: 70%;
 }
+
+.profile-image {
+  height: 100%;
+  object-fit: cover;
+  width: 100%;
+}
 </style>

+ 49 - 20
src/components/dashboard/DashboardLastDoneSchedules.vue

@@ -1,25 +1,53 @@
 <template>
   <div class="q-mx-md q-mb-md">
-    <div class="dashboard-section-title gradient-diarista q-mb-sm font16 fontbold">{{ $t('dashboard_client.last_schedules.title') }}</div>
+    <div
+      class="dashboard-section-title gradient-diarista q-mb-sm font16 fontbold"
+    >
+      {{ $t("dashboard_client.last_schedules.title") }}
+    </div>
 
     <div class="scroll-track">
       <q-card
         v-for="item in data"
         :key="item.id"
-        class="mini-card shadow-card bg-surface card-border"
         :flat="false"
+        class="mini-card shadow-card bg-surface card-border"
       >
         <q-card-section class="column q-pa-md q-gutter-y-xs text-text">
-          <q-avatar :style="avatarColors[item.provider_id % avatarColors.length]" size="56px" class="q-mx-auto">
-            <img v-if="item.provider_photo" :src="item.provider_photo" style="object-fit:cover;border-radius:50%;" />
-            <span v-else>{{ item.provider_name?.slice(0,1).toUpperCase() ?? '—' }}</span>
+          <q-avatar
+            class="q-mx-auto"
+            size="56px"
+            :style="avatarColors[item.provider_id % avatarColors.length]"
+          >
+            <img
+              v-if="item.provider_photo"
+              :src="item.provider_photo"
+              style="object-fit: cover; border-radius: 50%"
+            />
+
+            <span v-else>{{
+              item.provider_name?.slice(0, 1).toUpperCase() ?? "—"
+            }}</span>
           </q-avatar>
-          <span class="text-done-name font12 fontmedium"> {{ getFirstName(item.provider_name) || 'Prestador' }}</span>
-          <span class="text-done-district font9 fontbold text-grey-7">{{ item.provider_district != null ? item.provider_district : $t('dashboard_client.last_schedules.no_address') }}</span>
+
+          <span class="text-done-name font12 fontmedium">
+            {{ getFirstName(item.provider_name) || "Prestador" }}
+          </span>
+
+          <span class="text-done-district font9 fontbold text-grey-7">
+            {{
+              item.provider_district != null
+                ? item.provider_district
+                : $t("dashboard_client.last_schedules.no_address")
+            }}
+          </span>
+
           <q-btn
-            rounded color="secondary"
-            padding="1px 5px" size="sm"
             class="q-mt-xs full-width"
+            color="secondary"
+            padding="1px 5px"
+            rounded
+            size="sm"
             :label="$t('dashboard_client.last_schedules.reschedule')"
             @click="goToScheduling(item)"
           />
@@ -30,19 +58,15 @@
 </template>
 
 <script setup>
-import { useQuasar } from 'quasar';
-import SchedulingDialog from 'src/pages/search/components/SchedulingDialog.vue';
-import { getFirstName } from 'src/helpers/utils';
+import { avatarColors } from "src/helpers/avatarColors";
+import { getFirstName } from "src/helpers/utils";
+import { useQuasar } from "quasar";
+
+import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 
 defineProps({ data: { type: Array, default: () => [] } });
 
 const $q = useQuasar();
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-];
 
 const goToScheduling = (provider) => {
   $q.dialog({
@@ -61,8 +85,13 @@ const goToScheduling = (provider) => {
   overscroll-behavior-x: contain;
   scroll-snap-type: x proximity;
   padding-bottom: 8px;
-  &::-webkit-scrollbar { display: none; }
-  &::after { content: ''; flex: 0 0 1px; }
+  &::-webkit-scrollbar {
+    display: none;
+  }
+  &::after {
+    content: "";
+    flex: 0 0 1px;
+  }
 }
 .mini-card {
   flex: 0 0 130px;

+ 109 - 46
src/components/dashboard/DashboardNextSchedules.vue

@@ -1,6 +1,10 @@
 <template>
   <div class="q-mx-md q-mb-md">
-    <div class="dashboard-section-title gradient-diarista q-mb-sm font16 fontbold">{{ $t('dashboard_client.next_schedules.title') }}</div>
+    <div
+      class="dashboard-section-title gradient-diarista q-mb-sm font16 fontbold"
+    >
+      {{ $t("dashboard_client.next_schedules.title") }}
+    </div>
 
     <div class="scroll-wrapper">
       <div class="scroll-track">
@@ -10,18 +14,36 @@
           :flat="false"
           class="schedule-card card-border shadow-card bg-surface"
         >
-          <q-card-section class="q-pa-md row col-12 no-wrap schedule-card-section">
+          <q-card-section
+            class="q-pa-md row col-12 no-wrap schedule-card-section"
+          >
             <div class="column text-center schedule-avatar col-4">
               <div class="col-7 column">
-                <q-avatar :style="avatarColors[item.id % avatarColors.length]" class="q-mx-auto">
-                  <img v-if="item.provider_photo" :src="item.provider_photo" style="object-fit:cover;border-radius:50%;" />
-                  <span v-else>{{ item.provider_name?.slice(0,1).toUpperCase() ?? '—' }}</span>
+                <q-avatar
+                  class="q-mx-auto"
+                  :style="avatarColors[item.id % avatarColors.length]"
+                >
+                  <img
+                    v-if="item.provider_photo"
+                    :src="item.provider_photo"
+                    style="object-fit: cover; border-radius: 50%"
+                  />
+
+                  <span v-else>
+                    {{
+                      item.provider_name?.slice(0, 1).toUpperCase() ?? "—"
+                    }}
+                  </span>
                 </q-avatar>
               </div>
 
               <div class="col-5 column justify-end font10 fontbold">
                 <span class="text-pill text-primary customColor">
-                  {{ item.schedule_type === 'custom' ? $t('dashboard_client.next_schedules.tag_custom') : $t('dashboard_client.next_schedules.tag_default') }}
+                  {{
+                    item.schedule_type === "custom"
+                      ? $t("dashboard_client.next_schedules.tag_custom")
+                      : $t("dashboard_client.next_schedules.tag_default")
+                  }}
                 </span>
               </div>
             </div>
@@ -29,8 +51,13 @@
             <div class="col-4 column text-text q-px-lg schedule-main">
               <div class="column schedule-info">
                 <div>
-                  <span class="text-provider-name provider-name-ellipsis font12 fontbold">
-                    {{ getFirstName(item.provider_name) || $t('dashboard_client.next_schedules.no_provider') }}
+                  <span
+                    class="text-provider-name provider-name-ellipsis font12 fontbold"
+                  >
+                    {{
+                      getFirstName(item.provider_name) ||
+                      $t("dashboard_client.next_schedules.no_provider")
+                    }}
                   </span>
                 </div>
 
@@ -41,17 +68,19 @@
                     </span>
 
                     <span class="text-schedule-date-regular">
-                      {{ ', ' + formatDayMonth(item.date) }}
+                      {{ ", " + formatDayMonth(item.date) }}
                     </span>
                   </div>
 
                   <div class="schedule-line q-pt-xs">
                     <span class="text-schedule-date-regular">
-                      {{ $t('dashboard_client.next_schedules.from') }}&nbsp;
+                      {{ $t("dashboard_client.next_schedules.from") }}&nbsp;
                     </span>
 
                     <span class="text-schedule-date-bold fontbold">
-                      {{ item.start_time?.slice(0,5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ item.end_time?.slice(0,5) }}
+                      {{ item.start_time?.slice(0, 5) }}
+                      {{ $t("dashboard_client.next_schedules.to") }}
+                      {{ item.end_time?.slice(0, 5) }}
                     </span>
                   </div>
                 </div>
@@ -59,15 +88,21 @@
 
               <div class="schedule-place">
                 <div class="column justify-end">
-                  <div class="row items-center no-wrap text-pill-place schedule-place-content">
-
+                  <div
+                    class="row items-center no-wrap text-pill-place schedule-place-content"
+                  >
                     <span class="font10 fontbold q-ml-sm">
                       <q-icon
-                        :name="addressIcon(item.address?.address_type ?? item.custom_address_type)"
                         color="primary"
                         size="12px"
+                        :name="addressIcon(item.address?.address_type ?? item.custom_address_type)"
                       />
-                      {{ addressLabel(item.address?.address_type ?? item.custom_address_type) }}
+                      {{
+                        addressLabel(
+                          item.address?.address_type ??
+                            item.custom_address_type,
+                        )
+                      }}
                     </span>
                   </div>
                 </div>
@@ -80,7 +115,9 @@
                   {{ formatScheduleTotal(item) }}
                 </span>
 
-                <span class="text-price-label schedule-price-label font12 fontmedium">
+                <span
+                  class="text-price-label schedule-price-label font12 fontmedium"
+                >
                   {{ formatLabelByPeriodType(item.period_type) }}
                 </span>
               </div>
@@ -89,9 +126,11 @@
                 <q-btn
                   class="full-width"
                   color="primary"
+                  no-caps
                   padding="1px 5px"
+                  rounded
                   size="sm"
-                  unelevated rounded no-caps
+                  unelevated
                   :label="$t('dashboard_client.next_schedules.details')"
                   @click="emit('view-details', item)"
                 />
@@ -105,56 +144,73 @@
 </template>
 
 <script setup>
-import { formatCurrency,getFirstName,formatLabelByPeriodType } from 'src/helpers/utils';
-import { getSchedulePaymentType, getScheduleTotalWithPlatformFee } from 'src/helpers/paymentPlatformFees';
-import { usePaymentPlatformFees } from 'src/composables/usePaymentPlatformFees';
-import { onMounted } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { avatarColors } from "src/helpers/avatarColors";
+
+import {
+  formatCurrency,
+  formatLabelByPeriodType,
+  getFirstName,
+} from "src/helpers/utils";
+
+import {
+  getSchedulePaymentType,
+  getScheduleTotalWithPlatformFee,
+} from "src/helpers/paymentPlatformFees";
+
+import { onMounted } from "vue";
+import { useI18n } from "vue-i18n";
+import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 
 defineProps({ data: { type: Array, default: () => [] } });
 
-const emit = defineEmits(['view-details']);
+const emit = defineEmits(["view-details"]);
 
 const { t } = useI18n();
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
 
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-];
-
-const addressIcon = (type) => type === 'home' ? 'mdi-home-outline' : 'mdi-office-building-outline';
+const addressIcon = (type) =>
+  type === "home" ? "mdi-home-outline" : "mdi-office-building-outline";
 
 const addressLabel = (type) => {
-  if (type === 'home')       return t('address.types.commercial.home');
-  if (type === 'apartment')  return t('dashboard_client.next_schedules.place_apartment');
-  if (type === 'commercial') return t('address.types.commercial.commercial');
+  if (type === "home") return t("address.types.commercial.home");
+
+  if (type === "apartment")
+    return t("dashboard_client.next_schedules.place_apartment");
 
-  return t('dashboard_client.next_schedules.place_unknown');
+  if (type === "commercial") return t("address.types.commercial.commercial");
+
+  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');
+  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));
+  return formatCurrency(
+    getScheduleTotalWithPlatformFee(
+      item,
+      getSchedulePaymentType(item),
+      platformFees.value,
+    ),
+  );
 };
 
 const formatDayMonth = (iso) => {
-  if (!iso) return '';
+  if (!iso) return "";
 
-  return new Date(iso).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
+  return new Date(iso).toLocaleDateString("pt-BR", {
+    day: "2-digit",
+    month: "2-digit",
+  });
 };
 
 const formatWeekday = (iso) => {
-  if (!iso) return '';
+  if (!iso) return "";
 
   const d = new Date(iso);
 
-  const w = d.toLocaleDateString('pt-BR', { weekday: 'long' });
+  const w = d.toLocaleDateString("pt-BR", { weekday: "long" });
 
   return w.charAt(0).toUpperCase() + w.slice(1);
 };
@@ -165,7 +221,9 @@ onMounted(() => {
 </script>
 
 <style scoped lang="scss">
-.scroll-wrapper { overflow: hidden; }
+.scroll-wrapper {
+  overflow: hidden;
+}
 
 .scroll-track {
   display: flex;
@@ -175,8 +233,13 @@ onMounted(() => {
   overscroll-behavior-x: contain;
   scroll-snap-type: x proximity;
   padding-bottom: 8px;
-  &::-webkit-scrollbar { display: none; }
-  &::after { content: ''; flex: 0 0 1px; }
+  &::-webkit-scrollbar {
+    display: none;
+  }
+  &::after {
+    content: "";
+    flex: 0 0 1px;
+  }
 }
 
 .schedule-card {
@@ -269,7 +332,7 @@ onMounted(() => {
 }
 
 .customColor {
-  background: linear-gradient(180deg, #8B5CF6 50%, #EC4899 50%);
+  background: linear-gradient(180deg, #8b5cf6 50%, #ec4899 50%);
   background-clip: text;
   -webkit-background-clip: text;
   -webkit-text-fill-color: transparent;

+ 198 - 48
src/components/dashboard/DashboardPendingSchedules.vue

@@ -5,62 +5,128 @@
         <q-card
           v-for="item in data"
           :key="item.id"
-          class="pending-card card-border shadow-card bg-surface"
           :flat="false"
+          class="pending-card card-border shadow-card bg-surface"
           @click="seeDetails(item)"
         >
           <q-card-section class="q-pa-md">
-
             <div class="row no-wrap items-start q-mb-sm">
-              <q-avatar size="40px" :style="avatarColors[item.id % avatarColors.length]" class="q-mr-sm flex-shrink-0">
-                <img v-if="item.provider_photo" :src="item.provider_photo" style="object-fit:cover;border-radius:50%;" />
-                <span v-else>{{ item.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}</span>
+              <q-avatar
+                class="q-mr-sm flex-shrink-0"
+                size="40px"
+                :style="avatarColors[item.id % avatarColors.length]"
+              >
+                <img
+                  v-if="providerPhoto(item)"
+                  :src="providerPhoto(item)"
+                  style="object-fit: cover; border-radius: 50%"
+                />
+
+                <span v-else>
+                  {{
+                    providerName(item).slice(0, 2).toUpperCase() || "??"
+                  }}
+                </span>
               </q-avatar>
 
               <div class="col column no-wrap overflow-hidden">
                 <span class="font12 fontmedium text-text">
-                  <span v-if="item.status == 'pending'">
-                    {{ $t('dashboard_client.pending_schedules.requesting_with') }}
+                  <span v-if="type === 'servicePackage'">
+                    {{
+                      $t("dashboard_client.pending_schedules.pay_to_provider")
+                    }}
+                  </span>
+                  <span v-else-if="item.status == 'pending'">
+                    {{
+                      $t("dashboard_client.pending_schedules.requesting_with")
+                    }}
                   </span>
+
                   <span v-else-if="item.status == 'accepted'">
-                    {{ $t('dashboard_client.pending_schedules.pay_to_provider') }}
+                    {{
+                      $t("dashboard_client.pending_schedules.pay_to_provider")
+                    }}
                   </span>
-                  
-                  <span class="font12 fontbold"> {{ ' ' + (getFirstName(item.provider_name) || '—') }}</span>
+
+                  <span class="font12 fontbold">
+                    {{ " " + (getFirstName(providerName(item)) || "—") }}</span
+                  >
                 </span>
+
                 <div class="row items-center q-mt-xs">
-                  <q-icon name="mdi-clock-outline" size="13px" color="grey-5" class="q-mr-xs" />
-                  <span class="font9 fontmedium text-grey-5">{{ item.time_since_request }}</span>
+                  <q-icon
+                    class="q-mr-xs"
+                    color="grey-5"
+                    name="mdi-clock-outline"
+                    size="13px"
+                  />
+
+                  <span class="font9 fontmedium text-grey-5">
+                    {{ displayTime(item) }}
+                  </span>
                 </div>
               </div>
 
-              <div class="clock-badge-col q-ml-sm flex-shrink-0 column items-center">
+              <div
+                class="clock-badge-col q-ml-sm flex-shrink-0 column items-center"
+              >
                 <div class="clock-badge">
-                  <q-icon name="mdi-clock-outline" size="14px" color="white" />
+                  <q-icon
+                    color="white"
+                    name="mdi-clock-outline"
+                    size="14px"
+                  />
                 </div>
-                <span class="font10 fontmedium text-primary q-mt-xs badge-status-text">
-                  {{ $t(`dashboard_client.pending_schedules.status.${item.status ?? 'pending'}`) }}
+
+                <span
+                  class="font10 fontmedium text-primary q-mt-xs badge-status-text"
+                >
+                  {{
+                    type === 'servicePackage'
+                      ? $t("dashboard_client.pending_schedules.status.accepted")
+                      : $t(
+                          `dashboard_client.pending_schedules.status.${item.status ?? "pending"}`,
+                        )
+                  }}
                 </span>
               </div>
             </div>
 
             <div class="progress-track q-mb-md">
-              <div class="progress-fill" :style="{ width: progressPercent(item.status) + '%' }" />
+              <div
+                class="progress-fill"
+                :style="{ width: progressPercent(item.status) + '%' }"
+              />
             </div>
 
             <div class="row items-center no-wrap">
               <q-btn
-                flat no-caps dense
-                :label="$t('dashboard_client.pending_schedules.cancel_btn')"
+                v-if="type !== 'servicePackage'"
+                class="q-mr-sm flex-shrink-0"
                 color="primary"
+                dense
+                flat
+                no-caps
                 size="xs"
-                class="q-mr-sm flex-shrink-0"
+                :label="$t('dashboard_client.pending_schedules.cancel_btn')"
                 @click="$emit('cancel', item)"
               />
+
               <q-space />
-              <span class="font9 fontregular text-grey-6 col ellipsis text-right">
-                <q-icon name="mdi-map-marker-outline" size="13px" color="grey-6" class="q-mr-xs flex-shrink-0" />
-                {{ [item.address?.address, item.address?.number, item.address?.district].filter(Boolean).join(', ') || '—' }}
+
+              <span
+                class="font9 fontregular text-grey-6 col ellipsis text-right"
+              >
+                <q-icon
+                  class="q-mr-xs flex-shrink-0"
+                  color="grey-6"
+                  name="mdi-map-marker-outline"
+                  size="13px"
+                />
+
+                {{
+                  displayAddress(item)
+                }}
               </span>
             </div>
           </q-card-section>
@@ -71,38 +137,79 @@
 </template>
 
 <script setup>
-import {getFirstName} from 'src/helpers/utils';
+import { computed } from "vue";
+import { avatarColors } from "src/helpers/avatarColors";
+import { getFirstName } from "src/helpers/utils";
+
+const props = defineProps({
+  data: { type: Array, default: () => [] },
+  type: { type: String, default: 'schedule' },
+});
+
+const emit = defineEmits(["view-details", "cancel"]);
 
+const isServicePackage = computed(() => props.type === 'servicePackage');
 
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-];
+const providerPhoto = (item) => {
+  if (isServicePackage.value) {
+    return item.provider?.profile_media?.url ?? item.provider_photo ?? null;
+  }
+  return item.provider_photo;
+};
+
+const providerName = (item) => {
+  if (isServicePackage.value) {
+    return item.provider?.user?.name ?? item.provider_name ?? '';
+  }
+  return item.provider_name ?? '';
+};
+
+const displayAddress = (item) => {
+  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.address?.address,
+    item.address?.number,
+    item.address?.district,
+  ].filter(Boolean).join(', ') || '—';
+};
+
+const displayTime = (item) => {
+  if (isServicePackage.value) {
+    return item.schedules?.length
+      ? `${item.schedules.length} agendamentos`
+      : '—';
+  }
+  return item.time_since_request;
+};
 
 const statusProgressMap = {
-  pending:  20,
+  pending: 20,
   accepted: 40,
-  paid:     60,
-  started:  80,
+  paid: 60,
+  started: 80,
   finished: 100,
 };
 
 const progressPercent = (status) => statusProgressMap[status] ?? 20;
 
-defineProps({ data: { type: Array, default: () => [] } });
-const emit = defineEmits(['view-details', 'cancel']);
-
 const seeDetails = (item) => {
-  if (item.status === 'accepted') {
-    emit('view-details', item);
+  if (isServicePackage.value || item.status === "accepted") {
+    emit("view-details", item);
   }
 };
 </script>
 
 <style scoped lang="scss">
-.scroll-wrapper { overflow: hidden; }
+.scroll-wrapper {
+  overflow: hidden;
+}
 .scroll-track {
   display: flex;
   flex-direction: row;
@@ -111,8 +218,13 @@ const seeDetails = (item) => {
   overscroll-behavior-x: contain;
   scroll-snap-type: x proximity;
   padding-bottom: 8px;
-  &::-webkit-scrollbar { display: none; }
-  &::after { content: ''; flex: 0 0 1px; }
+  &::-webkit-scrollbar {
+    display: none;
+  }
+  &::after {
+    content: "";
+    flex: 0 0 1px;
+  }
 }
 .pending-card {
   min-width: 80%;
@@ -128,7 +240,7 @@ const seeDetails = (item) => {
   width: 24px;
   height: 24px;
   border-radius: 50%;
-  background: linear-gradient(135deg, #8B5CF6, #EC4899);
+  background: linear-gradient(135deg, #8b5cf6, #ec4899);
   display: flex;
   align-items: center;
   justify-content: center;
@@ -140,16 +252,54 @@ const seeDetails = (item) => {
 
 .progress-track {
   width: 100%;
-  height: 5px;
-  background: #E2E8F0;
-  border-radius: 3px;
+  height: 6px;
+  background: #e2e8f0;
+  border-radius: 999px;
   overflow: hidden;
 }
 
 .progress-fill {
+  position: relative;
   height: 100%;
-  border-radius: 3px;
-  background: linear-gradient(90deg, #8B5CF6, #EC4899);
-  transition: width 0.4s ease;
+  border-radius: inherit;
+
+  background: linear-gradient(
+    90deg,
+    #3b5cff,
+    #7a4dff,
+    #d94cff
+  );
+
+  transition: width .4s ease;
+  overflow: hidden;
+}
+
+.progress-fill::after {
+  content: "";
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  left: -35%;
+  width: 35%;
+
+  background: linear-gradient(
+    90deg,
+    transparent,
+    rgba(255,255,255,.45),
+    transparent
+  );
+
+  transform: skewX(-20deg);
+  animation: progressShine 2s linear infinite;
+}
+
+@keyframes progressShine {
+  from {
+    left: -35%;
+  }
+
+  to {
+    left: 135%;
+  }
 }
 </style>

+ 142 - 62
src/components/dashboard/DashboardProvidersClose.vue

@@ -1,11 +1,30 @@
 <template>
   <div class="q-mx-md q-mb-lg">
     <div class="row items-center justify-between no-wrap q-mb-sm">
-      <div class="dashboard-section-title gradient-diarista font16 fontbold">{{ $t('dashboard_client.providers_close.title') }}</div>
+      <div class="dashboard-section-title gradient-diarista font16 fontbold">
+        {{ $t("dashboard_client.providers_close.title") }}
+      </div>
+
       <div class="text-text-light">
-        <q-btn flat color="text-light" size="sm" icon="mdi-chevron-left" @click="setPeriodTypePrevious"/>
-        <span class="font10 fontbold">{{ showCorrectLabels() }}</span>
-        <q-btn flat color="text-light" size="sm" icon="mdi-chevron-right" @click="setPeriodTypeNext"/>
+        <q-btn
+          color="text-light"
+          flat
+          icon="mdi-chevron-left"
+          size="sm"
+          @click="setPeriodTypePrevious"
+        />
+
+        <span class="font10 fontbold">
+          {{ showCorrectLabels() }}
+        </span>
+
+        <q-btn
+          color="text-light"
+          flat
+          icon="mdi-chevron-right"
+          size="sm"
+          @click="setPeriodTypeNext"
+        />
       </div>
     </div>
 
@@ -13,50 +32,106 @@
       <q-card
         v-for="p in data"
         :key="p.provider_id"
-        class="card-border bg-page text-text q-mb-sm"
         :flat="false"
+        class="card-border bg-page text-text q-mb-sm"
       >
         <q-card-section class="row no-wrap q-pa-sm">
           <div class="row no-wrap full-width">
             <div class="col-2">
-              <q-avatar :style="avatarColors[p.provider_id % avatarColors.length]" class="">
-                <img v-if="p.provider_photo" :src="p.provider_photo" style="object-fit:cover;border-radius:50%;" />
-                <span v-else>{{ p.provider_name?.slice(0,1).toUpperCase() ?? '—' }}</span>
+              <q-avatar
+                class=""
+                :style="avatarColors[p.provider_id % avatarColors.length]"
+              >
+                <img
+                  v-if="p.provider_photo"
+                  :src="p.provider_photo"
+                  style="object-fit: cover; border-radius: 50%"
+                />
+
+                <span v-else>
+                  {{ p.provider_name?.slice(0, 1).toUpperCase() ?? "—" }}
+                </span>
               </q-avatar>
             </div>
 
             <div class="col-10 row">
               <div class="column col-9 justify-between">
-                <span class="text-provider-close-name font12 fontmedium">{{ getFirstName(p.provider_name) }}</span>
-                <span class="text-provider-close-region font9 fontmedium">{{ p.district }}</span>
+                <span class="text-provider-close-name font12 fontmedium">
+                  {{ getFirstName(p.provider_name) }}
+                </span>
+
+                <span
+                  v-if="getProviderGenderLabel(p, t)"
+                  class="font9 fontmedium text-grey-7"
+                >
+                  {{ getProviderGenderLabel(p, t) }}
+                </span>
+
+                <span class="text-provider-close-region font9 fontmedium">
+                  {{ p.district }}
+                </span>
+
                 <div class="row items-center justify-between q-pr-lg">
                   <div class="row items-center">
-                    <q-icon name="mdi-star" color="warning" size="12px" />
+                    <q-icon
+                      color="warning"
+                      name="mdi-star"
+                      size="12px"
+                    />
+
                     <span class="text-provider-close-rating font9 fontmedium">
-                      {{ p.average_rating != null ? (Number(p.average_rating).toFixed(1) + ' (' + (p.total_reviews ?? 0) + ')') : ('(' + (p.total_reviews ?? 0) + ')') }}
+                      {{
+                        p.average_rating != null
+                          ? Number(p.average_rating).toFixed(1) +
+                            " (" +
+                            (p.total_reviews ?? 0) +
+                            ")"
+                          : "(" + (p.total_reviews ?? 0) + ")"
+                      }}
                     </span>
                   </div>
+
                   <div class="row items-center">
-                    <q-icon name="mdi-broom" color="secondary" size="16px" />
-                    <span class="text-provider-close-jobs font9 fontmedium">{{ p.total_services ?? 0 }}</span>
+                    <q-icon
+                      color="secondary"
+                      name="mdi-broom"
+                      size="16px"
+                    />
+
+                    <span class="text-provider-close-jobs font9 fontmedium">
+                      {{ p.total_services ?? 0 }}
+                    </span>
                   </div>
+
                   <div class="row items-center">
-                    <q-icon name="mdi-map-marker-outline" color="text" size="16px" />
-                    <span class="text-provider-close-jobs font9 fontmedium">{{ formatDistance(p.distance_km) }}</span>
+                    <q-icon
+                      color="text"
+                      name="mdi-map-marker-outline"
+                      size="16px"
+                    />
+
+                    <span class="text-provider-close-jobs font9 fontmedium">
+                      {{ formatDistance(p.distance_km) }}
+                    </span>
                   </div>
                 </div>
               </div>
 
-              <div class="column col-3 justify-between text-center items-center">
+              <div
+                class="column col-3 justify-between text-center items-center"
+              >
                 <span class="text-provider-close-price font12 fontmedium">
                   {{ showCorrectValues(p) }}
                 </span>
+
                 <div class="full-width">
                   <q-btn
-                    unelevated rounded no-caps
                     color="primary"
-                    size="sm"
+                    no-caps
                     padding="3px 12px"
+                    rounded
+                    size="sm"
+                    unelevated
                     :label="$t('dashboard_client.providers_close.schedule')"
                     @click="goToScheduling(p)"
                   />
@@ -71,77 +146,83 @@
 </template>
 
 <script setup>
-import { useQuasar } from 'quasar';
-import { formatCurrency,getFirstName } from 'src/helpers/utils';
-import SchedulingDialog from 'src/pages/search/components/SchedulingDialog.vue';
-import { ref } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { avatarColors } from "src/helpers/avatarColors";
+import { formatCurrency, getFirstName } from "src/helpers/utils";
+import { getProviderGenderLabel } from "src/helpers/provider";
+import { ref } from "vue";
+import { useI18n } from "vue-i18n";
+import { useQuasar } from "quasar";
+
+import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 
 defineProps({ data: { type: Array, default: () => [] } });
 
-const $q = useQuasar();
 const { t } = useI18n();
+
+const $q = useQuasar();
+
 const currentPeriodType = ref(8);
+
 const periodTypeMap = ref({
-  2: 'daily_price_2h',
-  4: 'daily_price_4h',
-  6: 'daily_price_6h',
-  8: 'daily_price_8h',
+  2: "daily_price_2h",
+  4: "daily_price_4h",
+  6: "daily_price_6h",
+  8: "daily_price_8h",
 });
 
 const formatDistance = (distance) => {
-  if (distance === null || distance === undefined || distance === '') return '—';
+  if (distance === null || distance === undefined || distance === "")
+    return "—";
 
   const numericDistance = Number(distance);
-  if (!Number.isFinite(numericDistance)) return '—';
 
-  return `${numericDistance.toLocaleString('pt-BR', { maximumFractionDigits: 1 })} km`;
+  if (!Number.isFinite(numericDistance)) return "—";
+
+  return `${numericDistance.toLocaleString("pt-BR", { maximumFractionDigits: 1 })} km`;
 };
 
-const showCorrectValues = (p) => {
-  const priceKey = periodTypeMap.value[currentPeriodType.value];
-  const price = priceKey ? p[priceKey] : null;
+const setPeriodTypePrevious = () => {
+  const previousPeriod = currentPeriodType.value - 2;
 
-  return price !== null && price !== undefined
-    ? formatCurrency(price)
-    : t('dashboard_client.providers_close.no_price');
+  if (periodTypeMap.value[previousPeriod]) {
+    currentPeriodType.value = previousPeriod;
+  }
+};
+
+const setPeriodTypeNext = () => {
+  const nextPeriod = currentPeriodType.value + 2;
+
+  if (periodTypeMap.value[nextPeriod]) {
+    currentPeriodType.value = nextPeriod;
+  }
 };
 
 const showCorrectLabels = () => {
   switch (currentPeriodType.value) {
     case 8:
-      return t('dashboard_client.providers_close.until_8h');
+      return t("dashboard_client.providers_close.until_8h");
     case 6:
-      return t('dashboard_client.providers_close.until_6h');
+      return t("dashboard_client.providers_close.until_6h");
     case 4:
-      return t('dashboard_client.providers_close.until_4h');
+      return t("dashboard_client.providers_close.until_4h");
     case 2:
-      return t('dashboard_client.providers_close.until_2h');
+      return t("dashboard_client.providers_close.until_2h");
     default:
-      return '';
+      return "";
   }
 };
 
-const setPeriodTypePrevious = () => {
-  const previousPeriod = currentPeriodType.value - 2;
-  if (periodTypeMap.value[previousPeriod]) {
-    currentPeriodType.value = previousPeriod;
-  }
-};
+const showCorrectValues = (p) => {
+  const priceKey = periodTypeMap.value[currentPeriodType.value];
 
-const setPeriodTypeNext = () => {
-  const nextPeriod = currentPeriodType.value + 2;
-  if (periodTypeMap.value[nextPeriod]) {
-    currentPeriodType.value = nextPeriod;
-  }
+  const price = priceKey ? p[priceKey] : null;
+
+  return price !== null && price !== undefined
+    ? formatCurrency(price)
+    : t("dashboard_client.providers_close.no_price");
 };
 
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-];
+//
 
 const goToScheduling = (provider) => {
   $q.dialog({
@@ -151,5 +232,4 @@ const goToScheduling = (provider) => {
 };
 </script>
 
-<style scoped lang="scss">
-</style>
+<style scoped lang="scss"></style>

+ 196 - 69
src/components/dashboard/DashboardTodaySchedules.vue

@@ -5,96 +5,204 @@
         <q-card
           v-for="item in data"
           :key="item.id"
-          class="today-card card-border shadow-card bg-surface"
           :flat="false"
+          class="today-card card-border shadow-card bg-surface"
         >
           <q-card-section class="q-pa-md">
             <div class="row no-wrap items-start q-mb-xs">
-              <q-avatar size="40px" class="flex-shrink-0 q-mr-sm">
-                <img v-if="item.provider_photo" :src="item.provider_photo" />
-                <span v-else :style="avatarColors[item.id % avatarColors.length]" class="full-width full-height flex flex-center" style="border-radius:50%;">
-                  {{ item.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}
+              <q-avatar
+                class="flex-shrink-0 q-mr-sm"
+                size="40px"
+              >
+                <img
+                  v-if="item.provider_photo"
+                  :src="item.provider_photo"
+                />
+
+                <span
+                  v-else
+                  :style="avatarColors[item.id % avatarColors.length]"
+                  class="full-width full-height flex flex-center"
+                  style="border-radius: 50%"
+                >
+                  {{ item.provider_name?.slice(0, 2).toUpperCase() ?? "??" }}
                 </span>
               </q-avatar>
+
               <div class="col-7 column no-wrap overflow-hidden justify-center">
                 <span class="font12 fontmedium text-text leading-tight">
-                  <template v-if="cardState(item) === 'awaiting_code'">{{ $t('dashboard_client.today_schedules.start_with') }}</template>
-                  <template v-else-if="cardState(item) === 'in_progress'">{{ $t('dashboard_client.today_schedules.started_by') }}</template>
-                  <template v-else>{{ $t('dashboard_client.today_schedules.finished_by') }}</template>
-                  <span class="fontbold">  {{ ' ' + (getFirstName(item.provider_name) || '—') }}</span>
+                  <template v-if="cardState(item) === 'awaiting_code'">
+                    {{ $t("dashboard_client.today_schedules.start_with") }}
+                  </template>
+
+                  <template v-else-if="cardState(item) === 'in_progress'">
+                    {{ $t("dashboard_client.today_schedules.started_by") }}
+                  </template>
+
+                  <template v-else-if="cardState(item) === 'finished'">
+                    {{ $t("dashboard_client.today_schedules.finished_by") }}
+                  </template>
+
+                  <template v-else-if="cardState(item) === 'cancelled'">
+                    {{ $t("dashboard_client.today_schedules.cancelled") }}
+                  </template>
+
+                  <span class="fontbold">
+                    {{ " " + (getFirstName(item.provider_name) || "—") }}
+                  </span>
                 </span>
+
                 <div class="row items-center q-mt-xs no-wrap">
-                  <q-icon name="mdi-clock-outline" size="13px" class="q-mr-xs gradient-diarista" />
+                  <q-icon
+                    class="q-mr-xs gradient-diarista"
+                    name="mdi-clock-outline"
+                    size="13px"
+                  />
+
                   <span class="font9 fontregular text-grey-6">
-                    {{ $t('dashboard_client.next_schedules.from') }} {{ item.start_time?.slice(0, 5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ item.end_time?.slice(0, 5) }}
+                    {{ $t("dashboard_client.next_schedules.from") }}
+                    {{ item.start_time?.slice(0, 5) }}
+                    {{ $t("dashboard_client.next_schedules.to") }}
+                    {{ item.end_time?.slice(0, 5) }}
                   </span>
                 </div>
               </div>
-              <div class="flex-shrink-0 q-ml-sm column items-center justify-start">
+
+              <div
+                class="flex-shrink-0 q-ml-sm column items-center justify-start"
+              >
                 <template v-if="cardState(item) === 'awaiting_code'">
                   <div class="column items-center">
-                    <span class="font9 fontbold text-primary q-mb-xs">{{ $t('dashboard_client.today_schedules.code_label') }}</span>
-                    <div class="code-pill fontbold font10 bg-primary">{{ item.code }}</div>
+                    <span class="font9 fontbold text-primary q-mb-xs">
+                      {{ $t("dashboard_client.today_schedules.code_label") }}
+                    </span>
+
+                    <div class="code-pill fontbold font10 bg-primary">
+                      {{ item.code }}
+                    </div>
                   </div>
                 </template>
 
                 <template v-else-if="cardState(item) === 'in_progress'">
                   <div class="column items-center">
                     <div class="clock-badge">
-                      <q-icon name="mdi-clock-outline" size="15px" color="white" />
+                      <q-icon
+                        color="white"
+                        name="mdi-clock-outline"
+                        size="15px"
+                      />
                     </div>
-                    <span class="font10 fontregular badge-status-text text-primary q-mt-xs">
-                      {{ $t('dashboard_client.today_schedules.in_progress') }}
+
+                    <span
+                      class="font10 fontregular badge-status-text text-primary q-mt-xs"
+                    >
+                      {{ $t("dashboard_client.today_schedules.in_progress") }}
                     </span>
                   </div>
                 </template>
 
-                <template v-else>
-                  <div v-if="item.client_reviewed" class="rate-btn reviewed-badge">
-                    <q-icon name="mdi-star" size="12px" class="q-mr-xs" />
-                    {{ $t('dashboard_client.schedule_rating.reviewed_badge') }}
+                <template v-else-if="cardState(item) === 'finished'">
+                  <div
+                    v-if="item.client_reviewed"
+                    class="rate-btn reviewed-badge"
+                  >
+                    <q-icon
+                      class="q-mr-xs"
+                      name="mdi-star"
+                      size="12px"
+                    />
+
+                    {{ $t("dashboard_client.schedule_rating.reviewed_badge") }}
                   </div>
+
                   <q-btn
                     v-else
-                    unelevated no-caps
                     class="rate-btn"
+                    no-caps
+                    unelevated
                     @click.stop="emit('rate', item)"
                   >
                     <div class="column items-center">
-                      <q-icon name="mdi-star-outline" size="12px" class="q-mr-xs" />
-                      {{ $t('dashboard_client.today_schedules.rate_btn') }}
+                      <q-icon
+                        class="q-mr-xs"
+                        name="mdi-star-outline"
+                        size="12px"
+                      />
+
+                      {{ $t("dashboard_client.today_schedules.rate_btn") }}
                     </div>
                   </q-btn>
                 </template>
+
+                <template v-else-if="cardState(item) === 'cancelled'">
+                  <div class="text-negative font10 fontbold text-center">
+                    {{ $t("dashboard_client.today_schedules.cancelled") }}
+                  </div>
+                </template>
               </div>
             </div>
 
-
             <div class="progress-track q-mb-sm">
-              <div class="progress-fill" :class="cardState(item) === 'finished' ? 'progress-fill--finished' : ''" :style="{ width: progressByState(item) + '%' }" />
+              <div
+                class="progress-fill"
+                :class="cardState(item) === 'finished' ? 'progress-fill--finished' : ''"
+                :style="{ width: progressByState(item) + '%' }"
+              />
             </div>
+
             <div class="row items-center no-wrap">
               <template v-if="cardState(item) !== 'finished'">
                 <q-btn
-                  flat no-caps dense
-                  :label="$t('dashboard_client.today_schedules.help_btn')"
+                  class="flex-shrink-0"
                   color="primary"
+                  dense
+                  flat
+                  no-caps
                   size="xs"
-                  class="flex-shrink-0"
+                  :label="$t('dashboard_client.today_schedules.help_btn')"
                   @click.stop="openHelp"
                 />
+
                 <q-space />
+
                 <template v-if="cardState(item) === 'awaiting_code'">
-                  <span class="font9 fontregular text-grey-6 col ellipsis text-right">
-                    <q-icon name="mdi-map-marker-outline" size="13px" color="grey-6" class="q-mr-xs flex-shrink-0" />
-                    {{ [item.address?.address, item.address?.number, item.address?.district].filter(Boolean).join(', ') || '—' }}
+                  <span
+                    class="font9 fontregular text-grey-6 col ellipsis text-right"
+                  >
+                    <q-icon
+                      class="q-mr-xs flex-shrink-0"
+                      color="grey-6"
+                      name="mdi-map-marker-outline"
+                      size="13px"
+                    />
+
+                    {{
+                      [
+                        item.address?.address,
+                        item.address?.number,
+                        item.address?.district,
+                      ]
+                        .filter(Boolean)
+                        .join(", ") || "—"
+                    }}
                   </span>
                 </template>
+
                 <template v-else>
-                  <q-icon name="mdi-clock-outline" size="13px" class="q-mr-xs flex-shrink-0 gradient-diarista" />
-                  <span class="font9 fontregular text-grey-6 text-right text-no-wrap">
-                    {{ $t('dashboard_client.today_schedules.end_time_label') }} 
-                    <span class="gradient-diarista">{{ item.end_time?.slice(0, 5) }}</span>
+                  <q-icon
+                    class="q-mr-xs flex-shrink-0 gradient-diarista"
+                    name="mdi-clock-outline"
+                    size="13px"
+                  />
+
+                  <span
+                    class="font9 fontregular text-grey-6 text-right text-no-wrap"
+                  >
+                    {{ $t("dashboard_client.today_schedules.end_time_label") }}
+
+                    <span class="gradient-diarista">
+                      {{ item.end_time?.slice(0, 5) }}
+                    </span>
                   </span>
                 </template>
               </template>
@@ -107,44 +215,59 @@
 </template>
 
 <script setup>
-import { useQuasar } from 'quasar'
-import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
-import {getFirstName} from 'src/helpers/utils'
+import { avatarColors } from "src/helpers/avatarColors";
+import { getFirstName } from "src/helpers/utils";
+import { useQuasar } from "quasar";
+
+import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 
-defineProps({ data: { type: Array, default: () => [] } })
-const emit = defineEmits(['rate'])
 
-const $q = useQuasar()
+defineProps({ data: { type: Array, default: () => [] } });
 
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-]
+const emit = defineEmits(["rate"]);
+
+const $q = useQuasar();
 
 const cardState = (item) => {
-  if (!item.code_verified) return 'awaiting_code'
-  const [h, m] = (item.end_time || '23:59').slice(0, 5).split(':').map(Number)
-  const endTime = new Date()
-  endTime.setHours(h, m, 0, 0)
-  return new Date() >= endTime ? 'finished' : 'in_progress'
-}
+  switch (item.status) {
+    case "finished":
+      return "finished";
+
+    case "started":
+      return "in_progress";
+
+    case "accepted":
+    case "paid":
+      return "awaiting_code";
+
+    case "cancelled":
+      return "cancelled";
+
+    default:
+      return "awaiting_code";
+  }
+};
 
 const progressByState = (item) => {
-  const state = cardState(item)
-  if (state === 'awaiting_code') return 60
-  if (state === 'in_progress')   return 80
-  return 100
-}
+  const state = cardState(item);
+
+  if (state === "awaiting_code") return 60;
+  if (state === "in_progress")   return 80;
+
+  return 100;
+};
+
+//
 
 const openHelp = () => {
-  $q.dialog({ component: ProfileHelpDialog })
-}
+  $q.dialog({ component: ProfileHelpDialog });
+};
 </script>
 
 <style scoped lang="scss">
-.scroll-wrapper { overflow: hidden; }
+.scroll-wrapper {
+  overflow: hidden;
+}
 .scroll-track {
   display: flex;
   flex-direction: row;
@@ -153,8 +276,13 @@ const openHelp = () => {
   overscroll-behavior-x: contain;
   scroll-snap-type: x proximity;
   padding-bottom: 8px;
-  &::-webkit-scrollbar { display: none; }
-  &::after { content: ''; flex: 0 0 1px; }
+  &::-webkit-scrollbar {
+    display: none;
+  }
+  &::after {
+    content: "";
+    flex: 0 0 1px;
+  }
 }
 
 .today-card {
@@ -174,7 +302,7 @@ const openHelp = () => {
   width: 26px;
   height: 26px;
   border-radius: 50%;
-  background: linear-gradient(135deg, #8B5CF6, #EC4899);
+  background: linear-gradient(135deg, #8b5cf6, #ec4899);
   display: flex;
   align-items: center;
   justify-content: center;
@@ -185,7 +313,7 @@ const openHelp = () => {
 }
 
 .rate-btn {
-  background: #EC4899;
+  background: #ec4899;
   color: white;
   white-space: nowrap;
   border-radius: 10px !important;
@@ -206,7 +334,7 @@ const openHelp = () => {
 .progress-track {
   width: 100%;
   height: 5px;
-  background: #E2E8F0;
+  background: #e2e8f0;
   border-radius: 3px;
   overflow: hidden;
 }
@@ -214,12 +342,11 @@ const openHelp = () => {
 .progress-fill {
   height: 100%;
   border-radius: 3px;
-  background: linear-gradient(90deg, #8B5CF6, #EC4899);
+  background: linear-gradient(90deg, #8b5cf6, #ec4899);
   transition: width 0.4s ease;
 
   &--finished {
     background: #22c55e;
   }
 }
-
 </style>

+ 142 - 93
src/components/dashboard/NextSchedulesDetailsDialog.vue

@@ -1,21 +1,42 @@
 <template>
-  <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="next-schedule-dialog-card bg-surface" :flat="false">
-
+  <q-dialog
+    ref="dialogRef"
+    @hide="onDialogHide"
+  >
+    <q-card
+      class="next-schedule-dialog-card bg-surface"
+      :flat="false"
+    >
       <q-card-section class="column items-center q-pt-lg q-pb-sm">
-        <q-avatar size="80px" :style="avatarStyle" class="fontbold text-h5 q-mb-sm">
-          <img v-if="details?.provider_photo" :src="details.provider_photo" />
+        <q-avatar
+          class="fontbold text-h5 q-mb-sm"
+          size="80px"
+          :style="avatarStyle"
+        >
+          <img
+            v-if="details?.provider_photo"
+            :src="details.provider_photo"
+          />
 
           <span v-else>
-            {{ schedule.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}
+            {{ schedule.provider_name?.slice(0, 2).toUpperCase() ?? "??" }}
           </span>
         </q-avatar>
 
         <div class="provider-name font16 fontbold">
-          {{ getFirstName(schedule.provider_name) || '—' }}
+          {{ getFirstName(schedule.provider_name) || "—" }}
 
-          <span v-if="providerAge !== null" class="text-grey-6">
-            {{ '(' + providerAge + ' ' + $t('dashboard_client.next_schedules.provider_age_unit') + ')' }}
+          <span
+            v-if="providerAge !== null"
+            class="text-grey-6"
+          >
+            {{
+              "(" +
+              providerAge +
+              " " +
+              $t("dashboard_client.next_schedules.provider_age_unit") +
+              ")"
+            }}
           </span>
         </div>
 
@@ -23,8 +44,16 @@
           {{ schedule.address?.district }}
         </div>
 
-        <div v-if="schedule.address" class="font9 fontmedium text-grey-7 q-mt-xs">
-          <q-icon name="mdi-map-marker" color="text" size="14px" class="q-mr-xs" />
+        <div
+          v-if="schedule.address"
+          class="font9 fontmedium text-grey-7 q-mt-xs"
+        >
+          <q-icon
+            class="q-mr-xs"
+            color="text"
+            name="mdi-map-marker"
+            size="14px"
+          />
 
           {{ formatAddress(schedule.address) }}
         </div>
@@ -35,36 +64,31 @@
       <q-card-section class="q-py-sm">
         <template v-if="loadingDetails">
           <div class="row justify-center q-py-sm">
-            <q-spinner-dots color="primary" size="24px" />
+            <q-spinner-dots
+              color="primary"
+              size="24px"
+            />
           </div>
         </template>
 
         <template v-else>
           <div
-            v-for="sp in details?.specialities"
-            :key="sp.id"
+            v-for="service in servicesList"
+            :key="service.label"
             class="row col-12 items-center q-gutter-x-sm q-mb-xs text-center"
           >
             <div class="full-width">
               <q-icon
-                :name="sp.has_speciality ? 'mdi-check' : 'mdi-close'"
-                color="primary"
+                color="secondary"
+                name="mdi-check"
                 size="12px"
               />
 
               <span class="text-grey-8 font10 fontmedium q-pl-sm">
-                {{ sp.description }}
+                {{ service.label }}
               </span>
             </div>
           </div>
-
-          <div v-if="!details?.specialities?.length" class="row items-center q-gutter-x-sm">
-            <q-icon color="secondary" name="mdi-check" size="16px" />
-
-            <span class="text-grey-8 font10 fontmedium">
-              {{ $t('dashboard_client.next_schedules.default_service') }}
-            </span>
-          </div>
         </template>
       </q-card-section>
 
@@ -73,7 +97,7 @@
       <q-card-section class="q-py-md q-px-lg font12 fontmedium">
         <div class="detail-row">
           <span class="detail-label text-primary q-pr-sm">
-            {{ $t('dashboard_client.pending_schedules.detail_date') }}
+            {{ $t("dashboard_client.pending_schedules.detail_date") }}
           </span>
 
           <span class="detail-value">
@@ -83,24 +107,26 @@
 
         <div class="detail-row">
           <span class="detail-label text-primary q-pr-sm">
-            {{ $t('dashboard_client.pending_schedules.detail_time') }}
+            {{ $t("dashboard_client.pending_schedules.detail_time") }}
           </span>
 
           <span class="detail-value">
-            {{ schedule.start_time?.slice(0, 5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ schedule.end_time?.slice(0, 5) }}
+            {{ schedule.start_time?.slice(0, 5) }}
+            {{ $t("dashboard_client.next_schedules.to") }}
+            {{ schedule.end_time?.slice(0, 5) }}
           </span>
         </div>
 
         <div class="detail-row-total font16 fontbold">
           <span class="detail-label text-primary q-pr-sm">
-            {{ $t('dashboard_client.pending_schedules.detail_total') }}
+            {{ $t("dashboard_client.pending_schedules.detail_total") }}
           </span>
 
           <span class="total-value">
             {{ formatCurrency(total) }}
           </span>
         </div>
-        
+
         <q-separator class="q-my-sm divisoria-tracejada" />
       </q-card-section>
 
@@ -142,111 +168,134 @@
 </template>
 
 <script setup>
-import { computed, onMounted, ref } from 'vue'
-import { formatAddress, getFirstName,formatCurrency } from 'src/helpers/utils';
-import { getScheduleClienteDetails } from 'src/api/dashboard'
-import { getSchedulePaymentType, getScheduleTotalWithPlatformFee } from 'src/helpers/paymentPlatformFees'
-import { useDialogPluginComponent, useQuasar } from 'quasar'
-import { useI18n } from 'vue-i18n'
-import { usePaymentPlatformFees } from 'src/composables/usePaymentPlatformFees'
+import { avatarColors } from "src/helpers/avatarColors";
+import { computed, onMounted, ref } from "vue";
+import { formatAddress, formatCurrency, getFirstName } from "src/helpers/utils";
+import { getScheduleClienteDetails } from "src/api/dashboard";
+
+import {
+  getSchedulePaymentType,
+  getScheduleTotalWithPlatformFee,
+} from "src/helpers/paymentPlatformFees";
+
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 
-import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
-import ScheduleCancelDialog from './ScheduleCancelDialog.vue'
+import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
+import ScheduleCancelDialog from "./ScheduleCancelDialog.vue";
 
 const props = defineProps({
-  schedule: { type: Object, required: true }
-})
+  schedule: { type: Object, required: true },
+});
 
-defineEmits([...useDialogPluginComponent.emits])
+defineEmits([...useDialogPluginComponent.emits]);
 
-const { t } = useI18n()
-const $q    = useQuasar()
+const { t } = useI18n();
+const $q = useQuasar();
 
-const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent()
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
 
-const { platformFees, loadPlatformFees } = usePaymentPlatformFees()
+const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
 
-const details        = ref(null)
-const loadingDetails = ref(true)
+const details = ref(null);
+const loadingDetails = ref(true);
+
+const avatarStyle = computed(
+  () => avatarColors[props.schedule.id % avatarColors.length],
+);
 
 const parseLocalDate = (dateStr) => {
-  if (!dateStr) return null
+  if (!dateStr) return null;
 
-  const s = String(dateStr)
+  const s = String(dateStr);
 
-  const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
+  const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
 
-  if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3])
+  if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
 
-  const dmy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/)
+  const dmy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
 
-  if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1])
+  if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
 
-  return null
-}
+  return null;
+};
 
 const fullDateLabel = computed(() => {
-  if (props.schedule.formatted_date) return props.schedule.formatted_date
+  if (props.schedule.formatted_date) return props.schedule.formatted_date;
 
-  const d = parseLocalDate(props.schedule.date)
+  const d = parseLocalDate(props.schedule.date);
 
-  if (!d) return props.schedule.date ?? ''
+  if (!d) return props.schedule.date ?? "";
 
-  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' })
-})
+  return d.toLocaleDateString("pt-BR", {
+    day: "2-digit",
+    month: "long",
+    year: "numeric",
+  });
+});
 
 const providerAge = computed(() => {
-  if (!details.value?.provider_birth_date) return null
+  if (!details.value?.provider_birth_date) return null;
+
+  const birth = new Date(details.value.provider_birth_date);
 
-  const birth = new Date(details.value.provider_birth_date)
+  const today = new Date();
 
-  const today = new Date()
+  let age = today.getFullYear() - birth.getFullYear();
 
-  let age = today.getFullYear() - birth.getFullYear()
+  const m = today.getMonth() - birth.getMonth();
 
-  const m = today.getMonth() - birth.getMonth()
+  if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--;
 
-  if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--
+  return age;
+});
 
-  return age
-})
+const servicesList = computed(() => {
+  const scheduleType =
+    details.value?.schedule_type ?? props.schedule.schedule_type;
 
-const total = computed(() => getScheduleTotalWithPlatformFee(
-  props.schedule,
-  getSchedulePaymentType(props.schedule),
-  platformFees.value
-))
+  const specialities = details.value?.specialities ?? [];
+
+  if (scheduleType === "custom" && specialities.length) {
+    return specialities.map((speciality) => ({
+      label: speciality.description,
+    }));
+  }
 
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-]
-const avatarStyle = computed(() => avatarColors[props.schedule.id % avatarColors.length])
+  return [{ label: t("dashboard_client.next_schedules.default_service") }];
+});
+
+const total = computed(() =>
+  getScheduleTotalWithPlatformFee(
+    props.schedule,
+    getSchedulePaymentType(props.schedule),
+    platformFees.value,
+  ),
+);
 
 const openCancelDialog = () => {
   $q.dialog({
-    component: ScheduleCancelDialog, componentProps: { schedule: props.schedule }
-  })
-}
+    component: ScheduleCancelDialog,
+    componentProps: { schedule: props.schedule },
+  });
+};
 
 const openHelp = () => {
-  $q.dialog({ component: ProfileHelpDialog })
-}
+  $q.dialog({ component: ProfileHelpDialog });
+};
 
 onMounted(async () => {
   try {
-    details.value = await getScheduleClienteDetails(props.schedule.id)
+    details.value = await getScheduleClienteDetails(props.schedule.id);
 
-    loadPlatformFees().catch(() => {})
+    loadPlatformFees().catch(() => {});
   } catch {
-    $q.notify({ message: t('http.errors.failed'), color: 'negative' })
+    $q.notify({ message: t("http.errors.failed"), color: "negative" });
   } finally {
-    loadingDetails.value = false
+    loadingDetails.value = false;
   }
-})
-
+});
 </script>
 
 <style scoped lang="scss">
@@ -258,7 +307,7 @@ onMounted(async () => {
 }
 
 .provider-name {
-  color: #8B5CF6;
+  color: #8b5cf6;
 }
 
 .detail-row {

+ 124 - 79
src/components/dashboard/ScheduleAcceptedDialog.vue

@@ -1,24 +1,33 @@
 <template>
-  <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="accepted-dialog-card bg-surface" :flat="false">
-
+  <q-dialog
+    ref="dialogRef"
+    @hide="onDialogHide"
+  >
+    <q-card
+      class="accepted-dialog-card bg-surface"
+      :flat="false"
+    >
       <q-card-section class="column items-center q-pt-lg q-pb-sm">
-        <q-avatar size="80px" :style="avatarStyle" class="text-h5 q-mb-sm">
-          {{ schedule.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}
+        <q-avatar
+          class="text-h5 q-mb-sm"
+          size="80px"
+          :style="avatarStyle"
+        >
+          {{ displayProviderName.slice(0, 2).toUpperCase() || "??" }}
         </q-avatar>
 
         <div class="font16 fontbold provider-name">
-          {{ getFirstName(schedule.provider_name) || '—' }}
+          {{ getFirstName(displayProviderName) || "—" }}
         </div>
 
         <div class="font14 fontmedium text-text">
-          {{ schedule.address?.district || '' }}
+          {{ displayDistrict }}
         </div>
       </q-card-section>
 
       <q-card-section class="text-center q-pt-xs q-pb-md">
         <div class="accepted-title font16 fontbold">
-          {{ $t('dashboard_client.pending_schedules.accepted_title') }}
+          {{ $t("dashboard_client.pending_schedules.accepted_title") }}
         </div>
       </q-card-section>
 
@@ -27,36 +36,31 @@
       <q-card-section class="q-py-md q-px-lg">
         <div class="detail-row">
           <span class="text-primary font14 fontmedium">
-            {{ $t('dashboard_client.pending_schedules.detail_date') }}
+            {{ $t("dashboard_client.pending_schedules.detail_date") }}
           </span>
 
           <span class="detail-value">
-            {{ formattedDate }}
+            {{ displayDate }}
           </span>
         </div>
+
         <div class="detail-row">
           <span class="text-primary font14 fontmedium">
-            {{ $t('dashboard_client.pending_schedules.detail_time') }}
+            {{ $t("dashboard_client.pending_schedules.detail_time") }}
           </span>
 
           <span class="detail-valued text-text">
-            {{ schedule.start_time?.slice(0, 5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ schedule.end_time?.slice(0, 5) }}
+            {{ displayStartTime }}
+            {{ $t("dashboard_client.next_schedules.to") }}
+            {{ displayEndTime }}
           </span>
         </div>
 
-        <div v-if="hasCartDiscount" class="detail-row">
-          <span class="text-primary font14 fontmedium">
-            {{ $t('dashboard_client.pending_schedules.detail_total') }}
-          </span>
-
-          <span class="total-value font14 fontbold">
-            {{ formatCurrency(pixTotal) }}
-          </span>
-        </div>
-
-        <div v-if="!hasCartDiscount" class="detail-row">
+        <div class="detail-row">
           <span class="text-primary font14 fontmedium">
-            {{ $t('dashboard_client.pending_schedules.detail_pix_total') }}
+            {{ hasServicePackageDiscount
+                ? $t("dashboard_client.pending_schedules.detail_package_total")
+                : $t("dashboard_client.pending_schedules.detail_pix_total") }}
           </span>
 
           <span class="total-value font14 fontbold">
@@ -64,9 +68,12 @@
           </span>
         </div>
 
-        <div v-if="!hasCartDiscount && pixDiscount > 0" class="detail-row discount-row">
+        <div
+          v-if="pixDiscount > 0"
+          class="detail-row discount-row"
+        >
           <span class="font13 fontmedium">
-            {{ $t('dashboard_client.pending_schedules.detail_pix_discount') }}
+            {{ $t("dashboard_client.pending_schedules.detail_pix_discount") }}
           </span>
 
           <span class="font13 fontbold">
@@ -74,9 +81,11 @@
           </span>
         </div>
 
-        <div v-if="!hasCartDiscount" class="detail-row">
+        <div class="detail-row">
           <span class="text-primary font14 fontmedium">
-            {{ $t('dashboard_client.pending_schedules.detail_credit_card_total') }}
+            {{
+              $t("dashboard_client.pending_schedules.detail_credit_card_total")
+            }}
           </span>
 
           <span class="total-value font14 fontbold">
@@ -105,89 +114,125 @@
           @click="onDialogHide"
         />
       </q-card-section>
-
     </q-card>
   </q-dialog>
 </template>
 
 <script setup>
-import { computed, onMounted } from 'vue'
-import { formatCurrency, getFirstName } from 'src/helpers/utils'
-import { getSchedulePlatformFeeRate, scheduleUsesCartDiscount } from 'src/helpers/paymentPlatformFees'
-import { useDialogPluginComponent, useQuasar } from 'quasar'
-import { usePaymentStore } from 'src/stores/payment'
-import { usePaymentPlatformFees } from 'src/composables/usePaymentPlatformFees'
-import SchedulePaymentDialog from './SchedulePaymentDialog.vue'
-import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
+import { avatarColors } from "src/helpers/avatarColors";
+import { computed, onMounted } from "vue";
+import { formatCurrency, getFirstName } from "src/helpers/utils";
+
+import {
+  getSchedulePlatformFeeRate,
+  scheduleUsesServicePackageDiscount,
+} from "src/helpers/paymentPlatformFees";
+
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
+import { usePaymentStore } from "src/stores/payment";
+
+import SchedulePaymentDialog from "./SchedulePaymentDialog.vue";
+import SchedulePaymentPixDialog from "./SchedulePaymentPixDialog.vue";
 
 const props = defineProps({
-  schedule: { type: Object, required: true }
-})
+  servicePackage: { type: Object, required: true },
+});
 
-defineEmits([...useDialogPluginComponent.emits])
+defineEmits([...useDialogPluginComponent.emits]);
 
-const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent()
+const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 
-const $q = useQuasar()
+const $q = useQuasar();
 
-const paymentStore = usePaymentStore()
+const paymentStore = usePaymentStore();
 
-const { platformFees, loadPlatformFees } = usePaymentPlatformFees()
+const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
 
-const baseAmount = computed(() => Number(props.schedule.total_amount) || 0)
+const item = computed(() => props.servicePackage);
 
-const creditCardTotal = computed(() => parseFloat((baseAmount.value + platformFee('credit_card')).toFixed(2)))
+const displayProviderName = computed(() => {
+  return item.value.provider?.user?.name ?? item.value.provider_name ?? '';
+});
 
-const hasCartDiscount = computed(() => scheduleUsesCartDiscount(props.schedule))
+const displayDistrict = computed(() => {
+  return item.value.schedules?.[0]?.address?.district ?? item.value.address?.district ?? '';
+});
 
-const formattedDate = computed(() => {
-  if (props.schedule.formatted_date) return props.schedule.formatted_date
+const displayDate = computed(() => {
+  const src = item.value.schedules?.[0] ?? item.value;
+  if (!src) return '';
+  if (src.formatted_date) return src.formatted_date;
+  const raw = String(src.date || '');
+  const 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 raw = String(props.schedule.date || '')
+const displayStartTime = computed(() => {
+  return item.value.schedules?.[0]?.start_time?.slice(0, 5) ?? '';
+});
 
-  const m = raw.match(/^(\d{4})-(\d{2})-(\d{2})/)
+const displayEndTime = computed(() => {
+  return item.value.schedules?.[0]?.end_time?.slice(0, 5) ?? '';
+});
 
-  if (!m) return raw
+const avatarStyle = computed(
+  () => avatarColors[item.value.id % avatarColors.length],
+);
 
-  const d = new Date(+m[1], +m[2] - 1, +m[3])
+const baseAmount = computed(() => Number(item.value.total_amount) || 0);
 
-  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' })
-})
+const creditCardTotal = computed(() =>
+  parseFloat((baseAmount.value + platformFee("credit_card")).toFixed(2)),
+);
 
-const pixDiscount = computed(() => Math.max(0, parseFloat((creditCardTotal.value - pixTotal.value).toFixed(2))))
-const pixTotal    = computed(() => parseFloat((baseAmount.value + platformFee('pix')).toFixed(2)))
+const hasServicePackageDiscount = computed(() =>
+  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 feeRate = getSchedulePlatformFeeRate(props.schedule, paymentType, platformFees.value)
+  const feeRate = getSchedulePlatformFeeRate(
+    item.value,
+    paymentType,
+    platformFees.value,
+  );
 
-  return parseFloat((baseAmount.value * (feeRate ?? 0)).toFixed(2))
-}
+  return parseFloat((baseAmount.value * (feeRate ?? 0)).toFixed(2));
+};
 
 const onGoToPayment = () => {
-  const hasValidPixPayment = !!paymentStore.getValidPixPayment(props.schedule.id)
+  const validPixPayment = paymentStore.getValidPixPaymentForServicePackage(item.value.id);
+  const hasValidPixPayment = !!validPixPayment;
 
   $q.dialog({
-    component: hasValidPixPayment ? SchedulePaymentPixDialog : SchedulePaymentDialog,
+    component: hasValidPixPayment
+      ? SchedulePaymentPixDialog
+      : SchedulePaymentDialog,
     componentProps: {
-      schedule: props.schedule, ...(hasValidPixPayment ? { total: pixTotal.value } : {}),
+      servicePackage: item.value,
+      ...(hasValidPixPayment ? { total: pixTotal.value } : {}),
     },
   }).onOk(() => {
-    onDialogOK()
-  })
-}
-
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-]
-
-const avatarStyle = computed(() => avatarColors[props.schedule.id % avatarColors.length])
+    onDialogOK();
+  });
+};
 
 onMounted(() => {
-  loadPlatformFees().catch(() => {})
-})
+  loadPlatformFees().catch(() => {});
+});
 </script>
 
 <style scoped lang="scss">
@@ -200,11 +245,11 @@ onMounted(() => {
 
 .accepted-title {
   line-height: 1.3;
-  color: #8B5CF6;
+  color: #8b5cf6;
 }
 
 .provider-name {
-  color: #8B5CF6;
+  color: #8b5cf6;
 }
 
 .detail-row {

+ 31 - 15
src/components/dashboard/SchedulePaymentDialog.vue

@@ -3,7 +3,7 @@
     <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">
-        <q-btn icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
+        <q-btn class="header-back-btn" icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
         <q-space />
         <span class="font16 fontbold gradient-diarista">
           {{ $t('payment.title') }}
@@ -120,7 +120,7 @@ import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
 import SchedulePaymentProcessingDialog from './SchedulePaymentProcessingDialog.vue'
 
 const props = defineProps({
-  schedule: {
+  servicePackage: {
     type: Object,
     required: true,
   },
@@ -135,26 +135,33 @@ const store = userStore()
 const paymentStore = usePaymentStore()
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees()
 
+const item = computed(() => props.servicePackage)
+
 const selectedMethod = ref(null)
 const agreedToTerms = ref(false)
 const paymentMethods = ref([])
 const loadingCards = ref(false)
 
 const selectedPaymentType = computed(() => selectedMethod.value === 'pix' ? 'pix' : 'credit_card')
-const selectedTotal = computed(() => getScheduleTotalWithPlatformFee(props.schedule, selectedPaymentType.value, platformFees.value))
+const selectedTotal = computed(() => getScheduleTotalWithPlatformFee(item.value, selectedPaymentType.value, platformFees.value))
+
+const getAddress = () => {
+  return item.value.schedules?.[0]?.address ?? item.value.address ?? null;
+};
 
 const addressTypeLabel = computed(() => {
-  const type = props.schedule.address?.address_type
-  if (!type) return ''
-  return t(`profile.address.type.${type}`, type)
-})
+  const addr = getAddress();
+  const type = addr?.address_type;
+  if (!type) return '';
+  return t(`profile.address.type.${type}`, type);
+});
 
 const addressFullText = computed(() => {
-  const a = props.schedule.address
-  if (!a) return ''
-  const parts = [a.address, a.number, a.district].filter(Boolean)
-  return parts.join(', ')
-})
+  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 && agreedToTerms.value)
 
@@ -193,7 +200,10 @@ const openAddCard = () => {
 const openPixPayment = () => {
   $q.dialog({
     component: SchedulePaymentPixDialog,
-    componentProps: { schedule: props.schedule, total: selectedTotal.value },
+    componentProps: {
+      servicePackage: item.value,
+      total: selectedTotal.value,
+    },
   }).onOk(() => {
     onDialogOK()
   })
@@ -208,7 +218,11 @@ const onConfirm = () => {
 
   $q.dialog({
     component: SchedulePaymentProcessingDialog,
-    componentProps: { schedule: props.schedule, clientPaymentMethodId, total: selectedTotal.value },
+    componentProps: {
+      servicePackage: item.value,
+      clientPaymentMethodId,
+      total: selectedTotal.value,
+    },
   }).onOk(() => {
     onDialogOK()
   })
@@ -217,7 +231,9 @@ const onConfirm = () => {
 onMounted(() => {
   loadPlatformFees().catch(() => {})
 
-  if (paymentStore.getValidPixPayment(props.schedule.id)) {
+  const existingPix = paymentStore.getValidPixPaymentForServicePackage(item.value.id)
+
+  if (existingPix) {
     openPixPayment()
     return
   }

+ 21 - 11
src/components/dashboard/SchedulePaymentPixDialog.vue

@@ -3,7 +3,7 @@
     <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">
-        <q-btn icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
+        <q-btn class="header-back-btn" icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
         <q-space />
         <span class="font16 fontbold gradient-diarista">
           {{ $t('payment.pix_title') }}
@@ -110,11 +110,11 @@
 import { computed, ref, onMounted, onUnmounted } from 'vue'
 import { useDialogPluginComponent, useQuasar, copyToClipboard } from 'quasar'
 import { formatCurrency } from 'src/helpers/utils'
-import { getSchedulePixPayment, paySchedule } from 'src/api/payment'
+import { getServicePackagePix, payServicePackage } from 'src/api/payment'
 import { usePaymentStore } from 'src/stores/payment'
 
 const props = defineProps({
-  schedule: { type: Object, required: true },
+  servicePackage: { type: Object, required: true },
   total: { type: Number, required: true },
 })
 
@@ -124,6 +124,9 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginC
 const $q = useQuasar()
 const paymentStore = usePaymentStore()
 
+const item = computed(() => props.servicePackage)
+const itemId = computed(() => item.value.id)
+
 const payment = ref(null)
 const success = ref(false)
 const processing = ref(true)
@@ -168,21 +171,21 @@ const applyPaymentStatus = (nextPayment) => {
   if (nextPayment.status === 'paid') {
     success.value = true
     processing.value = false
-    paymentStore.clearPixPayment(props.schedule.id)
+    paymentStore.clearPixPaymentForServicePackage(itemId.value)
     stopPolling()
     return
   }
 
   if (['failed', 'cancelled'].includes(nextPayment.status)) {
     processing.value = false
-    paymentStore.clearPixPayment(props.schedule.id)
+    paymentStore.clearPixPaymentForServicePackage(itemId.value)
     stopPolling()
     $q.notify({ type: 'negative', message: nextPayment.failure_message || 'Pagamento Pix não confirmado.' })
     onDialogCancel()
     return
   }
 
-  paymentStore.setPixPayment(props.schedule.id, nextPayment)
+  paymentStore.setPixPaymentForServicePackage(itemId.value, nextPayment)
 }
 
 const checkPaymentStatus = async () => {
@@ -190,7 +193,8 @@ const checkPaymentStatus = async () => {
 
   pollingInFlight = true
   try {
-    applyPaymentStatus(await getSchedulePixPayment(props.schedule.id))
+    const result = await getServicePackagePix(itemId.value)
+    applyPaymentStatus(result)
     updateCountdown()
   } catch (e) {
     console.error('Erro ao verificar pagamento Pix:', e)
@@ -217,7 +221,7 @@ const updateCountdown = () => {
   if (!pixExpiresAt.value && totalSeconds.value > 0) totalSeconds.value--
 
   if (pixExpiresAt.value && totalSeconds.value <= 0) {
-    paymentStore.clearPixPayment(props.schedule.id)
+    paymentStore.clearPixPaymentForServicePackage(itemId.value)
     stopPolling()
   }
 }
@@ -227,7 +231,7 @@ onMounted(async () => {
   countdownTimer = setInterval(updateCountdown, 1000)
 
   try {
-    const cachedPayment = paymentStore.getValidPixPayment(props.schedule.id)
+    const cachedPayment = paymentStore.getValidPixPaymentForServicePackage(itemId.value)
 
     if (cachedPayment) {
       applyPaymentStatus(cachedPayment)
@@ -236,12 +240,18 @@ onMounted(async () => {
       return
     }
 
-    applyPaymentStatus(await paySchedule(props.schedule.id, { payment_method: 'pix' }))
+    const paymentResult = await payServicePackage(itemId.value, { payment_method: 'pix' })
+
+    applyPaymentStatus(paymentResult)
     updateCountdown()
     startPolling()
   } catch (e) {
     console.error('Erro ao criar pagamento Pix:', e)
-    $q.notify({ type: 'negative', message: e?.response?.data?.message ?? 'Erro ao criar pagamento Pix.' })
+
+    if (e.response?.data?.error !== 'missing_payment_data') {
+      $q.notify({ type: 'negative', message: e?.response?.data?.message ?? 'Erro ao criar pagamento Pix.' })
+    }
+
     onDialogCancel()
   } finally {
     processing.value = false

+ 12 - 6
src/components/dashboard/SchedulePaymentProcessingDialog.vue

@@ -44,13 +44,13 @@
 </template>
 
 <script setup>
-import { ref, onMounted } from 'vue'
+import { ref, computed, onMounted } from 'vue'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import LogoDiariaSucesso from 'src/assets/diarinho-success-payment.svg';
-import { paySchedule } from 'src/api/payment'
+import { payServicePackage } from 'src/api/payment'
 
 const props = defineProps({
-  schedule: { type: Object, required: true },
+  servicePackage: { type: Object, required: true },
   clientPaymentMethodId: { type: Number, required: true },
   total: { type: Number, default: null },
 })
@@ -60,11 +60,13 @@ defineEmits([...useDialogPluginComponent.emits])
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
 
+const item = computed(() => props.servicePackage)
+
 const success = ref(false)
 
 onMounted(async () => {
   try {
-    const payment = await paySchedule(props.schedule.id, {
+    const payment = await payServicePackage(item.value.id, {
       payment_method: 'credit_card',
       client_payment_method_id: props.clientPaymentMethodId,
     })
@@ -75,8 +77,12 @@ onMounted(async () => {
 
     success.value = true
   } catch (e) {
-    console.error('Erro ao pagar agendamento:', e)
-    $q.notify({ type: 'negative', message: e?.response?.data?.message ?? e.message ?? 'Erro ao processar pagamento.' })
+    console.error('Erro ao pagar:', e)
+
+    if (e.response?.data?.error !== 'missing_payment_data') {
+      $q.notify({ type: 'negative', message: e?.response?.data?.message ?? e.message ?? 'Erro ao processar pagamento.' })
+    }
+
     onDialogCancel()
   }
 })

+ 223 - 133
src/components/dashboard/ScheduleRatingDialog.vue

@@ -1,52 +1,93 @@
 <template>
-  <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="rating-dialog-card bg-surface shadow-card" :flat="false">
-
+  <q-dialog
+    ref="dialogRef"
+    @hide="onDialogHide"
+  >
+    <q-card
+      class="rating-dialog-card bg-surface shadow-card"
+      :flat="false"
+    >
       <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 class="column items-center q-pb-sm">
-        <q-avatar size="64px" class="q-mb-sm">
+        <q-avatar
+          class="q-mb-sm"
+          size="64px"
+        >
           <span
-            :style="avatarStyle"
             class="full-width full-height flex flex-center"
-            style="border-radius: 50%;"
+            style="border-radius: 50%"
+            :style="avatarStyle"
           >
             {{ initials }}
           </span>
         </q-avatar>
-        <div class="font16 fontbold text-text text-center q-px-lg" style="line-height:1.3">
-          {{ $t('dashboard_client.schedule_rating.title') }}
-          <span class="text-primary"> {{ (getFirstName(schedule.provider_name) || '—') + '?' }}</span>
+
+        <div
+          class="font16 fontbold text-text text-center q-px-lg"
+          style="line-height: 1.3"
+        >
+          {{ $t("dashboard_client.schedule_rating.title") }}
+
+          <span class="text-primary">
+            {{ (getFirstName(schedule.provider_name) || "—") + "?" }}
+          </span>
         </div>
       </div>
 
       <div class="column items-center q-pb-xs">
         <q-rating
           v-model="stars"
-          :max="5"
-          size="lg"
           color="amber"
           icon="mdi-star-outline"
           icon-selected="mdi-star"
+          size="lg"
+          :max="5"
           @update:model-value="onStarsChange"
         />
       </div>
 
-      <q-card-section v-if="stars > 0" class="q-pt-xs q-pb-xs">
+      <q-card-section
+        v-if="stars > 0"
+        class="q-pt-xs q-pb-xs"
+      >
         <div class="text-grey-7 font14 fontbold text-center q-mb-sm">
-          {{ isNegative ? $t('dashboard_client.schedule_rating.negative_label') : $t('dashboard_client.schedule_rating.positive_label') }}
+          {{
+            isNegative
+              ? $t("dashboard_client.schedule_rating.negative_label")
+              : $t("dashboard_client.schedule_rating.positive_label")
+          }}
         </div>
-        <div v-if="loadingTags" class="row justify-center q-py-sm">
-          <q-spinner-dots color="primary" size="24px" />
+
+        <div
+          v-if="loadingTags"
+          class="row justify-center q-py-sm"
+        >
+          <q-spinner-dots
+            color="primary"
+            size="24px"
+          />
         </div>
-        <div v-else class="row justify-center q-gutter-xs">
+
+        <div
+          v-else
+          class="row justify-center q-gutter-xs"
+        >
           <div
             v-for="tag in tags"
             :key="tag.id"
-            class="tag-pill"
             :class="{ 'tag-pill--selected': selectedTagIds.includes(tag.id) }"
+            class="tag-pill"
             @click="toggleTag(tag.id)"
           >
             {{ tag.description }}
@@ -56,171 +97,207 @@
 
       <q-card-section class="q-pt-xs q-pb-xs q-px-lg">
         <div class="font14 fontbold text-text q-mb-xs text-center">
-          {{ $t('dashboard_client.schedule_rating.comment_placeholder') }}
+          {{ $t("dashboard_client.schedule_rating.comment_placeholder") }}
         </div>
+
         <q-input
           v-model="comment"
-          type="textarea"
-          outlined
-          dense
-          rows="3"
           color="primary"
-          input-class="text-black"
+          dense
           hide-bottom-space
+          input-class="text-black"
+          outlined
+          rows="3"
+          type="textarea"
         />
       </q-card-section>
 
       <q-card-section class="q-pt-xs q-pb-xs q-px-lg">
         <input
           ref="photoInputRef"
-          type="file"
           accept="image/jpeg,image/png,image/webp"
-          multiple
           class="hidden"
+          multiple
+          type="file"
           @change="onPhotosSelected"
         />
+
         <div class="row q-gutter-xs">
-          <div v-for="(preview, idx) in photoPreviews" :key="idx" class="photo-thumb">
+          <div
+            v-for="(preview, idx) in photoPreviews"
+            :key="idx"
+            class="photo-thumb"
+          >
             <img :src="preview" />
-            <q-btn round dense flat icon="close" size="xs" class="photo-thumb__remove" @click="removePhoto(idx)" />
+
+            <q-btn
+              class="photo-thumb__remove"
+              dense
+              flat
+              icon="close"
+              round
+              size="xs"
+              @click="removePhoto(idx)"
+            />
           </div>
-          <div v-if="photos.length < 5" class="photo-add" @click="photoInputRef.click()">
-            <q-icon name="mdi-camera-plus-outline" size="24px" color="grey-5" />
-            <div class="photo-add__label">{{ $t('dashboard_client.schedule_rating.add_photo') }}</div>
+
+          <div
+            v-if="photos.length < 5"
+            class="photo-add"
+            @click="photoInputRef.click()"
+          >
+            <q-icon
+              color="grey-5"
+              name="mdi-camera-plus-outline"
+              size="24px"
+            />
+
+            <div class="photo-add__label">
+              {{ $t("dashboard_client.schedule_rating.add_photo") }}
+            </div>
           </div>
         </div>
       </q-card-section>
 
-      <q-card-section v-if="stars > 0" class="q-pt-xs q-pb-xs q-px-lg">
+      <q-card-section
+        v-if="stars > 0"
+        class="q-pt-xs q-pb-xs q-px-lg"
+      >
         <q-checkbox
           v-model="checkboxValue"
-          :label="isNegative ? $t('dashboard_client.schedule_rating.block_label') : $t('dashboard_client.schedule_rating.favorite_label')"
+          checked-icon="mdi-check-circle"
+          class="text-text"
           color="primary"
           keep-color
-          class="text-text"
-          checked-icon="mdi-check-circle"
           unchecked-icon="mdi-checkbox-blank-circle-outline"
+          :label="isNegative ? $t('dashboard_client.schedule_rating.block_label') : $t('dashboard_client.schedule_rating.favorite_label')"
         />
       </q-card-section>
 
       <q-card-section class="q-pt-sm q-pb-xs q-px-lg row">
         <q-btn
-          unelevated
-          rounded
-          no-caps
-          full-width
+          class="submit-btn col-12"
           color="primary"
+          full-width
+          no-caps
           padding="8px 12px"
-          class="submit-btn col-12"
+          rounded
+          unelevated
+          :disable="stars === 0"
           :label="$t('dashboard_client.schedule_rating.submit_btn')"
           :loading="loading"
-          :disable="stars === 0"
           @click="submit"
         />
       </q-card-section>
 
       <q-card-section class="q-pt-xs q-pb-lg text-center">
-        <span class="text-text font12 fontbold cursor-pointer" @click="openHelp">
-          {{ $t('dashboard_client.schedule_rating.help_link') }}
+        <span
+          class="text-text font12 fontbold cursor-pointer"
+          @click="openHelp"
+        >
+          {{ $t("dashboard_client.schedule_rating.help_link") }}
         </span>
       </q-card-section>
-
     </q-card>
   </q-dialog>
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue'
-import { useDialogPluginComponent, useQuasar } from 'quasar'
-import { useI18n } from 'vue-i18n'
-import { createReview, getImprovementTypes } from 'src/api/review'
-import { userStore } from 'src/stores/user'
-import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
-import { getFirstName } from 'src/helpers/utils'
+import { avatarColors } from "src/helpers/avatarColors";
+import { computed, onMounted, ref } from "vue";
+import { createReview, getImprovementTypes } from "src/api/review";
+import { getFirstName } from "src/helpers/utils";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+import { userStore } from "src/stores/user";
+
+import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 
 const props = defineProps({
   schedule: {
     type: Object,
-    required: true
-  }
-})
-
-defineEmits([...useDialogPluginComponent.emits])
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
-const { t } = useI18n()
-const $q = useQuasar()
-const store = userStore()
-
-const stars = ref(0)
-const selectedTagIds = ref([])
-const comment = ref(null)
-const checkboxValue = ref(false)
-const tags = ref([])
-const loadingTags = ref(false)
-const loading = ref(false)
-const photos = ref([])
-const photoPreviews = ref([])
-const photoInputRef = ref(null)
-
-const isNegative = computed(() => stars.value > 0 && stars.value <= 2)
-const isPositive = computed(() => stars.value >= 3)
-
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-]
+    required: true,
+  },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
+  useDialogPluginComponent();
+
+const { t } = useI18n();
+const $q = useQuasar();
+const store = userStore();
+
+const stars = ref(0);
+const selectedTagIds = ref([]);
+const comment = ref(null);
+const checkboxValue = ref(false);
+const tags = ref([]);
+const loadingTags = ref(false);
+const loading = ref(false);
+const photos = ref([]);
+const photoPreviews = ref([]);
+const photoInputRef = ref(null);
 
 const avatarStyle = computed(() => {
-  const c = avatarColors[props.schedule.provider_id % avatarColors.length]
-  return { background: c.background, color: c.color }
-})
+  const c = avatarColors[props.schedule.provider_id % avatarColors.length];
+
+  return { background: c.background, color: c.color };
+});
 
 const initials = computed(() => {
-  const firstName = getFirstName(props.schedule.provider_name)
+  const firstName = getFirstName(props.schedule.provider_name);
 
-  return firstName?.slice(0, 2).toUpperCase() ?? '??'
-})
+  return firstName?.slice(0, 2).toUpperCase() ?? "??";
+});
 
-const onStarsChange = () => {
-  selectedTagIds.value = []
-  checkboxValue.value = false
-}
-
-const toggleTag = (id) => {
-  const idx = selectedTagIds.value.indexOf(id)
-  if (idx === -1) selectedTagIds.value.push(id)
-  else selectedTagIds.value.splice(idx, 1)
-}
+const isNegative = computed(() => stars.value > 0 && stars.value <= 2);
+const isPositive = computed(() => stars.value >= 3);
 
 const onPhotosSelected = (event) => {
-  const files = Array.from(event.target.files)
-  const remaining = 5 - photos.value.length
-  files.slice(0, remaining).forEach(file => {
-    photos.value.push(file)
-    photoPreviews.value.push(URL.createObjectURL(file))
-  })
-  event.target.value = ''
-}
+  const files = Array.from(event.target.files);
 
-const removePhoto = (idx) => {
-  URL.revokeObjectURL(photoPreviews.value[idx])
-  photos.value.splice(idx, 1)
-  photoPreviews.value.splice(idx, 1)
-}
+  const remaining = 5 - photos.value.length;
+
+  files.slice(0, remaining).forEach((file) => {
+    photos.value.push(file);
+    photoPreviews.value.push(URL.createObjectURL(file));
+  });
+
+  event.target.value = "";
+};
+
+const onStarsChange = () => {
+  selectedTagIds.value = [];
+  checkboxValue.value = false;
+};
 
 const openHelp = () => {
-  $q.dialog({ component: ProfileHelpDialog })
-}
+  $q.dialog({ component: ProfileHelpDialog });
+};
+
+const removePhoto = (idx) => {
+  URL.revokeObjectURL(photoPreviews.value[idx]);
+  photos.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 () => {
-  if (stars.value === 0) return
-  loading.value = true
+  if (stars.value === 0) return;
+
+  loading.value = true;
+
   try {
     await createReview({
       schedule_id: props.schedule.id,
-      origin: 'client',
+      origin: "client",
       origin_id: store.user.client.id,
       stars: stars.value,
       comment: comment.value || null,
@@ -229,32 +306,39 @@ const submit = async () => {
       block_client: false,
       favorite_provider: isPositive.value && checkboxValue.value,
       photos: photos.value,
-    })
+    });
 
-    onDialogOK(true)
+    onDialogOK(true);
   } catch (error) {
-    const status = error?.response?.status
+    const status = error?.response?.status;
+
     if (status === 422) {
-      $q.notify({ message: t('dashboard_client.schedule_rating.already_reviewed'), color: 'negative', icon: 'mdi-alert-circle-outline' })
+      $q.notify({
+        message: t("dashboard_client.schedule_rating.already_reviewed"),
+        color: "negative",
+        icon: "mdi-alert-circle-outline",
+      });
     } else {
-      $q.notify({ message: t('http.errors.failed'), color: 'negative' })
+      $q.notify({ message: t("http.errors.failed"), color: "negative" });
     }
   } finally {
-    loading.value = false
+    loading.value = false;
   }
-}
+};
 
 onMounted(async () => {
-  loadingTags.value = true
+  loadingTags.value = true;
+
   try {
-    const result = await getImprovementTypes('client')
-    tags.value = result ?? []
+    const result = await getImprovementTypes("client");
+
+    tags.value = result ?? [];
   } catch {
-    tags.value = []
+    tags.value = [];
   } finally {
-    loadingTags.value = false
+    loadingTags.value = false;
   }
-})
+});
 </script>
 
 <style scoped lang="scss">
@@ -271,12 +355,14 @@ onMounted(async () => {
   padding: 5px 14px;
   color: #6b7280;
   cursor: pointer;
-  transition: border-color 0.15s, color 0.15s;
+  transition:
+    border-color 0.15s,
+    color 0.15s;
   user-select: none;
 
   &--selected {
-    border-color: #8B5CF6;
-    color: #8B5CF6;
+    border-color: #8b5cf6;
+    color: #8b5cf6;
   }
 }
 
@@ -327,8 +413,12 @@ onMounted(async () => {
     line-height: 1.2;
   }
 
-  &:active { background: rgba(0, 0, 0, 0.03); }
+  &:active {
+    background: rgba(0, 0, 0, 0.03);
+  }
 }
 
-.hidden { display: none; }
+.hidden {
+  display: none;
+}
 </style>

+ 77 - 43
src/components/defaults/DefaultInputDatePicker.vue

@@ -3,24 +3,38 @@
     <q-input
       ref="inputRef"
       v-model="treatedDate"
-      :mask="inputMask"
-      :label
-      :rules
+      clearable
       :dense
       :error
       :error-message
-      clearable
+      :label
+      :mask="inputMask"
+      :rules
     >
       <template #append>
         <q-icon
-          :name="time ? 'mdi-calendar-clock' : 'mdi-calendar'"
           class="cursor-pointer"
+          :name="time ? 'mdi-calendar-clock' : 'mdi-calendar'"
         >
-          <q-popup-proxy cover transition-show="scale" transition-hide="scale">
+          <q-popup-proxy
+            cover
+            transition-hide="scale"
+            transition-show="scale"
+          >
             <template v-if="!time">
-              <q-date v-model="date" mask="YYYY-MM-DD" color="primary" class="bg-surface text-text calendar-custom">
+              <q-date
+                v-model="date"
+                color="primary"
+                mask="YYYY-MM-DD"
+                :class="['bg-surface', 'text-text', 'calendar-custom', calendarClass]"
+              >
                 <div class="row items-center justify-end">
-                  <q-btn v-close-popup label="Close" color="primary" flat />
+                  <q-btn
+                    v-close-popup
+                    color="primary"
+                    flat
+                    label="Close"
+                  />
                 </div>
               </q-date>
             </template>
@@ -29,11 +43,14 @@
               <q-tab-panels
                 v-model="activePanel"
                 animated
-                transition-prev="slide-right"
-                transition-next="slide-left"
                 class="bg-white"
+                transition-next="slide-left"
+                transition-prev="slide-right"
               >
-                <q-tab-panel name="date" class="q-pa-none">
+                <q-tab-panel
+                  class="q-pa-none"
+                  name="date"
+                >
                   <q-date
                     v-model="date"
                     mask="YYYY-MM-DD HH:mm"
@@ -41,8 +58,15 @@
                   />
                 </q-tab-panel>
 
-                <q-tab-panel name="time" class="q-pa-none">
-                  <q-time v-model="date" mask="YYYY-MM-DD HH:mm" format24h />
+                <q-tab-panel
+                  class="q-pa-none"
+                  name="time"
+                >
+                  <q-time
+                    v-model="date"
+                    format24h
+                    mask="YYYY-MM-DD HH:mm"
+                  />
                 </q-tab-panel>
               </q-tab-panels>
             </template>
@@ -54,27 +78,22 @@
 </template>
 
 <script setup>
-import { watch, ref, computed, useTemplateRef } from "vue";
+import { computed, ref, useTemplateRef, watch } from "vue";
 import { useI18n } from "vue-i18n";
 import masks from "src/helpers/masks";
 
-const { label, rules, time, dense } = defineProps({
-  label: {
+const treatedDate = defineModel();
+const untreatedDate = defineModel("untreatedDate");
+
+const { label, rules, time, dense, calendarClass } = defineProps({
+  calendarClass: {
     type: String,
-    default: () => useI18n().t("common.terms.date"),
-  },
-  rules: {
-    type: Array,
-    default: () => [],
+    default: "",
   },
   dense: {
     type: Boolean,
     default: false,
   },
-  time: {
-    type: Boolean,
-    default: false,
-  },
   error: {
     type: Boolean,
     default: false,
@@ -83,53 +102,65 @@ const { label, rules, time, dense } = defineProps({
     type: String,
     default: "",
   },
+  label: {
+    type: String,
+    default: () => useI18n().t("common.terms.date"),
+  },
+  rules: {
+    type: Array,
+    default: () => [],
+  },
+  time: {
+    type: Boolean,
+    default: false,
+  },
 });
 
 const qInputRef = useTemplateRef("inputRef");
 
-const treatedDate = defineModel();
-const untreatedDate = defineModel("untreatedDate");
-
 const activePanel = ref("date");
 const date = ref();
 
-const handleDateSelection = () => {
+const inputMask = computed(() => {
+  if (!qInputRef.value) return "";
+
   if (time) {
-    activePanel.value = "time";
+    return masks.Brasil.datetime;
   }
-};
+  return masks.Brasil.date;
+});
 
 const formatDate = (value) => {
   if (!value) return null;
 
   const [datePart, timePart] = value.split(" ");
+
   const formattedDate = datePart.split("-").reverse().join("/");
 
   return time && timePart ? `${formattedDate} ${timePart}` : formattedDate;
 };
 
+const handleDateSelection = () => {
+  if (time) {
+    activePanel.value = "time";
+  }
+};
+
 const unformatDate = (value) => {
   if (!value) return null;
 
   const [datePart, timePart] = value.split(" ");
+
   const formattedDate = datePart.split("/").reverse().join("-");
 
   return time && timePart ? `${formattedDate} ${timePart}` : formattedDate;
 };
 
-const inputMask = computed(() => {
-  if (!qInputRef.value) return "";
-
-  if (time) {
-    return masks.Brasil.datetime;
-  }
-  return masks.Brasil.date;
-});
-
 watch(date, (value) => {
   if (!value) return;
 
   untreatedDate.value = value;
+
   treatedDate.value = formatDate(value);
 });
 
@@ -139,8 +170,10 @@ watch(treatedDate, (value) => {
     untreatedDate.value = null;
     treatedDate.value = null;
     activePanel.value = "date";
+
     return;
   }
+
   date.value = unformatDate(value);
 });
 
@@ -152,15 +185,16 @@ watch(
       untreatedDate.value = null;
       treatedDate.value = null;
       activePanel.value = "date";
+
       return;
     }
+
     date.value = value;
+
     treatedDate.value = formatDate(value);
   },
   { immediate: true },
 );
 </script>
 
-<style scoped lang="scss">
-
-</style>
+<style scoped lang="scss"></style>

+ 0 - 29
src/components/layout/ScheduleCartMenu.vue

@@ -1,29 +0,0 @@
-<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
-<template>
-  <q-btn
-    class="schedule-cart-button"
-    color="primary"
-    icon="mdi-cart-outline"
-    round
-    unelevated
-    :to="{ name: 'ScheduleCartPage' }"
-  >
-    <q-badge v-if="cart.items.length" color="secondary" floating rounded>
-      {{ cart.items.length }}
-    </q-badge>
-
-    <q-tooltip>{{ $t('schedule_cart.title') }}</q-tooltip>
-  </q-btn>
-</template>
-
-<script setup>
-import { useScheduleCartStore } from "src/stores/scheduleCart";
-
-const cart = useScheduleCartStore();
-</script>
-
-<style scoped lang="scss">
-.schedule-cart-button {
-  box-shadow: 0 8px 24px rgba(166, 61, 247, 0.28);
-}
-</style>

+ 36 - 0
src/components/layout/ServicePackageMenu.vue

@@ -0,0 +1,36 @@
+<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+<template>
+  <q-btn
+    class="service-package-button"
+    color="primary"
+    icon="mdi-shopping-outline"
+    round
+    unelevated
+    :to="{ name: 'ScheduleCartPage' }"
+  >
+    <q-badge
+      v-if="servicePackage.items.length"
+      color="secondary"
+      floating
+      rounded
+    >
+      {{ servicePackage.items.length }}
+    </q-badge>
+
+    <q-tooltip>
+      {{ $t("service_package.title") }}
+    </q-tooltip>
+  </q-btn>
+</template>
+
+<script setup>
+import { useServicePackageStore } from "src/stores/servicePackage";
+
+const servicePackage = useServicePackageStore();
+</script>
+
+<style scoped lang="scss">
+.service-package-button {
+  box-shadow: 0 8px 24px rgba(166, 61, 247, 0.28);
+}
+</style>

+ 57 - 31
src/components/login/LoginStep1Panel.vue

@@ -1,61 +1,74 @@
 <template>
   <q-card-section class="full-width">
     <div class="text-text">
-      <span class="font12 fontbold">{{ $t("common.terms.email") }}</span>
+      <span class="font12 fontbold">
+        {{ $t("common.terms.email") }}
+      </span>
     </div>
 
     <q-input
       v-model="email"
       class="bg-surface q-mt-sm"
       hide-bottom-space
-      input-class="text-text font12 fontregular"
+      input-class="font12 fontmedium text-text"
       lazy-rules
       no-error-icon
       outlined
       rounded
       :bottom-slots="false"
+      :error="!!serverErrors.email"
+      :error-message="serverErrors.email"
       :placeholder="$t('common.terms.email')"
-      :rules="!phone ? [inputRules.requiredHideMessage, inputRules.email] : []"
+      :rules="!smsEnabled || !phone ? [inputRules.requiredHideMessage, inputRules.email] : [inputRules.email]"
+      @update:model-value="clearServerError('email')"
     />
 
-    <div class="col-12 row q-py-lg">
-      <div class="col-5 q-my-auto">
-        <q-separator class="q-my-sm bg-grey-4" />
-      </div>
+    <template v-if="smsEnabled">
+      <div class="col-12 q-py-lg row">
+        <div class="col-5 q-my-auto">
+          <q-separator class="bg-grey-4 q-my-sm" />
+        </div>
 
-      <span class="col text-center text-text font12">
-        {{ $t("common.ui.misc.or") }}
-      </span>
+        <span class="col font12 fontmedium text-center text-text">
+          {{ $t("common.ui.misc.or") }}
+        </span>
 
-      <div class="col-5 q-my-auto">
-        <q-separator class="q-my-sm bg-grey-4" />
+        <div class="col-5 q-my-auto">
+          <q-separator class="bg-grey-4 q-my-sm" />
+        </div>
       </div>
-    </div>
 
-    <div class="text-text">
-      <span class="font12 fontbold">{{ $t("common.terms.phone") }}</span>
+      <div class="text-text">
+        <span class="font12 fontbold">
+          {{ $t("common.terms.phone") }}
+        </span>
 
-      <span class="font12 fontregular">
-        {{ " (" + $t("common.terms.celular") + ")" }}
-      </span>
-    </div>
+        <span class="font12 fontlight">
+          {{ ` (${$t("common.terms.celular")})` }}
+        </span>
+      </div>
 
-    <q-input
-      v-model="phone"
-      class="bg-surface q-mt-sm"
-      hide-bottom-space
-      input-class="text-text font12 fontregular"
-      lazy-rules
-      mask="(##) #####-####"
-      no-error-icon
-      :bottom-slots="false"
-      :placeholder="$t('common.terms.phone')"
-      :rules="!email ? [inputRules.requiredHideMessage, inputRules.email] : []"
-    />
+      <q-input
+        v-model="phone"
+        class="bg-surface q-mt-sm"
+        hide-bottom-space
+        input-class="text-text"
+        lazy-rules
+        mask="(##) #####-####"
+        no-error-icon
+        :bottom-slots="false"
+        :error="!!serverErrors.phone"
+        :error-message="serverErrors.phone"
+        :placeholder="$t('common.terms.phone')"
+        :rules="!email ? [inputRules.requiredHideMessage, inputRules.phone] : [inputRules.phone]"
+        @update:model-value="clearServerError('phone')"
+      />
+    </template>
   </q-card-section>
 </template>
 
 <script setup>
+import { inject } from "vue";
 import { useInputRules } from "src/composables/useInputRules";
 
 const email = defineModel("email", {
@@ -68,5 +81,18 @@ const phone = defineModel("phone", {
   type: String,
 });
 
+defineProps({
+  serverErrors: {
+    type: Object,
+    default: () => ({}),
+  },
+  smsEnabled: {
+    type: Boolean,
+    default: false,
+  },
+});
+
+const clearServerError = inject("clearServerError", () => {});
+
 const { inputRules } = useInputRules();
 </script>

+ 13 - 1
src/components/login/LoginStep2Panel.vue

@@ -25,13 +25,16 @@
       lazy-rules
       mask="######"
       no-error-icon
+      :error="!!serverErrors.code"
+      :error-message="serverErrors.code"
       :rules="[inputRules.required]"
+      @update:model-value="clearServerError('code')"
     />
   </q-card-section>
 </template>
 
 <script setup>
-import { ref } from "vue";
+import { inject, ref } from "vue";
 
 import { useInputRules } from "src/composables/useInputRules";
 
@@ -40,6 +43,15 @@ const code = defineModel("code", {
   type: String,
 });
 
+defineProps({
+  serverErrors: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+
+const clearServerError = inject("clearServerError", () => {});
+
 const { inputRules } = useInputRules();
 
 const codeInputRef = ref(null);

+ 120 - 48
src/components/login/LoginStep3Panel.vue

@@ -2,7 +2,9 @@
   <q-card-section class="no-padding">
     <div>
       <div class="text-text">
-        <span class="font14 fontbold">{{ $t("auth.full_name") }}</span>
+        <span class="font14 fontbold">
+          {{ $t("auth.full_name") }}
+        </span>
       </div>
 
       <q-input
@@ -15,14 +17,19 @@
         no-error-icon
         outlined
         rounded
+        :error="!!serverErrors.name"
+        :error-message="serverErrors.name"
         :placeholder="$t('auth.full_name')"
         :rules="[inputRules.required]"
+        @update:model-value="clearServerError('name')"
       />
     </div>
 
     <div>
       <div class="text-text">
-        <span class="font14 fontbold">{{ $t("common.terms.document") }}</span>
+        <span class="font14 fontbold">
+          {{ $t("common.terms.document") }}
+        </span>
       </div>
 
       <q-input
@@ -35,14 +42,45 @@
         no-error-icon
         outlined
         rounded
+        :error="!!serverErrors.document"
+        :error-message="serverErrors.document"
         :placeholder="$t('common.terms.document')"
         :rules="[inputRules.cpfOrCnpj]"
+        @update:model-value="clearServerError('document')"
       />
     </div>
 
     <div>
       <div class="text-text">
-        <span class="font14 fontbold">{{ $t("common.terms.cep") }}</span>
+        <span class="font14 fontbold">
+          {{ $t("common.terms.phone") }}
+        </span>
+      </div>
+
+      <q-input
+        v-model="form.phone"
+        bg-color="surface"
+        class="q-mt-sm q-mb-md"
+        hide-bottom-space
+        input-class="text-text"
+        lazy-rules
+        mask="(##) #####-####"
+        no-error-icon
+        outlined
+        rounded
+        :error="!!serverErrors.phone"
+        :error-message="serverErrors.phone"
+        :placeholder="$t('common.terms.phone')"
+        :rules="[inputRules.required, inputRules.phone]"
+        @update:model-value="clearServerError('phone')"
+      />
+    </div>
+
+    <div>
+      <div class="text-text">
+        <span class="font14 fontbold">
+          {{ $t("common.terms.cep") }}
+        </span>
       </div>
 
       <q-input
@@ -57,9 +95,11 @@
         outlined
         placeholder="00000-000"
         rounded
+        :error="!!serverErrors.zip_code"
+        :error-message="serverErrors.zip_code"
         :loading="loadingCep"
         :rules="[inputRules.required, inputRules.cep]"
-        @update:model-value="onCepChange"
+        @update:model-value="(clearServerError('zip_code'), onCepChange($event))"
       />
     </div>
 
@@ -75,7 +115,9 @@
 
     <div>
       <div class="text-text">
-        <span class="font14 fontbold">{{ $t("common.terms.address") }}</span>
+        <span class="font14 fontbold">
+          {{ $t("common.terms.address") }}
+        </span>
       </div>
 
       <q-input
@@ -89,6 +131,8 @@
         outlined
         readonly
         rounded
+        :error="!!serverErrors.address"
+        :error-message="serverErrors.address"
         :placeholder="`${$t('common.terms.address')}...`"
         :rules="[inputRules.required]"
       />
@@ -98,7 +142,8 @@
       <div class="col-4">
         <div class="text-text">
           <span class="font14 fontbold">
-            {{ $t("common.terms.address_number") }}</span>
+            {{ $t("common.terms.address_number") }}
+          </span>
         </div>
 
         <q-input
@@ -112,13 +157,18 @@
           outlined
           placeholder="0000"
           rounded
+          :error="!!serverErrors.number"
+          :error-message="serverErrors.number"
           :rules="[inputRules.required]"
+          @update:model-value="clearServerError('number')"
         />
       </div>
 
       <div class="col-8">
         <div class="text-text">
-          <span class="font14 fontbold">{{ $t("common.terms.district") }}</span>
+          <span class="font14 fontbold">
+            {{ $t("common.terms.district") }}
+          </span>
         </div>
 
         <q-input
@@ -127,11 +177,15 @@
           class="q-mt-sm q-mb-md"
           hide-bottom-space
           input-class="text-text"
+          lazy-rules
           no-error-icon
           outlined
           readonly
           rounded
+          :error="!!serverErrors.district"
+          :error-message="serverErrors.district"
           :placeholder="`${$t('common.terms.district')}...`"
+          :rules="[inputRules.required]"
         />
       </div>
     </div>
@@ -139,24 +193,33 @@
     <div class="row q-col-gutter-sm">
       <div class="col-8">
         <div class="text-text">
-          <span class="font14 fontbold">{{ $t("common.terms.city") }}</span>
+          <span class="font14 fontbold">
+            {{ $t("common.terms.city") }}
+          </span>
         </div>
 
         <q-input
           v-model="form.city"
           bg-color="surface"
           class="q-mt-sm q-mb-md"
+          hide-bottom-space
           input-class="text-text"
+          lazy-rules
           no-error-icon
           outlined
           readonly
           rounded
+          :error="!!serverErrors.city"
+          :error-message="serverErrors.city"
+          :rules="[inputRules.required]"
         />
       </div>
 
       <div class="col-4">
         <div class="text-text">
-          <span class="font14 fontbold">{{ $t("common.terms.state") }}</span>
+          <span class="font14 fontbold">
+            {{ $t("common.terms.state") }}
+          </span>
         </div>
 
         <q-input
@@ -165,50 +228,48 @@
           class="q-mt-sm q-mb-md"
           hide-bottom-space
           input-class="text-text"
+          lazy-rules
           no-error-icon
           outlined
           readonly
           rounded
+          :error="!!serverErrors.state"
+          :error-message="serverErrors.state"
+          :rules="[inputRules.required]"
         />
       </div>
     </div>
 
     <div>
-      <q-checkbox
-        v-model="form.no_complement"
-        class="q-mb-md text-text font14 fontbold"
-        color="primary"
-        keep-color
-        :label="$t('auth.no_complement')"
-      />
-    </div>
-
-    <div>
-      <template v-if="!form.no_complement">
-        <div class="text-text">
-          <span class="font14 fontbold">
-            {{ $t("common.terms.complement") }}</span>
-        </div>
+      <div class="text-text">
+        <span class="font14 fontbold">
+          {{ $t("common.terms.complement") }}
+        </span>
+      </div>
 
-        <q-input
-          v-model="form.complement"
-          bg-color="surface"
-          class="q-mt-sm q-mb-md"
-          hide-bottom-space
-          input-class="text-text"
-          lazy-rules
-          no-error-icon
-          outlined
-          rounded
-          :placeholder="`${$t('common.ui.misc.example')}: Apartamento, Conjunto, Casa`"
-          :rules="!form.no_complement ? [inputRules.required] : []"
-        />
-      </template>
+      <q-input
+        v-model="form.complement"
+        bg-color="surface"
+        class="q-mt-sm q-mb-md"
+        hide-bottom-space
+        input-class="text-text"
+        lazy-rules
+        no-error-icon
+        outlined
+        rounded
+        :error="!!serverErrors.complement"
+        :error-message="serverErrors.complement"
+        :placeholder="`${$t('common.ui.misc.example')}: Apartamento, Conjunto, Casa`"
+        :rules="[inputRules.required]"
+        @update:model-value="clearServerError('complement')"
+      />
     </div>
 
     <div>
       <div class="text-text">
-        <span class="font14 fontbold">{{ $t("auth.address_nickname") }}</span>
+        <span class="font14 fontbold">
+          {{ $t("auth.address_nickname") }}
+        </span>
       </div>
 
       <q-input
@@ -253,13 +314,13 @@
         <q-chip
           v-for="type in addressTypes"
           :key="type.value"
+          :icon="type.icon"
+          :icon-selected="type.icon"
+          :outline="form.address_type !== type.value"
           :selected="form.address_type === type.value"
           clickable
           color="primary"
           text-color="surface"
-          :icon="type.icon"
-          :icon-selected="type.icon"
-          :outline="form.address_type !== type.value"
           @click="form.address_type = type.value"
         >
           {{ $t(type.label) }}
@@ -271,12 +332,13 @@
 
 <script setup>
 import axios from "axios";
-import { useQuasar } from "quasar";
-import { ref } from "vue";
-import { useI18n } from "vue-i18n";
 
-import { useInputRules } from "src/composables/useInputRules";
+import { inject, ref } from "vue";
 import { useGeolocation } from "src/composables/useGeolocation";
+import { useI18n } from "vue-i18n";
+import { useInputRules } from "src/composables/useInputRules";
+import { useQuasar } from "quasar";
+
 import LocationMapDialog from "src/components/shared/LocationMapDialog.vue";
 
 const form = defineModel({
@@ -284,10 +346,20 @@ const form = defineModel({
   type: Object,
 });
 
+defineProps({
+  serverErrors: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+
+const clearServerError = inject("clearServerError", () => {});
+
+const { requestPermission, getCurrentPosition } = useGeolocation();
 const { inputRules } = useInputRules();
-const $q = useQuasar();
 const { t } = useI18n();
-const { requestPermission, getCurrentPosition } = useGeolocation();
+
+const $q = useQuasar();
 
 const loadingCep = ref(false);
 const loadingLocation = ref(false);

+ 9 - 9
src/components/profile/ProfileAddressDialog.vue

@@ -2,7 +2,7 @@
   <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
     <div class="bg-page full-height no-shadow">
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
-        <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
+        <q-btn v-close-popup class="header-back-btn" icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
         <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('profile.address.title') }}</span>
         <q-space />
@@ -58,14 +58,14 @@
                         </q-item>
 
                         <q-item v-if="!address.is_primary" v-close-popup clickable @click="markAsPrimary(address)">
-                          <q-item-section avatar>
-                            <q-icon name="mdi-star" color="primary" />
-                          </q-item-section>
-                          <q-item-section>
-                            <q-item-label class="text-text">{{ $t('profile.address.mark_as_primary') }}</q-item-label>
-                          </q-item-section>
-                        </q-item>
-
+                            <q-item-section avatar>
+                              <q-icon name="mdi-map-marker" class="gradient-diarista"/>
+                            </q-item-section>
+
+                            <q-item-section>
+                              <q-item-label class="text-text"> {{ $t('profile.address.mark_as_primary') }} </q-item-label>
+                            </q-item-section>
+                          </q-item>
                         <q-separator />
                       </q-list>
                     </q-menu>

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

@@ -12,6 +12,7 @@
       >
         <q-btn
           v-close-popup
+          class="header-back-btn"
           color="primary"
           dense
           flat
@@ -203,7 +204,7 @@
               class="q-mb-md"
             >
               <q-banner
-                class="bg-orange-1 text-orange-9 q-mb-sm"
+                class="bg-orange-1 text-orange-9 q-mb-xl"
                 dense
                 rounded
               >
@@ -607,4 +608,4 @@ onMounted(() => {
 
   setUpdateFormAsOriginal();
 });
-</script>
+</script>

+ 340 - 133
src/components/profile/ProfileFavoritesDialog.vue

@@ -1,72 +1,158 @@
 <!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
+  <q-dialog
+    ref="dialogRef"
+    maximized
+    persistent
+    transition-hide="slide-right"
+    transition-show="slide-left"
+  >
     <div class="bg-page full-height column no-shadow">
+      <div
+        class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface"
+      >
+        <q-btn
+          v-close-popup
+          class="header-back-btn"
+          color="primary"
+          dense
+          flat
+          icon="mdi-chevron-left"
+          round
+        />
 
-      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
-        <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
-        <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('profile.favorites.title') }}</span>
+
+        <span
+          class="text-subtitle1 text-primary font16 fontbold gradient-diarista"
+        >
+          {{ $t("profile.favorites.title") }}
+        </span>
+
         <q-space />
-        <div style="width: 32px"></div>
+
+        <div style="width: 32px" />
       </div>
 
-      <div v-if="loading" class="col flex flex-center">
-        <q-spinner color="primary" size="3em" />
+      <div
+        v-if="loading"
+        class="col flex flex-center"
+      >
+        <q-spinner
+          color="primary"
+          size="3em"
+        />
       </div>
 
-      <div v-else-if="favorites.length === 0" class="col column items-center justify-center q-px-xl q-pb-xl">
+      <div
+        v-else-if="favorites.length === 0"
+        class="col column items-center justify-center q-px-xl q-pb-xl"
+      >
         <q-img
-          :src="diarinho"
-          style="width: 220px; height: 220px;"
-          fit="contain"
           class="q-mb-lg"
+          fit="contain"
+          style="width: 220px; height: 220px"
+          :src="diarinho"
         />
-        <p class="text-text text-center q-mb-xs" >
-          {{ $t('profile.favorites.empty_message') }}
+
+        <p class="text-text text-center q-mb-xs">
+          {{ $t("profile.favorites.empty_message") }}
         </p>
-        <p class="text-primary text-center  q-mb-xl">
-          {{ $t('profile.favorites.empty_cta') }}
+
+        <p class="text-primary text-center q-mb-xl">
+          {{ $t("profile.favorites.empty_cta") }}
         </p>
+
         <q-btn
           v-close-popup
-          unelevated
-          rounded
-          no-caps
-          color="primary"
           class="full-width q-py-sm"
+          color="primary"
+          no-caps
           padding="10px 16px"
+          rounded
+          unelevated
           :label="$t('profile.favorites.search_btn')"
           @click="goToSearch"
         />
       </div>
 
-      <div v-else class="col overflow-auto q-pb-xl">
-
+      <div
+        v-else
+        class="col overflow-auto q-pb-xl"
+      >
         <div class="q-mx-md q-mt-md q-pa-md">
           <p class="gradient-diarista text-center q-mb-md font12 fontbold">
-            {{ $t('profile.favorites.indicate_title') }}
+            {{ $t("profile.favorites.indicate_title") }}
           </p>
+
           <div class="row items-center">
             <div class="col column items-center">
-              <q-avatar :style="avatarStyle(highlightedFavorite)" size="72px" class=" text-h5 q-mb-sm shadow-1">
-                {{ highlightedFavorite?.provider_name?.charAt(0)?.toUpperCase() ?? '?' }}
+              <q-avatar
+                class="text-h5 q-mb-sm shadow-1"
+                size="72px"
+                :style="avatarStyle(highlightedFavorite)"
+              >
+                <img
+                  v-if="getProfileMediaUrl(highlightedFavorite)"
+                  class="profile-image"
+                  :src="getProfileMediaUrl(highlightedFavorite)"
+                />
+
+                <span v-else>
+                  {{
+                    highlightedFavorite?.provider_name
+                      ?.charAt(0)
+                      ?.toUpperCase() ?? "?"
+                  }}
+                </span>
               </q-avatar>
-              <span class="font14 fontbold text-dark" >{{ highlightedFavorite?.provider_name ?? '—' }}</span>
-              <span class="font12 fontmedium text-grey-6">{{ highlightedFavorite?.city_name ?? '—' }}</span>
+
+              <span class="font14 fontbold text-dark">
+                {{ highlightedFavorite?.provider_name ?? "—" }}
+              </span>
+
+              <span
+                v-if="getProviderGenderLabel(highlightedFavorite, t)"
+                class="font10 fontmedium text-grey-6"
+              >
+                {{ getProviderGenderLabel(highlightedFavorite, t) }}
+              </span>
+
+              <span class="font12 fontmedium text-grey-6">
+                {{ highlightedFavorite?.city_name ?? "—" }}
+              </span>
+
               <div class="row items-center q-gutter-sm q-mt-sm">
-                <q-btn flat round dense icon="mdi-chevron-left" color="primary" :disable="favorites.length <= 1" @click="prevHighlight" />
                 <q-btn
-                  rounded
-                  unelevated
-                  no-caps
                   color="primary"
+                  dense
+                  flat
+                  icon="mdi-chevron-left"
+                  round
+                  :disable="favorites.length <= 1"
+                  @click="prevHighlight"
+                />
+
+                <q-btn
+                  color="primary"
+                  no-caps
                   padding="4px 20px"
+                  rounded
                   size="sm"
+                  unelevated
                   :label="$t('profile.favorites.indicate_btn')"
                   @click="openIndicateDialog(highlightedFavorite)"
                 />
-                <q-btn flat round dense icon="mdi-chevron-right" color="primary" :disable="favorites.length <= 1" @click="nextHighlight" />
+
+                <q-btn
+                  color="primary"
+                  dense
+                  flat
+                  icon="mdi-chevron-right"
+                  round
+                  :disable="favorites.length <= 1"
+                  @click="nextHighlight"
+                />
               </div>
             </div>
           </div>
@@ -74,23 +160,31 @@
 
         <div class="q-mx-md q-mt-lg">
           <div class="row items-center justify-between q-mb-md">
-            <span class="font16 fontbold gradient-diarista">{{ $t('profile.favorites.providers_title') }}</span>
+            <span class="font16 fontbold gradient-diarista">
+              {{ $t("profile.favorites.providers_title") }}
+            </span>
+
             <div class="row items-center">
               <q-icon
+                :class="selectedHours === '2h' ? '' : 'cursor-pointer'"
+                :color="selectedHours === '2h' ? 'grey-4' : 'grey-6'"
                 name="mdi-chevron-left"
                 size="20px"
-                :color="selectedHours === '2h' ? 'grey-4' : 'grey-6'"
-                :class="selectedHours === '2h' ? '' : 'cursor-pointer'"
                 @click="nextSelectedHours"
               />
-              <span class="font12 fontbold text-grey-6 q-px-xs" style="user-select: none;">
-                {{ $t('profile.favorites.until') }} {{ selectedHours }}
+
+              <span
+                class="font12 fontbold text-grey-6 q-px-xs"
+                style="user-select: none"
+              >
+                {{ $t("profile.favorites.until") }} {{ selectedHours }}
               </span>
+
               <q-icon
+                :class="selectedHours === '8h' ? '' : 'cursor-pointer'"
+                :color="selectedHours === '8h' ? 'grey-4' : 'grey-6'"
                 name="mdi-chevron-right"
                 size="20px"
-                :color="selectedHours === '8h' ? 'grey-4' : 'grey-6'"
-                :class="selectedHours === '8h' ? '' : 'cursor-pointer'"
                 @click="prevSelectedHours"
               />
             </div>
@@ -99,57 +193,140 @@
           <div
             v-for="(item, index) in favorites"
             :key="item.id"
-            class="favorite-item row items-center no-wrap q-py-md"
             :class="{ 'item-separator': index < favorites.length - 1 }"
+            class="favorite-item row items-center no-wrap q-py-md"
           >
-            <div class="relative-position q-mr-md" style="margin-right: 16px;">
-              <q-avatar :style="avatarStyle(item)" size="60px" class="shadow-1">
-                {{ item.provider_name?.charAt(0)?.toUpperCase() ?? '?' }}
+            <div
+              class="relative-position q-mr-md"
+              style="margin-right: 16px"
+            >
+              <q-avatar
+                class="shadow-1"
+                size="60px"
+                :style="avatarStyle(item)"
+              >
+                <img
+                  v-if="getProfileMediaUrl(item)"
+                  class="profile-image"
+                  :src="getProfileMediaUrl(item)"
+                />
+
+                <span v-else>
+                  {{ item.provider_name?.charAt(0)?.toUpperCase() ?? "?" }}
+                </span>
               </q-avatar>
+
               <q-icon
+                class="absolute cursor-pointer"
                 name="mdi-heart"
                 size="26px"
-                class="absolute cursor-pointer"
-                style="bottom: -4px; right: -4px; color: #f518e3; filter: drop-shadow(0px 1px 2px rgba(0,0,0,0.2));"
+                style="bottom: -4px; right: -4px; color: #f518e3; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2))"
                 @click.stop="confirmRemove(item)"
               />
             </div>
 
-            <div class="col column justify-between" style="min-height: 60px;">
+            <div
+              class="col column justify-between"
+              style="min-height: 60px"
+            >
               <div class="row items-start justify-between">
                 <div class="column">
-                  <span class="provider-name font12 fontmedium" style="color: #4a4a4a; line-height: 1.1;">
-                    {{ item.provider_name ?? '—' }}
+                  <span
+                    class="provider-name font12 fontmedium"
+                    style="color: #4a4a4a; line-height: 1.1"
+                  >
+                    {{ item.provider_name ?? "—" }}
                   </span>
-                  <span class="provider-city font9 fontbold text-grey-7 q-pt-xs">
-                    {{ item.city_name ?? '—' }}
+
+                  <span
+                    v-if="getProviderGenderLabel(item, t)"
+                    class="font9 fontmedium text-grey-7 q-pt-xs"
+                  >
+                    {{ getProviderGenderLabel(item, t) }}
+                  </span>
+
+                  <span
+                    class="provider-city font9 fontbold text-grey-7 q-pt-xs"
+                  >
+                    {{ item.city_name ?? "—" }}
                   </span>
                 </div>
-                <span class="provider-price font12 fontregular text-grey-7" style="line-height: 1.1;">
-                  R${{ getPriceByHours(item) ? formatPrice(getPriceByHours(item)) : '—' }}
+
+                <span
+                  class="provider-price font12 fontregular text-grey-7"
+                  style="line-height: 1.1"
+                >
+                  R${{
+                    getPriceByHours(item)
+                      ? formatPrice(getPriceByHours(item))
+                      : "—"
+                  }}
                 </span>
               </div>
 
               <div class="row items-end justify-between q-mt-xs">
-                <div class="row items-center text-grey-5" style="margin-bottom: 2px;">
-                  <q-icon name="mdi-star" color="warning" size="12px" />
-                  <span class="q-ml-xs font9 fontregular" style="color: #666;">{{ item.average_rating ?? '—' }}</span>
-                  <span class="q-ml-xs font9 fontregular" style="margin-right: 2px;">({{ item.total_services ?? 0 }})</span>
-                  <q-icon name="mdi-circle-small" color="grey-4" size="12px" class="q-mx-xs" />
-                  <q-icon name="mdi-broom" size="12px" style="transform: scaleX(-1); color: #f518e3;" />
-                  <span class="q-ml-xs font9 fontregular" style="color: #666;">{{ item.total_services ?? '0' }}</span>
-                  <q-icon name="mdi-circle-small" color="grey-4" size="16px" class="q-mx-xs" />
-                  <span class="font9 fontregular">0Km</span>
+                <div
+                  class="row items-center text-grey-5"
+                  style="margin-bottom: 2px"
+                >
+                  <q-icon
+                    color="warning"
+                    name="mdi-star"
+                    size="12px"
+                  />
+
+                  <span
+                    class="q-ml-xs font9 fontregular"
+                    style="color: #666"
+                  >
+                    {{ item.average_rating ?? "—" }}
+                  </span>
+
+                  <span
+                    class="q-ml-xs font9 fontregular"
+                    style="margin-right: 2px"
+                  >
+                    ({{ item.total_services ?? 0 }})
+                  </span>
+
+                  <q-icon
+                    class="q-mx-xs"
+                    color="grey-4"
+                    name="mdi-circle-small"
+                    size="12px"
+                  />
+
+                  <q-icon
+                    name="mdi-broom"
+                    size="12px"
+                    style="color: #f518e3; transform: scaleX(-1)"
+                  />
+
+                  <span
+                    class="q-ml-xs font9 fontregular"
+                    style="color: #666"
+                  >
+                    {{ item.total_services ?? "0" }}
+                  </span>
+
+                  <q-icon
+                    class="q-mx-xs"
+                    color="grey-4"
+                    name="mdi-circle-small"
+                    size="16px"
+                  />
+
+                  <span class="font9 fontregular"> 0Km </span>
                 </div>
-                
+
                 <q-btn
-                  rounded
-                  unelevated
+                  class=""
                   no-caps
-                  style="background: #a63df7; color: white;"
                   padding="4px 20px"
+                  rounded
                   size="sm"
-                  class=""
+                  style="background: #a63df7; color: white"
+                  unelevated
                   :label="$t('profile.favorites.schedule_btn')"
                 />
               </div>
@@ -157,124 +334,149 @@
           </div>
         </div>
       </div>
-
     </div>
   </q-dialog>
 </template>
 
 <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 { useRouter } from 'vue-router';
-import { getClientFavoriteProviders, deleteClientFavoriteProvider } from 'src/api/clientFavoriteProvider';
-import ProfileFavoriteRemoveDialog from './ProfileFavoriteRemoveDialog.vue';
-import diarinho from 'src/assets/diarinho_perfil_cliente_favoritos.svg';
+import diarinho from "src/assets/diarinho_perfil_cliente_favoritos.svg";
+
+import { avatarColorsWithPurple as avatarColors } from "src/helpers/avatarColors";
+
+import { computed, onMounted, ref } from "vue";
+
+import {
+  deleteClientFavoriteProvider,
+  getClientFavoriteProviders,
+} from "src/api/clientFavoriteProvider";
+
+import { getProfileMediaUrl } from "src/helpers/profileMedia";
+import { getProviderGenderLabel } from "src/helpers/provider";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { useI18n } from "vue-i18n";
+import { useRouter } from "vue-router";
+import { userStore } from "src/stores/user";
+
+import ProfileFavoriteRemoveDialog from "./ProfileFavoriteRemoveDialog.vue";
 
 defineEmits([...useDialogPluginComponent.emits]);
 
 const { dialogRef } = useDialogPluginComponent();
-const $q = useQuasar();
+
 const { t } = useI18n();
-const store = userStore();
 
+const $q = useQuasar();
 const router = useRouter();
+const store = userStore();
+
 const favorites = ref([]);
-const loading = ref(false);
 const highlightIndex = ref(0);
-const selectedHours = ref('8h');
+const loading = ref(false);
+const selectedHours = ref("8h");
 
-const hoursOptions = ['8h', '6h', '4h', '2h'];
+const highlightedFavorite = computed(
+  () => favorites.value[highlightIndex.value] ?? null,
+);
 
-const prevSelectedHours = () => {
-  const currentIndex = hoursOptions.indexOf(selectedHours.value);
-  if (currentIndex > 0) {
-    selectedHours.value = hoursOptions[currentIndex - 1];
-  }
-};
+const hoursOptions = ["8h", "6h", "4h", "2h"];
 
-const nextSelectedHours = () => {
-  const currentIndex = hoursOptions.indexOf(selectedHours.value);
-  if (currentIndex < hoursOptions.length - 1) {
-    selectedHours.value = hoursOptions[currentIndex + 1];
-  }
+const avatarStyle = (item) => {
+  const index = (item?.id ?? 0) % avatarColors.length;
+
+  return avatarColors[index];
 };
 
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-  { background: '#ede0ff', color: '#6200ea' },
-];
+const confirmRemove = (item) => {
+  $q.dialog({
+    component: ProfileFavoriteRemoveDialog,
+  }).onOk(async () => {
+    try {
+      await deleteClientFavoriteProvider(item.id);
 
-const avatarStyle = (item) => {
-  const idx = (item?.id ?? 0) % avatarColors.length;
-  return avatarColors[idx];
+      favorites.value = favorites.value.filter(
+        (favorite) => favorite.id !== item.id,
+      );
+
+      if (highlightIndex.value >= favorites.value.length) {
+        highlightIndex.value = Math.max(0, favorites.value.length - 1);
+      }
+    } catch (error) {
+      console.error("Erro ao remover favorito:", error);
+    }
+  });
 };
 
 const formatPrice = (value) => {
-  return Number(value).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+  return Number(value).toLocaleString("pt-BR", {
+    maximumFractionDigits: 2,
+    minimumFractionDigits: 2,
+  });
 };
 
 const getPriceByHours = (item) => {
   const priceKey = `daily_price_${selectedHours.value}`;
+
   return item[priceKey] ?? item.daily_price_8h ?? 0;
 };
 
-const highlightedFavorite = computed(() => favorites.value[highlightIndex.value] ?? null);
-
-const prevHighlight = () => {
-  highlightIndex.value = highlightIndex.value === 0
-    ? favorites.value.length - 1
-    : highlightIndex.value - 1;
+const goToSearch = () => {
+  router.push({
+    name: "SearchPage",
+  });
 };
 
 const nextHighlight = () => {
   highlightIndex.value = (highlightIndex.value + 1) % favorites.value.length;
 };
 
-const goToSearch = () => {
-  router.push({
-    name: 'SearchPage'
-  });
-};
+const nextSelectedHours = () => {
+  const currentIndex = hoursOptions.indexOf(selectedHours.value);
 
-const confirmRemove = (item) => {
-  $q.dialog({
-    component: ProfileFavoriteRemoveDialog,
-  }).onOk(async () => {
-    try {
-      await deleteClientFavoriteProvider(item.id);
-      favorites.value = favorites.value.filter(f => f.id !== item.id);
-      if (highlightIndex.value >= favorites.value.length) {
-        highlightIndex.value = Math.max(0, favorites.value.length - 1);
-      }
-    } catch (error) {
-      console.error('Erro ao remover favorito:', error);
-    }
-  });
+  if (currentIndex < hoursOptions.length - 1) {
+    selectedHours.value = hoursOptions[currentIndex + 1];
+  }
 };
 
 const openIndicateDialog = (item) => {
-  const message = t('profile.favorites.indicate_whatsapp_message', {
-    name: item?.provider_name ?? '',
-    city: item?.city_name ?? '',
+  const message = t("profile.favorites.indicate_whatsapp_message", {
+    city: item?.city_name ?? "",
+    name: item?.provider_name ?? "",
+  });
+
+  console.log("[Indicar diarista - WhatsApp]", message);
+
+  $q.notify({
+    message: t("profile.favorites.indicate_coming_soon"),
+    type: "info",
   });
-  console.log('[Indicar diarista - WhatsApp]', message);
-  $q.notify({ type: 'info', message: t('profile.favorites.indicate_coming_soon') });
+};
+
+const prevHighlight = () => {
+  highlightIndex.value =
+    highlightIndex.value === 0
+      ? favorites.value.length - 1
+      : highlightIndex.value - 1;
+};
+
+const prevSelectedHours = () => {
+  const currentIndex = hoursOptions.indexOf(selectedHours.value);
+
+  if (currentIndex > 0) {
+    selectedHours.value = hoursOptions[currentIndex - 1];
+  }
 };
 
 onMounted(async () => {
   loading.value = true;
+
   try {
     const clientId = store.user?.client_id;
+
     if (clientId) {
       favorites.value = await getClientFavoriteProviders(clientId);
     }
   } catch (error) {
-    console.error('Erro ao carregar favoritos:', error);
+    console.error("Erro ao carregar favoritos:", error);
   } finally {
     loading.value = false;
   }
@@ -282,7 +484,6 @@ onMounted(async () => {
 </script>
 
 <style scoped lang="scss">
-
 .heart-badge {
   position: absolute;
   bottom: -2px;
@@ -334,6 +535,12 @@ onMounted(async () => {
   color: #2c3e50;
 }
 
+.profile-image {
+  height: 100%;
+  object-fit: cover;
+  width: 100%;
+}
+
 .shadow-profile {
   box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.1);
 }

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

@@ -3,7 +3,7 @@
     <div class="bg-page full-height column no-shadow">
 
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
-        <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
+        <q-btn v-close-popup class="header-back-btn" icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
         <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('profile.help.title') }}</span>
         <q-space />

+ 203 - 0
src/components/profile/ProfileInfoDialog.vue

@@ -0,0 +1,203 @@
+<template>
+  <q-dialog
+    ref="dialogRef"
+    @hide="onDialogHide"
+  >
+    <q-card class="provider-dialog">
+
+      <q-btn
+        flat
+        round
+        dense
+        icon="close"
+        class="close-btn"
+        @click="closeDialog"
+      />
+
+      <q-card-section class="provider-header">
+
+        <q-avatar
+          size="72px"
+          color="primary"
+          text-color="white"
+          class="provider-avatar"
+        >
+          <img
+            v-if="props.provider.provider_photo"
+            :src="props.provider.provider_photo"
+          >
+
+          <span
+            v-else
+            class="text-subtitle1 text-weight-bold"
+          >
+            {{ initials }}
+          </span>
+        </q-avatar>
+
+        <div class="provider-info">
+
+          <div class="text-h6 text-weight-bold">
+            {{ props.provider.provider_name }}
+          </div>
+
+          <div v-if="props.provider.age" class="text-grey-6 text-body2">
+            {{ `${props.provider.age}` }}
+            </div>
+
+          <div class="text-grey-6">
+            {{ props.provider.district }}
+          </div>
+
+          <div class="provider-stats">
+
+            <div class="row items-center">
+              <q-icon
+                color="amber"
+                name="mdi-star"
+                size="18px"
+              />
+
+              <span class="q-ml-xs">
+                {{ props.provider.average_rating }}
+              </span>
+
+              <span class="text-grey q-ml-xs">
+                {{ `(${props.provider.total_reviews})` }}
+              </span>
+            </div>
+
+            <div class="row items-center">
+              <q-icon
+                color="deep-purple"
+                name="mdi-broom"
+                size="18px"
+              />
+
+              <span class="q-ml-xs">
+                {{ `(${props.provider.total_services})` }}
+              </span>
+            </div>
+
+          </div>
+
+        </div>
+
+      </q-card-section>
+
+      <q-separator />
+
+      <!-- Especialidades -->
+      <q-list
+        v-if="props.provider.specialities?.length"
+        dense
+        class="q-pa-sm"
+      >
+        <q-item
+          v-for="speciality in props.provider.specialities"
+          :key="speciality.id"
+        >
+          <q-item-section avatar>
+            <q-icon
+              name="mdi-check-circle"
+              color="positive"
+            />
+          </q-item-section>
+
+          <q-item-section>
+            {{ speciality.description }}
+          </q-item-section>
+        </q-item>
+      </q-list>
+
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { useDialogPluginComponent } from "quasar";
+
+const props = defineProps({
+  provider: {
+    type: Object,
+    required: true,
+  },
+});
+
+defineEmits(useDialogPluginComponent.emits);
+
+const {
+  dialogRef,
+  onDialogHide,
+} = useDialogPluginComponent();
+
+const closeDialog = () => {
+  dialogRef.value.hide();
+};
+
+const initials = computed(() => {
+  if (!props.provider.provider_name) {
+    return "?";
+  }
+
+  return props.provider.provider_name
+    .trim()
+    .split(" ")
+    .slice(0, 2)
+    .map(name => name[0].toUpperCase())
+    .join("");
+});
+</script>
+
+<style scoped>
+.provider-dialog {
+  width: 380px;
+  max-width: 95vw;
+  border-radius: 14px;
+  overflow: hidden;
+}
+
+.provider-header {
+  display: flex;
+  align-items: center;
+  gap: 18px;
+  padding: 20px;
+}
+
+.provider-avatar {
+  flex-shrink: 0;
+}
+
+.provider-avatar img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.provider-info {
+  flex: 1;
+}
+
+.provider-info .text-h6 {
+  line-height: 1.2;
+}
+
+.provider-stats {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  margin-top: 8px;
+}
+
+.close-btn {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  z-index: 2;
+}
+
+.q-list .q-item {
+  min-height: 38px;
+}
+</style>

+ 18 - 8
src/components/profile/ProfilePaymentAddDialog.vue

@@ -9,6 +9,7 @@
     <div class="bg-page full-height column no-shadow">
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
         <q-btn
+          class="header-back-btn"
           color="primary"
           dense
           flat
@@ -122,7 +123,7 @@
                 class="col bg-surface input-border-dark"
                 hide-bottom-space
                 input-class="text-text"
-                mask="##/####"
+                mask="##/##"
                 outlined
                 :error="!!serverErrors.expiration"
                 :error-message="serverErrors.expiration"
@@ -158,7 +159,7 @@
                 mask="####"
                 outlined
                 placeholder="***"
-                type="password"
+                type="text"
                 unmasked-value
                 :error="!!serverErrors.cvv"
                 :error-message="serverErrors.cvv"
@@ -425,17 +426,26 @@ const validateExpiration = (val) => {
     return true;
   }
 
-  if (!validateCardExpiration(val)) {
-    return t('profile.payments.expired_card');
+  const [month, year] = val.split("/");
+
+  if (!month || !year) {
+    return t("profile.payments.expired_card");
   }
 
-  return true;
+  return validateCardExpiration(`${month}/20${year}`)
+    ? true
+    : t("profile.payments.expired_card");
 };
 
 //
 
 const onOKClick = async () => {
   const valid = await formRef.value?.validate();
+  const expirationFormatted = (() => {
+  const [month, year] = form.expiration.split("/");
+
+  return `${month}/20${year}`;
+})();
 
   if (!valid) {
     return;
@@ -446,7 +456,7 @@ const onOKClick = async () => {
       brand: form.brand,
       card_name: form.card_name,
       client_id: props.paymentMethod ? props.paymentMethod.client_id : props.clientId,
-      expiration: form.expiration,
+      expiration: expirationFormatted,
       holder_name: form.holder_name,
       is_active: form.is_active,
       last_four_digits: form.last_four_digits,
@@ -464,7 +474,7 @@ const onOKClick = async () => {
 
     const payload = {
       client_id: props.paymentMethod ? props.paymentMethod.client_id : props.clientId,
-      expiration: form.expiration,
+      expiration: expirationFormatted,
       holder_name: form.holder_name,
       last_four_digits: form.last_four_digits,
       token,
@@ -556,4 +566,4 @@ const onOKClick = async () => {
   color: #555;
 }
 
-</style>
+</style>

+ 1 - 1
src/components/profile/ProfilePaymentsDialog.vue

@@ -3,7 +3,7 @@
     <div class="bg-page full-height column no-shadow">
 
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
-        <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
+        <q-btn v-close-popup class="header-back-btn" icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
         <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('profile.payments.subtitle') }}</span>
       <q-space />

+ 123 - 62
src/components/profile/ProfilePrivacyDialog.vue

@@ -1,38 +1,75 @@
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
+  <q-dialog
+    ref="dialogRef"
+    maximized
+    persistent
+    transition-hide="slide-right"
+    transition-show="slide-left"
+  >
     <div class="bg-page full-height column no-shadow">
+      <div
+        class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface"
+      >
+        <q-btn
+          v-close-popup
+          class="header-back-btn"
+          color="primary"
+          dense
+          flat
+          icon="mdi-chevron-left"
+          round
+        />
 
-      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
-        <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
-        <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('profile.privacy.title') }}</span>
+
+        <span
+          class="text-subtitle1 text-primary font16 fontbold gradient-diarista"
+        >
+          {{ $t("profile.privacy.title") }}
+        </span>
+
         <q-space />
+
         <div style="width: 32px"></div>
       </div>
 
-      <div v-if="loading" class="col flex flex-center">
-        <q-spinner color="primary" size="3em" />
+      <div
+        v-if="loading"
+        class="col flex flex-center"
+      >
+        <q-spinner
+          color="primary"
+          size="3em"
+        />
       </div>
 
-      <div v-else-if="blocks.length === 0" class="col column items-center justify-center q-px-xl q-pb-xl">
+      <div
+        v-else-if="blocks.length === 0"
+        class="col column items-center justify-center q-px-xl q-pb-xl"
+      >
         <q-img
-          :src="diarinho"
-          style="width: 220px; height: 220px;"
-          fit="contain"
           class="q-mb-lg"
+          fit="contain"
+          style="width: 220px; height: 220px"
+          :src="diarinho"
         />
+
         <p class="text-center q-mb-xs font16 fontbold text-primary">
-          {{ $t('profile.privacy.empty_message') }}
+          {{ $t("profile.privacy.empty_message") }}
         </p>
+
         <p class="text-grey-6 text-center q-mb-xl font12 fontregular">
-          {{ $t('profile.privacy.empty_sub') }}
+          {{ $t("profile.privacy.empty_sub") }}
         </p>
       </div>
 
-      <div v-else class="col overflow-auto q-pb-xl">
+      <div
+        v-else
+        class="col overflow-auto q-pb-xl"
+      >
         <div class="q-px-md q-mt-md">
           <p class="text-primary q-mb-md font16 fontbold gradient-diarista">
-            {{ $t('profile.privacy.blocked_title') }}
+            {{ $t("profile.privacy.blocked_title") }}
           </p>
 
           <div
@@ -40,94 +77,119 @@
             :key="block.id"
             class="block-item row items-center no-wrap q-mb-md"
           >
-            <q-avatar size="48px" class="flex-shrink-0 q-mr-sm">
+            <q-avatar
+              class="flex-shrink-0 q-mr-sm"
+              size="48px"
+            >
               <span
-                :style="avatarStyle(block)"
                 class="full-width full-height flex flex-center"
-                style="border-radius: 50%;"
+                style="border-radius: 50%"
+                :style="avatarStyle(block)"
               >
-                {{ block.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}
+                {{ block.provider_name?.slice(0, 2).toUpperCase() ?? "??" }}
               </span>
             </q-avatar>
 
             <div class="col column no-wrap overflow-hidden">
-              <span class="font12 fontbold text-text" style="line-height: 1.3;">
+              <span
+                class="font12 fontbold text-text"
+                style="line-height: 1.3"
+              >
                 {{ block.provider_name }}
               </span>
-              <span v-if="block.provider_district" class="text-grey-7 font9 fontbold" >
+
+              <span
+                v-if="block.provider_district"
+                class="text-grey-7 font9 fontbold"
+              >
                 {{ block.provider_district }}
               </span>
-              <div v-if="block.provider_rating" class="row items-center q-mt-xs">
-                <q-icon name="mdi-star" color="amber" size="12px" class="q-mr-xs" />
-                <span class="text-grey-7 font9 fontregular">{{ Number(block.provider_rating).toFixed(1) }}</span>
+
+              <div
+                v-if="block.provider_rating"
+                class="row items-center q-mt-xs"
+              >
+                <q-icon
+                  class="q-mr-xs"
+                  color="amber"
+                  name="mdi-star"
+                  size="12px"
+                />
+
+                <span class="text-grey-7 font9 fontregular">
+                  {{ Number(block.provider_rating).toFixed(1) }}
+                </span>
               </div>
             </div>
 
             <q-btn
+              class="flex-shrink-0"
+              color="primary"
               no-caps
+              padding="4px 12px"
               rounded
-              color="primary"
               size="sm"
-              class="flex-shrink-0"
-              padding="4px 12px"
-              :loading="unblockingId === block.id"
               :label="$t('profile.privacy.unblock_btn')"
+              :loading="unblockingId === block.id"
               @click="unblock(block)"
             />
           </div>
         </div>
       </div>
-
     </div>
   </q-dialog>
 </template>
 
 <script setup>
-import { ref, onMounted } from 'vue'
-import { useDialogPluginComponent } from 'quasar'
-import { userStore } from 'src/stores/user'
-import { getClientProviderBlocks, deleteClientProviderBlock } from 'src/api/clientProviderBlock'
-import diarinho from 'src/assets/diarinho_perfil_cliente_favoritos.svg'
-
-defineEmits([...useDialogPluginComponent.emits])
-const { dialogRef } = useDialogPluginComponent()
-const store = userStore()
-
-const blocks = ref([])
-const loading = ref(false)
-const unblockingId = ref(null)
-
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-  { background: '#ede0ff', color: '#6200ea' },
-]
+import diarinho from "src/assets/diarinho_perfil_cliente_favoritos.svg";
+
+import { avatarColorsWithPurple as avatarColors } from "src/helpers/avatarColors";
+
+import {
+  deleteClientProviderBlock,
+  getClientProviderBlocks,
+} from "src/api/clientProviderBlock";
+
+import { onMounted, ref } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import { userStore } from "src/stores/user";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef } = useDialogPluginComponent();
+const store = userStore();
+
+const blocks = ref([]);
+const loading = ref(false);
+const unblockingId = ref(null);
 
 const avatarStyle = (block) => {
-  const idx = (block?.provider_id ?? 0) % avatarColors.length
-  return avatarColors[idx]
-}
+  const idx = (block?.provider_id ?? 0) % avatarColors.length;
+
+  return avatarColors[idx];
+};
 
 const unblock = async (block) => {
-  unblockingId.value = block.id
+  unblockingId.value = block.id;
+
   try {
-    await deleteClientProviderBlock(block.id)
-    blocks.value = blocks.value.filter(b => b.id !== block.id)
+    await deleteClientProviderBlock(block.id);
+
+    blocks.value = blocks.value.filter((b) => b.id !== block.id);
   } finally {
-    unblockingId.value = null
+    unblockingId.value = null;
   }
-}
+};
 
 onMounted(async () => {
-  loading.value = true
+  loading.value = true;
+
   try {
-    blocks.value = await getClientProviderBlocks(store.user.client.id) ?? []
+    blocks.value = (await getClientProviderBlocks(store.user.client.id)) ?? [];
   } finally {
-    loading.value = false
+    loading.value = false;
   }
-})
+});
 </script>
 
 <style scoped lang="scss">
@@ -140,7 +202,6 @@ onMounted(async () => {
   padding: 12px;
 }
 
-
 .flex-shrink-0 {
   flex-shrink: 0;
 }

+ 1 - 1
src/components/profile/SupportRequestDialog.vue

@@ -3,7 +3,7 @@
     <div class="bg-page full-height column no-shadow support-dialog">
 
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
-        <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
+        <q-btn v-close-popup class="header-back-btn" icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
         <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('support_request.title') }}</span>
         <q-space />

+ 83 - 78
src/composables/useInputRules.js

@@ -1,18 +1,91 @@
 import { useI18n } from "vue-i18n";
 
+const isValidCNPJ = (cnpj) => {
+  if (!cnpj) return false;
+
+  cnpj = cnpj.replace(/[^\d]+/g, "");
+
+  if (cnpj.length !== 14) return false;
+  if (/^(\d)\1+$/.test(cnpj)) return false;
+
+  let length = cnpj.length - 2;
+  let numbers = cnpj.substring(0, length);
+  let digits = cnpj.substring(length);
+  let sum = 0;
+  let pos = length - 7;
+
+  for (let i = length; i >= 1; i--) {
+    sum += parseInt(numbers.charAt(length - i)) * pos--;
+
+    if (pos < 2) pos = 9;
+  }
+
+  let result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
+
+  if (result !== parseInt(digits.charAt(0))) return false;
+
+  length = length + 1;
+  numbers = cnpj.substring(0, length);
+  sum = 0;
+  pos = length - 7;
+
+  for (let i = length; i >= 1; i--) {
+    sum += parseInt(numbers.charAt(length - i)) * pos--;
+
+    if (pos < 2) pos = 9;
+  }
+
+  result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
+
+  if (result !== parseInt(digits.charAt(1))) return false;
+
+  return true;
+};
+
+const isValidCPF = (cpf) => {
+  if (!cpf) return false;
+
+  cpf = cpf.replace(/[^\d]+/g, "");
+
+  if (cpf.length !== 11) return false;
+  if (/^(\d)\1+$/.test(cpf)) return false;
+
+  let sum = 0;
+
+  for (let i = 0; i < 9; i++) sum += parseInt(cpf.charAt(i)) * (10 - i);
+
+  let rev = 11 - (sum % 11);
+
+  if (rev === 10 || rev === 11) rev = 0;
+
+  if (rev !== parseInt(cpf.charAt(9))) return false;
+
+  sum = 0;
+
+  for (let i = 0; i < 10; i++) sum += parseInt(cpf.charAt(i)) * (11 - i);
+
+  rev = 11 - (sum % 11);
+
+  if (rev === 10 || rev === 11) rev = 0;
+
+  if (rev !== parseInt(cpf.charAt(10))) return false;
+
+  return true;
+};
+
 export const useInputRules = () => {
   const { t } = useI18n();
 
   const emailPattern =
     /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
 
- const passwordPattern = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
+  const passwordPattern = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
 
- const cepPattern = /^[0-9]{5}-[0-9]{3}$/;
+  const cepPattern = /^[0-9]{5}-[0-9]{3}$/;
 
   const inputRules = {
-    required:            (value) => !!value || t("validation.rules.required"),
-    requiredNumber:      (value) => !isNaN(value) || t("validation.rules.required"),
+    required: (value) => !!value || t("validation.rules.required"),
+    requiredNumber: (value) => !isNaN(value) || t("validation.rules.required"),
     requiredHideMessage: (value) => !!value,
 
     min: (min) => (value) =>
@@ -32,6 +105,11 @@ export const useInputRules = () => {
     email: (value) =>
       !value || emailPattern.test(value) || t("validation.rules.email"),
 
+    phone: (value) =>
+      !value ||
+      String(value).replace(/\D/g, "").length >= 10 ||
+      t("validation.rules.phone"),
+
     emails: (value) => {
       if (!value) return true;
 
@@ -43,7 +121,7 @@ export const useInputRules = () => {
       );
     },
 
-    cpf:  (value) => !value || isValidCPF(value) || t("validation.rules.cpf"),
+    cpf: (value) => !value || isValidCPF(value) || t("validation.rules.cpf"),
     cnpj: (value) => !value || isValidCNPJ(value) || t("validation.rules.cnpj"),
 
     cpfOrCnpj: (value) => {
@@ -93,76 +171,3 @@ export const useInputRules = () => {
     inputRules,
   };
 };
-
-function isValidCPF(cpf) {
-  if (!cpf) return false;
-
-  cpf = cpf.replace(/[^\d]+/g, "");
-
-  if (cpf.length !== 11)     return false;
-  if (/^(\d)\1+$/.test(cpf)) return false;
-
-  let sum = 0;
-
-  for (let i = 0; i < 9; i++) sum += parseInt(cpf.charAt(i)) * (10 - i);
-
-  let rev = 11 - (sum % 11);
-
-  if (rev === 10 || rev === 11) rev = 0;
-
-  if (rev !== parseInt(cpf.charAt(9))) return false;
-
-  sum = 0;
-
-  for (let i = 0; i < 10; i++) sum += parseInt(cpf.charAt(i)) * (11 - i);
-
-  rev = 11 - (sum % 11);
-
-  if (rev === 10 || rev === 11) rev = 0;
-
-  if (rev !== parseInt(cpf.charAt(10))) return false;
-
-  return true;
-}
-
-function isValidCNPJ(cnpj) {
-  if (!cnpj) return false;
-
-  cnpj = cnpj.replace(/[^\d]+/g, "");
-
-  if (cnpj.length !== 14)     return false;
-  if (/^(\d)\1+$/.test(cnpj)) return false;
-
-  let length  = cnpj.length - 2;
-  let numbers = cnpj.substring(0, length);
-  let digits  = cnpj.substring(length);
-  let sum     = 0;
-  let pos     = length - 7;
-
-  for (let i = length; i >= 1; i--) {
-    sum += parseInt(numbers.charAt(length - i)) * pos--;
-
-    if (pos < 2) pos = 9;
-  }
-
-  let result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
-
-  if (result !== parseInt(digits.charAt(0))) return false;
-
-  length  = length + 1;
-  numbers = cnpj.substring(0, length);
-  sum     = 0;
-  pos     = length - 7;
-
-  for (let i = length; i >= 1; i--) {
-    sum += parseInt(numbers.charAt(length - i)) * pos--;
-
-    if (pos < 2) pos = 9;
-  }
-
-  result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
-
-  if (result !== parseInt(digits.charAt(1))) return false;
-
-  return true;
-}

+ 11 - 9
src/composables/usePaymentPlatformFees.js

@@ -2,9 +2,10 @@ import { getPaymentPlatformFees } from 'src/api/payment'
 import { ref } from 'vue'
 
 const platformFees = ref({
-  pix:                  null,
-  credit_card:          null,
-  cart_min_3_schedules: null,
+  pix: null,
+  credit_card: null,
+  service_package_min_3_schedules_credit_card: null,
+  service_package_min_3_schedules_pix: null,
 })
 
 let loadingPromise = null
@@ -37,12 +38,13 @@ const normalizePlatformFees = (fees) => {
       fees?.credit_card_fee_rate,
       fees?.platform_credit_card_fee_rate,
     )),
-    cart_min_3_schedules: normalizeFee(firstDefined(
-      fees?.cart_min_3_schedules,
-      fees?.cartMin3Schedules,
-      fees?.cart_min_3_schedules_fee,
-      fees?.cart_min_3_schedules_fee_rate,
-      fees?.platform_cart_min_3_schedules_fee_rate,
+    service_package_min_3_schedules_credit_card: normalizeFee(firstDefined(
+      fees?.service_package_min_3_schedules_credit_card,
+      fees?.servicePackageMin3SchedulesCreditCard,
+    )),
+    service_package_min_3_schedules_pix: normalizeFee(firstDefined(
+      fees?.service_package_min_3_schedules_pix,
+      fees?.servicePackageMin3SchedulesPix,
     )),
   }
 

+ 46 - 34
src/composables/useScroll.js

@@ -1,4 +1,10 @@
-import { unref } from 'vue'
+import { unref } from "vue";
+
+const resolveElement = (reference) => {
+  const value = unref(reference);
+
+  return value?.$el ?? value;
+};
 
 export const useScroll = () => {
   /**
@@ -11,49 +17,55 @@ export const useScroll = () => {
    * @returns {boolean} - Whether the scroll was successful
    */
   const scrollToComponent = (targetRef, containerRef, options = {}) => {
-    const {
-      offset = 50,
-      duration = 150
-    } = options
+    const { offset = 50, duration = 150 } = options;
 
-    const target = unref(targetRef)
-    const container = unref(containerRef)
+    const targetElement = resolveElement(targetRef);
+    const container = unref(containerRef);
+    const containerElement = resolveElement(containerRef);
 
-    const targetElement = target?.$el
-    const containerElement = container?.$el
-
-    if (!targetElement || !containerElement) {
-      console.warn('useScroll: Target or container element not found')
-      return false
+    if (
+      !targetElement?.getBoundingClientRect ||
+      !containerElement?.getBoundingClientRect
+    ) {
+      console.warn("useScroll: Target or container element not found");
+      return false;
     }
 
     try {
-      let currentElement = targetElement
-      let offsetTop = 0
-
-      // Calculate total offset up to the scroll container
-      while (currentElement && currentElement !== containerElement) {
-        offsetTop += currentElement.offsetTop
-        currentElement = currentElement.offsetParent
+      if (!containerElement.contains(targetElement)) {
+        console.warn("useScroll: Target is not a child of the container");
+        return false;
       }
 
-      if (!currentElement) {
-        console.warn('useScroll: Target is not a child of the container')
-        return false
-      }
-
-      const targetPosition = Math.max(0, offsetTop - offset)
+      const currentPosition =
+        container?.getScrollPosition?.().top ?? containerElement.scrollTop ?? 0;
+      const targetPosition = Math.max(
+        0,
+        currentPosition +
+          targetElement.getBoundingClientRect().top -
+          containerElement.getBoundingClientRect().top -
+          offset,
+      );
 
-      container.setScrollPosition('vertical', targetPosition, duration)
+      if (typeof container?.setScrollPosition === "function") {
+        container.setScrollPosition("vertical", targetPosition, duration);
+      } else if (typeof containerElement.scrollTo === "function") {
+        containerElement.scrollTo({
+          top: targetPosition,
+          behavior: duration > 0 ? "smooth" : "auto",
+        });
+      } else {
+        containerElement.scrollTop = targetPosition;
+      }
 
-      return true
+      return true;
     } catch (error) {
-      console.error('useScroll: Error while scrolling', error)
-      return false
+      console.error("useScroll: Error while scrolling", error);
+      return false;
     }
-  }
+  };
 
   return {
-    scrollToComponent
-  }
-}
+    scrollToComponent,
+  };
+};

+ 36 - 1
src/css/app.scss

@@ -1,6 +1,35 @@
 @use "sass:map";
 @use "src/css/quasar.variables.scss";
 
+:root {
+  --system-bar-bg: #757575;
+}
+
+body::before,
+body::after {
+  content: "";
+  position: fixed;
+  left: 0;
+  right: 0;
+  background: var(--system-bar-bg);
+  pointer-events: none;
+  z-index: 10000;
+}
+
+body::before {
+  top: 0;
+  height: env(safe-area-inset-top, 0px);
+}
+
+body::after {
+  bottom: 0;
+  height: env(safe-area-inset-bottom, 0px);
+}
+
+.system-bar-spacer {
+  background: var(--system-bar-bg) !important;
+}
+
 .q-page {
   font-family: "Inter", sans-serif;
 }
@@ -108,6 +137,12 @@ input[type="number"]::-webkit-outer-spin-button {
   }
 }
 
+// Keep the back-arrow glyph aligned with the horizontal content gutter while
+// preserving the full round touch target and the centered header title.
+.header-back-btn {
+  transform: translateX(-16px);
+}
+
 .q-field .q-field__control {
   border: 1.5px solid #e4e4e4 !important;
   border-radius: 4px !important;
@@ -383,4 +418,4 @@ box-shadow: 1px 4px 4px 0px rgba(0,0,0,0.2);
       color: #1E293B !important;
     }
   }
-}
+}

+ 11 - 0
src/helpers/avatarColors.js

@@ -0,0 +1,11 @@
+export const avatarColors = [
+  { background: "#ffd5df", color: "#932e57" },
+  { background: "#d7e8ff", color: "#2158a8" },
+  { background: "#dfd", color: "#2a7a3b" },
+  { background: "#ffe5cc", color: "#8a4500" },
+];
+
+export const avatarColorsWithPurple = [
+  ...avatarColors,
+  { background: "#ede0ff", color: "#6200ea" },
+];

+ 19 - 4
src/helpers/paymentPlatformFees.js

@@ -1,11 +1,26 @@
-export const scheduleUsesCartDiscount = (schedule) =>
-  Number(schedule?.cart_items_count ?? schedule?.cartItemsCount ?? schedule?.cart?.items_count ?? 0) >= 3
+export const scheduleUsesServicePackageDiscount = (item) => {
+  const count = Number(
+    item?.schedules?.[0]?.service_package_items_count
+    ?? item?.service_package_items_count
+    ?? item?.cart_items_count
+    ?? item?.cartItemsCount
+    ?? item?.cart?.items_count
+    ?? 0
+  )
+  return count >= 3
+}
 
 export const getSchedulePlatformFeeRate = (schedule, paymentType, platformFees) => {
   const fees = platformFees?.value ?? platformFees ?? {}
 
-  if (scheduleUsesCartDiscount(schedule) && fees.cart_min_3_schedules !== null && fees.cart_min_3_schedules !== undefined) {
-    return fees.cart_min_3_schedules
+  if (scheduleUsesServicePackageDiscount(schedule)) {
+    const key = paymentType === 'credit_card'
+      ? 'service_package_min_3_schedules_credit_card'
+      : 'service_package_min_3_schedules_pix'
+
+    if (fees[key] !== null && fees[key] !== undefined) {
+      return fees[key]
+    }
   }
 
   return fees[paymentType]

+ 9 - 0
src/helpers/profileMedia.js

@@ -0,0 +1,9 @@
+export const getProfileMediaUrl = (resource) =>
+  resource?.profile_media_url ??
+  resource?.profile_media?.url ??
+  resource?.provider_photo ??
+  resource?.client?.profile_media_url ??
+  resource?.client?.profile_media?.url ??
+  resource?.provider?.profile_media_url ??
+  resource?.provider?.profile_media?.url ??
+  null;

+ 25 - 0
src/helpers/provider.js

@@ -0,0 +1,25 @@
+const genderTranslationKeys = {
+  female: "genders.female",
+  male: "genders.male",
+  other: "genders.not_informed",
+  prefer_not_to_say: "genders.not_informed",
+};
+
+export const getProviderGenderLabel = (provider, translate) => {
+  const providerData =
+    provider?.provider ?? provider?.user?.provider ?? provider ?? {};
+  const genderLabel =
+    providerData.gender_label ?? provider?.provider_gender_label;
+  const gender = providerData.gender ?? provider?.provider_gender;
+
+  if (!gender && !genderLabel) return "";
+
+  const translatedGender =
+    typeof translate === "function" && genderTranslationKeys[gender]
+      ? translate(genderTranslationKeys[gender])
+      : genderLabel || gender;
+
+  if (typeof translate !== "function") return translatedGender;
+
+  return translate("common.terms.gender") + ": " + translatedGender;
+};

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

@@ -4,6 +4,7 @@
     "actions": {
       "save": "Save",
       "cancel": "Cancel",
+      "close": "Close",
       "edit": "Edit",
       "add": "Add",
       "select": "request",
@@ -54,6 +55,7 @@
       "payment_amount": "Payment Amount",
       "language": "Language",
       "currency": "Currency",
+      "gender": "Gender",
       "interests": "Interests",
       "avatar": "Avatar",
       "banner": "Banner",
@@ -188,6 +190,7 @@
     "rules": {
       "required": "This field is required",
       "email": "This field must be a valid email | These fields must be valid emails",
+      "phone": "This field must be a valid phone number",
       "date": "This field must be a valid date",
       "min": "This field must have at least",
       "max": "This field must have at most",
@@ -436,7 +439,8 @@
       "in_progress": "in progress",
       "end_time_label": "Ends at",
       "rate_btn": "Rate",
-      "help_btn": "help"
+      "help_btn": "help",
+      "cancelled": "Service cancelled"
     },
     "next_schedules": {
       "title": "Next services",
@@ -523,6 +527,7 @@
       "detail_value": "Amount:",
       "detail_service_fee": "Total:",
       "detail_total": "Total:",
+      "detail_package_total": "Package total (Pix):",
       "detail_pix_total": "Pix total:",
       "detail_credit_card_total": "Credit card total:",
       "detail_pix_discount": "Pix discount:",
@@ -592,7 +597,7 @@
       "remove_cancel": "Cancel",
       "active": "Active",
       "inactive": "Inactive",
-      "mmyyyy": "mm/yyyy"
+      "mmyyyy": "mm/yy"
     },
     "favorites": {
       "title": "Favorites",
@@ -635,7 +640,7 @@
       "add_new_address_label": "Add a new address for your services",
       "edit_address": "Edit address",
       "edit": "Edit",
-      "mark_as_primary": "Mark as primary",
+      "mark_as_primary": "Use this",
       "main": "Primary",
       "no_addresses": "No addresses registered",
       "marked_as_primary": "Address marked as primary",
@@ -743,7 +748,7 @@
     "unknown_client": "Client",
     "select_service": "Select service",
     "no_slots_available": "No time slots available for this day.",
-    "book": "add to cart",
+    "book": "add to orders",
     "no_price": "to be agreed",
     "time_selection": {
       "subtitle": "Select the desired start and end time for the service.",
@@ -795,13 +800,13 @@
       "no_primary_address": "Please add a primary address in your profile to schedule a service."
     }
   },
-  "schedule_cart": {
-    "title": "Cart",
-    "empty_title": "Your cart is empty",
+  "service_package": {
+    "title": "Orders",
+    "empty_title": "You do not have any orders yet",
     "empty_message": "Add time slots from favorite professionals to send a request.",
-    "add_to_cart": "Add to cart",
-    "add_success": "Time slot added to cart.",
-    "local_cart_title": "Cart",
+    "add_to_cart": "Add to orders",
+    "add_success": "Time slot added to orders.",
+    "local_cart_title": "Orders",
     "orders_title": "Orders",
     "no_orders": "No orders created.",
     "order_title": "Order #{id}",
@@ -822,17 +827,18 @@
     "credit_card_fee": "Card total",
     "credit_card_total": "Card total",
     "pix_discount": "Pix discount: save R$ {value}",
-    "cart_fee_discount": "Cart total discount: R$ {value}",
-    "cart_discount_hint": "Add {count} more time slot(s) to get the cart discount.",
-    "cart_discount_applied": "Discount applied: 3 or more time slots in the cart.",
-    "cart_discount_total": "Total",
+    "discount_saved": "Orders total discount: R$ {value}",
+    "discount_hint": "Add {count} more time slot(s) to get the orders discount.",
+    "discount_applied": "Discount applied: 3 or more time slots in the orders.",
+    "discount_total": "Total",
     "provider": "Professional",
     "address": "Address",
     "quantity": "Quantity",
     "schedule_count": "{count} time slot(s)",
     "submit": "send request",
-    "close_cart": "close cart",
-    "cart_closed_success": "Cart closed. Wait for provider approval.",
+    "close": "close orders",
+    "closed_success": "Orders closed. Wait for provider approval.",
+    "same_provider_required": "All time slots must be from the same provider.",
     "unknown": "No information",
     "multiple_providers": "{count} professionals",
     "loading": "Loading...",
@@ -895,7 +901,7 @@
     "home": "Home",
     "search": "Search",
     "agenda": "Schedule",
-    "cart": "Cart",
+    "cart": "Orders",
     "profile": "Profile"
   },
   "provider": {
@@ -913,6 +919,11 @@
       }
     }
   },
+  "genders": {
+    "female": "Female",
+    "male": "Male",
+    "not_informed": "Not informed"
+  },
   "support_request": {
     "title": "Talk to an agent",
     "subtitle": "Fill in the fields below and a human agent will get in touch with you.",

+ 30 - 18
src/i18n/locales/es.json

@@ -4,6 +4,7 @@
     "actions": {
       "save": "Guardar",
       "cancel": "Cancelar",
+      "close": "Cerrar",
       "edit": "Editar",
       "add": "Añadir",
       "select": "pedido",
@@ -54,6 +55,7 @@
       "payment_amount": "Monto del pago",
       "language": "Idioma",
       "currency": "Moneda",
+      "gender": "Género",
       "interests": "Intereses",
       "avatar": "Avatar",
       "banner": "Banner",
@@ -188,6 +190,7 @@
     "rules": {
       "required": "Este campo es obligatorio",
       "email": "Este campo debe ser un correo electrónico válido | Estos campos deben ser correos electrónicos válidos",
+      "phone": "Este campo debe ser un número de teléfono válido",
       "date": "Este campo debe ser una fecha válida",
       "min": "Este campo debe tener al menos",
       "max": "Este campo debe tener como máximo",
@@ -436,7 +439,8 @@
       "in_progress": "en progreso",
       "end_time_label": "Término a las",
       "rate_btn": "Avaliar",
-      "help_btn": "ayuda"
+      "help_btn": "ayuda",
+      "cancelled": "Servicio cancelado"
     },
     "next_schedules": {
       "title": "Próximos servicios",
@@ -451,7 +455,8 @@
       "place_home": "casa",
       "place_apartment": "apartamento",
       "place_unknown": "N/A",
-      "details": "ver detalles"
+      "details": "ver detalles",
+      "default_service": "Limpieza estándar"
     },
     "client_proposals": {
       "candidate": "Candidato",
@@ -519,6 +524,7 @@
       "detail_value": "Valor:",
       "detail_service_fee": "Total:",
       "detail_total": "Total:",
+      "detail_package_total": "Total del paquete (Pix):",
       "detail_pix_total": "Total en Pix:",
       "detail_credit_card_total": "Total en tarjeta de crédito:",
       "detail_pix_discount": "Descuento en Pix:",
@@ -588,7 +594,7 @@
       "remove_cancel": "Cancelar",
       "active": "Activa",
       "inactive": "Inactiva",
-      "mmyyyy": "mm/aaaa"
+      "mmyyyy": "mm/aa"
     },
     "favorites": {
       "title": "Favoritos",
@@ -631,7 +637,7 @@
       "add_new_address_label": "Agrega una nueva dirección para tus servicios",
       "edit_address": "Editar dirección",
       "edit": "Editar",
-      "mark_as_primary": "Marcar como principal",
+      "mark_as_primary": "Usar esta",
       "main": "Principal",
       "no_addresses": "Sin direcciones registradas",
       "marked_as_primary": "Dirección marcada como principal",
@@ -739,7 +745,7 @@
     "unknown_client": "Cliente",
     "select_service": "Seleccionar servicio",
     "no_slots_available": "No hay horarios disponibles para este día.",
-    "book": "agregar al carrito",
+    "book": "agregar a pedidos",
     "no_price": "a convenir",
     "time_selection": {
       "subtitle": "Seleccione el horario deseado para inicio y fin del servicio.",
@@ -791,13 +797,13 @@
       "no_primary_address": "Agregue una dirección principal en su perfil para agendar un servicio."
     }
   },
-  "schedule_cart": {
-    "title": "Carrito",
-    "empty_title": "Su carrito está vacío",
+  "service_package": {
+    "title": "Pedidos",
+    "empty_title": "Todavía no tiene pedidos",
     "empty_message": "Agregue horarios de profesionales favoritos para enviar una solicitud.",
-    "add_to_cart": "Agregar al carrito",
-    "add_success": "Horario agregado al carrito.",
-    "local_cart_title": "Carrito",
+    "add_to_cart": "Agregar a pedidos",
+    "add_success": "Horario agregado a pedidos.",
+    "local_cart_title": "Pedidos",
     "orders_title": "Pedidos",
     "no_orders": "Ningún pedido creado.",
     "order_title": "Pedido #{id}",
@@ -818,17 +824,18 @@
     "credit_card_fee": "Total con tarjeta",
     "credit_card_total": "Total con tarjeta",
     "pix_discount": "Descuento con Pix: ahorre R$ {value}",
-    "cart_fee_discount": "Descuento en el total del carrito: R$ {value}",
-    "cart_discount_hint": "Agregue {count} horario(s) más para recibir descuento en el carrito.",
-    "cart_discount_applied": "Descuento aplicado: 3 o más horarios en el carrito.",
-    "cart_discount_total": "Total",
+    "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_applied": "Descuento aplicado: 3 o más horarios en los pedidos.",
+    "discount_total": "Total",
     "provider": "Profesional",
     "address": "Dirección",
     "quantity": "Cantidad",
     "schedule_count": "{count} horario(s)",
     "submit": "enviar solicitud",
-    "close_cart": "cerrar carrito",
-    "cart_closed_success": "Carrito cerrado. Espere la aprobación de los profesionales.",
+    "close": "cerrar pedidos",
+    "closed_success": "Pedidos cerrados. Espere la aprobación de los profesionales.",
+    "same_provider_required": "Todos los horarios deben ser del mismo profesional.",
     "unknown": "Sin información",
     "multiple_providers": "{count} profesionales",
     "loading": "Cargando...",
@@ -891,7 +898,7 @@
     "home": "Inicio",
     "search": "Buscar",
     "agenda": "Agenda",
-    "cart": "Carrito",
+    "cart": "Pedidos",
     "profile": "Perfil"
   },
   "provider": {
@@ -909,6 +916,11 @@
       }
     }
   },
+  "genders": {
+    "female": "Femenino",
+    "male": "Masculino",
+    "not_informed": "No informado"
+  },
   "support_request": {
     "title": "Hablar con un agente",
     "subtitle": "Completa los campos a continuación y un agente humano se pondrá en contacto contigo.",

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

@@ -4,6 +4,7 @@
     "actions": {
       "save": "Salvar",
       "cancel": "Cancelar",
+      "close": "Fechar",
       "edit": "Editar",
       "add": "Adicionar",
       "select": "solicitar",
@@ -54,6 +55,7 @@
       "payment_amount": "Valor do Pagamento",
       "language": "Idioma",
       "currency": "Moeda",
+      "gender": "Gênero",
       "interests": "Interesses",
       "avatar": "Avatar",
       "banner": "Banner",
@@ -188,6 +190,7 @@
     "rules": {
       "required": "Este campo é obrigatório",
       "email": "Este campo deve ser um e-mail válido | Estes campos devem ser e-mails válidos",
+      "phone": "Este campo deve ser um telefone válido",
       "date": "Este campo deve ser uma data válida",
       "min": "Este campo deve ter no mínimo",
       "max": "Este campo deve ter no máximo",
@@ -436,7 +439,8 @@
       "in_progress": "em andamento",
       "end_time_label": "Término às",
       "rate_btn": "Avaliar",
-      "help_btn": "ajuda"
+      "help_btn": "ajuda",
+      "cancelled": "Serviço cancelado"
     },
     "next_schedules": {
       "title": "Próximos serviços",
@@ -453,7 +457,7 @@
       "place_unknown": "N/A",
       "details": "ver detalhes",
       "provider_age_unit": "anos",
-      "default_service": "Faxina padrão",
+      "default_service": "Limpeza padrão",
       "btn_close": "fechar",
       "btn_help": "Ajuda"
     },
@@ -523,6 +527,7 @@
       "detail_value": "Valor:",
       "detail_service_fee": "Total:",
       "detail_total": "Total:",
+      "detail_package_total": "Total do pacote (Pix):",
       "detail_pix_total": "Total no Pix:",
       "detail_credit_card_total": "Total no cartão de crédito:",
       "detail_pix_discount": "Desconto no Pix:",
@@ -592,7 +597,7 @@
       "remove_cancel": "Cancelar",
       "active": "Ativo",
       "inactive": "Inativo",
-      "mmyyyy": "mm/aaaa"
+      "mmyyyy": "mm/aa"
     },
     "favorites": {
       "title": "Favoritos",
@@ -635,7 +640,7 @@
       "add_new_address_label": "Adicione um novo endereço para agendamentos",
       "edit_address": "Editar endereço",
       "edit": "Editar",
-      "mark_as_primary": "Marcar como principal",
+      "mark_as_primary": "Usar este",
       "main": "Principal",
       "no_addresses": "Nenhum endereço cadastrado",
       "marked_as_primary": "Endereço marcado como principal",
@@ -751,7 +756,7 @@
     "unknown_client": "Cliente",
     "select_service": "Selecionar serviço",
     "no_slots_available": "Não há horários disponíveis para este dia.",
-    "book": "carrinho",
+    "book": "pedidos",
     "no_price": "a combinar",
     "time_selection": {
       "subtitle": "Selecione o horário desejado para início e término do serviço.",
@@ -803,13 +808,13 @@
       "no_primary_address": "Cadastre um endereço principal no seu perfil para agendar."
     }
   },
-  "schedule_cart": {
-    "title": "Carrinho",
-    "empty_title": "Seu carrinho está vazio",
+  "service_package": {
+    "title": "Pedidos",
+    "empty_title": "Você ainda não tem pedidos",
     "empty_message": "Adicione horários de profissionais favoritos para enviar uma solicitação.",
-    "add_to_cart": "Adicionar ao carrinho",
-    "add_success": "Horário adicionado ao carrinho.",
-    "local_cart_title": "Carrinho",
+    "add_to_cart": "Adicionar aos pedidos",
+    "add_success": "Horário adicionado aos pedidos.",
+    "local_cart_title": "Pedidos",
     "orders_title": "Pedidos",
     "no_orders": "Nenhum pedido criado.",
     "order_title": "Pedido #{id}",
@@ -830,17 +835,18 @@
     "credit_card_fee": "Total no cartão",
     "credit_card_total": "Total no cartão",
     "pix_discount": "Desconto no Pix: economize R$ {value}",
-    "cart_fee_discount": "Desconto no total do carrinho: R$ {value}",
-    "cart_discount_hint": "Adicione mais {count} horário(s) para receber desconto no carrinho.",
-    "cart_discount_applied": "Desconto aplicado: 3 ou mais horários no carrinho.",
-    "cart_discount_total": "Total",
+    "discount_saved": "Desconto no total dos pedidos: R$ {value}",
+    "discount_hint": "Adicione mais {count} horário(s) para receber desconto nos pedidos.",
+    "discount_applied": "Desconto aplicado: 3 ou mais horários nos pedidos.",
+    "discount_total": "Total",
     "provider": "Profissional",
     "address": "Endereço",
     "quantity": "Quantidade",
     "schedule_count": "{count} horário(s)",
     "submit": "enviar solicitação",
-    "close_cart": "fechar carrinho",
-    "cart_closed_success": "Carrinho fechado. Aguarde a aprovação dos profissionais.",
+    "close": "fechar pedidos",
+    "closed_success": "Pedidos fechados. Aguarde a aprovação dos profissionais.",
+    "same_provider_required": "Todos os horários devem ser do mesmo profissional.",
     "unknown": "Sem informação",
     "multiple_providers": "{count} profissionais",
     "loading": "Carregando...",
@@ -903,7 +909,7 @@
     "home": "Início",
     "search": "Busca",
     "agenda": "Agenda",
-    "cart": "Carrinho",
+    "cart": "Pedidos",
     "profile": "Perfil"
   },
   "provider": {
@@ -926,6 +932,11 @@
     "unread": "Não lidas",
     "mark_all_read": " Marcar todas como lidas"
   },
+  "genders": {
+    "female": "Feminino",
+    "male": "Masculino",
+    "not_informed": "Não informado"
+  },
   "support_request": {
     "title": "Falar com atendente",
     "subtitle": "Preencha os campos abaixo e um atendente humano entrará em contato com você.",

+ 64 - 38
src/layouts/MainLayout.vue

@@ -1,22 +1,26 @@
 <template>
-  <q-layout class="main-layout relative" view="hHh lpR fFf">
+  <q-layout
+    class="main-layout relative"
+    view="hHh lpR fFf"
+  >
     <q-header
       v-if="$q.screen.lt.sm"
-      class="bg-surface"
-      :style="{
-        height: `env(safe-area-inset-top)`,
-      }"
+      class="system-bar-spacer"
+      :style="{ height: `env(safe-area-inset-top)` }"
     >
     </q-header>
 
     <q-page-container>
-      <q-page class="bg-surface main-layout-page" style="overflow: hidden;">
-        <q-scroll-area
-          ref="scrollAreaRef"
-          class="main-layout-scroll-area"
-          :content-active-style="{ width: '100%', maxWidth: '100%', overflowX: 'hidden', boxSizing: 'border-box' }"
-          :content-style="{ width: '100%', maxWidth: '100%', overflowX: 'hidden', boxSizing: 'border-box' }"
-          :style="scrollAreaHeight"
+      <q-page
+        class="bg-surface main-layout-page"
+        style="overflow: hidden"
+      >
+      <q-scroll-area
+        ref="scrollAreaRef"
+        class="main-layout-scroll-area"
+        :content-active-style="{ width: '100%', maxWidth: '100%', overflowX: 'hidden', boxSizing: 'border-box' }"
+        :content-style="{ width: '100%', maxWidth: '100%', overflowX: 'hidden', boxSizing: 'border-box' }"
+        :style="scrollAreaHeight"
         >
           <router-view v-slot="{ Component }">
             <Transition mode="out-in">
@@ -31,24 +35,31 @@
       </q-page>
     </q-page-container>
 
-    <q-footer v-if="$q.screen.lt.sm" class="provider-bottom-nav bg-white">
+    <q-footer
+      v-if="$q.screen.lt.sm"
+      class="provider-bottom-nav bg-white"
+    >
       <nav class="provider-bottom-nav-inner">
         <router-link
           v-for="item in navItems"
           :key="item.name"
-          :to="{ name: item.name }"
           :class="{ 'provider-bottom-nav-item--active': isNavItemActive(item) }"
+          :to="{ name: item.name }"
           class="provider-bottom-nav-item"
         >
           <span class="provider-bottom-nav-icon-wrap">
-            <q-icon :name="item.icon" class="provider-bottom-nav-icon" />
+            <q-icon
+              class="provider-bottom-nav-icon"
+              :name="item.icon"
+            />
+
             <q-badge
-              v-if="item.name === 'ScheduleCartPage' && cartCount"
+              v-if="item.name === 'ScheduleCartPage' && servicePackageCount"
               color="secondary"
               floating
               rounded
             >
-              {{ cartCount }}
+              {{ servicePackageCount }}
             </q-badge>
           </span>
 
@@ -57,6 +68,8 @@
           </span>
         </router-link>
       </nav>
+
+      <div class="system-bar-spacer provider-bottom-nav-safe-area"></div>
     </q-footer>
   </q-layout>
 </template>
@@ -66,43 +79,53 @@ import { computed, useTemplateRef, watch } from "vue";
 import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
 import { useRoute } from "vue-router";
-import { useScheduleCartStore } from "src/stores/scheduleCart";
+import { useServicePackageStore } from "src/stores/servicePackage";
 
 defineOptions({
   name: "MainLayout",
 });
 
-const $q    = useQuasar();
+const { t } = useI18n();
 const route = useRoute();
-
 const scrollAreaRef = useTemplateRef("scrollAreaRef");
+const $q = useQuasar();
 
-const { t } = useI18n();
+const servicePackage = useServicePackageStore();
 
-let oldValue = route.path;
+const servicePackageCount = computed(() => servicePackage.items.length);
 
 const navItems = computed(() => [
-  { name: "DashboardPage",     label: t('nav.home'),    icon: "mdi-home-outline" },
-  { name: "SearchPage",        label: t('nav.search'),  icon: "mdi-magnify" },
-  { name: "CalendarPage",      label: t('nav.agenda'),  icon: "mdi-calendar-blank-outline" },
-  { name:  "ScheduleCartPage", label: t('nav.cart'),    icon: "mdi-cart-outline" },
-  { name:  "ProfilePage",      label: t('nav.profile'), icon: "mdi-account-circle-outline" },
+  { name: "DashboardPage", label: t("nav.home"), icon: "mdi-home-outline" },
+  { name: "SearchPage", label: t("nav.search"), icon: "mdi-magnify" },
+  {
+    name: "CalendarPage",
+    label: t("nav.agenda"),
+    icon: "mdi-calendar-blank-outline",
+  },
+  {
+    name: "ScheduleCartPage",
+    label: t("nav.cart"),
+    icon: "mdi-shopping-outline",
+  },
+  {
+    name: "ProfilePage",
+    label: t("nav.profile"),
+    icon: "mdi-account-circle-outline",
+  },
 ]);
 
-const cart = useScheduleCartStore();
-
-const cartCount = computed(() => cart.items.length);
-
-const isNavItemActive = (item) => route.name === item.name;
-
 const scrollAreaHeight = computed(() => {
   if ($q.screen.lt.sm) {
-    return 'height: calc(100dvh - env(safe-area-inset-top) - 60px - env(safe-area-inset-bottom)) !important;';
+    return "height: calc(100dvh - env(safe-area-inset-top) - 60px - env(safe-area-inset-bottom)) !important;";
   }
 
-  return 'height: 100dvh !important;';
+  return "height: 100dvh !important;";
 });
 
+let oldValue = route.path;
+
+const isNavItemActive = (item) => route.name === item.name;
+
 watch(
   () => route.path,
   (value) => {
@@ -112,7 +135,7 @@ watch(
     }
 
     oldValue = value;
-  }
+  },
 );
 </script>
 <style scoped>
@@ -176,8 +199,11 @@ watch(
   display: grid;
   grid-template-columns: repeat(5, minmax(0, 1fr));
   align-items: end;
-  height: calc(60px + env(safe-area-inset-bottom));
-  padding: 0 0 env(safe-area-inset-bottom);
+  height: 60px;
+}
+
+.provider-bottom-nav-safe-area {
+  height: env(safe-area-inset-bottom, 0px);
 }
 
 .provider-bottom-nav-item {

+ 279 - 140
src/pages/LoginPage.vue

@@ -1,25 +1,50 @@
 <template>
   <q-page class="bg-surface-dark login-page">
-    <Transition mode="out-in" name="fade-slide">
+    <Transition
+      mode="out-in"
+      name="fade-slide"
+    >
       <div
         v-if="!clicked"
         key="splash"
         class="splash-screen"
         @click="clicked = true"
       >
-        <img class="splash-layer splash-layer--bg" :src="BackgroundLogin" />
-
-        <img class="splash-layer splash-layer--photo" :src="FotoDiarista" />
-
-        <img class="splash-layer splash-layer--logo" :src="LogoLogin" />
+        <img
+          class="splash-layer splash-layer--bg"
+          :src="BackgroundLogin"
+        />
+
+        <img
+          class="splash-layer splash-layer--photo"
+          :src="FotoDiarista"
+        />
+
+        <img
+          class="splash-layer splash-layer--logo"
+          :src="LogoLogin"
+        />
       </div>
 
-      <div v-else-if="steps === 5" key="step5" class="splash-screen">
-        <img class="splash-layer splash-layer--bg" :src="BackgroundLogin" />
+      <div
+        v-else-if="steps === 5"
+        key="step5"
+        class="splash-screen"
+      >
+        <img
+          class="splash-layer splash-layer--bg"
+          :src="BackgroundLogin"
+        />
 
-        <img class="splash-layer splash-layer--photo" :src="FotoDiarista" />
+        <img
+          class="splash-layer splash-layer--photo"
+          :src="FotoDiarista"
+        />
 
-        <img class="splash-layer splash-layer--logo-small" :src="LogoLogin" />
+        <img
+          class="splash-layer splash-layer--logo-small"
+          :src="LogoLogin"
+        />
 
         <div class="step4-card-wrapper">
           <LoginStep4Panel
@@ -30,37 +55,11 @@
         </div>
       </div>
 
-      <div v-else key="flow" class="flow-screen">
-        <div class="flow-header items-center row">
-          <q-btn
-            v-if="steps >= 4"
-            class="flow-back-btn"
-            color="primary"
-            dense
-            flat
-            icon="mdi-arrow-left"
-            round
-            @click="goBack"
-          />
-        </div>
-
-        <div class="flow-logo q-my-xl">
-          <q-img style="max-width: 180px" :src="LogoDiariaCampos" />
-        </div>
-
-        <div v-if="steps === 3" class="register-later-row">
-          <q-btn
-            align="right"
-            class="register-later-btn"
-            color="primary"
-            flat
-            icon-right="mdi-chevron-right-circle-outline"
-            no-caps
-            :label="$t('auth.register_later')"
-            @click="steps = 5"
-          />
-        </div>
-
+      <div
+        v-else
+        key="flow"
+        class="flow-screen"
+      >
         <q-form
           ref="loginForm"
           autocapitalize="off"
@@ -69,26 +68,81 @@
           class="flow-form"
           spellcheck="false"
           @submit="onSubmit"
+          @validation-error="scrollToInvalidField"
         >
-          <div
-            class="flow-content"
-            :class="{ 'flow-content--centered': steps < 3 }"
+          <q-scroll-area
+            ref="flowScrollArea"
+            class="flow-scroll"
           >
-            <LoginStep1Panel
-              v-if="steps === 1"
-              v-model:email="email"
-              v-model:phone="phone"
-            />
-
-            <LoginStep2Panel v-else-if="steps === 2" v-model:code="code" />
-
-            <LoginStep3Panel v-else-if="steps === 3" v-model="stepThreeForm" />
-
-            <LoginStepAvatarPanel
-              v-else-if="steps === 4"
-              v-model="stepAvatarForm"
-            />
-          </div>
+            <div class="flow-screen-content">
+              <div class="flow-header items-center row">
+                <q-btn
+                  v-if="steps >= 4"
+                  class="flow-back-btn"
+                  color="primary"
+                  dense
+                  flat
+                  icon="mdi-arrow-left"
+                  round
+                  @click="goBack"
+                />
+              </div>
+
+              <div class="flow-logo q-my-xl">
+                <q-img
+                  :src="LogoDiariaCampos"
+                  style="max-width: 180px"
+                />
+              </div>
+
+              <div
+                v-if="steps === 3"
+                class="register-later-row"
+              >
+                <q-btn
+                  align="right"
+                  class="register-later-btn"
+                  color="primary"
+                  flat
+                  icon-right="mdi-chevron-right-circle-outline"
+                  no-caps
+                  :label="$t('auth.register_later')"
+                  @click="goToLocationStep"
+                />
+              </div>
+
+              <div
+                ref="flowContent"
+                class="flow-content"
+                :class="{ 'flow-content--centered': steps < 3, 'flow-content--email-only': steps === 1 && !smsEnabled }"
+              >
+                <LoginStep1Panel
+                  v-if="steps === 1"
+                  v-model:email="email"
+                  v-model:phone="phone"
+                  :server-errors="serverErrors"
+                  :sms-enabled="smsEnabled"
+                />
+
+                <LoginStep2Panel
+                  v-else-if="steps === 2"
+                  v-model:code="code"
+                  :server-errors="serverErrors"
+                />
+
+                <LoginStep3Panel
+                  v-else-if="steps === 3"
+                  v-model="stepThreeForm"
+                  :server-errors="serverErrors"
+                />
+
+                <LoginStepAvatarPanel
+                  v-else-if="steps === 4"
+                  v-model="stepAvatarForm"
+                />
+              </div>
+            </div>
+          </q-scroll-area>
 
           <div class="flow-footer">
             <q-btn
@@ -98,7 +152,7 @@
               rounded
               type="submit"
               :label="$t('auth.continue')"
-              :loading="submitting"
+              :loading="loading"
             >
               <template #loading>
                 <q-spinner />
@@ -112,36 +166,41 @@
 </template>
 
 <script setup>
+import BackgroundLogin from "src/assets/background-login.svg";
+import FotoDiarista from "src/assets/foto_diarista_login.svg";
+import LogoDiariaCampos from "src/assets/logo_diaria_campos_login.svg";
+import LogoLogin from "src/assets/logo_diaria_login.svg";
+
 import {
   createUserAndClient,
   sendCode,
   updateMe,
   validateCode,
 } from "src/api/user";
+
+import { getServicesConfig } from "src/api/services";
+import { nextTick, onMounted, provide, ref, watch } from "vue";
 import { useAuth } from "src/composables/useAuth";
-import { useRegistrationFlowStore } from "src/stores/registrationFlow";
-import { userStore } from "src/stores/user";
-import { useQuasar } from "quasar";
-import { onMounted, ref } from "vue";
 import { useI18n } from "vue-i18n";
+import { useQuasar } from "quasar";
+import { useRegistrationFlowStore } from "src/stores/registrationFlow";
 import { useRouter } from "vue-router";
+import { useScroll } from "src/composables/useScroll";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
+import { userStore } from "src/stores/user";
 
-import BackgroundLogin from "src/assets/background-login.svg";
-import FotoDiarista from "src/assets/foto_diarista_login.svg";
-import LogoDiariaCampos from "src/assets/logo_diaria_campos_login.svg";
-import LogoLogin from "src/assets/logo_diaria_login.svg";
-
-import LoginStepAvatarPanel from "src/components/login/LoginStepAvatarPanel.vue";
 import LoginStep1Panel from "src/components/login/LoginStep1Panel.vue";
 import LoginStep2Panel from "src/components/login/LoginStep2Panel.vue";
 import LoginStep3Panel from "src/components/login/LoginStep3Panel.vue";
 import LoginStep4Panel from "src/components/login/LoginStep4Panel.vue";
+import LoginStepAvatarPanel from "src/components/login/LoginStepAvatarPanel.vue";
 
 const { setAuthDataFromPayload } = useAuth();
-const flowStore = useRegistrationFlowStore();
-const $q = useQuasar();
 const { t } = useI18n();
 const router = useRouter();
+const $q = useQuasar();
+
+const flowStore = useRegistrationFlowStore();
 
 const clicked = ref(false);
 const code = ref("");
@@ -149,9 +208,13 @@ const email = ref("");
 const isLogin = ref(false);
 
 const loginForm = ref(null);
+const flowContent = ref(null);
+const flowScrollArea = ref(null);
 
 const phone = ref("");
 
+const smsEnabled = ref(false);
+
 const stepAvatarForm = ref({
   avatar: null,
 });
@@ -172,11 +235,17 @@ const stepThreeForm = ref({
   nickname: "",
   no_complement: false,
   number: "",
+  phone: "",
   state: "",
   zip_code: "",
 });
 
-const submitting = ref(false);
+const { loading, serverErrors, execute } = useSubmitHandler();
+const { scrollToComponent } = useScroll();
+
+provide("clearServerError", (field) => {
+  serverErrors.value[field] = null;
+});
 
 const goBack = () => {
   if (steps.value === 4) {
@@ -186,8 +255,26 @@ const goBack = () => {
   }
 };
 
+const goToLocationStep = () => {
+  saveRegistrationData();
+
+  steps.value = 5;
+};
+
+const loadServicesConfig = async () => {
+  try {
+    const config = await getServicesConfig();
+
+    smsEnabled.value = config?.sms_enabled === true;
+  } catch (error) {
+    console.error("Failed to load services config", error);
+
+    smsEnabled.value = false;
+  }
+};
+
 const navigateToMap = (lat, lng) => {
-  flowStore.setCredentials(email.value, phone.value, code.value);
+  saveRegistrationData();
   flowStore.setInitialLocation(lat, lng);
   router.push({ name: "LocationMapPage" });
 };
@@ -201,20 +288,46 @@ const onDeviceLocation = ({ lat, lng }) => {
 };
 
 const onSubmit = async () => {
-  submitting.value = true;
-
   try {
     switch (steps.value) {
       case 1: {
-        const response = await sendValidationCode();
+        await execute(() => sendCode(email.value, phone.value));
+
+        if (Object.keys(serverErrors.value).length === 0) {
+          steps.value = 2;
+        }
 
-        isLogin.value = response.data.payload.isLogin;
         break;
       }
 
-      case 2:
-        await validateCodeInput();
+      case 2: {
+        let response;
+
+        await execute(async () => {
+          response = await validateCode(
+            email.value,
+            phone.value,
+            code.value,
+            isLogin.value,
+          );
+        });
+
+        if (
+          Object.keys(serverErrors.value).length === 0 &&
+          response?.status === 200
+        ) {
+          if (isLogin.value === true) {
+            await setAuthDataFromPayload(response.data.payload);
+
+            router.push({ name: "DashboardPage" });
+          } else {
+            stepThreeForm.value.phone = phone.value;
+            steps.value = 3;
+          }
+        }
+
         break;
+      }
 
       case 3:
         steps.value = 4;
@@ -222,13 +335,23 @@ const onSubmit = async () => {
 
       case 4:
         await registerUserAndClient();
+
+        if (Object.keys(serverErrors.value).length > 0) {
+          steps.value = 3;
+        }
+
         break;
 
       default:
         break;
     }
-  } finally {
-    submitting.value = false;
+  } catch (error) {
+    console.error("Login flow error", error);
+
+    $q.notify({
+      type: "negative",
+      message: t("auth.register_error"),
+    });
   }
 };
 
@@ -244,48 +367,39 @@ const registerUserAndClient = async () => {
   };
 
   Object.entries(stepThreeForm.value).forEach(([key, value]) => {
-    if (key === "no_complement") {
-      return;
-    }
-
-    if (key === "complement" && stepThreeForm.value.no_complement) {
+    if (key === "no_complement" || key === "complement") {
       return;
     }
 
     append(key, value);
   });
 
+  payload.complement = stepThreeForm.value.complement || "";
+
   append("code", code.value);
   append("email", email.value);
-  append("has_complement", !stepThreeForm.value.no_complement);
-  append("phone", phone.value);
+  append("has_complement", true);
+  append("phone", stepThreeForm.value.phone || phone.value);
 
-  try {
+  await execute(async () => {
     const response = await createUserAndClient(payload);
 
-    if (response.status >= 200 && response.status < 300) {
-      await setAuthDataFromPayload(response.data.payload);
+    await setAuthDataFromPayload(response.data.payload);
 
-      if (stepAvatarForm.value.avatar) {
-        const user = await updateMe({
-          avatar_base64: await fileToBase64(stepAvatarForm.value.avatar),
-        });
+    if (stepAvatarForm.value.avatar) {
+      const user = await updateMe({
+        avatar_base64: await fileToBase64(stepAvatarForm.value.avatar),
+      });
 
-        userStore().setUser(user);
-      }
-
-      router.push({ name: "DashboardPage" });
+      userStore().setUser(user);
     }
-  } catch (error) {
-    console.error("Failed to create user and client", error);
 
-    $q.notify({
-      type: "negative",
-      message: t("auth.register_error"),
-    });
-  }
+    router.push({ name: "DashboardPage" });
+  });
 };
 
+//
+
 const fileToBase64 = (file) =>
   new Promise((resolve, reject) => {
     const reader = new FileReader();
@@ -294,39 +408,41 @@ const fileToBase64 = (file) =>
     reader.readAsDataURL(file);
   });
 
-const sendValidationCode = async () => {
-  const response = await sendCode(email.value, phone.value);
+const saveRegistrationData = () => {
+  const registrationPhone = stepThreeForm.value.phone || phone.value;
 
-  if (response.status === 201) {
-    steps.value = 2;
-  } else {
-    console.error("Failed to send validation code");
-  }
+  flowStore.setCredentials(email.value, registrationPhone, code.value);
 
-  return response;
+  flowStore.setRegistrationData({
+    document: stepThreeForm.value.document,
+    name: stepThreeForm.value.name,
+    phone: registrationPhone,
+  });
 };
 
-const validateCodeInput = async () => {
-  const response = await validateCode(
-    email.value,
-    phone.value,
-    code.value,
-    isLogin.value,
-  );
-
-  if (response.status === 200) {
-    if (isLogin.value === true) {
-      await setAuthDataFromPayload(response.data.payload);
-      router.push({ name: "DashboardPage" });
-    } else {
-      steps.value = 3;
-    }
-  } else {
-    console.error("Invalid validation code");
+const scrollToInvalidField = async (field) => {
+  await nextTick();
+
+  const target = field ?? flowContent.value?.querySelector(".q-field--error");
+
+  if (target && flowScrollArea.value) {
+    scrollToComponent(target, flowScrollArea);
   }
 };
 
+watch(
+  [steps, () => Object.values(serverErrors.value).some(Boolean)],
+  ([, hasErrors]) => {
+    if (hasErrors) {
+      scrollToInvalidField();
+    }
+  },
+  { flush: "post" },
+);
+
 onMounted(() => {
+  loadServicesConfig();
+
   setTimeout(() => {
     clicked.value = true;
   }, 1500);
@@ -413,12 +529,19 @@ onMounted(() => {
 }
 
 .flow-screen {
-  display: flex;
-  flex-direction: column;
   width: 100%;
   max-width: 500px;
-  min-height: 100vh;
-  padding: 16px 20px;
+  height: 100vh;
+  height: 100dvh;
+  background: var(--q-surface-dark);
+}
+
+.flow-screen-content {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 100%;
+  padding: 16px 20px 0;
   background: var(--q-surface-dark);
 }
 
@@ -440,26 +563,42 @@ onMounted(() => {
 }
 
 .flow-form {
-  flex: 1;
+  height: 100%;
   display: flex;
   flex-direction: column;
 }
 
-.flow-content {
+.flow-scroll {
   flex: 1;
-  overflow-y: auto;
+  height: 0;
   min-height: 0;
 
+  :deep(.q-scrollarea__content) {
+    display: flex;
+    flex-direction: column;
+  }
+}
+
+.flow-content {
+  flex: 1;
+
   &--centered {
     display: flex;
     align-items: center;
     justify-content: center;
   }
+
+  &--email-only {
+    align-items: flex-start;
+    justify-content: flex-start;
+    padding-top: clamp(96px, 16vh, 140px);
+  }
 }
 
 .flow-footer {
   flex-shrink: 0;
-  padding: 8px 0 calc(12px + env(safe-area-inset-bottom));
+  padding: 8px 20px calc(12px + env(safe-area-inset-bottom));
+  background: var(--q-surface-dark);
 }
 
 .register-later-row {

+ 226 - 114
src/pages/agenda/CalendarPage.vue

@@ -1,51 +1,98 @@
 <template>
   <q-page class="bg-page q-pb-xl">
-    <div class="calendar-header row items-center q-px-md q-pt-md q-pb-sm bg-white">
-      <q-btn flat round dense icon="mdi-chevron-left" color="primary" @click="router.back()" />
-
-      <div class="col text-center font16 fontbold gradient-diarista text-primary">
-        {{ $t('dashboard_client.agenda.title') }}
+    <div
+      class="calendar-header row items-center q-px-md q-pt-md q-pb-sm bg-white"
+    >
+      <q-btn
+        class="header-back-btn"
+        color="primary"
+        dense
+        flat
+        icon="mdi-chevron-left"
+        round
+        @click="router.back()"
+      />
+
+      <div
+        class="col text-center font16 fontbold gradient-diarista text-primary"
+      >
+        {{ $t("dashboard_client.agenda.title") }}
       </div>
 
       <div style="width: 36px" />
     </div>
 
     <template v-if="loading">
-      <div class="row items-center justify-center full-width" style="height: 60vh">
+      <div
+        class="row items-center justify-center full-width"
+        style="height: 60vh"
+      >
         <q-spinner-dots color="primary" />
       </div>
     </template>
 
     <template v-else>
       <div class="q-mt-md q-mx-md">
-        <div class="section-title gradient-diarista q-mb-sm font16 fontbold">{{ $t('dashboard_client.agenda.upcoming_title') }}</div>
+        <div class="section-title gradient-diarista q-mb-sm font16 fontbold">
+          {{ $t("dashboard_client.agenda.upcoming_title") }}
+        </div>
 
         <template v-if="upcomingSchedules.length > 0">
           <q-card
             v-for="item in upcomingSchedules"
             :key="item.id"
-            class="calendar-card bg-surface shadow-card q-mb-sm"
             :flat="false"
+            class="calendar-card bg-surface shadow-card q-mb-sm"
           >
             <q-card-section class="q-pa-sm">
               <div class="row no-wrap items-start q-gutter-x-sm">
                 <q-avatar size="44px">
-                  <img v-if="item.provider_photo" :src="item.provider_photo" style="object-fit:cover" />
-                  <span v-else class="full-width full-height flex flex-center" :style="avatarColors[item.id % avatarColors.length]" style="border-radius:50%">{{ getFirstName(item.provider_name)?.slice(0, 2).toUpperCase() ?? '??' }}</span>
+                  <img
+                    v-if="item.provider_photo"
+                    :src="item.provider_photo"
+                    style="object-fit: cover"
+                  />
+
+                  <span
+                    v-else
+                    class="full-width full-height flex flex-center"
+                    style="border-radius: 50%"
+                    :style="avatarColors[item.id % avatarColors.length]"
+                  >
+                    {{
+                      getFirstName(item.provider_name)
+                        ?.slice(0, 2)
+                        .toUpperCase() ?? "??"
+                    }}
+                  </span>
                 </q-avatar>
+
                 <div class="col column text-text">
-                  <span class="font12 fontbold">{{ getFirstName(item.provider_name) || $t('dashboard_client.agenda.waiting_proposals') }}</span>
+                  <span class="font12 fontbold">
+                    {{
+                      getFirstName(item.provider_name) ||
+                      $t("dashboard_client.agenda.waiting_proposals")
+                    }}
+                  </span>
+
                   <div class="row items-center no-wrap">
-                    <span class="font10 fontbold">{{ formatWeekday(item.date) }}</span>
-                    <span class="font10 fontmedium">{{ ', ' + formatDayMonth(item.date) }}</span>
+                    <span class="font10 fontbold">
+                      {{ formatWeekday(item.date) }}
+                    </span>
+
+                    <span class="font10 fontmedium">
+                      {{ ", " + formatDayMonth(item.date) }}
+                    </span>
                   </div>
+
                   <span class="">
                     <span class="font10 fontmedium">
-                      {{ $t('dashboard_client.next_schedules.from') }}                  
+                      {{ $t("dashboard_client.next_schedules.from") }}
                     </span>
+
                     <span class="font10 fontbold q-ml-xs">
                       {{ item.start_time?.slice(0, 5) }}
-                      {{ $t('dashboard_client.next_schedules.to') }}
+                      {{ $t("dashboard_client.next_schedules.to") }}
                       {{ item.end_time?.slice(0, 5) }}
                     </span>
                   </span>
@@ -53,30 +100,42 @@
 
                 <div class="col-auto column items-end">
                   <q-chip
+                    class="font10"
                     dense
                     square
                     :color="statusBgColor(item.status)"
-                    :text-color="statusTextColor(item.status)"
                     :label="statusLabel(item.status)"
-                    class="font10"
+                    :text-color="statusTextColor(item.status)"
                   />
-                  <span class="text-price font12 fontbold">{{ formatScheduleTotal(item) }}</span>
-                  <span class="text-period font10 fontmedium">{{ periodLabel(item.period_type) }}</span>
+
+                  <span class="text-price font12 fontbold">
+                    {{ formatScheduleTotal(item) }}
+                  </span>
+
+                  <span class="text-period font10 fontmedium">
+                    {{ periodLabel(item.period_type) }}
+                  </span>
                 </div>
               </div>
 
               <div class="row items-center no-wrap q-mt-xs">
                 <span class="type-label text-primary font10 fontmedium">
-                  {{ item.schedule_type === 'custom' ? $t('dashboard_client.agenda.type_custom') : $t('dashboard_client.agenda.type_default') }}
+                  {{
+                    item.schedule_type === "custom"
+                      ? $t("dashboard_client.agenda.type_custom")
+                      : $t("dashboard_client.agenda.type_default")
+                  }}
                 </span>
+
                 <q-space />
+
                 <q-btn
-                  rounded
-                  no-caps
+                  class=""
                   color="primary"
-                  size="sm"
+                  no-caps
                   padding="4px 8px"
-                  class=""
+                  rounded
+                  size="sm"
                   :label="$t('dashboard_client.agenda.btn_view_details')"
                   @click="openDetailsDialog(item)"
                 />
@@ -85,41 +144,75 @@
           </q-card>
         </template>
 
-        <div v-else class="text-center text-grey-5 q-py-lg text-body2">
-          {{ $t('dashboard_client.agenda.empty_upcoming') }}
+        <div
+          v-else
+          class="text-center text-grey-5 q-py-lg text-body2"
+        >
+          {{ $t("dashboard_client.agenda.empty_upcoming") }}
         </div>
       </div>
 
       <div class="q-mt-lg q-mx-md">
-        <div class="section-title gradient-diarista q-mb-sm font16 fontbold">{{ $t('dashboard_client.agenda.completed_title') }}</div>
+        <div class="section-title gradient-diarista q-mb-sm font16 fontbold">
+          {{ $t("dashboard_client.agenda.completed_title") }}
+        </div>
 
         <template v-if="completedSchedules.length > 0">
           <q-card
             v-for="item in completedSchedules"
             :key="item.id"
-            class="calendar-card bg-surface shadow-card q-mb-sm"
             :flat="false"
+            class="calendar-card bg-surface shadow-card q-mb-sm"
           >
             <q-card-section class="q-pa-sm">
               <div class="row no-wrap items-start q-gutter-x-sm">
                 <q-avatar size="44px">
-                  <img v-if="item.provider_photo" :src="item.provider_photo" style="object-fit:cover" />
-                  <span v-else class="full-width full-height flex flex-center" :style="avatarColors[item.id % avatarColors.length]" style="border-radius:50%">{{ getFirstName(item.provider_name)?.slice(0, 2).toUpperCase() ?? '??' }}</span>
+                  <img
+                    v-if="item.provider_photo"
+                    :src="item.provider_photo"
+                    style="object-fit: cover"
+                  />
+
+                  <span
+                    v-else
+                    class="full-width full-height flex flex-center"
+                    style="border-radius: 50%"
+                    :style="avatarColors[item.id % avatarColors.length]"
+                  >
+                    {{
+                      getFirstName(item.provider_name)
+                        ?.slice(0, 2)
+                        .toUpperCase() ?? "??"
+                    }}
+                  </span>
                 </q-avatar>
 
                 <div class="col column text-text">
-                  <span class="font12 fontbold">{{ getFirstName(item.provider_name) || $t('dashboard_client.agenda.waiting_proposals') }}</span>
+                  <span class="font12 fontbold">
+                    {{
+                      getFirstName(item.provider_name) ||
+                      $t("dashboard_client.agenda.waiting_proposals")
+                    }}
+                  </span>
+
                   <div class="row items-center no-wrap">
-                    <span class="font10 fontbold">{{ formatWeekday(item.date) }}</span>
-                    <span class="font10 fontmedium">{{ ', ' + formatDayMonth(item.date) }}</span>
+                    <span class="font10 fontbold">
+                      {{ formatWeekday(item.date) }}
+                    </span>
+
+                    <span class="font10 fontmedium">
+                      {{ ", " + formatDayMonth(item.date) }}
+                    </span>
                   </div>
+
                   <span class="">
                     <span class="font10 fontmedium">
-                      {{ $t('dashboard_client.next_schedules.from') }}                  
+                      {{ $t("dashboard_client.next_schedules.from") }}
                     </span>
+
                     <span class="font10 fontbold q-ml-xs">
                       {{ item.start_time?.slice(0, 5) }}
-                      {{ $t('dashboard_client.next_schedules.to') }}
+                      {{ $t("dashboard_client.next_schedules.to") }}
                       {{ item.end_time?.slice(0, 5) }}
                     </span>
                   </span>
@@ -127,54 +220,68 @@
 
                 <div class="col-auto column items-end">
                   <q-chip
+                    class="font10"
                     dense
                     square
                     :color="statusBgColor(item.status)"
-                    :text-color="statusTextColor(item.status)"
                     :label="statusLabel(item.status)"
-                    class="font10"
+                    :text-color="statusTextColor(item.status)"
                   />
-                  <span class="text-price font12 fontbold">{{ formatScheduleTotal(item) }}</span>
-                  <span class="text-period font10 fontmedium">{{ periodLabel(item.period_type) }}</span>
+
+                  <span class="text-price font12 fontbold">
+                    {{ formatScheduleTotal(item) }}
+                  </span>
+
+                  <span class="text-period font10 fontmedium">
+                    {{ periodLabel(item.period_type) }}
+                  </span>
                 </div>
               </div>
 
               <div class="row items-center no-wrap q-mt-xs">
                 <span class="type-label text-primary font10 fontmedium">
-                  {{ item.schedule_type === 'custom' ? $t('dashboard_client.agenda.type_custom') : $t('dashboard_client.agenda.type_default') }}
+                  {{
+                    item.schedule_type === "custom"
+                      ? $t("dashboard_client.agenda.type_custom")
+                      : $t("dashboard_client.agenda.type_default")
+                  }}
                 </span>
+
                 <q-space />
+
                 <q-rating
-                  :model-value="item.client_reviewed ? item.client_stars : 0"
-                  :max="5"
-                  size="14px"
+                  class="q-mr-sm"
                   color="amber"
                   icon="mdi-star-outline"
                   icon-selected="mdi-star"
                   readonly
-                  class="q-mr-sm"
+                  size="14px"
+                  :max="5"
+                  :model-value="item.client_reviewed ? item.client_stars : 0"
                 />
+
                 <q-btn
                   v-if="item.client_reviewed"
-                  unelevated
-                  rounded
-                  no-caps
+                  class="btn-rate"
                   color="secondary"
-                  size="sm"
+                  no-caps
                   padding="4px 8px"
-                  class="btn-rate"
+                  rounded
+                  size="sm"
+                  unelevated
                   :label="$t('dashboard_client.agenda.btn_reschedule')"
                   @click="openSchedulingDialog(item)"
                 />
+
                 <q-btn
                   v-else
-                  unelevated
-                  rounded
-                  no-caps
+                  class="btn-rate"
                   color="secondary"
-                  size="sm"
+                  no-caps
                   padding="4px 8px"
-                  class="btn-rate"
+                  rounded
+                  size="sm"
+                  unelevated
                   :label="$t('dashboard_client.agenda.btn_rate')"
                   @click="openRatingDialog(item)"
                 />
@@ -183,8 +290,11 @@
           </q-card>
         </template>
 
-        <div v-else class="text-center text-grey-5 q-py-lg text-body2">
-          {{ $t('dashboard_client.agenda.empty_completed') }}
+        <div
+          v-else
+          class="text-center text-grey-5 q-py-lg text-body2"
+        >
+          {{ $t("dashboard_client.agenda.empty_completed") }}
         </div>
       </div>
     </template>
@@ -192,102 +302,112 @@
 </template>
 
 <script setup>
-import { ref, onMounted } from 'vue';
-import { useQuasar } from 'quasar';
-import { useI18n } from 'vue-i18n';
-import { getClientCalendar } from 'src/api/clientCalendar';
-import { formatCurrency,getFirstName } from 'src/helpers/utils';
-import { getSchedulePaymentType, getScheduleTotalWithPlatformFee } from 'src/helpers/paymentPlatformFees';
-import { usePaymentPlatformFees } from 'src/composables/usePaymentPlatformFees';
-import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
-import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
-import SchedulingDialog from 'src/pages/search/components/SchedulingDialog.vue';
-import { useRouter } from 'vue-router';
+import { avatarColors } from "src/helpers/avatarColors";
+import { formatCurrency, getFirstName } from "src/helpers/utils";
+import { getClientCalendar } from "src/api/clientCalendar";
+
+import {
+  getSchedulePaymentType,
+  getScheduleTotalWithPlatformFee,
+} from "src/helpers/paymentPlatformFees";
+
+import { onMounted, ref } from "vue";
+import { useI18n } from "vue-i18n";
+import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
+import { useQuasar } from "quasar";
+import { useRouter } from "vue-router";
+
+import NextSchedulesDetailsDialog from "src/components/dashboard/NextSchedulesDetailsDialog.vue";
+import ScheduleRatingDialog from "src/components/dashboard/ScheduleRatingDialog.vue";
+import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 
 const $q = useQuasar();
 const { t } = useI18n();
 const router = useRouter();
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
 
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-];
 const loading = ref(true);
 const upcomingSchedules = ref([]);
 const completedSchedules = ref([]);
 
 const parseLocalDate = (dateStr) => {
   if (!dateStr) return null;
-  const s = String(dateStr);
-  const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
+
+  const iso = dateStr.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})/);
+  const dmy = dateStr.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
   if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
   return null;
 };
 
 const formatWeekday = (dateStr) => {
   const d = parseLocalDate(dateStr);
-  if (!d) return '';
-  const w = d.toLocaleDateString('pt-BR', { weekday: 'long' });
+  if (!d) return "";
+  const w = d.toLocaleDateString("pt-BR", { weekday: "long" });
   return w.charAt(0).toUpperCase() + w.slice(1);
 };
 
 const formatDayMonth = (dateStr) => {
   const d = parseLocalDate(dateStr);
-  if (!d) return '';
-  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
+  if (!d) return "";
+  return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
 };
 
 const periodLabel = (periodType) => {
   const key = `period_types.${periodType}`;
   const translated = t(key);
-  return translated !== key ? translated : '';
+  return translated !== key ? translated : "";
 };
 
-const formatScheduleTotal = (item) => (
-  formatCurrency(getScheduleTotalWithPlatformFee(item, getSchedulePaymentType(item), platformFees.value))
-);
+//
+
+const formatScheduleTotal = (item) =>
+  formatCurrency(
+    getScheduleTotalWithPlatformFee(
+      item,
+      getSchedulePaymentType(item),
+      platformFees.value,
+    ),
+  );
 
 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'),
+    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',
+    pending: "warning-bg",
+    accepted: "success-bg",
+    paid: "success-bg",
+    started: "info-bg",
+    finished: "neutral-bg",
+    cancelled: "secondary-bg",
   };
-  return map[status] ?? 'neutral-bg';
+  return map[status] ?? "neutral-bg";
 };
 
 const statusTextColor = (status) => {
   const map = {
-    pending: 'warning',
-    accepted: 'success',
-    paid: 'success',
-    started: 'info',
-    finished: 'status-finished',
-    cancelled: 'secondary',
+    pending: "warning",
+    accepted: "success",
+    paid: "success",
+    started: "info",
+    finished: "status-finished",
+    cancelled: "secondary",
   };
-  return map[status] ?? 'text';
+  return map[status] ?? "text";
 };
 
+//
+
 const loadCalendar = async () => {
   const response = await getClientCalendar();
   if (response) {
@@ -301,7 +421,7 @@ const openDetailsDialog = (schedule) => {
     component: NextSchedulesDetailsDialog,
     componentProps: { schedule },
   }).onOk(async ({ action }) => {
-    if (action === 'cancelled') {
+    if (action === "cancelled") {
       await loadCalendar();
     }
   });
@@ -332,10 +452,7 @@ const openSchedulingDialog = (item) => {
 };
 
 onMounted(async () => {
-  await Promise.all([
-    loadCalendar(),
-    loadPlatformFees().catch(() => {}),
-  ]);
+  await Promise.all([loadCalendar(), loadPlatformFees().catch(() => {})]);
   loading.value = false;
 });
 </script>
@@ -354,9 +471,6 @@ onMounted(async () => {
   line-height: 1.2;
 }
 
-
-
-
 .text-price {
   color: #3a3a4a;
   white-space: nowrap;
@@ -368,9 +482,7 @@ onMounted(async () => {
   white-space: nowrap;
 }
 
-
 .btn-rate {
   padding: 3px 10px;
 }
-
 </style>

+ 9 - 18
src/pages/dashboard/DashboardPage.vue

@@ -15,10 +15,10 @@
         <DashboardSummaryInfos v-else :data="summaryInfos" />
         <DashboardPaymentIncomplete v-if="!hasPaymentMethods" />
         <DashboardPendingSchedules
-          v-if="pendingSchedules.length > 0"
-          :data="pendingSchedules"
-          @view-details="openAcceptedDialog"
-          @cancel="cancelSchedule"
+          v-if="pendingServicePackages.length > 0"
+          :data="pendingServicePackages"
+          :type="'servicePackage'"
+          @view-details="openServicePackagePaymentDialog"
         />
         <DashboardTodaySchedules v-if="todaySchedules.length > 0" :data="todaySchedules" @rate="openRatingDialog" />
         <DashboardScrollAreaSchedules />
@@ -51,22 +51,21 @@ import DashboardPendingCustomSchedules from 'src/pages/dashboard/components/Dash
 import DashboardClientProposals from 'src/pages/dashboard/components/DashboardClientProposals.vue';
 import { useRouter } from 'vue-router'
 import { onMounted, ref, computed } from 'vue';
-import { useDialogPluginComponent, useQuasar } from 'quasar';
+import { useQuasar } from 'quasar';
 import { dadosDashboard } from 'src/api/dashboard';
 import { userStore } from 'src/stores/user';
-import ScheduleCancelDialog from 'src/components/dashboard/ScheduleCancelDialog.vue';
 import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
 import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
 
 const router = useRouter()
 const $q = useQuasar();
 const store = userStore();
-const { onDialogOK } = useDialogPluginComponent();
 
 const hasPaymentMethods = ref(true);
 const headerBar = ref({});
 const summaryInfos = ref({});
 const pendingSchedules = ref([]);
+const pendingServicePackages = ref([]);
 const nextSchedules = ref([]);
 const clientProposals = ref([]);
 const customSchedulesNoProposals = ref([]);
@@ -80,10 +79,10 @@ const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSu
 const registrationComplete = computed(() => store.user?.registration_complete ?? true);
 
 
-const openAcceptedDialog = (schedule) => {
+const openServicePackagePaymentDialog = (servicePackage) => {
   $q.dialog({
     component: ScheduleAcceptedDialog,
-    componentProps: { schedule }
+    componentProps: { servicePackage }
   }).onOk(() => {
     reloadDashboard();
   });
@@ -96,6 +95,7 @@ const reloadDashboard = async (showLoader = true) => {
     headerBar.value = response.headerBar;
     summaryInfos.value = response.summaryInfos;
     pendingSchedules.value = response.pendingSchedules ?? [];
+    pendingServicePackages.value = response.pendingServicePackages ?? [];
     nextSchedules.value = response.nextSchedules ?? [];
     lastDoneSchedules.value = response.lastDoneSchedules ?? [];
     favoriteProviders.value = response.favoriteProviders ?? [];
@@ -135,15 +135,6 @@ const openNextScheduleDialog = (schedule) => {
   });
 };
 
-const cancelSchedule = (schedule) => {
-  $q.dialog({
-    component: ScheduleCancelDialog,
-    componentProps: { schedule: schedule }
-  }).onDismiss(() => {
-    onDialogOK(reloadDashboard());
-  })
-}
-
 const openRatingDialog = (schedule) => {
   $q.dialog({
     component: ScheduleRatingDialog,

+ 126 - 63
src/pages/dashboard/components/DashboardClientProposals.vue

@@ -1,42 +1,82 @@
 <template>
-  <div class="scroll-wrapper">    
-    <div class="dashboard-section-title gradient-diarista q-mb-sm font16 fontbold q-px-md">{{ $t('dashboard_client.client_proposals.title') }}</div>
+  <div class="scroll-wrapper">
+    <div
+      class="dashboard-section-title gradient-diarista q-mb-sm font16 fontbold q-px-md"
+    >
+      {{ $t("dashboard_client.client_proposals.title") }}
+    </div>
 
     <div class="scroll-track q-pb-md q-px-md text-text">
-
-      <q-card v-for="item in data" :key="item.id" class="proposal-card shadow-card" :flat="false">
+      <q-card
+        v-for="item in data"
+        :key="item.id"
+        :flat="false"
+        class="proposal-card shadow-card"
+      >
         <div class="row no-wrap">
           <div class="column">
-            <q-avatar :style="avatarColors[item.id % avatarColors.length]" class="q-mx-auto">
-              <img v-if="item.provider_photo" :src="item.provider_photo" style="object-fit:cover;border-radius:50%;" />
-              <span v-else>{{ item.provider_name?.slice(0, 1) ?? '—' }}</span>
+            <q-avatar
+              class="q-mx-auto"
+              :style="avatarColors[item.id % avatarColors.length]"
+            >
+              <img
+                v-if="item.provider_photo"
+                :src="item.provider_photo"
+                style="object-fit: cover; border-radius: 50%"
+              />
+
+              <span v-else>
+                {{ item.provider_name?.slice(0, 1) ?? "—" }}
+              </span>
             </q-avatar>
-            <q-space/>
+
+            <q-space />
+
             <div class="customColor font8 fontbold">
               <div>
-                {{ $t('dashboard_client.client_proposals.candidate') }} 
+                {{ $t("dashboard_client.client_proposals.candidate") }}
               </div>
+
               <div class="no-wrap">
-                {{ $t('dashboard_client.client_proposals.custom') }}
+                {{ $t("dashboard_client.client_proposals.custom") }}
               </div>
             </div>
           </div>
 
           <div class="content">
-
             <div class="row justify-between items-center text-text">
               <div>
                 <div class="font12 fontmedium">
                   {{ item.provider_name }}
-                  <span class="font9 fontregular">{{ $t('dashboard_client.client_proposals.age', { idade: item.idade }) }}</span>
+
+                  <span class="font9 fontregular">
+                    {{
+                      $t("dashboard_client.client_proposals.age", {
+                        idade: item.idade,
+                      })
+                    }}
+                  </span>
                 </div>
+
+                <span
+                  v-if="getProviderGenderLabel(item, t)"
+                  class="font9 fontregular text-grey-7"
+                >
+                  {{ getProviderGenderLabel(item, t) }}
+                </span>
+
                 <span class="font10 fontmedium">
-                  <q-icon name="star" size="12px" color="warning" class="font9 fontregular"/>
+                  <q-icon
+                    class="font9 fontregular"
+                    color="warning"
+                    name="star"
+                    size="12px"
+                  />
+
                   <span>
                     {{ item.average_rating }}
                   </span>
                 </span>
-
               </div>
             </div>
 
@@ -47,17 +87,19 @@
                 </span>
 
                 <span class="text-schedule-date-regular">
-                  {{ ', ' + formatDayMonth(item.date) }}
+                  {{ ", " + formatDayMonth(item.date) }}
                 </span>
               </div>
 
               <div class="schedule-line q-pt-xs">
                 <span class="text-schedule-date-regular">
-                  {{ $t('dashboard_client.next_schedules.from') }}&nbsp;
+                  {{ $t("dashboard_client.next_schedules.from") }}&nbsp;
                 </span>
 
                 <span class="text-schedule-date-bold fontbold">
-                  {{ item.start_time?.slice(0,5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ item.end_time?.slice(0,5) }}
+                  {{ item.start_time?.slice(0, 5) }}
+                  {{ $t("dashboard_client.next_schedules.to") }}
+                  {{ item.end_time?.slice(0, 5) }}
                 </span>
               </div>
             </div>
@@ -66,61 +108,79 @@
             <div class="price text-text">
               <span class="font9 fontmedium distance">
                 <span class="fontbold font10">
-                  {{ $t('dashboard_client.client_proposals.distance') }}
+                  {{ $t("dashboard_client.client_proposals.distance") }}
                 </span>
+
                 <span class="font10 fontregular q-ml-sm">
-                  {{ (item.distance_km ?? '--') + ' km' }}
+                  {{ (item.distance_km ?? "--") + " km" }}
                 </span>
               </span>
+
               <span class="font14 fontbold">
-                {{ filterCurrency(chooseprice(item.period_type, item.daily_price_8h)) }}            
+                {{
+                  filterCurrency(
+                    chooseprice(item.period_type, item.daily_price_8h),
+                  )
+                }}
               </span>
 
               <span class="col-6 font10 fontmedium">
                 {{ formatLabelByPeriodType(item.period_type) }}
               </span>
-
             </div>
 
             <div class="actions">
-              <q-btn label="recusar" size="sm" padding="4px 12px" flat class="btn-reject" @click="() => handleRefuseProposal(item.id)" />
-              <q-btn label="aceitar" size="sm" padding="4px 12px" class="btn-accept" @click="() => handleAcceptProposal(item.id)" />
+              <q-btn
+                class="btn-reject"
+                flat
+                label="recusar"
+                padding="4px 12px"
+                size="sm"
+                @click="() => handleRefuseProposal(item.id)"
+              />
+
+              <q-btn
+                class="btn-accept"
+                label="aceitar"
+                padding="4px 12px"
+                size="sm"
+                @click="() => handleAcceptProposal(item.id)"
+              />
             </div>
-
           </div>
-
         </div>
       </q-card>
-
     </div>
   </div>
 </template>
 
 <script setup>
-import { acceptProposal, refuseProposal } from 'src/api/customSchedules'
-import { chooseprice, formatLabelByPeriodType, filterCurrency } from 'src/helpers/utils'
+import { acceptProposal, refuseProposal } from "src/api/customSchedules";
+import { avatarColors } from "src/helpers/avatarColors";
+
+import {
+  chooseprice,
+  filterCurrency,
+  formatLabelByPeriodType,
+} from "src/helpers/utils";
+
+import { getProviderGenderLabel } from "src/helpers/provider";
+import { useI18n } from "vue-i18n";
+
+const emit = defineEmits(["refreshData"]);
 
 defineProps({
   data: {
     type: Array,
-    default: () => []
-  }
-})
-
-
-const emit = defineEmits(['refreshData'])
+    default: () => [],
+  },
+});
 
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd', color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-];
+const { t } = useI18n();
 
 // const formatTime = (time) => {
 //   if (!time) return '';
 
-
 //   const [hour, minute] = time.split(':');
 //   return `${hour}:${minute}`;
 // };
@@ -140,32 +200,21 @@ const avatarColors = [
 //   return `${weekday}, ${day}-${month}`;
 // };
 
-const handleRefuseProposal = async (proposalId) => {
-  // isLoading.value = true
-  try {
-     await refuseProposal(proposalId)
-    
-    // await loadProposals()
-     emit('refreshData')
-  } catch (error) {
-    console.log(error);
-  } finally {
-    // isLoading.value = false
-  }
-}
-
 const formatDayMonth = (iso) => {
-  if (!iso) return '';
+  if (!iso) return "";
 
-  return new Date(iso).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
+  return new Date(iso).toLocaleDateString("pt-BR", {
+    day: "2-digit",
+    month: "2-digit",
+  });
 };
 
 const formatWeekday = (iso) => {
-  if (!iso) return '';
+  if (!iso) return "";
 
   const d = new Date(iso);
 
-  const w = d.toLocaleDateString('pt-BR', { weekday: 'long' });
+  const w = d.toLocaleDateString("pt-BR", { weekday: "long" });
 
   return w.charAt(0).toUpperCase() + w.slice(1);
 };
@@ -173,16 +222,30 @@ const formatWeekday = (iso) => {
 const handleAcceptProposal = async (proposalId) => {
   // isLoading.value = true
   try {
-    await acceptProposal(proposalId)
+    await acceptProposal(proposalId);
 
-     emit('refreshData')
+    emit("refreshData");
     // onDialogOK()
   } catch (error) {
     console.log(error);
   } finally {
     // isLoading.value = false
   }
-}
+};
+
+const handleRefuseProposal = async (proposalId) => {
+  // isLoading.value = true
+  try {
+    await refuseProposal(proposalId);
+
+    // await loadProposals()
+    emit("refreshData");
+  } catch (error) {
+    console.log(error);
+  } finally {
+    // isLoading.value = false
+  }
+};
 </script>
 
 <style scoped lang="scss">
@@ -271,7 +334,7 @@ const handleAcceptProposal = async (proposalId) => {
 }
 
 .customColor {
-  background: linear-gradient(180deg, #8B5CF6 50%, #EC4899 50%);
+  background: linear-gradient(180deg, #8b5cf6 50%, #ec4899 50%);
   background-clip: text;
   -webkit-background-clip: text;
   -webkit-text-fill-color: transparent;

+ 178 - 87
src/pages/location/AddressCompletionPage.vue

@@ -2,53 +2,107 @@
   <q-page class="address-completion-page bg-surface">
     <div class="address-completion-inner">
       <q-btn
-        flat
-        dense
-        round
+        class="address-completion-back"
         color="primary"
+        dense
+        flat
         icon="mdi-arrow-left"
-        class="address-completion-back"
+        round
         @click="router.back()"
       />
 
-      <div class="address-completion-content">
+      <q-form
+        ref="addressForm"
+        class="address-completion-content"
+      >
         <div class="text-text">
-          <span class="font14 fontbold">{{ $t('common.terms.address') }}</span>
+          <span class="font14 fontbold">{{ $t("common.terms.document") }}</span>
         </div>
+
         <q-input
-          :model-value="flowStore.confirmedAddress"
+          v-model="form.document"
+          class="bg-surface q-mt-sm q-mb-md"
+          hide-bottom-space
+          input-class="text-text"
+          lazy-rules
+          no-error-icon
           outlined
           rounded
-          readonly
+          :placeholder="$t('common.terms.document')"
+          :rules="[inputRules.cpfOrCnpj]"
+        />
+
+        <div class="text-text">
+          <span class="font14 fontbold">{{ $t("common.terms.phone") }}</span>
+        </div>
+
+        <q-input
+          v-model="form.phone"
           class="bg-surface q-mt-sm q-mb-md"
+          hide-bottom-space
           input-class="text-text"
+          lazy-rules
+          mask="(##) #####-####"
+          no-error-icon
+          outlined
+          rounded
+          :placeholder="$t('common.terms.phone')"
+          :rules="[inputRules.required, inputRules.phone]"
+        />
+
+        <div class="text-text">
+          <span class="font14 fontbold">{{ $t("common.terms.address") }}</span>
+        </div>
+
+        <q-input
+          :model-value="flowStore.confirmedAddress"
+          class="bg-surface q-mt-sm q-mb-md"
+          input-class="text-text"
+          outlined
+          readonly
+          rounded
         />
 
         <div class="row q-col-gutter-sm">
           <div class="col-4">
             <div class="text-text">
-              <span class="font14 fontbold">{{ $t('common.terms.address_number') }}</span>
+              <span class="font14 fontbold">{{
+                $t("common.terms.address_number")
+              }}</span>
             </div>
+
             <q-input
               v-model="form.number"
-              outlined
-              rounded
               class="bg-surface q-mt-sm q-mb-md"
-              placeholder="0000"
+              hide-bottom-space
               input-class="text-text"
+              lazy-rules
+              no-error-icon
+              outlined
+              placeholder="0000"
+              rounded
+              :rules="[inputRules.required]"
             />
           </div>
+
           <div class="col-8">
             <div class="text-text">
-              <span class="font14 fontbold">{{ $t('common.terms.district') }}</span>
+              <span class="font14 fontbold">{{
+                $t("common.terms.district")
+              }}</span>
             </div>
+
             <q-input
               :model-value="flowStore.confirmedDistrict"
-              outlined
-              rounded
-              readonly
+              :rules="[inputRules.required]"
               class="bg-surface q-mt-sm q-mb-md"
+              hide-bottom-space
               input-class="text-text"
+              lazy-rules
+              no-error-icon
+              outlined
+              readonly
+              rounded
             />
           </div>
         </div>
@@ -56,105 +110,117 @@
         <div class="row q-col-gutter-sm">
           <div class="col-8">
             <div class="text-text">
-              <span class="font14 fontbold">{{ $t('common.terms.city') }}</span>
+              <span class="font14 fontbold">{{ $t("common.terms.city") }}</span>
             </div>
+
             <q-input
               :model-value="flowStore.confirmedCity"
-              outlined
-              rounded
-              readonly
+              :rules="[inputRules.required]"
               class="bg-surface q-mt-sm q-mb-md"
+              hide-bottom-space
               input-class="text-text"
+              lazy-rules
+              no-error-icon
+              outlined
+              readonly
+              rounded
             />
           </div>
+
           <div class="col-4">
             <div class="text-text">
-              <span class="font14 fontbold">{{ $t('common.terms.state') }}</span>
+              <span class="font14 fontbold">
+                {{ $t("common.terms.state") }}
+              </span>
             </div>
+
             <q-input
               :model-value="flowStore.confirmedState"
-              outlined
-              rounded
-              readonly
+              :rules="[inputRules.required]"
               class="bg-surface q-mt-sm q-mb-md"
+              hide-bottom-space
               input-class="text-text"
+              lazy-rules
+              no-error-icon
+              outlined
+              readonly
+              rounded
             />
           </div>
         </div>
 
-        <q-checkbox
-          v-model="form.no_complement"
-          :label="$t('auth.no_complement')"
-          color="primary"
-          keep-color
-          class="q-mb-md text-text"
-        />
+        <div class="text-text">
+          <span class="font14 fontbold">{{ $t("auth.complement") }}</span>
+        </div>
 
-        <template v-if="!form.no_complement">
-          <div class="text-text">
-            <span class="font14 fontbold">{{ $t('auth.complement') }}</span>
-          </div>
-          <q-input
-            v-model="form.complement"
-            outlined
-            rounded
-            class="bg-surface q-mt-sm q-mb-md"
-            :placeholder="$t('auth.complement_placeholder')"
-            input-class="text-text"
-          />
-        </template>
+        <q-input
+          v-model="form.complement"
+          class="bg-surface q-mt-sm q-mb-md"
+          hide-bottom-space
+          input-class="text-text"
+          lazy-rules
+          no-error-icon
+          outlined
+          rounded
+          :placeholder="$t('auth.complement_placeholder')"
+          :rules="[inputRules.required]"
+        />
 
         <div class="text-text">
-          <span class="font14 fontbold">{{ $t('auth.address_nickname') }}</span>
+          <span class="font14 fontbold">{{ $t("auth.address_nickname") }}</span>
         </div>
+
         <q-input
           v-model="form.nickname"
+          class="bg-surface q-mt-sm q-mb-md"
+          input-class="text-text"
           outlined
           rounded
-          class="bg-surface q-mt-sm q-mb-md"
           :placeholder="$t('auth.address_nickname_placeholder')"
-          input-class="text-text"
         />
 
         <div class="text-text">
-          <span class="font14 fontbold">{{ $t('auth.address_instructions') }}</span>
+          <span class="font14 fontbold">
+            {{ $t("auth.address_instructions") }}
+          </span>
         </div>
+
         <q-input
           v-model="form.instructions"
+          autogrow
+          class="bg-surface q-mt-sm q-mb-md"
+          input-class="text-text"
           outlined
           rounded
-          class="bg-surface q-mt-sm q-mb-md"
-          type="textarea"
           rows="3"
-          autogrow
-          input-class="text-text"
+          type="textarea"
         />
 
         <div class="row q-gutter-sm q-mt-xs q-mb-xl">
           <q-chip
             v-for="type in addressTypes"
             :key="type.value"
+            :icon="type.icon"
+            :icon-selected="type.icon"
+            :outline="form.address_type !== type.value"
             :selected="form.address_type === type.value"
             clickable
             color="primary"
-            :outline="form.address_type !== type.value"
             text-color="surface"
-            :icon="type.icon"
-            :icon-selected="type.icon"
             @click="form.address_type = type.value"
           >
             {{ type.label }}
           </q-chip>
         </div>
-      </div>
+      </q-form>
 
       <div class="address-completion-footer">
         <q-btn
+          class="full-width"
           color="primary-button"
-          :label="$t('auth.confirm_address')"
-          rounded
           padding="14px 16px"
-          class="full-width"
+          rounded
+          :label="$t('auth.confirm_address')"
           :loading="submitting"
           @click="handleConfirm"
         />
@@ -164,48 +230,68 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
-import { useQuasar } from 'quasar';
-import { useI18n } from 'vue-i18n';
-import { useRegistrationFlowStore } from 'src/stores/registrationFlow';
-import { useAuth } from 'src/composables/useAuth';
-import { createUserAndClient } from 'src/api/user';
+import { computed, onMounted, ref } from "vue";
+import { createUserAndClient } from "src/api/user";
+import { useAuth } from "src/composables/useAuth";
+import { useI18n } from "vue-i18n";
+import { useInputRules } from "src/composables/useInputRules";
+import { useQuasar } from "quasar";
+import { useRegistrationFlowStore } from "src/stores/registrationFlow";
+import { useRouter } from "vue-router";
 
+const { t } = useI18n();
 const router = useRouter();
 const $q = useQuasar();
-const { t } = useI18n();
-const flowStore = useRegistrationFlowStore();
 const { setAuthDataFromPayload } = useAuth();
+const { inputRules } = useInputRules();
 
+const flowStore = useRegistrationFlowStore();
+
+const addressForm = ref(null);
 const submitting = ref(false);
 
 const form = ref({
-  number: '',
+  document: flowStore.document || "",
+  number: "",
   no_complement: false,
-  complement: '',
-  nickname: '',
-  instructions: '',
-  address_type: 'home',
+  complement: "",
+  nickname: "",
+  instructions: "",
+  address_type: "home",
+  phone: flowStore.phone || "",
 });
 
 const addressTypes = computed(() => [
-  { value: 'home', label: t('auth.address_type_home'), icon: 'mdi-home-outline' },
-  { value: 'commercial', label: t('auth.address_type_commercial'), icon: 'mdi-office-building-outline' },
-  { value: 'other', label: t('auth.address_type_other'), icon: 'mdi-map-marker-outline' },
+  {
+    value: "home",
+    label: t("auth.address_type_home"),
+    icon: "mdi-home-outline",
+  },
+  {
+    value: "commercial",
+    label: t("auth.address_type_commercial"),
+    icon: "mdi-office-building-outline",
+  },
+  {
+    value: "other",
+    label: t("auth.address_type_other"),
+    icon: "mdi-map-marker-outline",
+  },
 ]);
 
 const handleConfirm = async () => {
-  if (!form.value.no_complement && !form.value.complement?.trim()) {
-    $q.notify({ type: 'warning', message: t('auth.complement_required') });
-    return;
-  }
+  const isValid = await addressForm.value?.validate();
+
+  if (!isValid) return;
 
   submitting.value = true;
+
   try {
     const payload = {
       email: flowStore.email || undefined,
-      phone: flowStore.phone || undefined,
+      phone: form.value.phone,
+      document: form.value.document,
+      name: flowStore.name || undefined,
       code: flowStore.code,
       zip_code: flowStore.confirmedZipCode || undefined,
       address: flowStore.confirmedAddress || undefined,
@@ -215,21 +301,26 @@ const handleConfirm = async () => {
       state: flowStore.confirmedState || undefined,
       latitude: flowStore.confirmedLat,
       longitude: flowStore.confirmedLng,
-      has_complement: !form.value.no_complement,
-      complement: form.value.no_complement ? null : (form.value.complement || null),
+      has_complement: true,
+
+      complement: form.value.complement || "",
+
       nickname: form.value.nickname || null,
       instructions: form.value.instructions || null,
       address_type: form.value.address_type,
     };
 
     const response = await createUserAndClient(payload);
+
     if (response.status >= 200 && response.status < 300) {
       await setAuthDataFromPayload(response.data.payload);
+
       flowStore.clear();
-      router.push({ name: 'DashboardPage' });
+
+      router.push({ name: "DashboardPage" });
     }
   } catch {
-    $q.notify({ type: 'negative', message: t('auth.register_error') });
+    $q.notify({ type: "negative", message: t("auth.register_error") });
   } finally {
     submitting.value = false;
   }
@@ -237,10 +328,10 @@ const handleConfirm = async () => {
 
 onMounted(() => {
   if (!flowStore.hasConfirmedLocation() || !flowStore.hasCredentials()) {
-    router.replace({ name: 'LoginPage' });
+    router.replace({ name: "LoginPage" });
     return;
   }
-  form.value.number = flowStore.confirmedNumber || '';
+  form.value.number = flowStore.confirmedNumber || "";
 });
 </script>
 

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

@@ -0,0 +1,618 @@
+<template>
+  <q-page class="service-package-page bg-page q-pb-xl">
+    <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-service-package">
+      <q-btn
+        class="header-back-btn"
+        color="primary"
+        dense
+        flat
+        icon="mdi-chevron-left"
+        round
+        @click="router.back()"
+      />
+
+      <div
+        class="col text-center font16 fontbold gradient-diarista text-primary"
+      >
+        {{ $t("service_package.title") }}
+      </div>
+
+      <div style="width: 36px"></div>
+    </div>
+
+    <div
+      v-if="servicePackage.items.length === 0"
+      class="q-px-md q-pt-md"
+    >
+      <q-card
+        :flat="false"
+        class="empty-card bg-surface shadow-card card-border"
+      >
+        <q-card-section class="column items-center text-center q-pa-lg">
+          <q-icon
+            color="grey-5"
+            name="mdi-shopping-outline"
+            size="42px"
+          />
+
+          <span class="font14 fontbold text-text q-mt-sm">
+            {{ $t("service_package.empty_title") }}
+          </span>
+
+          <span class="font12 text-grey-7 q-mt-xs">
+            {{ $t("service_package.empty_message") }}
+          </span>
+
+          <q-btn
+            class="q-mt-md"
+            color="primary"
+            no-caps
+            padding="8px 18px"
+            rounded
+            unelevated
+            :label="$t('service_package.view_favorites')"
+            @click="router.push({ name: 'ProfilePage' })"
+          />
+        </q-card-section>
+      </q-card>
+
+      <q-card
+        v-if="servicePackage.lastCreatedGroup"
+        :flat="false"
+        class="created-card bg-surface shadow-card card-border q-mt-md"
+      >
+        <q-card-section class="q-pa-md">
+          <div class="row items-center no-wrap">
+            <q-icon
+              color="positive"
+              name="mdi-check-circle-outline"
+              size="24px"
+            />
+
+            <div class="column q-ml-sm">
+              <span class="font14 fontbold text-text">
+                {{ $t("service_package.last_request_title") }}
+              </span>
+
+              <span class="font12 text-grey-7">
+                {{
+                  $t("service_package.created_count", {
+                    count: servicePackage.lastCreatedGroup.schedule_ids.length,
+                  })
+                }}
+              </span>
+            </div>
+          </div>
+        </q-card-section>
+      </q-card>
+    </div>
+
+    <template v-else>
+      <div class="q-px-md q-pt-md column q-gutter-y-sm">
+        <div class="service-package-discount-banner row items-center no-wrap q-pa-sm">
+          <span class="font12 fontbold text-positive q-ml-sm">
+            {{ servicePackageDiscountMessage }}
+          </span>
+        </div>
+
+        <q-card
+          v-for="(item, index) in servicePackage.items"
+          :key="item.local_id"
+          :flat="false"
+          class="service-package-item-card bg-surface shadow-card card-border"
+        >
+          <q-card-section class="row no-wrap q-pa-md">
+            <q-avatar
+              color="indigo-1"
+              size="44px"
+              text-color="indigo-4"
+            >
+              <img
+                v-if="getProfileMediaUrl(item)"
+                :src="getProfileMediaUrl(item)"
+                style="object-fit: cover"
+              />
+
+              <span v-else>
+                {{ getProviderInitial(item) }}
+              </span>
+            </q-avatar>
+
+            <div class="column col q-ml-sm">
+              <span class="font14 fontbold text-text">
+                {{ item.provider_name }}
+              </span>
+
+              <span class="font12 text-grey-7">
+                {{ formatDate(item.date) }}
+              </span>
+
+              <span class="font12 text-grey-7">
+                {{
+                  $t("service_package.time_range", {
+                    start: item.start_time,
+                    end: item.end_time,
+                  })
+                }}
+              </span>
+
+              <div class="row items-center q-gutter-x-sm q-mt-xs">
+                <q-chip
+                  class="font10"
+                  color="purple-1"
+                  dense
+                  square
+                  text-color="primary"
+                >
+                  {{
+                    $t("service_package.hours_short", { hours: item.period_type })
+                  }}
+                </q-chip>
+
+                <q-chip
+                  class="font10"
+                  color="grey-2"
+                  dense
+                  square
+                  text-color="grey-8"
+                >
+                  {{
+                    item.offers_meal
+                      ? $t("service_package.with_meal")
+                      : $t("service_package.without_meal")
+                  }}
+                </q-chip>
+              </div>
+
+              <div class="item-prices q-mt-sm">
+                <div class="row items-center no-wrap">
+                  <span class="font11 fontbold text-primary">
+                    {{ $t("service_package.credit_card_total") }}
+                  </span>
+
+                  <q-space />
+
+                  <span class="font12 fontbold text-primary">
+                    {{ formatCurrencyValue(getCreditCardItemTotal(item)) }}
+                  </span>
+                </div>
+              </div>
+            </div>
+
+            <q-btn
+              color="negative"
+              dense
+              flat
+              icon="mdi-delete-outline"
+              round
+              @click="servicePackage.removeItem(index)"
+            />
+          </q-card-section>
+        </q-card>
+      </div>
+
+      <div class="q-px-md q-pt-md">
+        <q-card
+          :flat="false"
+          class="checkout-card bg-surface shadow-card card-border"
+        >
+          <q-card-section class="q-pa-md">
+            <div class="row items-center no-wrap q-mb-sm">
+              <span class="font14 fontbold text-primary">
+                {{ $t("service_package.summary") }}
+              </span>
+
+              <q-space />
+            </div>
+
+            <div class="summary-row row items-center no-wrap q-py-xs">
+              <span class="font12 text-grey-7">
+                {{ $t("service_package.provider") }}
+              </span>
+
+              <q-space />
+
+              <span class="font12 fontbold text-text text-right">
+                {{ providerName }}
+              </span>
+            </div>
+
+            <div class="summary-row row items-center no-wrap q-py-xs">
+              <span class="font12 text-grey-7">
+                {{ $t("service_package.address") }}
+              </span>
+
+              <q-space />
+
+              <span class="font12 fontbold text-text text-right">
+                {{ primaryAddressLabel }}
+              </span>
+            </div>
+
+            <div class="summary-row row items-center no-wrap q-py-xs">
+              <span class="font12 text-grey-7">
+                {{ $t("service_package.quantity") }}
+              </span>
+
+              <q-space />
+
+              <span class="font12 fontbold text-text">
+                {{
+                  $t("service_package.schedule_count", {
+                    count: servicePackage.items.length,
+                  })
+                }}
+              </span>
+            </div>
+
+            <div class="summary-row row items-center no-wrap q-py-xs">
+              <span class="font12 fontbold text-primary">
+                {{ $t("service_package.credit_card_total") }}
+              </span>
+
+              <q-space />
+
+              <span class="font14 fontbold text-primary">
+                {{ formatCurrencyValue(creditCardTotalAmount) }}
+              </span>
+            </div>
+
+            <div
+              v-if="creditCardDiscountAmount > 0"
+              class="summary-discount row items-center no-wrap q-py-xs q-px-sm q-mt-sm"
+            >
+              <span class="font12 fontbold text-positive q-ml-xs">
+                {{
+                  $t("service_package.discount_saved", {
+                    value: formatPrice(creditCardDiscountAmount),
+                  })
+                }}
+              </span>
+            </div>
+          </q-card-section>
+
+          <q-card-actions class="q-px-md q-pb-md">
+            <q-btn
+              class="full-width"
+              color="primary"
+              no-caps
+              padding="10px 16px"
+              rounded
+              unelevated
+              :disable="addressLoading || !platformFeesReady || !hasSingleProvider"
+              :label="$t('service_package.close')"
+              :loading="submitting"
+              @click="submitServicePackage"
+            />
+          </q-card-actions>
+        </q-card>
+      </div>
+    </template>
+  </q-page>
+</template>
+
+<script setup>
+import { computed, onMounted, ref } from "vue";
+import { createScheduleServicePackage } from "src/api/servicePackage";
+import { getAddresses } from "src/api/address";
+import { getProfileMediaUrl } from "src/helpers/profileMedia";
+import { useI18n } from "vue-i18n";
+import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
+import { useQuasar } from "quasar";
+import { useRouter } from "vue-router";
+import { userStore } from "src/stores/user";
+import { useServicePackageStore } from "src/stores/servicePackage";
+
+const { t } = useI18n();
+const router = useRouter();
+const $q = useQuasar();
+const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
+
+const servicePackage = useServicePackageStore();
+const store = userStore();
+
+const addressLoading = ref(false);
+const primaryAddress = ref(null);
+const submitting = ref(false);
+
+const isValidFee = (fee) =>
+  fee !== null && fee !== undefined && Number.isFinite(Number(fee));
+
+const roundMoney = (value) => Number(Number(value ?? 0).toFixed(2));
+
+const roundNullableMoney = (value) => {
+  if (value === null || value === undefined || !Number.isFinite(Number(value)))
+    return null;
+
+  return roundMoney(value);
+};
+
+const sumItems = (callback) => {
+  let total = 0;
+
+  for (const item of servicePackage.items) {
+    const itemTotal = callback(item);
+
+    if (itemTotal === null) return null;
+
+    total += itemTotal;
+  }
+
+  return total;
+};
+
+const getTotalWithFee = (item, fee) => {
+  if (!isValidFee(fee)) return null;
+
+  return roundMoney(Number(item.base_amount ?? 0) * (1 + Number(fee)));
+};
+
+const hasServicePackageDiscount = computed(() => servicePackage.items.length >= 3);
+
+const getStandardCreditCardItemTotal = (item) =>
+  getTotalWithFee(item, platformFees.value.credit_card);
+
+const getCreditCardItemTotal = (item) =>
+  getTotalWithFee(
+    item,
+    hasServicePackageDiscount.value
+      ? platformFees.value.service_package_min_3_schedules_credit_card
+      : platformFees.value.credit_card,
+  );
+
+const creditCardTotalAmount = computed(() =>
+  roundNullableMoney(sumItems(getCreditCardItemTotal)),
+);
+
+const creditCardDiscountAmount = computed(() => {
+  if (!hasServicePackageDiscount.value) return 0;
+
+  return (
+    roundNullableMoney(
+      sumItems((item) => {
+        const standardTotal = getStandardCreditCardItemTotal(item);
+        const discountedTotal = getCreditCardItemTotal(item);
+
+        if (standardTotal === null || discountedTotal === null) return null;
+
+        return standardTotal - discountedTotal;
+      }),
+    ) ?? 0
+  );
+});
+
+const missingSchedulesForDiscount = computed(() =>
+  Math.max(3 - servicePackage.items.length, 0),
+);
+
+const servicePackageDiscountMessage = computed(() => {
+  if (hasServicePackageDiscount.value) {
+    return t("service_package.discount_applied");
+  }
+
+  return t("service_package.discount_hint", {
+    count: missingSchedulesForDiscount.value,
+  });
+});
+
+const platformFeesReady = computed(
+  () =>
+    isValidFee(platformFees.value.credit_card) &&
+    (!hasServicePackageDiscount.value ||
+      isValidFee(
+        platformFees.value.service_package_min_3_schedules_credit_card,
+      )),
+);
+
+const hasSingleProvider = computed(() => {
+  const providers = new Set(
+    servicePackage.items.map((item) => item.provider_id).filter(Boolean),
+  );
+
+  return providers.size <= 1;
+});
+
+const primaryAddressLabel = computed(() => {
+  if (addressLoading.value) return t("service_package.loading");
+  if (!primaryAddress.value) return t("service_package.no_primary_address");
+
+  return [
+    primaryAddress.value.address,
+    primaryAddress.value.number,
+    primaryAddress.value.district,
+  ]
+    .filter(Boolean)
+    .join(", ");
+});
+
+const providerName = computed(() => {
+  const providerNames = [
+    ...new Set(servicePackage.items.map((item) => item.provider_name).filter(Boolean)),
+  ];
+
+  if (providerNames.length === 0) return t("service_package.unknown");
+  if (providerNames.length === 1) return providerNames[0];
+
+  return t("service_package.multiple_providers", { count: providerNames.length });
+});
+
+const normalizeDate = (value) =>
+  String(value ?? "")
+    .replace(/\//g, "-")
+    .slice(0, 10);
+
+const formatDate = (value) => {
+  const parsed = new Date(`${normalizeDate(value)}T12:00:00`);
+
+  return new Intl.DateTimeFormat("pt-BR", {
+    day: "2-digit",
+    month: "2-digit",
+    year: "numeric",
+  }).format(parsed);
+};
+
+const formatPrice = (value) => {
+  if (
+    value === null ||
+    value === undefined ||
+    !Number.isFinite(Number(value))
+  ) {
+    return t("service_package.loading");
+  }
+
+  return Number(value ?? 0).toLocaleString("pt-BR", {
+    minimumFractionDigits: 2,
+    maximumFractionDigits: 2,
+  });
+};
+
+const formatCurrencyValue = (value) => {
+  if (
+    value === null ||
+    value === undefined ||
+    !Number.isFinite(Number(value))
+  ) {
+    return t("service_package.loading");
+  }
+
+  return t("service_package.currency_value", { value: formatPrice(value) });
+};
+
+const getProviderInitial = (item) =>
+  item.provider_name?.slice(0, 1).toUpperCase() ??
+  t("service_package.provider_initial");
+
+const loadPrimaryAddress = async () => {
+  const clientId = store.user?.client_id ?? store.user?.client?.id;
+
+  if (!clientId) return;
+
+  addressLoading.value = true;
+
+  try {
+    const addresses = await getAddresses("client", clientId);
+
+    primaryAddress.value =
+      (addresses ?? []).find((address) => address.is_primary) ?? null;
+  } catch {
+    primaryAddress.value = null;
+  } finally {
+    addressLoading.value = false;
+  }
+};
+
+const submitServicePackage = async () => {
+  if (!servicePackage.items.length || submitting.value) return;
+
+  if (!hasSingleProvider.value) {
+    $q.notify({
+      type: "warning",
+      message: t("service_package.same_provider_required"),
+    });
+
+    return;
+  }
+
+  if (!primaryAddress.value) {
+    $q.notify({
+      type: "warning",
+      message: t("service_package.no_primary_address_notify"),
+    });
+
+    return;
+  }
+
+  const payload = {
+    client_id: store.user.client_id ?? store.user?.client?.id,
+    address_id: primaryAddress.value.id,
+    schedule_type: "default",
+
+    schedules: servicePackage.items.map((item) => ({
+      provider_id: item.provider_id,
+      date: item.date,
+      period_type: item.period_type,
+      start_time: item.start_time,
+      end_time: item.end_time,
+      total_amount: item.base_amount,
+      offers_meal: item.offers_meal,
+    })),
+  };
+
+  submitting.value = true;
+
+  try {
+    const createdServicePackage = await createScheduleServicePackage(payload);
+
+    const createdSchedules = createdServicePackage?.schedules ?? [];
+
+    const scheduleIds =
+      createdServicePackage?.schedule_ids ??
+      createdSchedules.map((schedule) => schedule.id);
+
+    servicePackage.setLastCreatedGroup({
+      local_group_id: createdServicePackage?.id ?? `sp-${Date.now()}`,
+      service_package_id: createdServicePackage?.id ?? null,
+      schedule_ids: scheduleIds,
+      schedules: createdSchedules,
+    });
+
+    servicePackage.clear();
+
+    $q.notify({
+      type: "positive",
+      message: t("service_package.closed_success"),
+    });
+  } catch (error) {
+    const message =
+      error?.response?.data?.message ??
+      error?.message ??
+      t("service_package.submit_error");
+
+    $q.notify({ type: "negative", message });
+  } finally {
+    submitting.value = false;
+  }
+};
+
+onMounted(() => {
+  loadPrimaryAddress();
+  loadPlatformFees().catch(() => {});
+});
+</script>
+
+<style scoped lang="scss">
+.service-package-page {
+  min-height: 100%;
+}
+
+.shadow-service-package {
+  box-shadow: 0 1px 8px rgba(0, 0, 0, 0.1);
+}
+
+.service-package-item-card,
+.checkout-card,
+.empty-card,
+.created-card {
+  border-radius: 8px;
+}
+
+.summary-row {
+  border-top: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+.item-prices {
+  max-width: 220px;
+}
+
+.service-package-discount-banner {
+  background: rgba(34, 197, 94, 0.1);
+  border: 1px solid rgba(34, 197, 94, 0.22);
+  border-radius: 8px;
+}
+
+.summary-discount {
+  background: rgba(34, 197, 94, 0.12);
+  border-radius: 8px;
+}
+
+</style>

+ 45 - 29
src/pages/profile/ProfileEditDialog.vue

@@ -11,6 +11,7 @@
         class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile"
       >
         <q-btn
+          class="header-back-btn"
           color="primary"
           dense
           flat
@@ -23,20 +24,30 @@
 
         <span
           class="text-subtitle1 text-primary font16 fontbold gradient-diarista"
-          >{{ $t("profile.edit_data") }}</span
         >
+          {{ $t("profile.edit_data") }}
+        </span>
 
         <q-space />
 
         <div style="width: 32px"></div>
       </div>
 
-      <div v-if="loading" class="flex flex-center q-pa-xl">
-        <q-spinner color="primary" size="3em" />
+      <div
+        v-if="loading"
+        class="flex flex-center q-pa-xl"
+      >
+        <q-spinner
+          color="primary"
+          size="3em"
+        />
       </div>
 
       <template v-else>
-        <q-scroll-area class="col" style="height: calc(100vh - 72px)">
+        <q-scroll-area
+          class="col"
+          style="height: calc(100vh - 72px)"
+        >
           <div class="column items-center q-mt-xl q-mb-md">
             <q-avatar
               class="text-h2 shadow-1"
@@ -50,9 +61,9 @@
                 style="object-fit: cover; width: 100%; height: 100%"
               />
 
-              <span v-else>{{
-                form.name ? form.name.charAt(0).toUpperCase() : ""
-              }}</span>
+              <span v-else>
+                {{ form.name ? form.name.charAt(0).toUpperCase() : "" }}
+              </span>
             </q-avatar>
 
             <input
@@ -73,7 +84,10 @@
             />
           </div>
 
-          <q-form ref="formRef" @submit.prevent="onSubmit">
+          <q-form
+            ref="formRef"
+            @submit.prevent="onSubmit"
+          >
             <div class="q-px-xl q-gutter-y-lg">
               <div>
                 <div class="text-grey-8 q-mb-sm font12 fontbold">
@@ -211,6 +225,7 @@
 
 <script setup>
 import { Cookies, useDialogPluginComponent } from "quasar";
+import { getProfileMediaUrl } from "src/helpers/profileMedia";
 import { i18n } from "src/boot/i18n";
 import { onMounted, ref } from "vue";
 import { updateMe } from "src/api/user";
@@ -218,12 +233,12 @@ import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
 import { useInputRules } from "src/composables/useInputRules";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
 
+defineEmits([...useDialogPluginComponent.emits]);
+
 const props = defineProps({
   userData: { type: Object, default: null },
 });
 
-defineEmits([...useDialogPluginComponent.emits]);
-
 const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
 
 const { form, hasUpdatedFields, setUpdateFormAsOriginal } =
@@ -240,11 +255,11 @@ const {
 
 const { inputRules } = useInputRules();
 
-const formRef       = ref(null);
-const fileInputRef  = ref(null);
-const loading       = ref(false);
-const avatarFile    = ref(null);
+const avatarFile = ref(null);
 const avatarPreview = ref(null);
+const fileInputRef = ref(null);
+const formRef = ref(null);
+const loading = ref(false);
 
 const normalizeLocale = (loc) => {
   if (!loc) return "pt";
@@ -262,6 +277,15 @@ const selectedLocale = ref(
   normalizeLocale(i18n.global.locale.value ?? i18n.global.locale),
 );
 
+//
+const fileToBase64 = (file) =>
+  new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onload = () => resolve(reader.result);
+    reader.onerror = () => reject(reader.error);
+    reader.readAsDataURL(file);
+  });
+
 const onFileSelected = (event) => {
   const file = event.target.files[0];
 
@@ -278,23 +302,15 @@ const onLocaleChange = (val) => {
   Cookies.set("locale", val, { expires: 365, path: "/" });
 };
 
-const fileToBase64 = (file) =>
-  new Promise((resolve, reject) => {
-    const reader = new FileReader();
-    reader.onload = () => resolve(reader.result);
-    reader.onerror = () => reject(reader.error);
-    reader.readAsDataURL(file);
-  });
-
 const onSubmit = async () => {
   const valid = await formRef.value.validate();
 
   if (!valid) return;
 
   const payload = {
-    name:     form.name,
-    email:    form.email,
-    phone:    form.phone,
+    name: form.name,
+    email: form.email,
+    phone: form.phone,
     document: form.document || null,
   };
 
@@ -309,12 +325,12 @@ onMounted(async () => {
   if (props.userData) {
     const data = props.userData;
 
-    form.name     = data.name            || "";
-    form.email    = data.email           || "";
-    form.phone    = data.phone           || "";
+    form.name = data.name || "";
+    form.email = data.email || "";
+    form.phone = data.phone || "";
     form.document = data.client_document || "";
 
-    avatarPreview.value = data.client?.profile_media?.url ?? null;
+    avatarPreview.value = getProfileMediaUrl(data);
 
     setUpdateFormAsOriginal(data);
 

+ 272 - 102
src/pages/profile/ProfilePage.vue

@@ -1,36 +1,73 @@
 <!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
 <template>
   <q-page class="bg-page q-pb-xl">
-    <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile">
+    <div
+      class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile"
+    >
       <q-space />
-      <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('profile.title') }}</span>
+
+      <span
+        class="text-subtitle1 text-primary font16 fontbold gradient-diarista"
+      >
+        {{ $t("profile.title") }}
+      </span>
+
       <q-space />
     </div>
 
     <div class="q-px-md q-mt-md">
-      <q-card class="profile-card bg-surface shadow-card card-border" :flat="false">
-        <q-btn flat round dense icon="mdi-share-variant-outline" color="grey-6" class="absolute-top-right q-ma-sm" />
-        
+      <q-card
+        class="profile-card bg-surface shadow-card card-border"
+        :flat="false"
+      >
         <q-card-section class="column items-center q-pb-md">
-          <q-avatar size="70px" color="indigo-1" text-color="indigo-4" class="shadow-card text-h5">
-            <img v-if="user.client?.profile_media?.url" :src="user.client.profile_media.url" style="object-fit: cover;" />
-            <span v-else>{{ user.name ? user.name.charAt(0).toUpperCase() : '' }}</span>
+          <q-avatar
+            class="shadow-card text-h5"
+            color="indigo-1"
+            size="70px"
+            text-color="indigo-4"
+          >
+            <img
+              v-if="profileMediaUrl"
+              class="profile-image"
+              :src="profileMediaUrl"
+            />
+
+            <span v-else>
+              {{ user.name ? user.name.charAt(0).toUpperCase() : "" }}
+            </span>
           </q-avatar>
-          
-          <div class="fonte-nome-profile q-mt-md text-dark font16 fontbold">{{ user.name || '—' }}</div>
-          <div class="fonte-email-profile text-grey-6 q-my-sm font10 fontmedium">{{ user.email || '—' }}</div>
-          <div class="fonte-telefone-profile text-grey-7 font12 fontmedium">{{ user.phone || '—' }}</div> 
-          
-          <q-btn 
-            outline 
-            no-caps 
+
+          <div class="fonte-nome-profile q-mt-md text-dark font16 fontbold">
+            {{ user.name || "—" }}
+          </div>
+
+          <div
+            class="fonte-email-profile text-grey-6 q-my-sm font10 fontmedium"
+          >
+            {{ user.email || "—" }}
+          </div>
+
+          <div class="fonte-telefone-profile text-grey-7 font12 fontmedium">
+            {{ user.phone || "—" }}
+          </div>
+
+          <q-btn
+            class="full-width q-mt-sm btn-edit-profile text-text"
+            no-caps
+            outline
             padding="6px 16px"
-            class="full-width q-mt-sm btn-edit-profile text-text" 
             @click="openEditProfile"
           >
             <div class="row items-center q-gutter-x-sm">
-              <q-icon name="mdi-account-outline" size="18px" />
-              <span class="font10 fontbold">{{ $t('profile.edit_profile') }}</span>
+              <q-icon
+                name="mdi-account-outline"
+                size="18px"
+              />
+
+              <span class="font10 fontbold">
+                {{ $t("profile.edit_profile") }}
+              </span>
             </div>
           </q-btn>
         </q-card-section>
@@ -38,152 +75,284 @@
     </div>
 
     <div class="q-mt-md q-px-md column">
-      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="openPaymentsDialog">
+      <div
+        class="menu-item row items-center no-wrap cursor-pointer q-py-sm"
+        @click="openPaymentsDialog"
+      >
         <div class="column">
-          <span class="menu-title gradient-diarista font16 fontbold">{{ $t('profile.payments.title') }}</span>
-          <span class="text-text font12 fontregular">{{ $t('profile.payments.description') }}</span>
+          <span class="menu-title gradient-diarista font16 fontbold">
+            {{ $t("profile.payments.title") }}
+          </span>
+
+          <span class="text-text font12 fontregular">
+            {{ $t("profile.payments.description") }}
+          </span>
         </div>
-        <q-space/>
-        <q-icon name="mdi-chevron-right" color="primary" size="md" />
+
+        <q-space />
+
+        <q-icon
+          color="primary"
+          name="mdi-chevron-right"
+          size="md"
+        />
       </div>
 
-      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="openFavoritesDialog">
+      <div
+        class="menu-item row items-center no-wrap cursor-pointer q-py-sm"
+        @click="openFavoritesDialog"
+      >
         <div class="column">
-          <span class="menu-title gradient-diarista font16 fontbold">{{ $t('profile.favorites.title') }}</span>
-          <span class="text-text font12 fontregular">{{ $t('profile.favorites.description') }}</span>
+          <span class="menu-title gradient-diarista font16 fontbold">
+            {{ $t("profile.favorites.title") }}
+          </span>
+
+          <span class="text-text font12 fontregular">
+            {{ $t("profile.favorites.description") }}
+          </span>
         </div>
-        <q-space/>
-        <q-icon name="mdi-chevron-right" color="primary" size="md" />
+
+        <q-space />
+
+        <q-icon
+          color="primary"
+          name="mdi-chevron-right"
+          size="md"
+        />
       </div>
 
-      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="openAddressDialog">
+      <div
+        class="menu-item row items-center no-wrap cursor-pointer q-py-sm"
+        @click="openAddressDialog"
+      >
         <div class="column">
-          <span class="menu-title gradient-diarista font16 fontbold">{{ $t('profile.address.title') }}</span>
-          <span class="text-text font12 fontregular">{{ $t('profile.address.description') }}</span>
+          <span class="menu-title gradient-diarista font16 fontbold">
+            {{ $t("profile.address.title") }}
+          </span>
+
+          <span class="text-text font12 fontregular">
+            {{ $t("profile.address.description") }}
+          </span>
         </div>
-        <q-space/>
-        <q-icon name="mdi-chevron-right" color="primary" size="md" />
+
+        <q-space />
+
+        <q-icon
+          color="primary"
+          name="mdi-chevron-right"
+          size="md"
+        />
       </div>
 
-      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="openHelpDialog">
+      <div
+        class="menu-item row items-center no-wrap cursor-pointer q-py-sm"
+        @click="openHelpDialog"
+      >
         <div class="column">
-          <span class="menu-title gradient-diarista font16 fontbold">{{ $t('profile.help.title') }}</span>
-          <span class="text-text font12 fontregular">{{ $t('profile.help.description') }}</span>
+          <span class="menu-title gradient-diarista font16 fontbold">
+            {{ $t("profile.help.title") }}
+          </span>
+
+          <span class="text-text font12 fontregular">
+            {{ $t("profile.help.description") }}
+          </span>
         </div>
-        <q-space/>
-        <q-icon name="mdi-chevron-right" color="primary" size="md" />
+
+        <q-space />
+
+        <q-icon
+          color="primary"
+          name="mdi-chevron-right"
+          size="md"
+        />
       </div>
 
-      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="openPrivacyDialog">
+      <div
+        class="menu-item row items-center no-wrap cursor-pointer q-py-sm"
+        @click="openPrivacyDialog"
+      >
         <div class="column">
-          <span class="menu-title gradient-diarista font16 fontbold">{{ $t('profile.privacy.title') }}</span>
-          <span class="text-text font12 fontregular">{{ $t('profile.privacy.description') }}</span>
+          <span class="menu-title gradient-diarista font16 fontbold">
+            {{ $t("profile.privacy.title") }}
+          </span>
+
+          <span class="text-text font12 fontregular">
+            {{ $t("profile.privacy.description") }}
+          </span>
         </div>
-        <q-space/>
-        <q-icon name="mdi-chevron-right" color="primary" size="md" />
+
+        <q-space />
+
+        <q-icon
+          color="primary"
+          name="mdi-chevron-right"
+          size="md"
+        />
       </div>
 
-      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="openPrivacyPolicy">
+      <div
+        class="menu-item row items-center no-wrap cursor-pointer q-py-sm"
+        @click="openPrivacyPolicy"
+      >
         <div class="column">
-          <span class="menu-title gradient-diarista font16 fontbold">{{ $t('profile.privacy_policy.title') }}</span>
-          <span class="text-text font12 fontregular">{{ $t('profile.privacy_policy.description') }}</span>
+          <span class="menu-title gradient-diarista font16 fontbold">
+            {{ $t("profile.privacy_policy.title") }}
+          </span>
+
+          <span class="text-text font12 fontregular">
+            {{ $t("profile.privacy_policy.description") }}
+          </span>
         </div>
-        <q-space/>
-        <q-icon name="mdi-chevron-right" color="primary" size="md" />
+
+        <q-space />
+
+        <q-icon
+          color="primary"
+          name="mdi-chevron-right"
+          size="md"
+        />
       </div>
 
-      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="openSupportPage">
+      <div
+        class="menu-item row items-center no-wrap cursor-pointer q-py-sm"
+        @click="openSupportPage"
+      >
         <div class="column">
-          <span class="menu-title gradient-diarista font16 fontbold">{{ $t('profile.support_page.title') }}</span>
-          <span class="text-text font12 fontregular">{{ $t('profile.support_page.description') }}</span>
+          <span class="menu-title gradient-diarista font16 fontbold">
+            {{ $t("profile.support_page.title") }}
+          </span>
+
+          <span class="text-text font12 fontregular">
+            {{ $t("profile.support_page.description") }}
+          </span>
         </div>
-        <q-space/>
-        <q-icon name="mdi-chevron-right" color="primary" size="md" />
+
+        <q-space />
+
+        <q-icon
+          color="primary"
+          name="mdi-chevron-right"
+          size="md"
+        />
       </div>
 
-      <q-separator class="q-my-sm bg-grey-3" inset />
+      <q-separator
+        class="q-my-sm bg-grey-3"
+        inset
+      />
 
-      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="handleLogout">
+      <div
+        class="menu-item row items-center no-wrap cursor-pointer q-py-sm"
+        @click="handleLogout"
+      >
         <div class="column">
-          <span class="menu-title text-text font16 fontbold">{{ $t('profile.logout.title') }}</span>
-          <span class="text-text font12 fontregular">{{ $t('profile.logout.description') }}</span>
+          <span class="menu-title text-text font16 fontbold">
+            {{ $t("profile.logout.title") }}
+          </span>
+
+          <span class="text-text font12 fontregular">
+            {{ $t("profile.logout.description") }}
+          </span>
         </div>
-        <q-space/>
-        <q-icon name="mdi-chevron-right" color="primary" size="md" />
+
+        <q-space />
+
+        <q-icon
+          color="primary"
+          name="mdi-chevron-right"
+          size="md"
+        />
       </div>
     </div>
   </q-page>
 </template>
 
 <script setup>
-import { ref, onMounted } from 'vue';
-import { useQuasar } from 'quasar';
-import { Browser } from '@capacitor/browser';
-import { useAuth } from 'src/composables/useAuth';
-import { getUser } from 'src/api/user';
-import { userStore } from 'src/stores/user';
-import ProfileEditDialog from './ProfileEditDialog.vue';
-import ProfileAddressDialog from 'src/components/profile/ProfileAddressDialog.vue';
-import ProfilePaymentsDialog from 'src/components/profile/ProfilePaymentsDialog.vue';
-import ProfileFavoritesDialog from 'src/components/profile/ProfileFavoritesDialog.vue';
-import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue';
-import ProfilePrivacyDialog from 'src/components/profile/ProfilePrivacyDialog.vue';
-import { useRouter } from 'vue-router';
-
-const PRIVACY_POLICY_URL = 'https://politicas.softpar.inf.br/politicas/politicaDiaristaCliente.html';
-const SUPPORT_PAGE_URL   = 'https://politicas.softpar.inf.br/suportes/suporteDiaristaCliente.html';
+import { Browser } from "@capacitor/browser";
+import { computed, onMounted, ref } from "vue";
+import { getProfileMediaUrl } from "src/helpers/profileMedia";
+import { getUser } from "src/api/user";
+import { useAuth } from "src/composables/useAuth";
+import { useQuasar } from "quasar";
+import { useRouter } from "vue-router";
+import { userStore } from "src/stores/user";
+
+import ProfileAddressDialog from "src/components/profile/ProfileAddressDialog.vue";
+import ProfileEditDialog from "./ProfileEditDialog.vue";
+import ProfileFavoritesDialog from "src/components/profile/ProfileFavoritesDialog.vue";
+import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
+import ProfilePaymentsDialog from "src/components/profile/ProfilePaymentsDialog.vue";
+import ProfilePrivacyDialog from "src/components/profile/ProfilePrivacyDialog.vue";
 
+const router = useRouter();
 const $q = useQuasar();
+const { logout } = useAuth();
+
 const store = userStore();
 
-const { logout } = useAuth();
-const router = useRouter();
 const user = ref({
-  name: '',
-  email: '',
-  phone: ''
+  email: "",
+  name: "",
+  phone: "",
 });
 
+const profileMediaUrl = computed(() => getProfileMediaUrl(user.value));
+
+const PRIVACY_POLICY_URL =
+  "https://politicas.softpar.inf.br/politicas/politicaDiaristaCliente.html";
+const SUPPORT_PAGE_URL =
+  "https://politicas.softpar.inf.br/suportes/suporteDiaristaCliente.html";
+
+const handleLogout = async () => {
+  await logout();
+
+  router.push("/login");
+};
+
+const openAddressDialog = () => {
+  $q.dialog({
+    component: ProfileAddressDialog,
+  });
+};
+
 const openEditProfile = () => {
   $q.dialog({
     component: ProfileEditDialog,
     componentProps: {
-      userData: user.value
-    }
+      userData: user.value,
+    },
   }).onOk((updatedUser) => {
     user.value = { ...user.value, ...updatedUser };
+
     store.setUser({ ...store.user, ...updatedUser });
-    getUser().then(data => { user.value = data; });
-  });
-};
 
-const openPaymentsDialog = () => {
-  $q.dialog({
-    component: ProfilePaymentsDialog
+    getUser().then((data) => {
+      user.value = data;
+    });
   });
 };
 
 const openFavoritesDialog = () => {
   $q.dialog({
-    component: ProfileFavoritesDialog
+    component: ProfileFavoritesDialog,
   });
 };
 
-const openAddressDialog = () => {
+const openHelpDialog = () => {
   $q.dialog({
-    component: ProfileAddressDialog
+    component: ProfileHelpDialog,
   });
 };
 
-const openHelpDialog = () => {
+const openPaymentsDialog = () => {
   $q.dialog({
-    component: ProfileHelpDialog
+    component: ProfilePaymentsDialog,
   });
 };
 
 const openPrivacyDialog = () => {
   $q.dialog({
-    component: ProfilePrivacyDialog
+    component: ProfilePrivacyDialog,
   });
 };
 
@@ -195,17 +364,12 @@ const openSupportPage = async () => {
   await Browser.open({ url: SUPPORT_PAGE_URL });
 };
 
-const handleLogout = async () => {
-  await logout();
-  router.push('/login');
-};
-
 onMounted(async () => {
   try {
     const data = await getUser();
     user.value = data;
   } catch (error) {
-    console.error('Erro ao carregar perfil:', error);
+    console.error("Erro ao carregar perfil:", error);
   }
 });
 </script>
@@ -215,6 +379,12 @@ onMounted(async () => {
   position: relative;
 }
 
+.profile-image {
+  height: 100%;
+  object-fit: cover;
+  width: 100%;
+}
+
 .btn-edit-profile {
   border-radius: 6px;
 }
@@ -222,7 +392,7 @@ onMounted(async () => {
 .menu-item {
   transition: background 0.3s;
   &:active {
-    background: rgba(0,0,0,0.05);
+    background: rgba(0, 0, 0, 0.05);
   }
 }
 

+ 0 - 529
src/pages/schedule-cart/ScheduleCartPage.vue

@@ -1,529 +0,0 @@
-<template>
-  <q-page class="schedule-cart-page bg-page q-pb-xl">
-    <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-cart">
-      <q-btn
-        color="primary"
-        dense
-        flat
-        icon="mdi-chevron-left"
-        round @click="router.back()"
-      />
-
-      <div class="col text-center font16 fontbold gradient-diarista text-primary">
-        {{ $t('schedule_cart.title') }}
-      </div>
-
-      <div style="width: 36px"></div>
-    </div>
-
-    <div v-if="cart.items.length === 0" class="q-px-md q-pt-md">
-      <q-card :flat="false" class="empty-card bg-surface shadow-card card-border">
-        <q-card-section class="column items-center text-center q-pa-lg">
-          <q-icon
-            color="grey-5"
-            name="mdi-cart-off"
-            size="42px"
-          />
-
-          <span class="font14 fontbold text-text q-mt-sm">
-            {{ $t('schedule_cart.empty_title') }}
-          </span>
-
-          <span class="font12 text-grey-7 q-mt-xs">
-            {{ $t('schedule_cart.empty_message') }}
-          </span>
-
-          <q-btn
-            class="q-mt-md"
-            color="primary"
-            no-caps
-            padding="8px 18px"
-            rounded
-            unelevated
-            :label="$t('schedule_cart.view_favorites')"
-            @click="router.push({ name: 'ProfilePage' })"
-          />
-        </q-card-section>
-      </q-card>
-
-      <q-card v-if="cart.lastCreatedGroup" :flat="false" class="created-card bg-surface shadow-card card-border q-mt-md">
-        <q-card-section class="q-pa-md">
-          <div class="row items-center no-wrap">
-            <q-icon
-              color="positive"
-              name="mdi-check-circle-outline"
-              size="24px"
-            />
-
-            <div class="column q-ml-sm">
-              <span class="font14 fontbold text-text">
-                {{ $t('schedule_cart.last_request_title') }}
-              </span>
-
-              <span class="font12 text-grey-7">
-                {{ $t('schedule_cart.created_count', { count: cart.lastCreatedGroup.schedule_ids.length }) }}
-              </span>
-            </div>
-          </div>
-        </q-card-section>
-      </q-card>
-    </div>
-
-    <template v-else>
-      <div class="q-px-md q-pt-md column q-gutter-y-sm">
-        <div class="cart-discount-banner row items-center no-wrap q-pa-sm">
-          <q-icon
-            color="positive"
-            name="mdi-sale-outline"
-            size="20px"
-          />
-
-          <span class="font12 fontbold text-positive q-ml-sm">
-            {{ cartDiscountMessage }}
-          </span>
-        </div>
-
-        <q-card
-          v-for="(item, index) in cart.items"
-          :key="item.local_id"
-          :flat="false"
-          class="cart-item-card bg-surface shadow-card card-border"
-        >
-          <q-card-section class="row no-wrap q-pa-md">
-            <q-avatar color="indigo-1" size="44px" text-color="indigo-4">
-              <span>
-                {{ getProviderInitial(item) }}
-              </span>
-            </q-avatar>
-
-            <div class="column col q-ml-sm">
-              <span class="font14 fontbold text-text">
-                {{ item.provider_name }}
-              </span>
-
-              <span class="font12 text-grey-7">
-                {{ formatCartDate(item.date) }}
-              </span>
-
-              <span class="font12 text-grey-7">
-                {{ $t('schedule_cart.time_range', { start: item.start_time, end: item.end_time }) }}
-              </span>
-
-              <div class="row items-center q-gutter-x-sm q-mt-xs">
-                <q-chip
-                  class="font10"
-                  color="purple-1"
-                  dense
-                  square
-                  text-color="primary"
-                >
-                  {{ $t('schedule_cart.hours_short', { hours: item.period_type }) }}
-                </q-chip>
-
-                <q-chip class="font10" color="grey-2" dense square text-color="grey-8">
-                  {{ item.offers_meal ? $t('schedule_cart.with_meal') : $t('schedule_cart.without_meal') }}
-                </q-chip>
-              </div>
-
-              <div class="item-prices q-mt-sm">
-                <div v-if="!hasCartDiscount" class="row items-center no-wrap">
-                  <span class="font11 fontbold text-primary">{{ $t('schedule_cart.pix_total') }}</span>
-
-                  <q-space />
-
-                  <span class="font12 fontbold text-primary">
-                    {{ formatCurrencyValue(getStandardPixItemTotal(item)) }}
-                  </span>
-                </div>
-
-                <div v-if="!hasCartDiscount" class="row items-center no-wrap q-mt-xs">
-                  <span class="font11 text-grey-7">
-                    {{ $t('schedule_cart.credit_card_total') }}
-                  </span>
-
-                  <q-space />
-
-                  <span class="font12 fontbold text-text">
-                    {{ formatCurrencyValue(getCreditCardItemTotal(item)) }}
-                  </span>
-                </div>
-
-                <div v-if="hasCartDiscount" class="row items-center no-wrap q-mt-xs">
-                  <span class="font11 fontbold text-positive">
-                    {{ $t('schedule_cart.cart_discount_total') }}
-                  </span>
-
-                  <q-space />
-
-                  <span class="font12 fontbold text-positive">
-                    {{ formatCurrencyValue(getCartDiscountItemTotal(item)) }}
-                  </span>
-                </div>
-              </div>
-            </div>
-
-            <q-btn
-              color="negative"
-              dense
-              flat
-              icon="mdi-delete-outline"
-              round
-              @click="cart.removeItem(index)"
-            />
-          </q-card-section>
-        </q-card>
-      </div>
-
-      <div class="q-px-md q-pt-md">
-        <q-card :flat="false" class="checkout-card bg-surface shadow-card card-border">
-          <q-card-section class="q-pa-md">
-            <div class="row items-center no-wrap q-mb-sm">
-              <span class="font14 fontbold text-primary">
-                {{ $t('schedule_cart.summary') }}
-              </span>
-
-              <q-space />
-            </div>
-
-            <div class="summary-row row items-center no-wrap q-py-xs">
-              <span class="font12 text-grey-7">
-                {{ $t('schedule_cart.provider') }}
-              </span>
-
-              <q-space />
-
-              <span class="font12 fontbold text-text text-right">
-                {{ providerName }}
-              </span>
-            </div>
-
-            <div class="summary-row row items-center no-wrap q-py-xs">
-              <span class="font12 text-grey-7">
-                {{ $t('schedule_cart.address') }}
-              </span>
-
-              <q-space />
-
-              <span class="font12 fontbold text-text text-right">
-                {{ primaryAddressLabel }}
-              </span>
-            </div>
-
-            <div class="summary-row row items-center no-wrap q-py-xs">
-              <span class="font12 text-grey-7">
-                {{ $t('schedule_cart.quantity') }}
-              </span>
-
-              <q-space />
-
-              <span class="font12 fontbold text-text">
-                {{ $t('schedule_cart.schedule_count', { count: cart.items.length }) }}
-              </span>
-            </div>
-
-            <div v-if="!hasCartDiscount" class="summary-row row items-center no-wrap q-py-xs">
-              <span class="font12 fontbold text-primary">
-                {{ $t('schedule_cart.pix_total') }}
-              </span>
-
-              <q-space />
-
-              <span class="font14 fontbold text-primary">
-                {{ formatCurrencyValue(pixTotalAmount) }}
-              </span>
-            </div>
-
-            <div v-if="hasCartDiscount" class="summary-row row items-center no-wrap q-py-xs">
-              <span class="font12 fontbold text-positive">
-                {{ $t('schedule_cart.cart_discount_total') }}
-              </span>
-
-              <q-space />
-
-              <span class="font14 fontbold text-positive">
-                {{ formatCurrencyValue(cartDiscountTotalAmount) }}
-              </span>
-            </div>
-
-            <div v-if="pixDiscountAmount > 0" class="summary-discount row items-center no-wrap q-py-xs q-px-sm q-mt-sm">
-              <q-icon color="positive" name="mdi-tag-outline" size="18px" />
-
-              <span class="font12 fontbold text-positive q-ml-xs">
-                {{ $t('schedule_cart.cart_fee_discount', { value: formatPrice(pixDiscountAmount) }) }}
-              </span>
-            </div>
-          </q-card-section>
-
-          <q-card-actions class="q-px-md q-pb-md">
-            <q-btn
-              class="full-width"
-              color="primary"
-              no-caps
-              padding="10px 16px"
-              rounded
-              unelevated
-              :disable="addressLoading || !platformFeesReady"
-              :label="$t('schedule_cart.close_cart')"
-              :loading="submitting"
-              @click="submitCart"
-            />
-          </q-card-actions>
-        </q-card>
-      </div>
-    </template>
-  </q-page>
-</template>
-
-<script setup>
-import { computed, onMounted, ref } from "vue";
-import { useRouter } from "vue-router";
-import { useI18n } from "vue-i18n";
-import { useQuasar } from "quasar";
-import { getAddresses } from "src/api/address";
-import { createScheduleCart } from "src/api/cart";
-import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
-import { useScheduleCartStore } from "src/stores/scheduleCart";
-import { userStore } from "src/stores/user";
-
-const router = useRouter();
-const $q     = useQuasar();
-const { t }  = useI18n();
-
-const cart  = useScheduleCartStore();
-const store = userStore();
-
-const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
-
-const primaryAddress = ref(null);
-const addressLoading = ref(false);
-const submitting     = ref(false);
-
-const cartDiscountMessage = computed(() => {
-  if (hasCartDiscount.value) {
-    return t("schedule_cart.cart_discount_applied");
-  }
-
-  return t("schedule_cart.cart_discount_hint", { count: missingSchedulesForDiscount.value });
-});
-
-const cartDiscountTotalAmount = computed(() => roundNullableMoney(sumCartItems(getCartDiscountItemTotal)));
-
-const hasCartDiscount = computed(() => cart.items.length >= 3);
-
-const missingSchedulesForDiscount = computed(() => Math.max(3 - cart.items.length, 0));
-
-const platformFeesReady = computed(() =>
-  isValidFee(platformFees.value.pix)         &&
-  isValidFee(platformFees.value.credit_card) &&
-  (!hasCartDiscount.value || isValidFee(platformFees.value.cart_min_3_schedules))
-);
-
-const pixDiscountAmount = computed(() => {
-  if (!hasCartDiscount.value) return 0;
-
-  return roundNullableMoney(sumCartItems((item) => {
-    const standardPixItemTotal   = getStandardPixItemTotal(item);
-    const discountedPixItemTotal = getCartDiscountItemTotal(item);
-
-    if (standardPixItemTotal === null || discountedPixItemTotal === null) return null;
-
-    return standardPixItemTotal - discountedPixItemTotal;
-  })) ?? 0;
-});
-
-const pixTotalAmount = computed(() => roundNullableMoney(sumCartItems(getStandardPixItemTotal)));
-
-const primaryAddressLabel = computed(() => {
-  if (addressLoading.value)  return t("schedule_cart.loading");
-  if (!primaryAddress.value) return t("schedule_cart.no_primary_address");
-
-  return [
-    primaryAddress.value.address,
-    primaryAddress.value.number,
-    primaryAddress.value.district,
-  ].filter(Boolean).join(", ");
-});
-
-const providerName = computed(() => {
-  const providerNames = [...new Set(cart.items.map((item) => item.provider_name).filter(Boolean))];
-
-  if (providerNames.length === 0) return t("schedule_cart.unknown");
-  if (providerNames.length === 1) return providerNames[0];
-
-  return t("schedule_cart.multiple_providers", { count: providerNames.length });
-});
-
-const formatCartDate = (value) => {
-  const parsed = new Date(`${normalizeDate(value)}T12:00:00`);
-
-  return new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" }).format(parsed);
-};
-
-const formatCurrencyValue = (value) => {
-  if (value === null || value === undefined || !Number.isFinite(Number(value))) {
-    return t("schedule_cart.loading");
-  }
-
-  return t("schedule_cart.currency_value", { value: formatPrice(value) });
-};
-
-const formatPrice = (value) => {
-  if (value === null || value === undefined || !Number.isFinite(Number(value))) {
-    return t("schedule_cart.loading");
-  }
-
-  return Number(value ?? 0).toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
-};
-
-const getCartDiscountItemTotal = (item) => getTotalWithFee(item, platformFees.value.cart_min_3_schedules);
-
-const getCreditCardItemTotal = (item) => getTotalWithFee(item, platformFees.value.credit_card);
-
-const getProviderInitial = (item) => item.provider_name?.slice(0, 1).toUpperCase() ?? t("schedule_cart.provider_initial");
-
-const getStandardPixItemTotal = (item) => getTotalWithFee(item, platformFees.value.pix);
-
-const getTotalWithFee = (item, fee) => {
-  if (!isValidFee(fee)) return null;
-
-  return roundMoney(Number(item.base_amount ?? 0) * (1 + Number(fee)));
-};
-
-const isValidFee = (fee) => fee !== null && fee !== undefined && Number.isFinite(Number(fee));
-
-const loadPrimaryAddress = async () => {
-  const clientId = store.user?.client_id ?? store.user?.client?.id;
-
-  if (!clientId) return;
-
-  addressLoading.value = true;
-
-  try {
-    const addresses = await getAddresses("client", clientId);
-
-    primaryAddress.value = (addresses ?? []).find((address) => address.is_primary) ?? null;
-  } catch {
-    primaryAddress.value = null;
-  } finally {
-    addressLoading.value = false;
-  }
-};
-
-const normalizeDate = (value) => String(value ?? "").replace(/\//g, "-").slice(0, 10);
-
-const roundMoney = (value) => Number(Number(value ?? 0).toFixed(2));
-
-const roundNullableMoney = (value) => {
-  if (value === null || value === undefined || !Number.isFinite(Number(value))) return null;
-
-  return roundMoney(value);
-};
-
-const submitCart = async () => {
-  if (!cart.items.length || submitting.value) return;
-
-  if (!primaryAddress.value) {
-    $q.notify({ type: "warning", message: t("schedule_cart.no_primary_address_notify") });
-
-    return;
-  }
-
-  const payload = {
-    client_id:     store.user.client_id ?? store.user?.client?.id,
-    address_id:    primaryAddress.value.id,
-    schedule_type: "default",
-
-    schedules: cart.items.map((item) => ({
-      provider_id:  item.provider_id,
-      date:         item.date,
-      period_type:  item.period_type,
-      start_time:   item.start_time,
-      end_time:     item.end_time,
-      total_amount: item.base_amount,
-      offers_meal:  item.offers_meal,
-    })),
-  };
-
-  submitting.value = true;
-
-  try {
-    const createdCart = await createScheduleCart(payload);
-
-    const createdSchedules = createdCart?.schedules ?? [];
-
-    const scheduleIds = createdCart?.schedule_ids ?? createdSchedules.map((schedule) => schedule.id);
-
-    cart.setLastCreatedGroup({
-      local_group_id: createdCart?.id ?? `cart-${Date.now()}`,
-      cart_id:        createdCart?.id ?? null,
-      schedule_ids:   scheduleIds,
-      schedules:      createdSchedules,
-    });
-
-    cart.clear();
-
-    $q.notify({ type: "positive", message: t("schedule_cart.cart_closed_success") });
-  } catch (error) {
-    const message = error?.response?.data?.message ?? error?.message ?? t("schedule_cart.submit_error");
-
-    $q.notify({ type: "negative", message });
-  } finally {
-    submitting.value = false;
-  }
-};
-
-const sumCartItems = (callback) => {
-  let total = 0;
-
-  for (const item of cart.items) {
-    const itemTotal = callback(item);
-
-    if (itemTotal === null) return null;
-
-    total += itemTotal;
-  }
-
-  return total;
-};
-
-onMounted(() => {
-  loadPrimaryAddress();
-  loadPlatformFees().catch(() => {});
-});
-</script>
-
-<style scoped lang="scss">
-.schedule-cart-page {
-  min-height: 100%;
-}
-
-.shadow-cart {
-  box-shadow: 0 1px 8px rgba(0, 0, 0, 0.1);
-}
-
-.cart-item-card,
-.checkout-card,
-.empty-card,
-.created-card {
-  border-radius: 8px;
-}
-
-.summary-row {
-  border-top: 1px solid rgba(0, 0, 0, 0.05);
-}
-
-.item-prices {
-  max-width: 220px;
-}
-
-.cart-discount-banner {
-  background: rgba(34, 197, 94, 0.1);
-  border: 1px solid rgba(34, 197, 94, 0.22);
-  border-radius: 8px;
-}
-
-.summary-discount {
-  background: rgba(34, 197, 94, 0.12);
-  border-radius: 8px;
-}
-</style>

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

@@ -2,7 +2,7 @@
 <template>
   <q-page class="sob-medida-page">
     <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile">
-      <q-btn flat round dense icon="mdi-chevron-left" color="primary" @click="router.back()" />
+      <q-btn class="header-back-btn" flat round dense icon="mdi-chevron-left" color="primary" @click="router.back()" />
       <q-space />
       <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('sob_medida.page_title') }}</span>
       <q-space />
@@ -708,4 +708,4 @@ onMounted(async () => {
     max-width: 460px;
   }
 }
-</style>
+</style>

+ 297 - 125
src/pages/search/SearchPage.vue

@@ -1,33 +1,54 @@
 <!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
 <template>
   <q-page class="bg-page">
+    <div
+      class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-search"
+    >
+      <q-btn
+        class="header-back-btn"
+        color="primary"
+        dense
+        flat
+        icon="mdi-chevron-left"
+        round
+        @click="router.back()"
+      />
 
-    <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-search">
-      <q-btn flat round dense icon="mdi-chevron-left" color="primary" @click="router.back()" />
-      <div class="col text-center font16 fontbold gradient-diarista text-primary">
-        {{ $t('search_page.title') }}
+      <div
+        class="col text-center font16 fontbold gradient-diarista text-primary"
+      >
+        {{ $t("search_page.title") }}
       </div>
+
       <div style="width: 36px" />
     </div>
 
     <div class="q-px-md q-pt-md q-pb-sm">
-      <q-card class="custom-schedule-card bg-surface shadow-card q-pa-sm" :flat="false">
+      <q-card
+        class="custom-schedule-card bg-surface shadow-card q-pa-sm"
+        :flat="false"
+      >
         <q-card-section class="row items-center no-wrap q-pa-sm q-gutter-x-sm">
           <span class="col text-text font12 fontmedium fonte-hint">
-            {{ $t('search_page.custom_schedule_description') }}
+            {{ $t("search_page.custom_schedule_description") }}
           </span>
+
           <q-btn
+            class="font12 fontmedium custom-schedule-btn card-border"
             color="secondary"
             no-caps
-            unelevated
             padding="8px 16px"
-            class="font12 fontmedium custom-schedule-btn card-border"
+            unelevated
             @click="router.push({ name: 'SobMedidaPage' })"
           >
             <template #default>
               <div class="column items-center q-gutter-y-xs">
-                <q-icon name="mdi-scissors-cutting" size="16px" />
-                <span>{{ $t('search_page.custom_schedule_btn') }}</span>
+                <q-icon
+                  name="mdi-scissors-cutting"
+                  size="16px"
+                />
+
+                <span>{{ $t("search_page.custom_schedule_btn") }}</span>
               </div>
             </template>
           </q-btn>
@@ -38,94 +59,199 @@
     <div class="row items-center q-px-md q-py-md q-gutter-x-sm">
       <q-input
         v-model="searchName"
-        :placeholder="$t('search_page.search_placeholder')"
-        outlined
-        rounded
-        dense
+        class="col bg-white search-input"
         clearable
         debounce="400"
-        class="col bg-white search-input"
+        dense
         input-class="text-text"
+        outlined
+        rounded
+        :placeholder="$t('search_page.search_placeholder')"
         @update:model-value="onNameChange"
       >
         <template #append>
-          <q-icon name="mdi-magnify" color="grey-5" />
+          <q-icon
+            color="grey-5"
+            name="mdi-magnify"
+          />
         </template>
       </q-input>
+
       <q-btn
-        flat round dense
-        icon="mdi-tune-variant"
+        :class="{ 'filter-active': activeSort || activeDate }"
         color="grey-6"
+        dense
+        flat
+        icon="mdi-tune-variant"
+        round
         size="md"
-        :class="{ 'filter-active': activeSort || activeDate }"
         @click="openFilterDialog"
       />
     </div>
 
     <div class="row items-center justify-between no-wrap q-px-md q-pb-sm">
-      <div class="dashboard-section-title font16 fontbold gradient-diarista">{{ $t('search_page.choose_provider') }}</div>
+      <div class="dashboard-section-title font16 fontbold gradient-diarista">
+        {{ $t("search_page.choose_provider") }}
+      </div>
+
       <div class="row items-center no-wrap text-text">
-        <q-btn flat dense round icon="mdi-chevron-left" color="text" size="sm" @click="setPeriodTypePrevious" />
+        <q-btn
+          color="text"
+          dense
+          flat
+          icon="mdi-chevron-left"
+          round
+          size="sm"
+          @click="setPeriodTypePrevious"
+        />
+
         <span class="font10 fontbold">{{ periodLabel }}</span>
-        <q-btn flat dense round icon="mdi-chevron-right" color="text" size="sm" @click="setPeriodTypeNext" />
+
+        <q-btn
+          color="text"
+          dense
+          flat
+          icon="mdi-chevron-right"
+          round
+          size="sm"
+          @click="setPeriodTypeNext"
+        />
       </div>
     </div>
 
-    <div v-if="loading" class="row items-center justify-center q-py-xl">
-      <q-spinner-dots color="primary" size="40px" />
+    <div
+      v-if="loading"
+      class="row items-center justify-center q-py-xl"
+    >
+      <q-spinner-dots
+        color="primary"
+        size="40px"
+      />
     </div>
 
     <template v-else>
-      <div v-if="sortedProviders.length === 0" class="text-center text-grey-6 q-px-md q-py-lg text-body2">
-        {{ $t('search_page.no_results') }}
+      <div
+        v-if="sortedProviders.length === 0"
+        class="text-center text-grey-6 q-px-md q-py-lg text-body2"
+      >
+        {{ $t("search_page.no_results") }}
       </div>
 
-      <div v-else class="column q-px-md q-pb-xl">
+      <div
+        v-else
+        class="column q-px-md q-pb-xl"
+      >
         <q-card
           v-for="p in sortedProviders"
           :key="p.provider_id"
-          class="card-border bg-page text-text q-mb-sm"
           :flat="false"
+          class="card-border bg-page text-text q-mb-sm"
         >
           <q-card-section class="row no-wrap q-pa-sm">
             <div class="row no-wrap full-width">
               <div class="col-2">
-                <q-avatar :style="p.profile_media_url ? {} : avatarColors[p.provider_id % avatarColors.length]">
-                  <img v-if="p.profile_media_url" :src="p.profile_media_url" style="object-fit: cover; width: 100%; height: 100%;" />
-                  <span v-else>{{ getFirstName(p.provider_name)?.slice(0, 1).toUpperCase() ?? '—' }}</span>
+                <q-avatar
+                  :style="
+                    p.profile_media_url
+                      ? {}
+                      : avatarColors[p.provider_id % avatarColors.length]
+                  "
+                >
+                  <img
+                    v-if="p.profile_media_url"
+                    :src="p.profile_media_url"
+                    style="object-fit: cover; width: 100%; height: 100%"
+                  />
+
+                  <span v-else>{{
+                    getFirstName(p.provider_name)?.slice(0, 1).toUpperCase() ??
+                    "—"
+                  }}</span>
                 </q-avatar>
               </div>
 
               <div class="col-10 row">
                 <div class="column col-9 justify-between">
-                  <span class="text-provider-close-nam font12 fontbold">{{ getFirstName(p.provider_name) || 'Prestador' }}</span>
-                  <span class="text-provider-close-region font9 fontbold text-grey-7">{{ p.district }}</span>
+                  <span class="text-provider-close-nam font12 fontbold">
+                    {{
+                      getFirstName(p.provider_name) || "Prestador"
+                    }}
+                  </span>
+
+                  <span
+                    v-if="getProviderGenderLabel(p, t)"
+                    class="font9 fontmedium text-grey-7"
+                  >
+                    {{ getProviderGenderLabel(p, t) }}
+                  </span>
+
+                  <span
+                    class="text-provider-close-region font9 fontbold text-grey-7"
+                   >
+                    {{ p.district }}
+                  </span>
+
                   <div class="row items-center justify-between q-pr-lg">
                     <div class="row items-center">
-                      <q-icon name="mdi-star" color="warning" size="12px" />
+                      <q-icon
+                        color="warning"
+                        name="mdi-star"
+                        size="12px"
+                      />
+
                       <span class="text-provider-close-rating font9 fontmedium">
-                        {{ p.average_rating != null ? (Number(p.average_rating).toFixed(1) + ' (' + (p.total_reviews ?? 0) + ')') : ('(' + (p.total_reviews ?? 0) + ')') }}
+                        {{
+                          p.average_rating != null
+                            ? Number(p.average_rating).toFixed(1) +
+                              " (" +
+                              (p.total_reviews ?? 0) +
+                              ")"
+                            : "(" + (p.total_reviews ?? 0) + ")"
+                        }}
                       </span>
                     </div>
+
                     <div class="row items-center">
-                      <q-icon name="mdi-broom" color="secondary" size="12px" />
-                      <span class="text-provider-close-jobs font9 fontmedium">{{ p.total_services ?? 0 }}</span>
+                      <q-icon
+                        color="secondary"
+                        name="mdi-broom"
+                        size="12px"
+                      />
+
+                      <span class="text-provider-close-jobs font9 fontmedium">
+                        {{ p.total_services ?? 0 }}
+                      </span>
                     </div>
+
                     <div class="row items-center">
-                      <q-icon name="mdi-map-marker-outline" color="text" size="12px" />
-                      <span class="text-provider-close-jobs font9 fontmedium">{{ (p.distance_km ?? '--') + ' km' }}</span>
+                      <q-icon
+                        color="text"
+                        name="mdi-map-marker-outline"
+                        size="12px"
+                      />
+
+                      <span class="text-provider-close-jobs font9 fontmedium">
+                        {{ (p.distance_km ?? "--") + " km" }}
+                      </span>
                     </div>
                   </div>
                 </div>
 
-                <div class="column col-3 justify-between text-center items-center">
-                  <span class="text-provider-close-price font12 fontbold">{{ priceByPeriod(p) }}</span>
+                <div
+                  class="column col-3 justify-between text-center items-center"
+                >
+                  <span class="text-provider-close-price font12 fontbold">
+                    {{ priceByPeriod(p) }}
+                  </span>
+
                   <div class="full-width">
                     <q-btn
-                      unelevated rounded no-caps
                       color="primary"
-                      size="sm"
+                      no-caps
                       padding="3px 12px"
+                      rounded
+                      size="sm"
+                      unelevated
                       :label="$t('search_page.schedule_btn')"
                       @click="goToScheduling(p)"
                     />
@@ -137,100 +263,131 @@
         </q-card>
       </div>
     </template>
-
   </q-page>
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
-import { useI18n } from 'vue-i18n';
-import { useQuasar } from 'quasar';
-import { buscaPrestadores } from 'src/api/dashboard';
-import { formatCurrency,getFirstName } from 'src/helpers/utils';
-import SearchFilterDialog from 'src/pages/search/components/SearchFilterDialog.vue';
-import SchedulingDialog from 'src/pages/search/components/SchedulingDialog.vue';
+import { avatarColors } from "src/helpers/avatarColors";
+import { buscaPrestadores } from "src/api/dashboard";
+import { computed, onMounted, ref } from "vue";
+import { formatCurrency, getFirstName } from "src/helpers/utils";
+import { getProviderGenderLabel } from "src/helpers/provider";
+import { useI18n } from "vue-i18n";
+import { useQuasar } from "quasar";
+import { useRouter } from "vue-router";
+
+import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
+import SearchFilterDialog from "src/pages/search/components/SearchFilterDialog.vue";
 
 const { t } = useI18n();
 const router = useRouter();
 const $q = useQuasar();
 
+const activeDate = ref(null);
+const activeSort = ref(null);
 const allProviders = ref([]);
-const loading      = ref(true);
-const searchName   = ref('');
-const activeDate   = ref(null);
-const activeSort   = ref(null);
-
 const currentPeriodType = ref(8);
-const periodTypeMap = { 2: 'daily_price_2h', 4: 'daily_price_4h', 6: 'daily_price_6h', 8: 'daily_price_8h' };
+const loading = ref(true);
+const searchName = ref("");
 
-const periodLabel = computed(() => {
-  const labels = { 8: t('search_page.until_8h'), 6: t('search_page.until_6h'), 4: t('search_page.until_4h'), 2: t('search_page.until_2h') };
-  return labels[currentPeriodType.value] ?? '';
-});
-
-const priceByPeriod = (p) => {
-  const key = periodTypeMap[currentPeriodType.value];
-  return p[key] ? formatCurrency(p[key]) : t('search_page.no_price');
+const periodTypeMap = {
+  2: "daily_price_2h",
+  4: "daily_price_4h",
+  6: "daily_price_6h",
+  8: "daily_price_8h",
 };
 
-// eslint-disable-next-line no-unused-vars
-const formatDistance = (distance) => {
-  if (distance === null || distance === undefined || distance === '') return '—';
-
-  const numericDistance = Number(distance);
-  if (!Number.isFinite(numericDistance)) return '—';
-
-  return `${numericDistance.toLocaleString('pt-BR', { maximumFractionDigits: 1 })} km`;
-};
-
-const setPeriodTypePrevious = () => {
-  const prev = currentPeriodType.value - 2;
-  if (periodTypeMap[prev]) currentPeriodType.value = prev;
-};
+const periodLabel = computed(() => {
+  const labels = {
+    8: t("search_page.until_8h"),
+    6: t("search_page.until_6h"),
+    4: t("search_page.until_4h"),
+    2: t("search_page.until_2h"),
+  };
 
-const setPeriodTypeNext = () => {
-  const next = currentPeriodType.value + 2;
-  if (periodTypeMap[next]) currentPeriodType.value = next;
-};
+  return labels[currentPeriodType.value] ?? "";
+});
 
 const sortedProviders = computed(() => {
   const list = [...allProviders.value];
+
   const priceKey = periodTypeMap[currentPeriodType.value];
 
   switch (activeSort.value) {
-    case 'price_asc':
-      return list.sort((a, b) => Number(a[priceKey] ?? 0) - Number(b[priceKey] ?? 0));
-    case 'price_desc':
-      return list.sort((a, b) => Number(b[priceKey] ?? 0) - Number(a[priceKey] ?? 0));
-    case 'rating_desc':
-      return list.sort((a, b) => Number(b.average_rating ?? 0) - Number(a.average_rating ?? 0));
-    case 'rating_asc':
-      return list.sort((a, b) => Number(a.average_rating ?? 0) - Number(b.average_rating ?? 0));
-    case 'reviews_desc':
-      return list.sort((a, b) => Number(b.total_reviews ?? 0) - Number(a.total_reviews ?? 0));
-    case 'reviews_asc':
-      return list.sort((a, b) => Number(a.total_reviews ?? 0) - Number(b.total_reviews ?? 0));
-    case 'services_desc':
-      return list.sort((a, b) => Number(b.total_services ?? 0) - Number(a.total_services ?? 0));
-    case 'services_asc':
-      return list.sort((a, b) => Number(a.total_services ?? 0) - Number(b.total_services ?? 0));
-    case 'oldest':
-      return list.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
-    case 'newest':
-      return list.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
+    case "price_asc":
+      return list.sort(
+        (a, b) => Number(a[priceKey] ?? 0) - Number(b[priceKey] ?? 0),
+      );
+    case "price_desc":
+      return list.sort(
+        (a, b) => Number(b[priceKey] ?? 0) - Number(a[priceKey] ?? 0),
+      );
+    case "rating_desc":
+      return list.sort(
+        (a, b) => Number(b.average_rating ?? 0) - Number(a.average_rating ?? 0),
+      );
+    case "rating_asc":
+      return list.sort(
+        (a, b) => Number(a.average_rating ?? 0) - Number(b.average_rating ?? 0),
+      );
+    case "reviews_desc":
+      return list.sort(
+        (a, b) => Number(b.total_reviews ?? 0) - Number(a.total_reviews ?? 0),
+      );
+    case "reviews_asc":
+      return list.sort(
+        (a, b) => Number(a.total_reviews ?? 0) - Number(b.total_reviews ?? 0),
+      );
+    case "services_desc":
+      return list.sort(
+        (a, b) => Number(b.total_services ?? 0) - Number(a.total_services ?? 0),
+      );
+    case "services_asc":
+      return list.sort(
+        (a, b) => Number(a.total_services ?? 0) - Number(b.total_services ?? 0),
+      );
+    case "oldest":
+      return list.sort(
+        (a, b) => new Date(a.created_at) - new Date(b.created_at),
+      );
+    case "newest":
+      return list.sort(
+        (a, b) => new Date(b.created_at) - new Date(a.created_at),
+      );
     default:
-      return list.sort((a, b) => Number(b.average_rating ?? 0) - Number(a.average_rating ?? 0));
+      return list.sort(
+        (a, b) => Number(b.average_rating ?? 0) - Number(a.average_rating ?? 0),
+      );
   }
 });
 
+// eslint-disable-next-line no-unused-vars
+const formatDistance = (distance) => {
+  if (distance === null || distance === undefined || distance === "")
+    return "—";
+
+  const numericDistance = Number(distance);
+  if (!Number.isFinite(numericDistance)) return "—";
+
+  return `${numericDistance.toLocaleString("pt-BR", { maximumFractionDigits: 1 })} km`;
+};
+
+const goToScheduling = (provider) => {
+  $q.dialog({
+    component: SchedulingDialog,
+    componentProps: { provider },
+  });
+};
+
 const loadProviders = async () => {
   loading.value = true;
+
   try {
-    allProviders.value = await buscaPrestadores({
-      name: searchName.value,
-      date: activeDate.value ?? '',
-    }) ?? [];
+    allProviders.value =
+      (await buscaPrestadores({
+        date: activeDate.value ?? "",
+        name: searchName.value,
+      })) ?? [];
   } catch {
     allProviders.value = [];
   } finally {
@@ -238,59 +395,74 @@ const loadProviders = async () => {
   }
 };
 
-const onNameChange = () => loadProviders();
+const onNameChange = () => {
+  loadProviders();
+};
 
 const openFilterDialog = () => {
   $q.dialog({
     component: SearchFilterDialog,
     componentProps: {
-      initialSort: activeSort.value,
       initialDate: activeDate.value,
+      initialSort: activeSort.value,
     },
-  }).onOk(({ sort, date }) => {
+  }).onOk(({ date, sort }) => {
     const dateChanged = date !== activeDate.value;
-    activeSort.value = sort;
+
     activeDate.value = date;
+    activeSort.value = sort;
+
     if (dateChanged) loadProviders();
   });
 };
 
-const goToScheduling = (provider) => {
-  $q.dialog({
-    component: SchedulingDialog,
-    componentProps: { provider },
-  });
+const priceByPeriod = (provider) => {
+  const key = periodTypeMap[currentPeriodType.value];
+
+  return provider[key]
+    ? formatCurrency(provider[key])
+    : t("search_page.no_price");
 };
 
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-];
+const setPeriodTypeNext = () => {
+  const next = currentPeriodType.value + 2;
+
+  if (periodTypeMap[next]) currentPeriodType.value = next;
+};
+
+const setPeriodTypePrevious = () => {
+  const previous = currentPeriodType.value - 2;
 
-onMounted(() => loadProviders());
+  if (periodTypeMap[previous]) currentPeriodType.value = previous;
+};
+
+onMounted(loadProviders);
 </script>
 
 <style scoped lang="scss">
 .shadow-search {
   box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
 }
+
 .search-input {
   :deep(.q-field__control) {
     border-radius: 28px;
   }
 }
+
 .custom-schedule-card {
   border-radius: 12px;
 }
+
 .custom-schedule-btn {
   flex-shrink: 0;
   min-width: 72px;
 }
+
 .filter-active {
   color: var(--q-primary) !important;
 }
+
 .fonte-hint {
   line-height: 100%;
   letter-spacing: -0.04em;

+ 0 - 560
src/pages/search/components/OrderSummaryDialog.vue

@@ -1,560 +0,0 @@
-<template>
-  <q-dialog ref="dialogRef" maximized transition-show="slide-up" transition-hide="slide-down">
-    <div class="dialog-root">
-
-      <div class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white">
-        <q-btn v-close-popup color="primary" dense flat icon="mdi-chevron-left" round />
-
-        <div class="col text-center font16 fontbold gradient-diarista">
-          {{ $t('scheduling_page.title') }}
-        </div>
-        <div style="width: 36px" />
-      </div>
-
-      <div class="dialog-body">
-
-        <div class="q-px-md q-pt-md">
-          <div class="info-banner card-border q-pa-md">
-            <div class="font14 fontbold text-primary">
-              {{ $t('scheduling_page.order_summary.info_text') }}
-            </div>
-
-            <div class="font12 fontmedium text-primary q-mt-xs">
-              {{ $t('scheduling_page.order_summary.info_note') }}
-            </div>
-          </div>
-        </div>
-
-        <div class="q-px-md q-pt-md">
-          <div class="font16 fontbold gradient-diarista q-mb-sm">
-            {{ $t('scheduling_page.order_summary.title') }}
-          </div>
-
-          <q-card
-            v-for="(booking, idx) in bookings"
-            :key="idx"
-            :flat="false"
-            class="card-border bg-surface q-mb-sm shadow-card q-px-md q-pb-md"
-          >
-            <q-card-section class="q-pa-md row items-center no-wrap">
-              <div class="col text-center">
-                <div class="text-text">
-                  <span class="font14 fontregular">
-                    {{ $t('scheduling_page.order_summary.service_label') }}
-                  </span>
-
-                  <span class="font14 fontbold">
-                    {{ ` ${booking.serviceType.label} (${booking.serviceType.hours})` }}
-                  </span>
-                </div>
-
-                <div class="font14 fontbold text-text">
-                  {{ formatDate(booking.date) }}
-                </div>
-
-                <div class="font14 fontbold text-text">
-                  {{ $t('scheduling_page.order_summary.time_range', { start: booking.slot.startHour, end: booking.slot.endHour }) }}
-                </div>
-              </div>
-
-              <q-btn
-                color="grey-5"
-                flat round dense
-                icon="mdi-minus-circle-outline"
-                padding="4px 8px"
-                @click="confirmRemove(idx)"
-              />
-            </q-card-section>
-
-            <q-btn
-              class="full-width q-mt-sm"
-              color="secondary"
-              padding="4px 8px"
-              unelevated rounded no-caps
-              :label="$t('scheduling_page.order_summary.send_btn')"
-              @click="submitOrder"
-            />
-          </q-card>
-
-          <div class="full-width text-center">
-            <q-btn
-              class="q-mt-xs"
-              color="primary"
-              outline rounded no-caps
-              :disable="showCalendar"
-              :label="$t('scheduling_page.order_summary.add_date_btn')"
-              @click="showCalendar = true"
-            />
-          </div>
-        </div>
-
-        <div v-if="showCalendar" class="q-px-md q-pt-lg q-pb-xl">
-          <div v-if="loadingAvailability" class="row items-center justify-center q-py-lg">
-            <q-spinner-dots color="primary" size="36px" />
-          </div>
-
-          <div v-else class="calendar-wrapper shadow-card">
-            <q-date
-              v-model="addDateValue"
-              class=""
-              color="purple"
-              minimal
-              :first-day-of-week="0"
-              :options="dateOptions"
-              @update:model-value="onAddDateSelected"
-            />
-          </div>
-        </div>
-      </div>
-    </div>
-  </q-dialog>
-</template>
-
-<script setup>
-import { computed, onMounted, ref } from 'vue';
-import { createSchedule, getClientProviderBlocks } from 'src/api/schedule';
-import { date } from 'quasar';
-import { getAddresses } from 'src/api/address';
-import { getProviderBlockedDays, getProviderWorkingDays } from 'src/api/providerAvailability';
-import { useDialogPluginComponent, useQuasar } from 'quasar';
-import { useI18n } from 'vue-i18n';
-import { userStore } from 'src/stores/user';
-import { useRouter } from 'vue-router';
-import { useScheduleCartStore } from 'src/stores/scheduleCart';
-
-import ServiceSelectionSheet from './ServiceSelectionSheet.vue';
-import ServiceTimeSelectionDialog from './ServiceTimeSelectionDialog.vue';
-
-const props = defineProps({
-  provider:       { type: Object, required: true },
-  initialBooking: { type: Object, required: true },
-});
-
-defineEmits([...useDialogPluginComponent.emits]);
-
-const $q     = useQuasar();
-const router = useRouter();
-
-const { dialogRef, onDialogOK } = useDialogPluginComponent();
-
-const { t, locale } = useI18n();
-
-const store        = userStore();
-const scheduleCart = useScheduleCartStore();
-
-const bookings            = ref([props.initialBooking]);
-const submitting          = ref(false);
-const primaryAddress      = ref(null);
-const showCalendar        = ref(false);
-const addDateValue        = ref(null);
-const loadingAvailability = ref(false);
-const workingDays         = ref([]);
-const blockedDays         = ref([]);
-
-const providerClientBlocks = ref({ existing_schedules: [], fully_blocked_weeks: [] });
-
-const availableWeekDays = computed(() =>
-  [...new Set(workingDays.value.map(wd => wd.day))]
-);
-
-const blockedDateSet = computed(() =>
-  new Set(blockedDays.value.filter(bd => bd.period === 'all').map(bd => bd.date))
-);
-
-const blockedWeekStartSet = computed(() =>
-  new Set(providerClientBlocks.value.fully_blocked_weeks ?? [])
-);
-
-const confirmRemove = (idx) => {
-  $q.dialog({
-    title:      t('scheduling_page.order_summary.remove_confirm_title'),
-    cancel:     { label: t('scheduling_page.order_summary.remove_confirm_cancel'), flat: true, color: 'grey-6' },
-    ok:         { label: t('scheduling_page.order_summary.remove_confirm_ok'), unelevated: true, color: 'primary', rounded: true, noCaps: true },
-    persistent: true,
-  }).onOk(() => {
-    bookings.value.splice(idx, 1);
-  });
-};
-
-const dateOptions = (d) => {
-  const today = date.formatDate(new Date(), 'YYYY/MM/DD');
-
-  if (d < today)               return false;
-  if (wouldExceedWeekLimit(d)) return false;
-
-  const raw = normalizeDate(d);
-
-  const parsed = new Date(`${raw}T12:00:00`);
-
-  const dayOfWeek     = parsed.getDay();
-  const isWorking     = availableWeekDays.value.includes(dayOfWeek);
-  const isBlocked     = blockedDateSet.value.has(raw);
-  const isWeekBlocked = blockedWeekStartSet.value.has(getWeekStart(raw));
-
-  return isWorking && !isBlocked && !isWeekBlocked;
-};
-
-const formatDate = (dateStr) => {
-  const d = new Date(normalizeDate(dateStr) + 'T12:00:00');
-
-  const localeMap = { pt: 'pt-BR', en: 'en-US', es: 'es-ES' };
-
-  const loc = localeMap[locale.value] ?? 'pt-BR';
-
-  const weekday       = new Intl.DateTimeFormat(loc, { weekday: 'long' }).format(d);
-  const dateFormatted = new Intl.DateTimeFormat(loc, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(d);
-
-  return `${weekday.charAt(0).toUpperCase() + weekday.slice(1)}, ${dateFormatted}`;
-};
-
-const formatHour = (h) => `${String(h).padStart(2, '0')}:00`;
-
-const getCartWeekCount = (newDateStr) => {
-  const newWeek = getWeekStart(newDateStr);
-
-  return scheduleCart.items.filter((item) =>
-    Number(item.provider_id) === Number(props.provider.provider_id) &&
-    getWeekStart(item.date) === newWeek
-  ).length;
-};
-
-const getLocalWeekCount = (newDateStr) => {
-  const newWeek = getWeekStart(newDateStr);
-
-  return bookings.value.filter((booking) => getWeekStart(booking.date) === newWeek).length;
-};
-
-const getServerWeekCount = (newDateStr) => {
-  const newWeek = getWeekStart(newDateStr);
-
-  return (providerClientBlocks.value.existing_schedules ?? []).filter(
-    (schedule) => getWeekStart(schedule.date) === newWeek
-  ).length;
-};
-
-const getWeekStart = (dateStr) => {
-  const d = new Date(normalizeDate(dateStr) + 'T12:00:00');
-
-  d.setDate(d.getDate() - d.getDay());
-
-  return d.toISOString().slice(0, 10);
-};
-
-const loadAvailability = async () => {
-  loadingAvailability.value = true;
-
-  try {
-    const clientId = store.user?.client_id;
-
-    const defaultClientBlocks = { existing_schedules: [], fully_blocked_weeks: [] };
-
-    const [wd, bd, clientBlocks] = await Promise.all([
-      getProviderWorkingDays(props.provider.provider_id),
-      getProviderBlockedDays(props.provider.provider_id),
-      clientId
-        ? getClientProviderBlocks(clientId, props.provider.provider_id)
-        : Promise.resolve(defaultClientBlocks),
-    ]);
-
-    workingDays.value = wd ?? [];
-    blockedDays.value = bd ?? [];
-
-    providerClientBlocks.value = clientBlocks ?? defaultClientBlocks;
-  } catch {
-    workingDays.value = [];
-    blockedDays.value = [];
-
-    providerClientBlocks.value = { existing_schedules: [], fully_blocked_weeks: [] };
-  } finally {
-    loadingAvailability.value = false;
-  }
-};
-
-const loadPrimaryAddress = async () => {
-  try {
-    const clientId = store.user?.client_id;
-
-    if (!clientId) return;
-
-    const addresses = await getAddresses('client', clientId);
-
-    primaryAddress.value = (addresses ?? []).find(a => a.is_primary) ?? null;
-  } catch {
-    primaryAddress.value = null;
-  }
-};
-
-const normalizeDate = (d) => d.replace(/\//g, '-');
-
-const onAddDateSelected = (val) => {
-  if (!val) return;
-
-  addDateValue.value = null;
-
-  const valFormatted = normalizeDate(val);
-
-  const blocksOfDate = blockedDays.value.filter(
-    bd => bd.date === valFormatted && bd.period !== 'all'
-  );
-
-  const dayOfWeek = new Date(`${valFormatted}T12:00:00`).getDay();
-
-  const dayPeriods = workingDays.value
-    .filter(wd => wd.day === dayOfWeek)
-    .map(wd => wd.period);
-
-  const workingDayBlocks = [];
-
-  if (!dayPeriods.includes('afternoon')) {
-    workingDayBlocks.push({ init_hour: '14:00:00', end_hour: '20:00:00' });
-  }
-
-  if (!dayPeriods.includes('morning')) {
-    workingDayBlocks.push({ init_hour: '7:00:00', end_hour: '13:00:00' });
-  }
-
-  const existingBookingBlocks = bookings.value
-    .filter(b => b.date.replace(/\//g, '-') === valFormatted)
-    .map(b => ({
-      init_hour: `${b.slot.startHour}:00:00`,
-      end_hour:  `${b.slot.endHour}:00:00`,
-    }));
-
-  const serverBookingBlocks = (providerClientBlocks.value.existing_schedules ?? [])
-    .filter((schedule) => schedule.date === valFormatted)
-    .map((schedule) => ({
-      init_hour: schedule.start_time,
-      end_hour:  schedule.end_time,
-    }));
-
-  const cartBookingBlocks = scheduleCart.items
-    .filter((item) =>
-      Number(item.provider_id) === Number(props.provider.provider_id) &&
-      normalizeDate(item.date) === valFormatted
-    )
-    .map((item) => ({
-      init_hour: item.start_time,
-      end_hour:  item.end_time,
-    }));
-
-  const partialBlocks = [
-    ...blocksOfDate,
-    ...workingDayBlocks,
-    ...existingBookingBlocks,
-    ...serverBookingBlocks,
-    ...cartBookingBlocks,
-  ];
-
-  $q.dialog({
-    component: ServiceSelectionSheet, componentProps: { provider: props.provider, selectedDate: val, partialBlocks },
-  }).onOk(({ serviceType, date: date_, provider: prov }) => {
-    $q.dialog({
-      component: ServiceTimeSelectionDialog, componentProps: { serviceType, selectedDate: date_, provider: prov, partialBlocks },
-    }).onOk((booking) => {
-      if (wouldExceedWeekLimit(booking.date)) {
-        $q.notify({ type: 'negative', message: t('scheduling_page.order_summary.week_limit_error') });
-
-        return;
-      }
-
-      bookings.value.push(booking);
-
-      showCalendar.value = false;
-    });
-  });
-};
-
-const submitOrder = async () => {
-  if (!primaryAddress.value) {
-    $q.notify({ type: 'warning', message: t('scheduling_page.order_summary.no_primary_address') });
-
-    return;
-  }
-
-  const payloads = bookings.value.map(b => ({
-    client_id:     store.user.client_id,
-    provider_id:   props.provider.provider_id,
-    address_id:    primaryAddress.value.id,
-    schedule_type: 'default',
-    date:          normalizeDate(b.date),
-    period_type:   b.serviceType.hoursCount,
-    start_time:    formatHour(b.slot.startHour),
-    end_time:      formatHour(b.slot.endHour),
-    total_amount:  b.serviceType.basePrice ?? b.serviceType.price,
-    offers_meal:   b.meal === 'offer' ? true : b.meal === 'no_offer' ? false : null,
-  }));
-
-  submitting.value = true;
-
-  try {
-    await Promise.all(payloads.map((payload) => createSchedule(payload)));
-
-    $q.notify({ type: 'positive', message: t('scheduling_page.order_summary.submit_success') });
-
-    onDialogOK();
-  } catch (err) {
-    const msg = err?.response?.data?.message
-      ?? err?.message
-      ?? t('scheduling_page.order_summary.submit_error');
-
-    $q.notify({ type: 'negative', message: msg });
-  } finally {
-    submitting.value = false;
-
-    router.push({ name: 'DashboardPage' });
-  }
-};
-
-const wouldExceedWeekLimit = (newDateStr) => {
-  const count = getServerWeekCount(newDateStr) + getLocalWeekCount(newDateStr) + getCartWeekCount(newDateStr);
-
-  return count >= 2;
-};
-
-onMounted(() => Promise.all([loadAvailability(), loadPrimaryAddress()]));
-</script>
-
-<style scoped lang="scss">
-.dialog-root {
-  width: 100vw;
-  max-width: 100vw;
-  height: 100vh;
-  max-height: 100vh;
-  display: flex;
-  flex-direction: column;
-  overflow: hidden;
-  background: #F9FAFB;
-}
-
-.dialog-header {
-  flex-shrink: 0;
-  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
-}
-
-.dialog-body {
-  flex: 1;
-  min-height: 0;
-  overflow-y: auto;
-  overflow-x: clip;
-}
-
-.info-banner {
-  background: rgba(139, 92, 246, 0.08);
-  border: 1px solid rgba(139, 92, 246, 0.2);
-}
-
-.calendar-wrapper {
-  border-radius: 20px;
-  overflow: hidden;
-  background: white;
-
-  :deep(.q-date) { background: white; width: 100%; }
-  :deep(.q-date__main),
-  :deep(.q-date__content),
-  :deep(.q-date__calendar) { background: white !important; }
-
-  :deep(.q-date__calendar-item .q-btn) {
-    min-width: 0 !important;
-    padding: 6px 2px !important;
-  }
-  :deep(.q-date__calendar-item .q-btn.disabled),
-  :deep(.q-date__calendar-item .q-btn[disabled]) { opacity: 1 !important; }
-  :deep(.q-date__calendar-item .q-btn.disabled .q-btn__content),
-  :deep(.q-date__calendar-item .q-btn[disabled] .q-btn__content) { color: #CBD5E1 !important; }
-
-  :deep(.q-date__calendar-days .q-btn__content) { font-weight: 500; color: #1E293B; }
-  :deep(.q-date__calendar-weekdays > div) { color: #6366F1; font-weight: 700; opacity: 0.8; }
-  :deep(.q-date__navigation) {
-    .q-btn { color: #6366F1 !important; }
-    .q-btn__content { color: #6366F1 !important; }
-  }
-  :deep(.q-date__event) { bottom: 4px; height: 6px; width: 6px; border-radius: 50%; }
-  :deep(.q-date__today .q-btn__content) { color: #7c4dff !important; background: #7c4dff15; border-radius: 50%; }
-  :deep(.q-date__selected .q-btn__content) {
-    background: #6366F1 !important;
-    color: white !important;
-    border-radius: 50%;
-    box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
-  }
-}
-
-.calendar-custom {
-  border-radius: 20px;
-  background-color: white !important;
-
-  :deep(.q-date__main) {
-    background-color: white !important;
-  }
-
-  :deep(.q-date__content) {
-    background-color: white !important;
-  }
-
-  :deep(.q-date__calendar) {
-    background-color: white !important;
-  }
-
-  :deep(.q-date__calendar-item--out) {
-    .q-btn__content {
-      color: #CBD5E1 !important;
-    }
-  }
-
-  :deep(.q-date__calendar-days .q-btn__content) {
-    font-weight: 500;
-    color: #1E293B;
-  }
-
-  :deep(.q-date__calendar-weekdays > div) {
-    color: #6366F1;
-    font-weight: 700;
-    opacity: 0.8;
-  }
-
-  :deep(.q-date__navigation) {
-    .q-btn {
-      color: #1E293B !important;
-    }
-    .q-btn__content {
-      color: #1E293B !important;
-    }
-  }
-
-  :deep(.q-date__nav-btn-month),
-  :deep(.q-date__nav-btn-year) {
-    color: #6366F1 !important;
-    font-weight: 700;
-  }
-
-  :deep(.q-date__event) {
-    bottom: 4px;
-    height: 6px;
-    width: 6px;
-    border-radius: 50%;
-  }
-
-  :deep(.q-date__today) {
-    .q-btn__content {
-      color: #7c4dff !important;
-      background: #7c4dff15;
-      border-radius: 50%;
-    }
-  }
-
-  :deep(.q-date__selected) {
-    .q-btn__content {
-      background: #6366F1 !important;
-      color: white !important;
-      border-radius: 50%;
-      box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
-    }
-  }
-
-  :deep(.q-date__view--months),
-  :deep(.q-date__view--years) {
-    .q-btn {
-      color: #1E293B !important;
-    }
-  }
-}
-</style>

+ 337 - 203
src/pages/search/components/SchedulingDialog.vue

@@ -6,9 +6,12 @@
     transition-show="slide-up"
   >
     <div class="dialog-root">
-      <div class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white">
+      <div
+        class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white"
+      >
         <q-btn
           v-close-popup
+          class="header-back-btn"
           color="primary"
           dense
           flat
@@ -16,8 +19,10 @@
           round
         />
 
-        <div class="col text-center font16 fontbold text-primary gradient-diarista q-mb-xs">
-          {{ $t('scheduling_page.title') }}
+        <div
+          class="col text-center font16 fontbold text-primary gradient-diarista q-mb-xs"
+        >
+          {{ $t("scheduling_page.title") }}
         </div>
 
         <div style="width: 36px"></div>
@@ -26,48 +31,74 @@
       <div class="dialog-body">
         <div class="q-px-md q-pt-md">
           <div class="font16 fontbold gradient-diarista q-mb-xs">
-            {{ $t('scheduling_page.about_provider') }}
+            {{ $t("scheduling_page.about_provider") }}
           </div>
 
-          <q-card :flat="false" class="card-border shadow-card bg-surface text-text">
+          <q-card
+            class="card-border shadow-card bg-surface text-text"
+            :flat="false"
+          >
             <q-card-section class="q-pa-md">
               <div class="row items-center no-wrap q-gutter-x-md">
                 <div class="col-2">
-                  <q-avatar :style="avatarColors[provider.provider_id % avatarColors.length]">
+                  <q-avatar
+                    :style="
+                      avatarColors[provider.provider_id % avatarColors.length]
+                    "
+                  >
                     <img
-                      v-if="provider.provider_photo"
-                      :src="provider.provider_photo"
-                      style="object-fit:cover;border-radius:50%;"
+                      v-if="getProfileMediaUrl(provider)"
+                      style="object-fit: cover; border-radius: 50%"
+                      :src="getProfileMediaUrl(provider)"
                     />
 
                     <span v-else>
-                      {{ getFirstName(provider.provider_name)?.slice(0, 2).toUpperCase() ?? '??' }}
+                      {{
+                        getFirstName(provider.provider_name)
+                          ?.slice(0, 2)
+                          .toUpperCase() ?? "??"
+                      }}
                     </span>
                   </q-avatar>
                 </div>
 
                 <div class="col-3 column">
                   <div class="font12 fontbold text-text">
-                   {{ getFirstName(provider?.provider_name) ?? '—' }}
+                    {{ getFirstName(provider?.provider_name) ?? "—" }}
                   </div>
 
                   <div class="font10 fontmedium text-grey-7">
-                    {{ provider?.district ?? '—' }}
+                    {{ provider?.district ?? "—" }}
                   </div>
                 </div>
 
                 <div class="col-3">
                   <div class="row items-center q-gutter-x-md q-mt-xs">
                     <div class="row items-center">
-                      <q-icon color="warning" name="mdi-star" size="12px" />
+                      <q-icon
+                        color="warning"
+                        name="mdi-star"
+                        size="12px"
+                      />
 
                       <span class="font9 fontmedium q-ml-xs">
-                        {{ (provider?.average_rating != null ? Number(provider.average_rating).toFixed(1) : '') + ' (' + (provider?.total_reviews ?? 0) + ')' }}
+                        {{
+                          (provider?.average_rating != null
+                            ? Number(provider.average_rating).toFixed(1)
+                            : "") +
+                          " (" +
+                          (provider?.total_reviews ?? 0) +
+                          ")"
+                        }}
                       </span>
                     </div>
 
                     <div class="row items-center">
-                      <q-icon color="secondary" name="mdi-broom" size="14px" />
+                      <q-icon
+                        color="secondary"
+                        name="mdi-broom"
+                        size="14px"
+                      />
 
                       <span class="font9 fontmedium q-ml-xs">
                         {{ provider?.total_services ?? 0 }}
@@ -81,10 +112,10 @@
                     color="pink-4"
                     dense
                     flat
-                    :icon="isFavorite ? 'mdi-heart' : 'mdi-heart-outline'"
-                    :loading="favoriteLoading"
                     round
                     size="sm"
+                    :icon="isFavorite ? 'mdi-heart' : 'mdi-heart-outline'"
+                    :loading="favoriteLoading"
                     @click="toggleFavorite"
                   />
 
@@ -95,6 +126,7 @@
                     icon="mdi-information-outline"
                     round
                     size="sm"
+                    @click.stop="openProviderInfo"
                   />
                 </div>
               </div>
@@ -104,14 +136,23 @@
 
         <div class="q-px-md q-pt-lg">
           <div class="font16 fontbold gradient-diarista q-mb-xs">
-            {{ $t('scheduling_page.schedule_service') }}
+            {{ $t("scheduling_page.schedule_service") }}
           </div>
 
-          <div v-if="loadingAvailability" class="row items-center justify-center q-py-lg">
-            <q-spinner-dots color="primary" size="36px" />
+          <div
+            v-if="loadingAvailability"
+            class="row items-center justify-center q-py-lg"
+          >
+            <q-spinner-dots
+              color="primary"
+              size="36px"
+            />
           </div>
 
-          <div v-else class="calendar-wrapper shadow-card q-mb-md">
+          <div
+            v-else
+            class="calendar-wrapper shadow-card q-mb-md"
+          >
             <q-date
               v-model="selectedDate"
               class="full-width"
@@ -127,11 +168,11 @@
         <div class="q-px-md q-pt-sm q-pb-xl">
           <div class="row items-center justify-between q-mb-sm">
             <div class="font16 fontbold gradient-diarista">
-              {{ $t('scheduling_page.reviews_title') }}
+              {{ $t("scheduling_page.reviews_title") }}
             </div>
 
-            <span class=" text-text cursor-pointer">
-              {{ $t('scheduling_page.see_all') }}
+            <span class="text-text cursor-pointer">
+              {{ $t("scheduling_page.see_all") }}
 
               <q-icon
                 class="text-text"
@@ -140,18 +181,27 @@
             </span>
           </div>
 
-          <div v-if="loadingReviews" class="row items-center justify-center q-py-md">
+          <div
+            v-if="loadingReviews"
+            class="row items-center justify-center q-py-md"
+          >
             <q-spinner-dots
               color="primary"
               size="36px"
             />
           </div>
 
-          <div v-else-if="reviews.length === 0" class="text-center text-grey-6 text-body2 q-py-md">
-            {{ $t('scheduling_page.no_reviews') }}
+          <div
+            v-else-if="reviews.length === 0"
+            class="text-center text-grey-6 text-body2 q-py-md"
+          >
+            {{ $t("scheduling_page.no_reviews") }}
           </div>
 
-          <div v-else class="reviews-scroll">
+          <div
+            v-else
+            class="reviews-scroll"
+          >
             <q-card
               v-for="review in reviews"
               :key="review.id"
@@ -160,20 +210,29 @@
             >
               <q-card-section class="q-pa-sm">
                 <div class="row items-center no-wrap q-gutter-x-sm q-mb-xs">
-                  <q-avatar :style="avatarColors[review.schedule?.client?.id % avatarColors.length]">
+                  <q-avatar
+                    :style="
+                      avatarColors[
+                        review.schedule?.client?.id % avatarColors.length
+                      ]
+                    "
+                  >
                     <img
                       v-if="review.schedule?.client?.profile_photo"
+                      style="object-fit: cover; border-radius: 50%"
                       :src="review.schedule?.client?.profile_photo"
-                      style="object-fit:cover;border-radius:50%;"
                     />
 
                     <span v-else>
-                      {{review.schedule?.client?.name?.slice(0,1) ?? '—' }}
+                      {{ getFirstName(review.schedule?.client?.name)?.slice(0, 1) ?? "—" }}
                     </span>
                   </q-avatar>
 
                   <div class="col-3 text-text font10 fontbold">
-                    {{ review.schedule?.client?.name ?? $t('scheduling_page.unknown_client') }}
+                    {{
+                      getFirstName(review.schedule?.client?.name) ??
+                      $t("scheduling_page.unknown_client")
+                    }}
                   </div>
 
                   <div class="row items-center q-mb-xs q-my-auto">
@@ -188,7 +247,7 @@
                 </div>
 
                 <div class="font9 fontregular text-text review-comment">
-                  {{ review.comment ?? '--------' }}
+                  {{ review.comment ?? "--------" }}
                 </div>
               </q-card-section>
             </q-card>
@@ -200,139 +259,160 @@
 </template>
 
 <script setup>
-import { computed, onMounted, ref } from 'vue';
-import { date } from 'quasar';
-import { getClientProviderBlocks } from 'src/api/schedule';
-import { getProviderBlockedDays, getProviderWorkingDays } from 'src/api/providerAvailability';
-import { getProviderReceivedReviews } from 'src/api/review';
+import { avatarColors } from "src/helpers/avatarColors";
+import { computed, onMounted, ref } from "vue";
+
 import {
   createClientFavoriteProvider,
   deleteClientFavoriteProvider,
   getClientFavoriteProviders,
-} from 'src/api/clientFavoriteProvider';
-import { useDialogPluginComponent, useQuasar } from 'quasar';
-import { useI18n } from 'vue-i18n';
-import { useScheduleCartStore } from 'src/stores/scheduleCart';
-import { userStore } from 'src/stores/user';
-import {getFirstName} from 'src/helpers/utils';
-import OrderSummaryDialog from './OrderSummaryDialog.vue';
-import ServiceSelectionSheet from './ServiceSelectionSheet.vue';
-import ServiceTimeSelectionDialog from './ServiceTimeSelectionDialog.vue';
+} from "src/api/clientFavoriteProvider";
 
-const props = defineProps({
-  provider: { required: true, type: Object },
-});
+import { date, useDialogPluginComponent, useQuasar } from "quasar";
+import { getClientProviderBlocks } from "src/api/schedule";
+import { getFirstName } from "src/helpers/utils";
+import { getProfileMediaUrl } from "src/helpers/profileMedia";
+
+import {
+  getProviderBlockedDays,
+  getProviderWorkingDays,
+} from "src/api/providerAvailability";
+
+import { getProviderReceivedReviews } from "src/api/review";
+import { useI18n } from "vue-i18n";
+import { useRouter } from "vue-router";
+import { userStore } from "src/stores/user";
+import { useServicePackageStore } from "src/stores/servicePackage";
+
+import ServicePackageSummaryDialog from "./ServicePackageSummaryDialog.vue";
+import ServiceSelectionSheet from "./ServiceSelectionSheet.vue";
+import ServiceTimeSelectionDialog from "./ServiceTimeSelectionDialog.vue";
+import ProfileInfoDialog from "src/components/profile/ProfileInfoDialog.vue";
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { dialogRef } = useDialogPluginComponent();
+const props = defineProps({
+  provider: { required: true, type: Object },
+});
 
-const $q    = useQuasar();
 const { t } = useI18n();
+const $q = useQuasar();
+const router = useRouter();
+const { dialogRef } = useDialogPluginComponent();
 
+const servicePackage = useServicePackageStore();
 const store = userStore();
 
-const scheduleCart = useScheduleCartStore();
-
-const selectedDate        = ref(null);
-const workingDays         = ref([]);
-const blockedDays         = ref([]);
-const loadingAvailability = ref(true);
-const reviews             = ref([]);
-const loadingReviews      = ref(true);
-const isFavorite          = ref(false);
-const favoriteId          = ref(null);
-const favoriteLoading     = ref(false);
-
-const avatarColors = [
-  { background: '#ffd5df', color: '#932e57' },
-  { background: '#d7e8ff', color: '#2158a8' },
-  { background: '#dfd',    color: '#2a7a3b' },
-  { background: '#ffe5cc', color: '#8a4500' },
-];
-
+const blockedDays = ref([]);
 const bookings = ref([]);
-
-const providerClientBlocks = ref({ existing_schedules: [], fully_blocked_weeks: [] });
-
-const availableWeekDays = computed(() =>
-  [...new Set(workingDays.value.map((wd) => wd.day))]
-);
-
-const blockedDateSet = computed(() =>
-  new Set(
-    blockedDays.value
-      .filter((bd) => bd.period === 'all')
-      .map((bd) => bd.date)
-  )
+const favoriteId = ref(null);
+const favoriteLoading = ref(false);
+const isFavorite = ref(false);
+const loadingAvailability = ref(true);
+const loadingReviews = ref(true);
+const providerClientBlocks = ref({
+  existing_schedules: [],
+  fully_blocked_weeks: [],
+});
+const reviews = ref([]);
+const selectedDate = ref(null);
+const workingDays = ref([]);
+
+const availableWeekDays = computed(() => [
+  ...new Set(workingDays.value.map((wd) => wd.day)),
+]);
+
+const blockedDateSet = computed(
+  () =>
+    new Set(
+      blockedDays.value
+        .filter((bd) => bd.period === "all")
+        .map((bd) => bd.date),
+    ),
 );
 
-const blockedWeekStartSet = computed(() =>
-  new Set(providerClientBlocks.value.fully_blocked_weeks ?? [])
+const blockedWeekStartSet = computed(
+  () => new Set(providerClientBlocks.value.fully_blocked_weeks ?? []),
 );
 
-const dateOptions = (d) => {
-  const today = date.formatDate(new Date(), 'YYYY/MM/DD');
+const normalizeDate = (dateStr) => (dateStr ?? "").replace(/\//g, "-");
 
-  if (d < today) return false;
-
-  const raw       = normalizeDate(d);
-  const parsed    = new Date(`${raw}T12:00:00`);
-  const dayOfWeek = parsed.getDay();
-
-  const isWorkingDay  = availableWeekDays.value.includes(dayOfWeek);
-  const isBlocked     = blockedDateSet.value.has(raw);
-  const isWeekBlocked = blockedWeekStartSet.value.has(getWeekStart(raw));
+const getWeekStart = (dateStr) => {
+  const normalizedDate = normalizeDate(dateStr);
 
-  if (!isWorkingDay || isBlocked || isWeekBlocked) return false;
+  const d = new Date(`${normalizedDate}T12:00:00`);
 
-  if (wouldExceedWeekLimit(raw)) return false;
+  d.setDate(d.getDate() - d.getDay());
 
-  return true;
+  return d.toISOString().split("T")[0];
 };
 
-const formatHour = (h) => `${String(h).padStart(2, '0')}:00`;
-
-const getCartWeekCount = (selectedDate) => {
+const getServicePackageWeekCount = (selectedDate) => {
   const weekStart = getWeekStart(selectedDate);
 
-  return scheduleCart.items.filter((item) =>
-    Number(item.provider_id) === Number(props.provider.provider_id) &&
-    getWeekStart(item.date) === weekStart
+  return servicePackage.items.filter(
+    (item) =>
+      Number(item.provider_id) === Number(props.provider.provider_id) &&
+      getWeekStart(item.date) === weekStart,
   ).length;
 };
 
 const getLocalWeekCount = (selectedDate) => {
   const weekStart = getWeekStart(selectedDate);
 
-  return bookings.value.filter((booking) => getWeekStart(booking.date) === weekStart).length;
+  return bookings.value.filter(
+    (booking) => getWeekStart(booking.date) === weekStart,
+  ).length;
 };
 
 const getServerWeekCount = (selectedDate) => {
   const weekStart = getWeekStart(selectedDate);
 
   return (providerClientBlocks.value.existing_schedules ?? []).filter(
-    (schedule) => getWeekStart(schedule.date) === weekStart
+    (schedule) => getWeekStart(schedule.date) === weekStart,
   ).length;
 };
 
-const getWeekStart = (dateStr) => {
-  const normalizedDate = normalizeDate(dateStr);
+const wouldExceedWeekLimit = (selectedDate) => {
+  const servicePackageCount = getServicePackageWeekCount(selectedDate);
+  const localCount = getLocalWeekCount(selectedDate);
+  const serverCount = getServerWeekCount(selectedDate);
 
-  const d = new Date(`${normalizedDate}T12:00:00`);
+  return serverCount + localCount + servicePackageCount >= 2;
+};
 
-  d.setDate(d.getDate() - d.getDay());
+const dateOptions = (d) => {
+  const today = date.formatDate(new Date(), "YYYY/MM/DD");
+
+  if (d < today) return false;
+
+  const raw = normalizeDate(d);
+  const parsed = new Date(`${raw}T12:00:00`);
+  const dayOfWeek = parsed.getDay();
+
+  const isWorkingDay = availableWeekDays.value.includes(dayOfWeek);
+  const isBlocked = blockedDateSet.value.has(raw);
+  const isWeekBlocked = blockedWeekStartSet.value.has(getWeekStart(raw));
+
+  if (!isWorkingDay || isBlocked || isWeekBlocked) return false;
 
-  return d.toISOString().split('T')[0];
+  if (wouldExceedWeekLimit(raw)) return false;
+
+  return true;
 };
 
+const formatHour = (h) => `${String(h).padStart(2, "0")}:00`;
+
 const loadAvailability = async () => {
   loadingAvailability.value = true;
 
   try {
     const clientId = store.user?.client?.id;
 
-    const defaultClientBlocks = { existing_schedules: [], fully_blocked_weeks: [] };
+    const defaultClientBlocks = {
+      existing_schedules: [],
+      fully_blocked_weeks: [],
+    };
 
     const [wd, bd, clientBlocks] = await Promise.all([
       getProviderWorkingDays(props.provider.provider_id),
@@ -350,7 +430,10 @@ const loadAvailability = async () => {
   } catch {
     blockedDays.value = [];
 
-    providerClientBlocks.value = { existing_schedules: [], fully_blocked_weeks: [] };
+    providerClientBlocks.value = {
+      existing_schedules: [],
+      fully_blocked_weeks: [],
+    };
 
     workingDays.value = [];
   } finally {
@@ -358,20 +441,6 @@ const loadAvailability = async () => {
   }
 };
 
-const loadReviews = async () => {
-  loadingReviews.value = true;
-
-  try {
-    const all = await getProviderReceivedReviews(props.provider.provider_id);
-
-    reviews.value = (all ?? []).slice(0, 10);
-  } catch {
-    reviews.value = [];
-  } finally {
-    loadingReviews.value = false;
-  }
-};
-
 const loadFavoriteState = async () => {
   const clientId = store.user?.client?.id;
   if (!clientId) return;
@@ -379,7 +448,7 @@ const loadFavoriteState = async () => {
   try {
     const favorites = await getClientFavoriteProviders(clientId);
     const match = (favorites ?? []).find(
-      (f) => Number(f.provider_id) === Number(props.provider.provider_id)
+      (f) => Number(f.provider_id) === Number(props.provider.provider_id),
     );
 
     isFavorite.value = !!match;
@@ -390,43 +459,20 @@ const loadFavoriteState = async () => {
   }
 };
 
-const toggleFavorite = async () => {
-  if (favoriteLoading.value) return;
-
-  const clientId = store.user?.client?.id;
-  if (!clientId) {
-    $q.notify({ type: 'negative', message: t('scheduling_page.favorite_error') });
-    return;
-  }
-
-  favoriteLoading.value = true;
+const loadReviews = async () => {
+  loadingReviews.value = true;
 
   try {
-    if (isFavorite.value) {
-      if (favoriteId.value) {
-        await deleteClientFavoriteProvider(favoriteId.value);
-      }
-      isFavorite.value = false;
-      favoriteId.value = null;
-      $q.notify({ type: 'positive', message: t('scheduling_page.favorite_removed') });
-    } else {
-      const created = await createClientFavoriteProvider({
-        client_id: clientId,
-        provider_id: props.provider.provider_id,
-      });
-      isFavorite.value = true;
-      favoriteId.value = created?.id ?? null;
-      $q.notify({ type: 'positive', message: t('scheduling_page.favorite_added') });
-    }
+    const all = await getProviderReceivedReviews(props.provider.provider_id);
+
+    reviews.value = (all ?? []).slice(0, 10);
   } catch {
-    $q.notify({ type: 'negative', message: t('scheduling_page.favorite_error') });
+    reviews.value = [];
   } finally {
-    favoriteLoading.value = false;
+    loadingReviews.value = false;
   }
 };
 
-const normalizeDate = (dateStr) => (dateStr ?? '').replace(/\//g, '-');
-
 const onDateSelected = (val) => {
   if (!val) return;
 
@@ -435,7 +481,7 @@ const onDateSelected = (val) => {
   const valFormatted = normalizeDate(val);
 
   const blocksOfDate = blockedDays.value.filter(
-    (bd) => bd.date === valFormatted && bd.period !== 'all'
+    (bd) => bd.date === valFormatted && bd.period !== "all",
   );
 
   const dayOfWeek = new Date(`${valFormatted}T12:00:00`).getDay();
@@ -446,35 +492,38 @@ const onDateSelected = (val) => {
 
   const workingDayBlocks = [];
 
-  if (!dayPeriods.includes('afternoon')) {
-    workingDayBlocks.push({ init_hour: '14:00:00', end_hour: '20:00:00' });
+  if (!dayPeriods.includes("afternoon")) {
+    workingDayBlocks.push({ init_hour: "14:00:00", end_hour: "20:00:00" });
   }
 
-  if (!dayPeriods.includes('morning')) {
-    workingDayBlocks.push({ init_hour: '07:00:00', end_hour: '13:00:00' });
+  if (!dayPeriods.includes("morning")) {
+    workingDayBlocks.push({ init_hour: "07:00:00", end_hour: "13:00:00" });
   }
 
   const localBookingBlocks = bookings.value
-    .filter((booking) => booking.date.replace(/\//g, '-') === valFormatted)
+    .filter((booking) => booking.date.replace(/\//g, "-") === valFormatted)
     .map((booking) => ({
-      end_hour:  `${booking.slot.endHour}:00:00`,
+      end_hour: `${booking.slot.endHour}:00:00`,
       init_hour: `${booking.slot.startHour}:00:00`,
     }));
 
-  const cartBookingBlocks = scheduleCart.items
-    .filter((item) =>
-      Number(item.provider_id) === Number(props.provider.provider_id) &&
-      normalizeDate(item.date) === valFormatted
+  const servicePackageBookingBlocks = servicePackage.items
+    .filter(
+      (item) =>
+        Number(item.provider_id) === Number(props.provider.provider_id) &&
+        normalizeDate(item.date) === valFormatted,
     )
     .map((item) => ({
-      end_hour:  item.end_time,
+      end_hour: item.end_time,
       init_hour: item.start_time,
     }));
 
-  const serverBookingBlocks = (providerClientBlocks.value.existing_schedules ?? [])
+  const serverBookingBlocks = (
+    providerClientBlocks.value.existing_schedules ?? []
+  )
     .filter((schedule) => normalizeDate(schedule.date) === valFormatted)
     .map((schedule) => ({
-      end_hour:  schedule.end_time,
+      end_hour: schedule.end_time,
       init_hour: schedule.start_time,
     }));
 
@@ -483,29 +532,54 @@ const onDateSelected = (val) => {
     ...workingDayBlocks,
     ...localBookingBlocks,
     ...serverBookingBlocks,
-    ...cartBookingBlocks,
+    ...servicePackageBookingBlocks,
   ];
 
   $q.dialog({
-    component: ServiceSelectionSheet, componentProps: { partialBlocks, provider: props.provider, selectedDate: val },
+    component: ServiceSelectionSheet,
+    componentProps: {
+      partialBlocks,
+      provider: props.provider,
+      selectedDate: val,
+    },
   }).onOk(({ action, serviceType, date: date_, provider: prov }) => {
     $q.dialog({
-      component: ServiceTimeSelectionDialog, componentProps: {partialBlocks, provider: prov, selectedDate: date_, serviceType },
+      component: ServiceTimeSelectionDialog,
+      componentProps: {
+        partialBlocks,
+        provider: prov,
+        selectedDate: date_,
+        serviceType,
+      },
     }).onOk((booking) => {
-      if (action === 'cart') {
-        scheduleCart.addItem({
-          date:          normalizeDate(booking.date),
-          end_time:      formatHour(booking.slot.endHour),
-          local_id:      `${prov.provider_id}-${booking.date}-${booking.slot.value}-${Date.now()}`,
-          offers_meal:   booking.meal === 'offer' ? true : booking.meal === 'no_offer' ? false : null,
-          period_type:   booking.serviceType.hoursCount,
-          provider_id:   prov.provider_id,
+      if (action === "servicePackage") {
+        servicePackage.addItem({
+          date: normalizeDate(booking.date),
+          end_time: formatHour(booking.slot.endHour),
+          local_id: `${prov.provider_id}-${booking.date}-${booking.slot.value}-${Date.now()}`,
+          offers_meal:
+            booking.meal === "offer"
+              ? true
+              : booking.meal === "no_offer"
+                ? false
+                : null,
+          period_type: booking.serviceType.hoursCount,
+          provider_id: prov.provider_id,
           provider_name: prov.provider_name,
-          start_time:    formatHour(booking.slot.startHour),
-          base_amount:   booking.serviceType.basePrice ?? booking.serviceType.price,
+          provider_photo: getProfileMediaUrl(prov),
+          start_time: formatHour(booking.slot.startHour),
+          base_amount:
+            booking.serviceType.basePrice ?? booking.serviceType.price,
         });
 
-        $q.notify({ message: t('schedule_cart.add_success'), type: 'positive' });
+        $q.notify({
+          message: t("service_package.add_success"),
+          type: "positive",
+        });
+
+        dialogRef.value.hide();
+
+        router.push({ name: "ScheduleCartPage" });
 
         return;
       }
@@ -513,7 +587,8 @@ const onDateSelected = (val) => {
       bookings.value.push(booking);
 
       $q.dialog({
-        component: OrderSummaryDialog, componentProps: { initialBooking: booking, provider: props.provider },
+        component: ServicePackageSummaryDialog,
+        componentProps: { initialBooking: booking, provider: props.provider },
       }).onOk(() => {
         dialogRef.value.hide();
       });
@@ -521,18 +596,71 @@ const onDateSelected = (val) => {
   });
 };
 
-const wouldExceedWeekLimit = (selectedDate) => {
-  const cartCount   = getCartWeekCount(selectedDate);
-  const localCount  = getLocalWeekCount(selectedDate);
-  const serverCount = getServerWeekCount(selectedDate);
+const openProviderInfo = () => {
+  $q.dialog({
+    component: ProfileInfoDialog,
+    componentProps: {
+      provider: props.provider,
+    },
+  });
+};
 
-  return (serverCount + localCount + cartCount) >= 2;
+const toggleFavorite = async () => {
+  if (favoriteLoading.value) return;
+
+  const clientId = store.user?.client?.id;
+  if (!clientId) {
+    $q.notify({
+      message: t("scheduling_page.favorite_error"),
+      type: "negative",
+    });
+
+    return;
+  }
+
+  favoriteLoading.value = true;
+
+  try {
+    if (isFavorite.value) {
+      if (favoriteId.value) {
+        await deleteClientFavoriteProvider(favoriteId.value);
+      }
+
+      favoriteId.value = null;
+      isFavorite.value = false;
+
+      $q.notify({
+        message: t("scheduling_page.favorite_removed"),
+        type: "positive",
+      });
+    } else {
+      const created = await createClientFavoriteProvider({
+        client_id: clientId,
+        provider_id: props.provider.provider_id,
+      });
+
+      favoriteId.value = created?.id ?? null;
+      isFavorite.value = true;
+
+      $q.notify({
+        message: t("scheduling_page.favorite_added"),
+        type: "positive",
+      });
+    }
+  } catch {
+    $q.notify({
+      message: t("scheduling_page.favorite_error"),
+      type: "negative",
+    });
+  } finally {
+    favoriteLoading.value = false;
+  }
 };
 
 onMounted(() => {
   loadAvailability();
-  loadReviews();
   loadFavoriteState();
+  loadReviews();
 });
 </script>
 
@@ -545,7 +673,7 @@ onMounted(() => {
   display: flex;
   flex-direction: column;
   overflow: hidden;
-  background: #F9FAFB;
+  background: #f9fafb;
 }
 
 .dialog-header {
@@ -571,7 +699,7 @@ onMounted(() => {
   }
 
   :deep(.q-date__calendar-days .q-btn__content) {
-    color: #1E293B !important;
+    color: #1e293b !important;
   }
 
   // dias desabilitados: visíveis mas opacos
@@ -589,14 +717,18 @@ onMounted(() => {
 
   // cabeçalho dos dias (dom, seg, ter, qua...)
   :deep(.q-date__calendar-weekdays > div) {
-    color: #6366F1;
+    color: #6366f1;
     font-weight: 700;
     opacity: 0.8;
   }
 
   :deep(.q-date__navigation) {
-    .q-btn { color: #6366F1 !important; }
-    .q-btn__content { color: #6366F1 !important; }
+    .q-btn {
+      color: #6366f1 !important;
+    }
+    .q-btn__content {
+      color: #6366f1 !important;
+    }
   }
 
   :deep(.q-date__event) {
@@ -614,7 +746,7 @@ onMounted(() => {
 
   :deep(.q-date__selected .q-btn__content),
   :deep(.q-date__calendar-item .q-btn.q-date__selected .q-btn__content) {
-    background: #6366F1 !important;
+    background: #6366f1 !important;
     color: #ffffff !important;
     border-radius: 50%;
     box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
@@ -622,7 +754,7 @@ onMounted(() => {
 
   :deep(.q-date__view--months .q-btn),
   :deep(.q-date__view--years .q-btn) {
-    color: #6366F1 !important;
+    color: #6366f1 !important;
   }
 
   :deep(.q-date__calendar-item--out) {
@@ -639,7 +771,9 @@ onMounted(() => {
   -webkit-overflow-scrolling: touch;
   scrollbar-width: none;
   padding-bottom: 8px;
-  &::-webkit-scrollbar { display: none; }
+  &::-webkit-scrollbar {
+    display: none;
+  }
 }
 
 .review-card {

+ 139 - 88
src/pages/search/components/SearchFilterDialog.vue

@@ -1,157 +1,197 @@
 <!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
 <template>
-  <q-dialog ref="dialogRef" position="standard" @hide="onDialogHide">
+  <q-dialog
+    ref="dialogRef"
+    position="standard"
+    @hide="onDialogHide"
+  >
     <q-card class="filter-card bg-surface text-text">
-
       <q-card-section class="row items-center q-pb-none">
-        <span class="font16 fontbold gradient-diarista">{{ $t('search_filter.title') }}</span>
+        <span class="font16 fontbold gradient-diarista">{{
+          $t("search_filter.title")
+        }}</span>
+
         <q-space />
-        <q-btn flat round dense icon="mdi-close" color="grey-6" @click="onDialogCancel" />
+
+        <q-btn
+          color="grey-6"
+          dense
+          flat
+          icon="mdi-close"
+          round
+          @click="onDialogCancel"
+        />
       </q-card-section>
 
       <q-separator class="q-mt-sm" />
 
       <q-card-section class="q-pt-sm q-pb-none scroll filter-body">
+        <div class="font14 fontbold text-grey-7 q-mb-sm">
+          {{ $t("search_filter.sort_by") }}
+        </div>
 
-        <div class="font14 fontbold text-grey-7 q-mb-sm">{{ $t('search_filter.sort_by') }}</div>
+        <div
+          v-for="group in sortGroups"
+          :key="group.key"
+          class="q-mb-md"
+        >
+          <div class="font12 fontmedium text-text q-mb-xs">
+            {{ group.label }}
+          </div>
 
-        <div v-for="group in sortGroups" :key="group.key" class="q-mb-md">
-          <div class="font12 fontmedium text-text q-mb-xs">{{ group.label }}</div>
-          <div class="row q-gutter-x-lg">
+          <div class="sort-options">
             <q-radio
               v-for="opt in group.options"
               :key="opt.value"
               v-model="localSort"
-              :val="opt.value"
-              :label="opt.label"
               :disable="opt.disable ?? false"
+              :label="opt.label"
+              :val="opt.value"
+              class="sort-option font12 fontmedium"
               color="primary"
-              keep-color
-              class="font12 fontmedium"
               dense
+              keep-color
             />
           </div>
         </div>
 
         <q-separator class="q-my-sm" />
 
-        <div class="font12 fontbold text-grey-7 q-mb-xs">{{ $t('search_filter.filter_by') }}</div>
-        <div class="font12 fontmedium text-text q-mb-xs">{{ $t('search_filter.availability') }}</div>
+        <div class="font12 fontbold text-grey-7 q-mb-xs">
+          {{ $t("search_filter.filter_by") }}
+        </div>
+
+        <div class="font12 fontmedium text-text q-mb-xs">
+          {{ $t("search_filter.availability") }}
+        </div>
+
         <DefaultInputDatePicker
           v-model:untreated-date="localDate"
-          :label="$t('search_filter.availability_placeholder')"
-          dense
+          calendar-class="search-filter-calendar"
           class="date-picker-primary"
+          dense
+          :label="$t('search_filter.availability_placeholder')"
         />
       </q-card-section>
 
       <q-separator />
+
       <q-card-actions class="q-px-md q-py-md row q-gutter-x-sm">
         <q-btn
-          outline
+          class="col"
           color="grey-6"
-          :label="$t('search_filter.clear')"
-          rounded
           no-caps
+          outline
+          rounded
           size="md"
-          class="col"
+          :label="$t('search_filter.clear')"
           @click="clearFilters"
         />
+
         <q-btn
+          class="col"
           color="primary-button"
-          :label="$t('search_filter.apply')"
-          rounded
           no-caps
-          unelevated
+          rounded
           size="md"
-          class="col"
+          unelevated
+          :label="$t('search_filter.apply')"
           @click="applyFilters"
         />
       </q-card-actions>
-
     </q-card>
   </q-dialog>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue';
-import { useDialogPluginComponent } from 'quasar';
-import { useI18n } from 'vue-i18n';
-import DefaultInputDatePicker from 'src/components/defaults/DefaultInputDatePicker.vue';
+import { computed, ref } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import { useI18n } from "vue-i18n";
+
+import DefaultInputDatePicker from "src/components/defaults/DefaultInputDatePicker.vue";
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { initialSort, initialDate } = defineProps({
-  initialSort: { type: String, default: null },
-  initialDate: { type: String, default: null },
+const { initialDate, initialSort } = defineProps({
+  initialDate: { default: null, type: String },
+  initialSort: { default: null, type: String },
 });
 
+const { dialogRef, onDialogCancel, onDialogHide, onDialogOK } =
+  useDialogPluginComponent();
 const { t } = useI18n();
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
 
-const localSort = ref(initialSort ?? null);
 const localDate = ref(initialDate ?? null);
+const localSort = ref(initialSort ?? null);
 
 const sortGroups = computed(() => [
   {
-    key: 'price',
-    label: t('search_filter.groups.price'),
+    key: "price",
+    label: t("search_filter.groups.price"),
     options: [
-      { value: 'price_desc', label: t('search_filter.sort.higher') },
-      { value: 'price_asc',  label: t('search_filter.sort.lower') },
+      { value: "price_desc", label: t("search_filter.sort.higher") },
+      { value: "price_asc", label: t("search_filter.sort.lower") },
     ],
   },
   {
-    key: 'distance',
-    label: t('search_filter.groups.distance'),
+    key: "distance",
+    label: t("search_filter.groups.distance"),
     options: [
-      { value: 'distance_desc', label: t('search_filter.sort.higher'), disable: true },
-      { value: 'distance_asc',  label: t('search_filter.sort.lower'),  disable: true },
+      {
+        value: "distance_desc",
+        label: t("search_filter.sort.higher"),
+        disable: true,
+      },
+      {
+        value: "distance_asc",
+        label: t("search_filter.sort.lower"),
+        disable: true,
+      },
     ],
   },
   {
-    key: 'reviews',
-    label: t('search_filter.groups.reviews'),
+    key: "reviews",
+    label: t("search_filter.groups.reviews"),
     options: [
-      { value: 'reviews_desc', label: t('search_filter.sort.higher') },
-      { value: 'reviews_asc',  label: t('search_filter.sort.lower') },
+      { value: "reviews_desc", label: t("search_filter.sort.higher") },
+      { value: "reviews_asc", label: t("search_filter.sort.lower") },
     ],
   },
   {
-    key: 'rating',
-    label: t('search_filter.groups.rating'),
+    key: "rating",
+    label: t("search_filter.groups.rating"),
     options: [
-      { value: 'rating_desc', label: t('search_filter.sort.higher') },
-      { value: 'rating_asc',  label: t('search_filter.sort.lower') },
+      { value: "rating_desc", label: t("search_filter.sort.higher") },
+      { value: "rating_asc", label: t("search_filter.sort.lower") },
     ],
   },
   {
-    key: 'services',
-    label: t('search_filter.groups.services'),
+    key: "services",
+    label: t("search_filter.groups.services"),
     options: [
-      { value: 'services_desc', label: t('search_filter.sort.higher') },
-      { value: 'services_asc',  label: t('search_filter.sort.lower') },
+      { value: "services_desc", label: t("search_filter.sort.higher") },
+      { value: "services_asc", label: t("search_filter.sort.lower") },
     ],
   },
   {
-    key: 'oldest',
-    label: t('search_filter.groups.oldest'),
+    key: "oldest",
+    label: t("search_filter.groups.oldest"),
     options: [
-      { value: 'oldest',  label: t('search_filter.sort.oldest_asc') },
-      { value: 'newest',  label: t('search_filter.sort.oldest_desc') },
+      { value: "oldest", label: t("search_filter.sort.oldest_asc") },
+      { value: "newest", label: t("search_filter.sort.oldest_desc") },
     ],
   },
 ]);
 
-const clearFilters = () => {
-  localSort.value = null;
-  localDate.value = null;
-  onDialogOK({ sort: null, date: null });
-};
-
 const applyFilters = () => {
   onDialogOK({ sort: localSort.value, date: localDate.value });
 };
+
+const clearFilters = () => {
+  localDate.value = null;
+  localSort.value = null;
+  onDialogOK({ date: null, sort: null });
+};
 </script>
 
 <style scoped lang="scss">
@@ -159,18 +199,32 @@ const applyFilters = () => {
   border-radius: 16px;
   width: min(92vw, 420px);
 }
+
 .filter-body {
   max-height: 60dvh;
   overflow-y: auto;
 }
+
+.sort-options {
+  column-gap: 16px;
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.sort-option {
+  width: 100%;
+}
+
 .date-picker-primary {
   :deep(.q-field__label) {
     color: var(--q-primary);
   }
+
   :deep(.q-field__native),
   :deep(.q-field__input) {
     color: var(--q-text, #555555) !important;
   }
+
   :deep(.q-icon) {
     color: var(--q-primary);
   }
@@ -178,33 +232,30 @@ const applyFilters = () => {
 </style>
 
 <style lang="scss">
-.q-popup-proxy {
-  .q-date {
-    background: var(--q-surface, #ffffff) !important;
-    color: var(--q-text, #555555) !important;
-    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
-
-    .q-date__header {
-      background: var(--q-primary);
-      color: #fff;
-    }
+.search-filter-calendar {
+  background: #fff !important;
+  color: #555 !important;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
 
-    .q-date__content {
-      color: var(--q-text, #555555) !important;
-    }
+  .q-date__header {
+    background: var(--q-primary);
+    color: #fff;
+  }
 
-    .q-date__calendar-item .q-btn {
-      color: var(--q-text, #555555) !important;
-    }
+  .q-date__content,
+  .q-date__navigation .q-btn,
+  .q-date__navigation .q-btn__content,
+  .q-date__calendar-weekdays > div {
+    color: #555 !important;
+  }
 
-    .q-date__navigation .q-btn {
-      color: var(--q-text, #555555) !important;
-    }
+  .q-date__calendar-days .q-btn:not(.bg-primary),
+  .q-date__calendar-days .q-btn:not(.bg-primary) .q-btn__content {
+    color: #555 !important;
+  }
 
-    .q-date__calendar-weekdays > div {
-      color: var(--q-text, #555555) !important;
-      opacity: 0.6;
-    }
+  .q-date__calendar-weekdays > div {
+    opacity: 0.6;
   }
 }
-</style>
+</style>

+ 690 - 0
src/pages/search/components/ServicePackageSummaryDialog.vue

@@ -0,0 +1,690 @@
+<template>
+  <q-dialog
+    ref="dialogRef"
+    maximized
+    transition-hide="slide-down"
+    transition-show="slide-up"
+  >
+    <div class="dialog-root">
+      <div
+        class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white"
+      >
+        <q-btn
+          v-close-popup
+          class="header-back-btn"
+          color="primary"
+          dense
+          flat
+          icon="mdi-chevron-left"
+          round
+        />
+
+        <div class="col text-center font16 fontbold gradient-diarista">
+          {{ $t("scheduling_page.title") }}
+        </div>
+
+        <div style="width: 36px" />
+      </div>
+
+      <div class="dialog-body">
+        <div class="q-px-md q-pt-md">
+          <div class="info-banner card-border q-pa-md">
+            <div class="font14 fontbold text-primary">
+              {{ $t("scheduling_page.order_summary.info_text") }}
+            </div>
+
+            <div class="font12 fontmedium text-primary q-mt-xs">
+              {{ $t("scheduling_page.order_summary.info_note") }}
+            </div>
+          </div>
+        </div>
+
+        <div class="q-px-md q-pt-md">
+          <div class="font16 fontbold gradient-diarista q-mb-sm">
+            {{ $t("scheduling_page.order_summary.title") }}
+          </div>
+
+          <q-card
+            v-for="(booking, idx) in bookings"
+            :key="idx"
+            :flat="false"
+            class="card-border bg-surface q-mb-sm shadow-card q-px-md q-pb-md"
+          >
+            <q-card-section class="q-pa-md row items-center no-wrap">
+              <div class="col text-center">
+                <div class="text-text">
+                  <span class="font14 fontregular">
+                    {{ $t("scheduling_page.order_summary.service_label") }}
+                  </span>
+
+                  <span class="font14 fontbold">
+                    {{
+                      ` ${booking.serviceType.label} (${booking.serviceType.hours})`
+                    }}
+                  </span>
+                </div>
+
+                <div class="font14 fontbold text-text">
+                  {{ formatDate(booking.date) }}
+                </div>
+
+                <div class="font14 fontbold text-text">
+                  {{
+                    $t("scheduling_page.order_summary.time_range", {
+                      start: booking.slot.startHour,
+                      end: booking.slot.endHour,
+                    })
+                  }}
+                </div>
+              </div>
+
+              <q-btn
+                color="grey-5"
+                dense
+                flat
+                icon="mdi-minus-circle-outline"
+                padding="4px 8px"
+                round
+                @click="confirmRemove(idx)"
+              />
+            </q-card-section>
+
+            <q-btn
+              class="full-width q-mt-sm"
+              color="secondary"
+              no-caps
+              padding="4px 8px"
+              rounded
+              unelevated
+              :label="$t('scheduling_page.order_summary.send_btn')"
+              @click="submitOrder"
+            />
+          </q-card>
+
+          <div class="full-width text-center">
+            <q-btn
+              class="q-mt-xs"
+              color="primary"
+              no-caps
+              outline
+              rounded
+              :disable="showCalendar"
+              :label="$t('scheduling_page.order_summary.add_date_btn')"
+              @click="showCalendar = true"
+            />
+          </div>
+        </div>
+
+        <div
+          v-if="showCalendar"
+          class="q-px-md q-pt-lg q-pb-xl"
+        >
+          <div
+            v-if="loadingAvailability"
+            class="row items-center justify-center q-py-lg"
+          >
+            <q-spinner-dots
+              color="primary"
+              size="36px"
+            />
+          </div>
+
+          <div
+            v-else
+            class="calendar-wrapper shadow-card"
+          >
+            <q-date
+              v-model="addDateValue"
+              class=""
+              color="purple"
+              minimal
+              :first-day-of-week="0"
+              :options="dateOptions"
+              @update:model-value="onAddDateSelected"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed, onMounted, ref } from "vue";
+import { createSchedule, getClientProviderBlocks } from "src/api/schedule";
+import { date, useDialogPluginComponent, useQuasar } from "quasar";
+import { getAddresses } from "src/api/address";
+
+import {
+  getProviderBlockedDays,
+  getProviderWorkingDays,
+} from "src/api/providerAvailability";
+
+import { useI18n } from "vue-i18n";
+import { useRouter } from "vue-router";
+import { userStore } from "src/stores/user";
+import { useServicePackageStore } from "src/stores/servicePackage";
+
+import ServiceSelectionSheet from "./ServiceSelectionSheet.vue";
+import ServiceTimeSelectionDialog from "./ServiceTimeSelectionDialog.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const props = defineProps({
+  initialBooking: { required: true, type: Object },
+  provider: { required: true, type: Object },
+});
+
+const { locale, t } = useI18n();
+const router = useRouter();
+const $q = useQuasar();
+const { dialogRef, onDialogOK } = useDialogPluginComponent();
+
+const servicePackage = useServicePackageStore();
+const store = userStore();
+
+const addDateValue = ref(null);
+const blockedDays = ref([]);
+const bookings = ref([props.initialBooking]);
+const loadingAvailability = ref(false);
+const primaryAddress = ref(null);
+
+const providerClientBlocks = ref({
+  existing_schedules: [],
+  fully_blocked_weeks: [],
+});
+
+const showCalendar = ref(false);
+const submitting = ref(false);
+const workingDays = ref([]);
+
+const availableWeekDays = computed(() => [
+  ...new Set(workingDays.value.map((wd) => wd.day)),
+]);
+
+const blockedDateSet = computed(
+  () =>
+    new Set(
+      blockedDays.value
+        .filter((bd) => bd.period === "all")
+        .map((bd) => bd.date),
+    ),
+);
+
+const blockedWeekStartSet = computed(
+  () => new Set(providerClientBlocks.value.fully_blocked_weeks ?? []),
+);
+
+const normalizeDate = (d) => d.replace(/\//g, "-");
+
+const getWeekStart = (dateStr) => {
+  const d = new Date(normalizeDate(dateStr) + "T12:00:00");
+
+  d.setDate(d.getDate() - d.getDay());
+
+  return d.toISOString().slice(0, 10);
+};
+
+const getServicePackageWeekCount = (newDateStr) => {
+  const newWeek = getWeekStart(newDateStr);
+
+  return servicePackage.items.filter(
+    (item) =>
+      Number(item.provider_id) === Number(props.provider.provider_id) &&
+      getWeekStart(item.date) === newWeek,
+  ).length;
+};
+
+const getLocalWeekCount = (newDateStr) => {
+  const newWeek = getWeekStart(newDateStr);
+
+  return bookings.value.filter(
+    (booking) => getWeekStart(booking.date) === newWeek,
+  ).length;
+};
+
+const getServerWeekCount = (newDateStr) => {
+  const newWeek = getWeekStart(newDateStr);
+
+  return (providerClientBlocks.value.existing_schedules ?? []).filter(
+    (schedule) => getWeekStart(schedule.date) === newWeek,
+  ).length;
+};
+
+const wouldExceedWeekLimit = (newDateStr) => {
+  const count =
+    getServerWeekCount(newDateStr) +
+    getLocalWeekCount(newDateStr) +
+    getServicePackageWeekCount(newDateStr);
+
+  return count >= 2;
+};
+
+const confirmRemove = (idx) => {
+  $q.dialog({
+    title: t("scheduling_page.order_summary.remove_confirm_title"),
+    cancel: {
+      label: t("scheduling_page.order_summary.remove_confirm_cancel"),
+      flat: true,
+      color: "grey-6",
+    },
+    ok: {
+      label: t("scheduling_page.order_summary.remove_confirm_ok"),
+      unelevated: true,
+      color: "primary",
+      rounded: true,
+      noCaps: true,
+    },
+    persistent: true,
+  }).onOk(() => {
+    bookings.value.splice(idx, 1);
+  });
+};
+
+const dateOptions = (d) => {
+  const today = date.formatDate(new Date(), "YYYY/MM/DD");
+
+  if (d < today) return false;
+  if (wouldExceedWeekLimit(d)) return false;
+
+  const raw = normalizeDate(d);
+
+  const parsed = new Date(`${raw}T12:00:00`);
+
+  const dayOfWeek = parsed.getDay();
+  const isWorking = availableWeekDays.value.includes(dayOfWeek);
+  const isBlocked = blockedDateSet.value.has(raw);
+  const isWeekBlocked = blockedWeekStartSet.value.has(getWeekStart(raw));
+
+  return isWorking && !isBlocked && !isWeekBlocked;
+};
+
+const formatDate = (dateStr) => {
+  const d = new Date(normalizeDate(dateStr) + "T12:00:00");
+
+  const localeMap = { pt: "pt-BR", en: "en-US", es: "es-ES" };
+
+  const loc = localeMap[locale.value] ?? "pt-BR";
+
+  const weekday = new Intl.DateTimeFormat(loc, { weekday: "long" }).format(d);
+  const dateFormatted = new Intl.DateTimeFormat(loc, {
+    day: "2-digit",
+    month: "2-digit",
+    year: "numeric",
+  }).format(d);
+
+  return `${weekday.charAt(0).toUpperCase() + weekday.slice(1)}, ${dateFormatted}`;
+};
+
+const formatHour = (h) => `${String(h).padStart(2, "0")}:00`;
+
+const loadAvailability = async () => {
+  loadingAvailability.value = true;
+
+  try {
+    const clientId = store.user?.client_id;
+
+    const defaultClientBlocks = {
+      existing_schedules: [],
+      fully_blocked_weeks: [],
+    };
+
+    const [wd, bd, clientBlocks] = await Promise.all([
+      getProviderWorkingDays(props.provider.provider_id),
+      getProviderBlockedDays(props.provider.provider_id),
+      clientId
+        ? getClientProviderBlocks(clientId, props.provider.provider_id)
+        : Promise.resolve(defaultClientBlocks),
+    ]);
+
+    workingDays.value = wd ?? [];
+    blockedDays.value = bd ?? [];
+
+    providerClientBlocks.value = clientBlocks ?? defaultClientBlocks;
+  } catch {
+    workingDays.value = [];
+    blockedDays.value = [];
+
+    providerClientBlocks.value = {
+      existing_schedules: [],
+      fully_blocked_weeks: [],
+    };
+  } finally {
+    loadingAvailability.value = false;
+  }
+};
+
+const loadPrimaryAddress = async () => {
+  try {
+    const clientId = store.user?.client_id;
+
+    if (!clientId) return;
+
+    const addresses = await getAddresses("client", clientId);
+
+    primaryAddress.value = (addresses ?? []).find((a) => a.is_primary) ?? null;
+  } catch {
+    primaryAddress.value = null;
+  }
+};
+
+const onAddDateSelected = (val) => {
+  if (!val) return;
+
+  addDateValue.value = null;
+
+  const valFormatted = normalizeDate(val);
+
+  const blocksOfDate = blockedDays.value.filter(
+    (bd) => bd.date === valFormatted && bd.period !== "all",
+  );
+
+  const dayOfWeek = new Date(`${valFormatted}T12:00:00`).getDay();
+
+  const dayPeriods = workingDays.value
+    .filter((wd) => wd.day === dayOfWeek)
+    .map((wd) => wd.period);
+
+  const workingDayBlocks = [];
+
+  if (!dayPeriods.includes("afternoon")) {
+    workingDayBlocks.push({ init_hour: "14:00:00", end_hour: "20:00:00" });
+  }
+
+  if (!dayPeriods.includes("morning")) {
+    workingDayBlocks.push({ init_hour: "7:00:00", end_hour: "13:00:00" });
+  }
+
+  const existingBookingBlocks = bookings.value
+    .filter((b) => b.date.replace(/\//g, "-") === valFormatted)
+    .map((b) => ({
+      init_hour: `${b.slot.startHour}:00:00`,
+      end_hour: `${b.slot.endHour}:00:00`,
+    }));
+
+  const serverBookingBlocks = (
+    providerClientBlocks.value.existing_schedules ?? []
+  )
+    .filter((schedule) => schedule.date === valFormatted)
+    .map((schedule) => ({
+      init_hour: schedule.start_time,
+      end_hour: schedule.end_time,
+    }));
+
+  const servicePackageBookingBlocks = servicePackage.items
+    .filter(
+      (item) =>
+        Number(item.provider_id) === Number(props.provider.provider_id) &&
+        normalizeDate(item.date) === valFormatted,
+    )
+    .map((item) => ({
+      init_hour: item.start_time,
+      end_hour: item.end_time,
+    }));
+
+  const partialBlocks = [
+    ...blocksOfDate,
+    ...workingDayBlocks,
+    ...existingBookingBlocks,
+    ...serverBookingBlocks,
+    ...servicePackageBookingBlocks,
+  ];
+
+  $q.dialog({
+    component: ServiceSelectionSheet,
+    componentProps: {
+      provider: props.provider,
+      selectedDate: val,
+      partialBlocks,
+    },
+  }).onOk(({ serviceType, date: date_, provider: prov }) => {
+    $q.dialog({
+      component: ServiceTimeSelectionDialog,
+      componentProps: {
+        serviceType,
+        selectedDate: date_,
+        provider: prov,
+        partialBlocks,
+      },
+    }).onOk((booking) => {
+      if (wouldExceedWeekLimit(booking.date)) {
+        $q.notify({
+          type: "negative",
+          message: t("scheduling_page.order_summary.week_limit_error"),
+        });
+
+        return;
+      }
+
+      bookings.value.push(booking);
+
+      showCalendar.value = false;
+    });
+  });
+};
+
+const submitOrder = async () => {
+  if (!primaryAddress.value) {
+    $q.notify({
+      type: "warning",
+      message: t("scheduling_page.order_summary.no_primary_address"),
+    });
+
+    return;
+  }
+
+  const payloads = bookings.value.map((b) => ({
+    client_id: store.user.client_id,
+    provider_id: props.provider.provider_id,
+    address_id: primaryAddress.value.id,
+    schedule_type: "default",
+    date: normalizeDate(b.date),
+    period_type: b.serviceType.hoursCount,
+    start_time: formatHour(b.slot.startHour),
+    end_time: formatHour(b.slot.endHour),
+    total_amount: b.serviceType.basePrice ?? b.serviceType.price,
+    offers_meal:
+      b.meal === "offer" ? true : b.meal === "no_offer" ? false : null,
+  }));
+
+  submitting.value = true;
+
+  try {
+    await Promise.all(payloads.map((payload) => createSchedule(payload)));
+
+    $q.notify({
+      type: "positive",
+      message: t("scheduling_page.order_summary.submit_success"),
+    });
+
+    onDialogOK();
+  } catch (err) {
+    const msg =
+      err?.response?.data?.message ??
+      err?.message ??
+      t("scheduling_page.order_summary.submit_error");
+
+    $q.notify({ type: "negative", message: msg });
+  } finally {
+    submitting.value = false;
+
+    router.push({ name: "DashboardPage" });
+  }
+};
+
+onMounted(() => Promise.all([loadAvailability(), loadPrimaryAddress()]));
+</script>
+
+<style scoped lang="scss">
+.dialog-root {
+  width: 100vw;
+  max-width: 100vw;
+  height: 100vh;
+  max-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  background: #f9fafb;
+}
+
+.dialog-header {
+  flex-shrink: 0;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+}
+
+.dialog-body {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+  overflow-x: clip;
+}
+
+.info-banner {
+  background: rgba(139, 92, 246, 0.08);
+  border: 1px solid rgba(139, 92, 246, 0.2);
+}
+
+.calendar-wrapper {
+  border-radius: 20px;
+  overflow: hidden;
+  background: white;
+
+  :deep(.q-date) {
+    background: white;
+    width: 100%;
+  }
+  :deep(.q-date__main),
+  :deep(.q-date__content),
+  :deep(.q-date__calendar) {
+    background: white !important;
+  }
+
+  :deep(.q-date__calendar-item .q-btn) {
+    min-width: 0 !important;
+    padding: 6px 2px !important;
+  }
+  :deep(.q-date__calendar-item .q-btn.disabled),
+  :deep(.q-date__calendar-item .q-btn[disabled]) {
+    opacity: 1 !important;
+  }
+  :deep(.q-date__calendar-item .q-btn.disabled .q-btn__content),
+  :deep(.q-date__calendar-item .q-btn[disabled] .q-btn__content) {
+    color: #cbd5e1 !important;
+  }
+
+  :deep(.q-date__calendar-days .q-btn__content) {
+    font-weight: 500;
+    color: #1e293b;
+  }
+  :deep(.q-date__calendar-weekdays > div) {
+    color: #6366f1;
+    font-weight: 700;
+    opacity: 0.8;
+  }
+  :deep(.q-date__navigation) {
+    .q-btn {
+      color: #6366f1 !important;
+    }
+    .q-btn__content {
+      color: #6366f1 !important;
+    }
+  }
+  :deep(.q-date__event) {
+    bottom: 4px;
+    height: 6px;
+    width: 6px;
+    border-radius: 50%;
+  }
+  :deep(.q-date__today .q-btn__content) {
+    color: #7c4dff !important;
+    background: #7c4dff15;
+    border-radius: 50%;
+  }
+  :deep(.q-date__selected .q-btn__content) {
+    background: #6366f1 !important;
+    color: white !important;
+    border-radius: 50%;
+    box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
+  }
+}
+
+.calendar-custom {
+  border-radius: 20px;
+  background-color: white !important;
+
+  :deep(.q-date__main) {
+    background-color: white !important;
+  }
+
+  :deep(.q-date__content) {
+    background-color: white !important;
+  }
+
+  :deep(.q-date__calendar) {
+    background-color: white !important;
+  }
+
+  :deep(.q-date__calendar-item--out) {
+    .q-btn__content {
+      color: #cbd5e1 !important;
+    }
+  }
+
+  :deep(.q-date__calendar-days .q-btn__content) {
+    font-weight: 500;
+    color: #1e293b;
+  }
+
+  :deep(.q-date__calendar-weekdays > div) {
+    color: #6366f1;
+    font-weight: 700;
+    opacity: 0.8;
+  }
+
+  :deep(.q-date__navigation) {
+    .q-btn {
+      color: #1e293b !important;
+    }
+    .q-btn__content {
+      color: #1e293b !important;
+    }
+  }
+
+  :deep(.q-date__nav-btn-month),
+  :deep(.q-date__nav-btn-year) {
+    color: #6366f1 !important;
+    font-weight: 700;
+  }
+
+  :deep(.q-date__event) {
+    bottom: 4px;
+    height: 6px;
+    width: 6px;
+    border-radius: 50%;
+  }
+
+  :deep(.q-date__today) {
+    .q-btn__content {
+      color: #7c4dff !important;
+      background: #7c4dff15;
+      border-radius: 50%;
+    }
+  }
+
+  :deep(.q-date__selected) {
+    .q-btn__content {
+      background: #6366f1 !important;
+      color: white !important;
+      border-radius: 50%;
+      box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
+    }
+  }
+
+  :deep(.q-date__view--months),
+  :deep(.q-date__view--years) {
+    .q-btn {
+      color: #1e293b !important;
+    }
+  }
+}
+</style>

+ 101 - 69
src/pages/search/components/ServiceSelectionSheet.vue

@@ -21,8 +21,11 @@
       <q-separator class="q-mt-sm" />
 
       <q-card-section class="q-pt-sm q-pb-md">
-        <div v-if="availableServiceTypes.length === 0" class="text-center text-grey-6 text-body2 q-py-md">
-          {{ $t('scheduling_page.no_slots_available') }}
+        <div
+          v-if="availableServiceTypes.length === 0"
+          class="text-center text-grey-6 text-body2 q-py-md"
+        >
+          {{ $t("scheduling_page.no_slots_available") }}
         </div>
 
         <div
@@ -51,12 +54,15 @@
             </div>
           </div>
 
-          <!-- <div class="font16 fontbold text-text q-mx-md" style="white-space: nowrap;">
-            {{ type.price != null ? formatPrice(type.price) : $t('scheduling_page.no_price') }}
-          </div> -->
+           <div
+              v-if="type.price"
+               class="service-price"
+            >
+              {{ formatCurrency(type.price) }}
+            </div>
 
           <div class="row no-wrap q-gutter-x-xs">
-             <q-btn
+            <q-btn
               color="secondary"
               no-caps
               padding="2px 16px"
@@ -64,12 +70,7 @@
               size="sm"
               unelevated
               :label="$t('common.actions.select')"
-              @click="onDialogOK({
-                action: 'cart',
-                serviceType: type,
-                date: selectedDate,
-                provider
-              })"
+              @click="onDialogOK({ action: 'servicePackage', serviceType: type, date: selectedDate, provider })"
             />
           </div>
         </div>
@@ -79,89 +80,120 @@
 </template>
 
 <script setup>
-import { computed } from 'vue';
-import { useDialogPluginComponent, useQuasar } from 'quasar';
-import { useI18n } from 'vue-i18n';
-import ServiceTypeInfoDialog from './ServiceTypeInfoDialog.vue';
+import { computed } from "vue";
+import { useDialogPluginComponent, useQuasar } from "quasar";
+import { formatCurrency } from "src/helpers/utils";
+import { useI18n } from "vue-i18n";
+
+import ServiceTypeInfoDialog from "./ServiceTypeInfoDialog.vue";
+
+defineEmits([...useDialogPluginComponent.emits]);
 
 const props = defineProps({
-  partialBlocks: { type: Array,  required: false, default: () => [] },
-  provider:      { type: Object, required: true },
-  selectedDate:  { type: String, required: true },
+  partialBlocks: { default: () => [], required: false, type: Array },
+  provider: { required: true, type: Object },
+  selectedDate: { required: true, type: String },
 });
 
-defineEmits([...useDialogPluginComponent.emits]);
+const $q = useQuasar();
 
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { dialogRef, onDialogCancel, onDialogHide, onDialogOK } =
+  useDialogPluginComponent();
 
-const $q    = useQuasar();
 const { t } = useI18n();
 
+const slotConflicts = (slotStart, slotEnd, blocks) =>
+  blocks.some((b) => {
+    const blockEnd = parseInt(b.end_hour);
+    const blockStart = parseInt(b.init_hour);
+
+    return slotEnd >= blockStart && slotStart <= blockEnd;
+  });
+
+const hasValidSlots = (hoursCount) => {
+  for (let s = 7; s + hoursCount <= 20; s++) {
+    if (!slotConflicts(s, s + hoursCount, props.partialBlocks)) return true;
+  }
+  return false;
+};
+
 const availableServiceTypes = computed(() =>
   [
     {
-      description: t('scheduling_page.service_types.integral.description'),
-      hours:       t('scheduling_page.service_types.integral.hours'),
-      hoursCount:  8,
-      key:         'integral',
-      label:       t('scheduling_page.service_types.integral.label'),
-      basePrice:   props.provider?.daily_price_8h_base ?? props.provider?.daily_price_8h ?? null,
-      price:       props.provider?.daily_price_8h ?? null,
+      description: t("scheduling_page.service_types.integral.description"),
+      hours: t("scheduling_page.service_types.integral.hours"),
+      hoursCount: 8,
+      key: "integral",
+      label: t("scheduling_page.service_types.integral.label"),
+      basePrice:
+        props.provider?.daily_price_8h_base ??
+        props.provider?.daily_price_8h ??
+        null,
+      price: props.provider?.daily_price_8h ?? null,
     },
     {
-      description: t('scheduling_page.service_types.padrao.description'),
-      hours:       t('scheduling_page.service_types.padrao.hours'),
-      hoursCount:  6,
-      key:         'padrao',
-      label:       t('scheduling_page.service_types.padrao.label'),
-      basePrice:   props.provider?.daily_price_6h_base ?? props.provider?.daily_price_6h ?? null,
-      price:       props.provider?.daily_price_6h ?? null,
+      description: t("scheduling_page.service_types.padrao.description"),
+      hours: t("scheduling_page.service_types.padrao.hours"),
+      hoursCount: 6,
+      key: "padrao",
+      label: t("scheduling_page.service_types.padrao.label"),
+      basePrice:
+        props.provider?.daily_price_6h_base ??
+        props.provider?.daily_price_6h ??
+        null,
+      price: props.provider?.daily_price_6h ?? null,
     },
     {
-      description: t('scheduling_page.service_types.meio_periodo.description'),
-      hours:       t('scheduling_page.service_types.meio_periodo.hours'),
-      hoursCount:  4,
-      key:         'meio_periodo',
-      label:       t('scheduling_page.service_types.meio_periodo.label'),
-      basePrice:   props.provider?.daily_price_4h_base ?? props.provider?.daily_price_4h ?? null,
-      price:       props.provider?.daily_price_4h ?? null,
+      description: t("scheduling_page.service_types.meio_periodo.description"),
+      hours: t("scheduling_page.service_types.meio_periodo.hours"),
+      hoursCount: 4,
+      key: "meio_periodo",
+      label: t("scheduling_page.service_types.meio_periodo.label"),
+      basePrice:
+        props.provider?.daily_price_4h_base ??
+        props.provider?.daily_price_4h ??
+        null,
+      price: props.provider?.daily_price_4h ?? null,
     },
     {
-      description: t('scheduling_page.service_types.diaria_rapida.description'),
-      hours:       t('scheduling_page.service_types.diaria_rapida.hours'),
-      hoursCount:  2,
-      key:         'diaria_rapida',
-      label:       t('scheduling_page.service_types.diaria_rapida.label'),
-      basePrice:   props.provider?.daily_price_2h_base ?? props.provider?.daily_price_2h ?? null,
-      price:       props.provider?.daily_price_2h ?? null,
+      description: t("scheduling_page.service_types.diaria_rapida.description"),
+      hours: t("scheduling_page.service_types.diaria_rapida.hours"),
+      hoursCount: 2,
+      key: "diaria_rapida",
+      label: t("scheduling_page.service_types.diaria_rapida.label"),
+      basePrice:
+        props.provider?.daily_price_2h_base ??
+        props.provider?.daily_price_2h ??
+        null,
+      price: props.provider?.daily_price_2h ?? null,
     },
-  ].filter(type => hasValidSlots(type.hoursCount))
+  ].filter((type) => hasValidSlots(type.hoursCount)),
 );
 
-// const formatPrice = (value) =>Number(value).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
-
-const hasValidSlots = (hoursCount) => {
-  for (let s = 7; s + hoursCount <= 20; s++) {
-    if (!slotConflicts(s, s + hoursCount, props.partialBlocks)) return true;
-  }
-  return false;
-};
-
 const openInfo = (type) => {
-  $q.dialog({ component: ServiceTypeInfoDialog, componentProps: { serviceType: type } });
-};
-
-const slotConflicts = (slotStart, slotEnd, blocks) =>
-  blocks.some(b => {
-    const blockEnd   = parseInt(b.end_hour);
-    const blockStart = parseInt(b.init_hour);
-
-    return slotEnd >= blockStart && slotStart <= blockEnd;
+  $q.dialog({
+    component: ServiceTypeInfoDialog,
+    componentProps: { serviceType: type },
   });
+};
 </script>
 
 <style scoped lang="scss">
 .sheet-card {
   border-radius: 20px 20px 0 0;
 }
+
+.service-price {
+  min-width: 88px;
+  margin: 0 20px;
+  text-align: right;
+
+  font-size: 18px;
+  font-weight: 700;
+  color: var(--q-text);
+}
+
+.service-btn {
+  min-width: 82px;
+}
 </style>

+ 126 - 64
src/pages/search/components/ServiceTimeSelectionDialog.vue

@@ -1,22 +1,33 @@
 <template>
-  <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="card-border bg-surface text-text time-card">
-
+  <q-dialog
+    ref="dialogRef"
+    @hide="onDialogHide"
+  >
+    <q-card
+      class="card-border bg-surface text-text time-card relative-position"
+    >
       <q-btn
-        flat round dense
-        icon="mdi-close-circle-outline"
+        class="absolute-top-right close-button q-mt-sm q-mr-sm"
         color="grey-5"
-        class="absolute-top-right q-mt-sm q-mr-sm"
-        @click="onDialogCancel"
+        dense
+        flat
+        icon="mdi-close-circle-outline"
+        round
+        type="button"
+        @click.stop="handleClose"
       />
 
       <q-card-section class="text-center q-pt-lg q-pb-xs">
         <div class="font16 fontbold text-text">
           {{ serviceType.label }}
-          <span class="text-grey-6">{{ '(' + serviceType.hoursCount + 'h)' }}</span>
+
+          <span class="text-grey-6">
+            {{ "(" + serviceType.hoursCount + "h)" }}
+          </span>
         </div>
+
         <div class="font14 fontmedium text-grey-6 q-mt-xs">
-          {{ $t('scheduling_page.time_selection.subtitle') }}
+          {{ $t("scheduling_page.time_selection.subtitle") }}
         </div>
       </q-card-section>
 
@@ -29,12 +40,12 @@
           >
             <q-radio
               v-model="selectedSlot"
-              :val="slot.value"
-              :label="slot.label"
-              color="primary"
               class="font12 fontbold text-primary"
-              keep-color
+              color="primary"
               dense
+              keep-color
+              :label="slot.label"
+              :val="slot.value"
             />
           </div>
         </div>
@@ -42,28 +53,31 @@
 
       <template v-if="hasMealSection">
         <q-separator class="q-mx-md" />
+
         <q-card-section class="q-py-sm">
           <div class="font14 fontbold text-text q-mb-sm text-center">
-            {{ $t('scheduling_page.time_selection.meal_section') }}
+            {{ $t("scheduling_page.time_selection.meal_section") }}
           </div>
-          <div class="column">
+
+          <div class="meal-options column items-start">
             <q-radio
               v-model="selectedMeal"
-              val="offer"
-              :label="$t('scheduling_page.time_selection.meal_offer')"
-              class="font12 fontbold text-primary q-mx-auto q-pb-sm"
+              class="font12 fontbold text-primary q-pb-sm"
               color="primary"
-              keep-color
               dense
+              keep-color
+              val="offer"
+              :label="$t('scheduling_page.time_selection.meal_offer')"
             />
+
             <q-radio
               v-model="selectedMeal"
-              val="no_offer"
-              :label="$t('scheduling_page.time_selection.meal_no_offer')"
-              class="font12 fontbold text-primary q-mx-auto q-pt-sm"
+              class="font12 fontbold text-primary q-pt-sm"
               color="primary"
-              keep-color
               dense
+              keep-color
+              val="no_offer"
+              :label="$t('scheduling_page.time_selection.meal_no_offer')"
             />
           </div>
         </q-card-section>
@@ -71,91 +85,139 @@
 
       <q-card-actions class="q-px-md q-pb-xs q-pt-sm">
         <q-btn
-          unelevated
-          rounded
-          no-caps
-          :label="$t('scheduling_page.time_selection.continue')"
-          :disable="!selectedSlot"
+          class="full-width"
           color="secondary"
+          no-caps
           padding="4px 8px"
-          class="full-width"
+          rounded
+          unelevated
+          :disable="!selectedSlot"
+          :label="$t('scheduling_page.time_selection.continue')"
           @click="handleContinue"
         />
       </q-card-actions>
 
-      <div v-if="pauseNote" class="text-center font12 fontmedium text-primary q-pb-md">
+      <div
+        v-if="pauseNote"
+        class="text-center font12 fontmedium text-primary q-pb-md"
+      >
         {{ pauseNote }}
       </div>
-
     </q-card>
   </q-dialog>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue';
-import { useDialogPluginComponent } from 'quasar';
-import { useI18n } from 'vue-i18n';
+import { computed, ref } from "vue";
+import { useDialogPluginComponent } from "quasar";
+import { useI18n } from "vue-i18n";
+
+defineEmits([...useDialogPluginComponent.emits]);
 
 const props = defineProps({
-  serviceType:   { type: Object, required: true },
-  provider:      { type: Object, required: true },
-  selectedDate:  { type: String, required: true },
-  partialBlocks: { type: Array,  required: false, default: () => [] },
+  partialBlocks: { default: () => [], required: false, type: Array },
+  provider: { required: true, type: Object },
+  selectedDate: { required: true, type: String },
+  serviceType: { required: true, type: Object },
 });
 
-defineEmits([...useDialogPluginComponent.emits]);
-
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 const { t } = useI18n();
 
-const selectedSlot = ref(null);
 const selectedMeal = ref(null);
-
-const handleContinue = () => {
-  const slotObj = timeSlots.value.find(s => s.value === selectedSlot.value);
-  onDialogOK({
-    serviceType: props.serviceType,
-    date: props.selectedDate,
-    slot: { value: selectedSlot.value, startHour: slotObj.startHour, endHour: slotObj.endHour },
-    meal: selectedMeal.value,
-  });
-};
+const selectedSlot = ref(null);
 
 const slotConflicts = (slotStart, slotEnd, blocks) =>
-  blocks.some(b => {
-    const blockStart = parseInt(b.init_hour);
-    const blockEnd   = parseInt(b.end_hour);
+  blocks.some((block) => {
+    const blockEnd = parseInt(block.end_hour);
+    const blockStart = parseInt(block.init_hour);
+
     return slotEnd >= blockStart && slotStart <= blockEnd;
   });
 
+const hasMealSection = computed(() => props.serviceType.hoursCount >= 6);
+
+const pauseNote = computed(() => {
+  const map = {
+    8: t("scheduling_page.time_selection.pause_note_8h"),
+    6: t("scheduling_page.time_selection.pause_note_6h"),
+    4: t("scheduling_page.time_selection.pause_note_4h"),
+  };
+
+  return map[props.serviceType.hoursCount] ?? null;
+});
+
 const timeSlots = computed(() => {
   const h = props.serviceType.hoursCount;
+
   const slots = [];
+
+  const now = new Date();
+
+  const today =
+    `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
+
+  const isToday =
+    props.selectedDate.replace(/\//g, "-") === today;
+
+  const minimumHour = now.getHours() + 2;
+
   for (let start = 7; start + h <= 20; start++) {
     const end = start + h;
-    if (slotConflicts(start, end, props.partialBlocks)) continue;
+
+    // Se a data for hoje, bloqueia horários com menos de 2 horas de antecedência
+    if (isToday && start < minimumHour) {
+      continue;
+    }
+
+    if (slotConflicts(start, end, props.partialBlocks)) {
+      continue;
+    }
+
     slots.push({
-      value: `${start}-${end}`,
       startHour: start,
       endHour: end,
       label: `${start}h às ${end}h`,
+      value: `${start}-${end}`,
     });
   }
+
   return slots;
 });
 
-const hasMealSection = computed(() =>
-  props.serviceType.hoursCount >= 6
-);
+const handleClose = () => {
+  dialogRef.value?.hide();
+};
 
-const pauseNote = computed(() => {
-  const map = { 8: t('scheduling_page.time_selection.pause_note_8h'), 6: t('scheduling_page.time_selection.pause_note_6h'), 4: t('scheduling_page.time_selection.pause_note_4h') };
-  return map[props.serviceType.hoursCount] ?? null;
-});
+const handleContinue = () => {
+  const slotObj = timeSlots.value.find(
+    (slot) => slot.value === selectedSlot.value,
+  );
+
+  onDialogOK({
+    date: props.selectedDate,
+    meal: selectedMeal.value,
+    serviceType: props.serviceType,
+    slot: {
+      endHour: slotObj.endHour,
+      startHour: slotObj.startHour,
+      value: selectedSlot.value,
+    },
+  });
+};
 </script>
 
 <style scoped lang="scss">
+.close-button {
+  z-index: 2;
+}
+
 .time-card {
   width: min(88vw, 360px);
 }
+
+.meal-options {
+  margin-inline: auto;
+  width: fit-content;
+}
 </style>

+ 23 - 10
src/pages/search/components/ServiceTypeInfoDialog.vue

@@ -1,11 +1,25 @@
 <template>
-  <q-dialog ref="dialogRef" @hide="onDialogHide">
-    <q-card class="card-border bg-surface text-text" style="width: min(88vw, 360px);">
-
+  <q-dialog
+    ref="dialogRef"
+    @hide="onDialogHide"
+  >
+    <q-card
+      class="card-border bg-surface text-text"
+      style="width: min(88vw, 360px)"
+    >
       <q-card-section class="row items-center q-pb-none">
         <div class="font16 fontbold text-text">{{ serviceType.label }}</div>
+
         <q-space />
-        <q-btn flat round dense icon="mdi-close" color="grey-6" @click="onDialogCancel" />
+
+        <q-btn
+          color="grey-6"
+          dense
+          flat
+          icon="mdi-close"
+          round
+          @click="onDialogCancel"
+        />
       </q-card-section>
 
       <q-card-section class="font14 fontmedium text-grey-7 q-pt-xs">
@@ -17,22 +31,21 @@
       <q-card-section class="font12 fontmedium text-text">
         {{ serviceType.description }}
       </q-card-section>
-
     </q-card>
   </q-dialog>
 </template>
 
 <script setup>
-import { useDialogPluginComponent } from 'quasar';
+import { useDialogPluginComponent } from "quasar";
+
+defineEmits([...useDialogPluginComponent.emits]);
 
 defineProps({
   serviceType: {
-    type: Object,
     required: true,
+    type: Object,
   },
 });
 
-defineEmits([...useDialogPluginComponent.emits]);
-
-const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
+const { dialogRef, onDialogCancel, onDialogHide } = useDialogPluginComponent();
 </script>

+ 4 - 4
src/router/routes/schedule-cart.route.js → src/router/routes/servicePackage.route.js

@@ -1,10 +1,10 @@
 export default [
   {
-    path: "carrinho",
+    path: "pedidos",
     name: "ScheduleCartPage",
-    component: () => import("src/pages/schedule-cart/ScheduleCartPage.vue"),
+    component: () => import("src/pages/orders/ServicePackagePage.vue"),
     meta: {
-      title: "schedule_cart.title",
+      title: "service_package.title",
       requireAuth: true,
       breadcrumbs: [
         {
@@ -13,7 +13,7 @@ export default [
         },
         {
           name: "ScheduleCartPage",
-          title: "schedule_cart.title",
+          title: "service_package.title",
         },
       ],
     },

+ 18 - 18
src/stores/payment.js

@@ -10,40 +10,40 @@ const isFutureDate = (date) => {
 };
 
 export const usePaymentStore = defineStore('payment', () => {
-  const pixPaymentsByScheduleId = ref({});
+  const pixPaymentsByServicePackageId = ref({});
 
-  const getValidPixPayment = (scheduleId) => {
-    const payment = pixPaymentsByScheduleId.value[scheduleId] ?? null;
+  const getValidPixPaymentForServicePackage = (servicePackageId) => {
+    const payment = pixPaymentsByServicePackageId.value[servicePackageId] ?? null;
     if (!payment) return null;
 
     if (!isFutureDate(getPixExpiresAt(payment))) {
-      clearPixPayment(scheduleId);
+      clearPixPaymentForServicePackage(servicePackageId);
       return null;
     }
 
     return payment;
   };
 
-  const setPixPayment = (scheduleId, payment) => {
-    if (!scheduleId || !payment) return;
-    pixPaymentsByScheduleId.value = {
-      ...pixPaymentsByScheduleId.value,
-      [scheduleId]: payment,
+  const setPixPaymentForServicePackage = (servicePackageId, payment) => {
+    if (!servicePackageId || !payment) return;
+    pixPaymentsByServicePackageId.value = {
+      ...pixPaymentsByServicePackageId.value,
+      [servicePackageId]: payment,
     };
   };
 
-  const clearPixPayment = (scheduleId) => {
-    if (!scheduleId || !pixPaymentsByScheduleId.value[scheduleId]) return;
+  const clearPixPaymentForServicePackage = (servicePackageId) => {
+    if (!servicePackageId || !pixPaymentsByServicePackageId.value[servicePackageId]) return;
 
-    const payments = { ...pixPaymentsByScheduleId.value };
-    delete payments[scheduleId];
-    pixPaymentsByScheduleId.value = payments;
+    const payments = { ...pixPaymentsByServicePackageId.value };
+    delete payments[servicePackageId];
+    pixPaymentsByServicePackageId.value = payments;
   };
 
   return {
-    pixPaymentsByScheduleId,
-    getValidPixPayment,
-    setPixPayment,
-    clearPixPayment,
+    pixPaymentsByServicePackageId,
+    getValidPixPaymentForServicePackage,
+    setPixPaymentForServicePackage,
+    clearPixPaymentForServicePackage,
   };
 });

+ 57 - 34
src/stores/registrationFlow.js

@@ -1,22 +1,24 @@
-import { defineStore } from 'pinia';
-import { ref } from 'vue';
+import { defineStore } from "pinia";
+import { ref } from "vue";
 
-export const useRegistrationFlowStore = defineStore('registrationFlow', () => {
-  const email = ref('');
-  const phone = ref('');
-  const code = ref('');
+export const useRegistrationFlowStore = defineStore("registrationFlow", () => {
+  const email = ref("");
+  const phone = ref("");
+  const code = ref("");
+  const document = ref("");
+  const name = ref("");
 
   const initialLat = ref(null);
   const initialLng = ref(null);
 
   const confirmedLat = ref(null);
   const confirmedLng = ref(null);
-  const confirmedAddress = ref('');
-  const confirmedNumber = ref('');
-  const confirmedDistrict = ref('');
-  const confirmedCity = ref('');
-  const confirmedState = ref('');
-  const confirmedZipCode = ref('');
+  const confirmedAddress = ref("");
+  const confirmedNumber = ref("");
+  const confirmedDistrict = ref("");
+  const confirmedCity = ref("");
+  const confirmedState = ref("");
+  const confirmedZipCode = ref("");
 
   const setCredentials = (e, p, c) => {
     email.value = e;
@@ -29,45 +31,66 @@ export const useRegistrationFlowStore = defineStore('registrationFlow', () => {
     initialLng.value = lng;
   };
 
+  const setRegistrationData = (data) => {
+    document.value = data.document ?? "";
+    name.value = data.name ?? "";
+    phone.value = data.phone ?? phone.value;
+  };
+
   const setConfirmedLocation = (data) => {
     confirmedLat.value = data.lat;
     confirmedLng.value = data.lng;
-    confirmedAddress.value = data.address ?? '';
-    confirmedNumber.value = data.number ?? '';
-    confirmedDistrict.value = data.district ?? '';
-    confirmedCity.value = data.city ?? '';
-    confirmedState.value = data.state ?? '';
-    confirmedZipCode.value = data.zip_code ?? '';
+    confirmedAddress.value = data.address ?? "";
+    confirmedNumber.value = data.number ?? "";
+    confirmedDistrict.value = data.district ?? "";
+    confirmedCity.value = data.city ?? "";
+    confirmedState.value = data.state ?? "";
+    confirmedZipCode.value = data.zip_code ?? "";
   };
 
   const clear = () => {
-    email.value = '';
-    phone.value = '';
-    code.value = '';
+    email.value = "";
+    phone.value = "";
+    code.value = "";
+    document.value = "";
+    name.value = "";
     initialLat.value = null;
     initialLng.value = null;
     confirmedLat.value = null;
     confirmedLng.value = null;
-    confirmedAddress.value = '';
-    confirmedNumber.value = '';
-    confirmedDistrict.value = '';
-    confirmedCity.value = '';
-    confirmedState.value = '';
-    confirmedZipCode.value = '';
+    confirmedAddress.value = "";
+    confirmedNumber.value = "";
+    confirmedDistrict.value = "";
+    confirmedCity.value = "";
+    confirmedState.value = "";
+    confirmedZipCode.value = "";
   };
 
   const hasCredentials = () => !!(email.value || phone.value) && !!code.value;
-  const hasInitialLocation = () => initialLat.value !== null && initialLng.value !== null;
-  const hasConfirmedLocation = () => confirmedLat.value !== null && confirmedLng.value !== null;
+  const hasInitialLocation = () =>
+    initialLat.value !== null && initialLng.value !== null;
+  const hasConfirmedLocation = () =>
+    confirmedLat.value !== null && confirmedLng.value !== null;
 
   return {
-    email, phone, code,
-    initialLat, initialLng,
-    confirmedLat, confirmedLng,
-    confirmedAddress, confirmedNumber, confirmedDistrict,
-    confirmedCity, confirmedState, confirmedZipCode,
+    email,
+    phone,
+    code,
+    document,
+    name,
+    initialLat,
+    initialLng,
+    confirmedLat,
+    confirmedLng,
+    confirmedAddress,
+    confirmedNumber,
+    confirmedDistrict,
+    confirmedCity,
+    confirmedState,
+    confirmedZipCode,
     setCredentials,
     setInitialLocation,
+    setRegistrationData,
     setConfirmedLocation,
     clear,
     hasCredentials,

+ 1 - 1
src/stores/scheduleCart.js → src/stores/servicePackage.js

@@ -1,7 +1,7 @@
 import { defineStore } from "pinia";
 import { ref } from "vue";
 
-export const useScheduleCartStore = defineStore("scheduleCart", () => {
+export const useServicePackageStore = defineStore("servicePackage", () => {
   const items = ref([]);
 
   const lastCreatedGroup = ref(null);