Pārlūkot izejas kodu

Merge branch 'development' of gogs.softpar.inf.br:Softpar/sfp_front_vue_diarista_cliente into development

Gustavo Zanatta 1 mēnesi atpakaļ
vecāks
revīzija
45ddc312e8
36 mainītis faili ar 1854 papildinājumiem un 296 dzēšanām
  1. 3 1
      .gitignore
  2. 749 4
      package-lock.json
  3. 1 0
      package.json
  4. 2 2
      src-capacitor/android/app/build.gradle
  5. 1 1
      src-capacitor/android/app/src/main/AndroidManifest.xml
  6. 1 1
      src-capacitor/android/app/src/main/java/inf/br/softpar/skeleton/MainActivity.java
  7. 2 2
      src-capacitor/android/app/src/main/res/values/strings.xml
  8. 1 1
      src-capacitor/capacitor.config.json
  9. 2 2
      src-capacitor/ios/App/App.xcodeproj/project.pbxproj
  10. 3 3
      src-capacitor/package.json
  11. 6 0
      src/api/clientCalendar.js
  12. 5 0
      src/api/user.js
  13. 5 5
      src/components/charts/CardIconChart.vue
  14. 5 5
      src/components/charts/CardIconMiniChart.vue
  15. 54 0
      src/components/dashboard/DashboardPaymentIncomplete.vue
  16. 58 0
      src/components/dashboard/DashboardRegistrationIncomplete.vue
  17. 5 5
      src/components/defaults/DefaultFilePicker.vue
  18. 9 2
      src/components/login/LoginStepFourPanel.vue
  19. 94 14
      src/components/login/LoginStepThreePanel.vue
  20. 29 18
      src/composables/useGeocodingApi.js
  21. 3 0
      src/composables/useGeolocation.js
  22. 26 1
      src/css/quasar.variables.scss
  23. 5 5
      src/css/table.scss
  24. 31 2
      src/i18n/locales/en.json
  25. 31 2
      src/i18n/locales/es.json
  26. 31 2
      src/i18n/locales/pt.json
  27. 1 1
      src/layouts/MainLayout.vue
  28. 2 0
      src/pages/LoginPage.vue
  29. 0 52
      src/pages/agenda/AgendaPage.vue
  30. 392 0
      src/pages/agenda/CalendarPage.vue
  31. 16 7
      src/pages/dashboard/DashboardPage.vue
  32. 116 57
      src/pages/location/AddressCompletionPage.vue
  33. 36 3
      src/pages/location/LocationMapPage.vue
  34. 123 95
      src/pages/profile/ProfileEditDialog.vue
  35. 3 0
      src/pages/profile/ProfilePage.vue
  36. 3 3
      src/router/routes/navbar.route.js

+ 3 - 1
.gitignore

@@ -24,10 +24,12 @@ Thumbs.db
 /src-cordova/platforms
 /src-cordova/plugins
 /src-cordova/www
-/src-cordova/node_modules
 /src-capacitor/node_modules
+/src-capacitor/platforms
+/src-capacitor/plugins
 /src-capacitor/www
 
+
 /android
 /ios
 /capacitor.config.json

+ 749 - 4
package-lock.json

@@ -31,6 +31,7 @@
       "devDependencies": {
         "@bufbuild/buf": "^1.54.0",
         "@bufbuild/protoc-gen-es": "^2.5.1",
+        "@capacitor/cli": "^8.3.3",
         "@capacitor/keyboard": "^7.0.1",
         "@capacitor/preferences": "^7.0.1",
         "@capacitor/status-bar": "^7.0.1",
@@ -312,6 +313,106 @@
         "typescript": "5.4.5"
       }
     },
+    "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==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/cli-framework-output": "^2.2.8",
+        "@ionic/utils-subprocess": "^3.0.1",
+        "@ionic/utils-terminal": "^2.3.5",
+        "commander": "^12.1.0",
+        "debug": "^4.4.0",
+        "env-paths": "^2.2.0",
+        "fs-extra": "^11.2.0",
+        "kleur": "^4.1.5",
+        "native-run": "^2.0.3",
+        "open": "^8.4.0",
+        "plist": "^3.1.0",
+        "prompts": "^2.4.2",
+        "rimraf": "^6.0.1",
+        "semver": "^7.6.3",
+        "tar": "^7.5.3",
+        "tslib": "^2.8.1",
+        "xml2js": "^0.6.2"
+      },
+      "bin": {
+        "cap": "bin/capacitor",
+        "capacitor": "bin/capacitor"
+      },
+      "engines": {
+        "node": ">=22.0.0"
+      }
+    },
+    "node_modules/@capacitor/cli/node_modules/commander": {
+      "version": "12.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+      "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@capacitor/cli/node_modules/define-lazy-prop": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+      "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@capacitor/cli/node_modules/is-docker": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+      "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "is-docker": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@capacitor/cli/node_modules/is-wsl": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+      "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@capacitor/cli/node_modules/open": {
+      "version": "8.4.2",
+      "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+      "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-lazy-prop": "^2.0.0",
+        "is-docker": "^2.1.1",
+        "is-wsl": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/@capacitor/core": {
       "version": "8.3.1",
       "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.1.tgz",
@@ -1406,6 +1507,186 @@
         "vue": "^3.0.0"
       }
     },
+    "node_modules/@ionic/cli-framework-output": {
+      "version": "2.2.8",
+      "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz",
+      "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/utils-terminal": "2.3.5",
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-array": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz",
+      "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-fs": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz",
+      "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/fs-extra": "^8.0.0",
+        "debug": "^4.0.0",
+        "fs-extra": "^9.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-fs/node_modules/fs-extra": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "at-least-node": "^1.0.0",
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@ionic/utils-object": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz",
+      "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-process": {
+      "version": "2.1.12",
+      "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz",
+      "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/utils-object": "2.1.6",
+        "@ionic/utils-terminal": "2.3.5",
+        "debug": "^4.0.0",
+        "signal-exit": "^3.0.3",
+        "tree-kill": "^1.2.2",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-process/node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/@ionic/utils-stream": {
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz",
+      "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-subprocess": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz",
+      "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/utils-array": "2.1.6",
+        "@ionic/utils-fs": "3.1.7",
+        "@ionic/utils-process": "2.1.12",
+        "@ionic/utils-stream": "3.1.7",
+        "@ionic/utils-terminal": "2.3.5",
+        "cross-spawn": "^7.0.3",
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-terminal": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz",
+      "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/slice-ansi": "^4.0.0",
+        "debug": "^4.0.0",
+        "signal-exit": "^3.0.3",
+        "slice-ansi": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0",
+        "tslib": "^2.0.1",
+        "untildify": "^4.0.0",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-terminal/node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/@ionic/utils-terminal/node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
     "node_modules/@isaacs/cliui": {
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -1509,6 +1790,19 @@
         "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
       }
     },
+    "node_modules/@isaacs/fs-minipass": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+      "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.0.4"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
     "node_modules/@jridgewell/gen-mapping": {
       "version": "0.3.12",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz",
@@ -2454,6 +2748,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/fs-extra": {
+      "version": "8.1.5",
+      "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz",
+      "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@types/geojson": {
       "version": "7946.0.16",
       "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
@@ -2564,6 +2868,13 @@
         "@types/send": "*"
       }
     },
+    "node_modules/@types/slice-ansi": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz",
+      "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/supercluster": {
       "version": "7.1.3",
       "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
@@ -2879,6 +3190,16 @@
       "integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==",
       "license": "MIT"
     },
+    "node_modules/@xmldom/xmldom": {
+      "version": "0.9.10",
+      "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
+      "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.6"
+      }
+    },
     "node_modules/abort-controller": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -3095,6 +3416,16 @@
       "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
       "license": "MIT"
     },
+    "node_modules/astral-regex": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+      "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/async": {
       "version": "3.2.6",
       "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -3108,6 +3439,16 @@
       "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
       "license": "MIT"
     },
+    "node_modules/at-least-node": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+      "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
     "node_modules/autoprefixer": {
       "version": "10.4.21",
       "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
@@ -3750,6 +4091,16 @@
         "fsevents": "~2.3.2"
       }
     },
+    "node_modules/chownr": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+      "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/ci-info": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz",
@@ -4557,6 +4908,16 @@
         "url": "https://github.com/fb55/entities?sponsor=1"
       }
     },
+    "node_modules/env-paths": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+      "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/es-define-property": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -5312,6 +5673,16 @@
         "reusify": "^1.0.4"
       }
     },
+    "node_modules/fd-slicer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+      "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pend": "~1.2.0"
+      }
+    },
     "node_modules/fdir": {
       "version": "6.4.6",
       "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
@@ -6483,6 +6854,16 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/kleur": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+      "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/kolorist": {
       "version": "1.8.0",
       "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
@@ -6815,15 +7196,28 @@
       }
     },
     "node_modules/minipass": {
-      "version": "7.1.2",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
-      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+      "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
       "dev": true,
-      "license": "ISC",
+      "license": "BlueOak-1.0.0",
       "engines": {
         "node": ">=16 || 14 >=14.17"
       }
     },
+    "node_modules/minizlib": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+      "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
     "node_modules/mitt": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
@@ -6884,6 +7278,55 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
+    "node_modules/native-run": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz",
+      "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/utils-fs": "^3.1.7",
+        "@ionic/utils-terminal": "^2.3.4",
+        "bplist-parser": "^0.3.2",
+        "debug": "^4.3.4",
+        "elementtree": "^0.1.7",
+        "ini": "^4.1.1",
+        "plist": "^3.1.0",
+        "split2": "^4.2.0",
+        "through2": "^4.0.2",
+        "tslib": "^2.6.2",
+        "yauzl": "^2.10.0"
+      },
+      "bin": {
+        "native-run": "bin/native-run"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/native-run/node_modules/bplist-parser": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz",
+      "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "big-integer": "1.6.x"
+      },
+      "engines": {
+        "node": ">= 5.10.0"
+      }
+    },
+    "node_modules/native-run/node_modules/ini": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
+      "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
+    },
     "node_modules/natural-compare": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -7315,6 +7758,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/perfect-debounce": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
@@ -7380,6 +7830,21 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/plist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz",
+      "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@xmldom/xmldom": "^0.9.10",
+        "base64-js": "^1.5.1",
+        "xmlbuilder": "^15.1.1"
+      },
+      "engines": {
+        "node": ">=10.4.0"
+      }
+    },
     "node_modules/postcss": {
       "version": "8.5.6",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -7485,6 +7950,30 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/prompts": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+      "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "kleur": "^3.0.3",
+        "sisteransi": "^1.0.5"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/prompts/node_modules/kleur": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+      "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/proto-list": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@@ -7881,6 +8370,110 @@
       "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
       "license": "MIT"
     },
+    "node_modules/rimraf": {
+      "version": "6.1.3",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz",
+      "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "glob": "^13.0.3",
+        "package-json-from-dist": "^1.0.1"
+      },
+      "bin": {
+        "rimraf": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rimraf/node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/rimraf/node_modules/brace-expansion": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+      "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/rimraf/node_modules/glob": {
+      "version": "13.0.6",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+      "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "minimatch": "^10.2.2",
+        "minipass": "^7.1.3",
+        "path-scurry": "^2.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rimraf/node_modules/lru-cache": {
+      "version": "11.3.6",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
+      "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/rimraf/node_modules/minimatch": {
+      "version": "10.2.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+      "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.5"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rimraf/node_modules/path-scurry": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+      "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/rollup": {
       "version": "4.45.1",
       "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
@@ -8711,6 +9304,31 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/sisteransi": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+      "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/slice-ansi": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+      "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "astral-regex": "^2.0.0",
+        "is-fullwidth-code-point": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+      }
+    },
     "node_modules/socket.io-client": {
       "version": "4.8.1",
       "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
@@ -8812,6 +9430,16 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/split2": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+      "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">= 10.x"
+      }
+    },
     "node_modules/stack-trace": {
       "version": "1.0.0-pre2",
       "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz",
@@ -9009,6 +9637,23 @@
         "url": "https://opencollective.com/synckit"
       }
     },
+    "node_modules/tar": {
+      "version": "7.5.15",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz",
+      "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/fs-minipass": "^4.0.0",
+        "chownr": "^3.0.0",
+        "minipass": "^7.1.2",
+        "minizlib": "^3.1.0",
+        "yallist": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/tar-stream": {
       "version": "3.1.7",
       "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
@@ -9021,6 +9666,16 @@
         "streamx": "^2.15.0"
       }
     },
+    "node_modules/tar/node_modules/yallist": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+      "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/terser": {
       "version": "5.43.1",
       "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
@@ -9057,6 +9712,31 @@
         "b4a": "^1.6.4"
       }
     },
+    "node_modules/through2": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
+      "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readable-stream": "3"
+      }
+    },
+    "node_modules/through2/node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/tiny-invariant": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -9127,6 +9807,16 @@
         "node": ">=0.6"
       }
     },
+    "node_modules/tree-kill": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+      "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "tree-kill": "cli.js"
+      }
+    },
     "node_modules/ts-api-utils": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -9987,6 +10677,40 @@
         "node": ">=12"
       }
     },
+    "node_modules/xml2js": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+      "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "sax": ">=0.6.0",
+        "xmlbuilder": "~11.0.0"
+      },
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/xml2js/node_modules/xmlbuilder": {
+      "version": "11.0.1",
+      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+      "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/xmlbuilder": {
+      "version": "15.1.1",
+      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
+      "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
     "node_modules/xmlhttprequest-ssl": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
@@ -10070,6 +10794,27 @@
         "node": ">=12"
       }
     },
+    "node_modules/yauzl": {
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+      "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "buffer-crc32": "~0.2.3",
+        "fd-slicer": "~1.1.0"
+      }
+    },
+    "node_modules/yauzl/node_modules/buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

+ 1 - 0
package.json

@@ -41,6 +41,7 @@
   "devDependencies": {
     "@bufbuild/buf": "^1.54.0",
     "@bufbuild/protoc-gen-es": "^2.5.1",
+    "@capacitor/cli": "^8.3.3",
     "@capacitor/keyboard": "^7.0.1",
     "@capacitor/preferences": "^7.0.1",
     "@capacitor/status-bar": "^7.0.1",

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

@@ -1,10 +1,10 @@
 apply plugin: 'com.android.application'
 
 android {
-    namespace "inf.br.softpar.skeleton"
+    namespace "inf.br.diarista.cliente"
     compileSdk rootProject.ext.compileSdkVersion
     defaultConfig {
-        applicationId "inf.br.softpar.skeleton"
+        applicationId "inf.br.diarista.cliente"
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
         versionCode 1

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

@@ -11,7 +11,7 @@
 
         <meta-data
             android:name="com.google.android.geo.API_KEY"
-            android:value="@string/maps_api_key"/>
+            android:value="AIzaSyDrVs7amXuXKEu8sYMr-dl8VjNZbgS2frk"/>
 
         <activity
             android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"

+ 1 - 1
src-capacitor/android/app/src/main/java/inf/br/softpar/skeleton/MainActivity.java

@@ -1,4 +1,4 @@
-package inf.br.softpar.skeleton;
+package inf.br.diarista.cliente;
 
 import android.os.Bundle;
 import android.webkit.WebView;

+ 2 - 2
src-capacitor/android/app/src/main/res/values/strings.xml

@@ -2,7 +2,7 @@
 <resources>
     <string name="app_name">Quasar App</string>
     <string name="title_activity_main">Quasar App</string>
-    <string name="package_name">inf.br.softpar.skeleton</string>
-    <string name="custom_url_scheme">inf.br.softpar.skeleton</string>
+    <string name="package_name">inf.br.diarista.cliente</string>
+    <string name="custom_url_scheme">inf.br.diarista.cliente</string>
     <string name="maps_api_key">google_maps_api_key</string>
 </resources>

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

@@ -1,5 +1,5 @@
 {
-  "appId": "inf.br.softpar.skeleton",
+  "appId": "inf.br.diarista.cliente",
   "appName": "Quasar App",
   "webDir": "www",
   "plugins": {

+ 2 - 2
src-capacitor/ios/App/App.xcodeproj/project.pbxproj

@@ -354,7 +354,7 @@
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				MARKETING_VERSION = 1.0;
 				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
-				PRODUCT_BUNDLE_IDENTIFIER = inf.br.softpar.skeleton;
+				PRODUCT_BUNDLE_IDENTIFIER = inf.br.diarista.cliente;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
 				SWIFT_VERSION = 5.0;
@@ -373,7 +373,7 @@
 				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				MARKETING_VERSION = 1.0;
-				PRODUCT_BUNDLE_IDENTIFIER = inf.br.softpar.skeleton;
+				PRODUCT_BUNDLE_IDENTIFIER = inf.br.diarista.cliente;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
 				SWIFT_VERSION = 5.0;

+ 3 - 3
src-capacitor/package.json

@@ -1,8 +1,8 @@
 {
-  "name": "quasar-skeleton",
+  "name": "diarista-cliente-app",
   "version": "0.0.1",
-  "description": "A skeleton for future projects",
-  "author": "Denis <denis.gnl@gmail.com>",
+  "description": "Aplicativo para o cliente do Diarista",
+  "author": "zntt <zanattagg@gmail.com>",
   "private": true,
   "dependencies": {
     "@capacitor/android": "^7.2.0",

+ 6 - 0
src/api/clientCalendar.js

@@ -0,0 +1,6 @@
+import api from 'src/api'
+
+export const getClientCalendar = async () => {
+  const { data } = await api.get('/dados-agenda-cliente')
+  return data.payload
+}

+ 5 - 0
src/api/user.js

@@ -44,3 +44,8 @@ export const createUserAndClient = async (data) => {
   const response = await api.post("/register-client", data);
   return response;
 }
+
+export const updateMe = async (data) => {
+  const { data: res } = await api.put('/me', data);
+  return res.payload;
+};

+ 5 - 5
src/components/charts/CardIconChart.vue

@@ -69,9 +69,9 @@ body.body--light {
   }
 }
 
-body.body--dark {
-  .background {
-    background: rgba(map.get($colors-dark, "primary"), 0.2) !important;
-  }
-}
+// body.body--dark {
+//   .background {
+//     background: rgba(map.get($colors-dark, "primary"), 0.2) !important;
+//   }
+// }
 </style>

+ 5 - 5
src/components/charts/CardIconMiniChart.vue

@@ -73,9 +73,9 @@ body.body--light {
   }
 }
 
-body.body--dark {
-  .background {
-    background: rgba(map.get($colors-dark, "primary"), 0.2) !important;
-  }
-}
+// body.body--dark {
+//   .background {
+//     background: rgba(map.get($colors-dark, "primary"), 0.2) !important;
+//   }
+// }
 </style>

+ 54 - 0
src/components/dashboard/DashboardPaymentIncomplete.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="incomplete-banner q-mx-md q-mb-md" @click="openPayments">
+    <div class="row items-center no-wrap q-pa-sm q-px-md">
+      <q-icon name="mdi-alert-outline" size="26px" color="primary" class="q-mr-md" />
+      <div class="col banner-text text-primary">
+        {{ $t('dashboard_client.payment_incomplete_title') }}
+      </div>
+      <q-btn
+        unelevated
+        no-caps
+        color="primary"
+        text-color="white"
+        :label="$t('dashboard_client.payment_incomplete_cta')"
+        class="q-ml-sm resolver-btn"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { useQuasar } from 'quasar';
+import ProfilePaymentsDialog from 'src/components/profile/ProfilePaymentsDialog.vue';
+
+const $q = useQuasar();
+
+const openPayments = () => {
+  $q.dialog({ component: ProfilePaymentsDialog });
+};
+</script>
+
+<style lang="scss" scoped>
+
+@use "src/css/quasar.variables.scss";
+
+.incomplete-banner {
+  border-radius: 12px;
+  background: $card-incomplete-payments;
+  cursor: pointer;
+}
+
+.banner-text {
+  font-size: 13px;
+  font-weight: 500;
+  line-height: 1.3;
+}
+
+.resolver-btn {
+  border-radius: 20px;
+  padding: 0px 4px;
+  font-size: 11px;
+  white-space: nowrap;
+  flex-shrink: 0;
+}
+</style>

+ 58 - 0
src/components/dashboard/DashboardRegistrationIncomplete.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="incomplete-banner q-ma-md" @click="openEdit">
+    <div class="row items-center no-wrap q-pa-sm q-px-md">
+      <q-icon name="mdi-alert-outline" size="26px" color="primary" class="q-mr-md" />
+      <div class="col banner-text text-primary">
+        {{ $t('dashboard_client.registration_incomplete_title') }}
+      </div>
+      <q-btn
+        unelevated
+        no-caps
+        color="primary"
+        text-color="white"
+        :label="$t('dashboard_client.registration_incomplete_cta')"
+        class="q-ml-sm resolver-btn"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { useQuasar } from 'quasar';
+import ProfileEditDialog from 'src/pages/profile/ProfileEditDialog.vue';
+import { userStore } from 'src/stores/user';
+
+const $q = useQuasar();
+const store = userStore();
+
+const openEdit = () => {
+  $q.dialog({
+    component: ProfileEditDialog,
+    componentProps: { userData: store.user },
+  }).onOk((updatedUser) => {
+    store.setUser(updatedUser);
+  });
+};
+</script>
+
+<style lang="scss" scoped>
+.incomplete-banner {
+  border-radius: 12px;
+  background: #ede9fe;
+  cursor: pointer;
+}
+
+.banner-text {
+  font-size: 13px;
+  font-weight: 500;
+  line-height: 1.3;
+}
+
+.resolver-btn {
+  border-radius: 20px;
+  padding: 0px 4px;
+  font-size: 11px;
+  white-space: nowrap;
+  flex-shrink: 0;
+}
+</style>

+ 5 - 5
src/components/defaults/DefaultFilePicker.vue

@@ -218,11 +218,11 @@ const handleDrop = (event) => {
 @use "src/css/quasar.variables.scss";
 
 .image-preview-container {
-  .body--dark & {
-    --image-bg-color: #{map.get($colors-dark, "surface")};
-    --image-border-color: #{map.get($colors-dark, "primary")};
-    --image-border-hover-color: #{map.get($colors-dark, "primary-dark")};
-  }
+  // .body--dark & {
+  //   --image-bg-color: #{map.get($colors-dark, "surface")};
+  //   --image-border-color: #{map.get($colors-dark, "primary")};
+  //   --image-border-hover-color: #{map.get($colors-dark, "primary-dark")};
+  // }
 
   .body--light & {
     --image-bg-color: #{map.get($colors, "surface")};

+ 9 - 2
src/components/login/LoginStepFourPanel.vue

@@ -100,8 +100,15 @@ const useLocation = async () => {
     }
     const position = await getCurrentPosition();
     emit('device-location', position);
-  } catch {
-    $q.notify({ type: 'negative', message: t('auth.location_permission_denied') });
+  } catch (err) {
+    // GeolocationPositionError.PERMISSION_DENIED = 1
+    const isPermissionError = err?.code === 1 || err?.message?.toLowerCase().includes('denied');
+    $q.notify({
+      type: 'warning',
+      message: isPermissionError
+        ? t('auth.location_permission_denied')
+        : t('auth.location_error'),
+    });
   } finally {
     loadingLocation.value = false;
   }

+ 94 - 14
src/components/login/LoginStepThreePanel.vue

@@ -1,6 +1,6 @@
 <template>
   <q-card-section class="no-padding">
-    <div class="">
+    <div>
       <div class="text-text">
         <span class="text-weight-medium">{{ $t('auth.full_name') }}</span>
       </div>
@@ -18,7 +18,7 @@
       />
     </div>
 
-    <div class="">
+    <div>
       <div class="text-text">
         <span class="text-weight-medium">{{ $t('common.terms.cpf') }}</span>
       </div>
@@ -37,7 +37,7 @@
       />
     </div>
 
-    <div class="">
+    <div>
       <div class="text-text">
         <span class="text-weight-medium">{{ $t('common.terms.cep') }}</span>
       </div>
@@ -48,7 +48,7 @@
         rounded
         class="bg-surface q-mt-sm q-mb-md"
         input-class="text-text"
-        placeholder="00000-00"
+        placeholder="00000-000"
         hide-bottom-space
         :rules="[inputRules.required, inputRules.cep]"
         lazy-rules
@@ -58,7 +58,7 @@
       />
     </div>
 
-    <div class="">
+    <div>
       <div class="text-text">
         <span class="text-weight-medium">{{ $t('common.terms.address') }}</span>
       </div>
@@ -77,7 +77,76 @@
       />
     </div>
 
-    <div class="">
+    <div class="row q-col-gutter-sm">
+      <div class="col-4">
+        <div class="text-text">
+          <span class="text-weight-medium">{{ $t('common.terms.address_number') }}</span>
+        </div>
+        <q-input
+          v-model="form.number"
+          no-error-icon
+          outlined
+          rounded
+          class="bg-surface q-mt-sm q-mb-md"
+          input-class="text-text"
+          placeholder="0000"
+          hide-bottom-space
+          :rules="[inputRules.required]"
+          lazy-rules
+        />
+      </div>
+      <div class="col-8">
+        <div class="text-text">
+          <span class="text-weight-medium">{{ $t('common.terms.district') }}</span>
+        </div>
+        <q-input
+          v-model="form.district"
+          no-error-icon
+          outlined
+          rounded
+          class="bg-surface q-mt-sm q-mb-md"
+          input-class="text-text"
+          :placeholder="`${$t('common.terms.district')}...`"
+          hide-bottom-space
+          readonly
+        />
+      </div>
+    </div>
+
+    <div class="row q-col-gutter-sm">
+      <div class="col-8">
+        <div class="text-text">
+          <span class="text-weight-medium">{{ $t('common.terms.city') }}</span>
+        </div>
+        <q-input
+          v-model="form.city"
+          no-error-icon
+          outlined
+          rounded
+          class="bg-surface q-mt-sm q-mb-md"
+          input-class="text-text"
+          hide-bottom-space
+          readonly
+        />
+      </div>
+      <div class="col-4">
+        <div class="text-text">
+          <span class="text-weight-medium">{{ $t('common.terms.state') }}</span>
+        </div>
+        <q-input
+          v-model="form.state"
+          no-error-icon
+          outlined
+          rounded
+          class="bg-surface q-mt-sm q-mb-md"
+          input-class="text-text"
+          hide-bottom-space
+          readonly
+        />
+      </div>
+    </div>
+
+    <div>
       <q-checkbox
         v-model="form.no_complement"
         :label="$t('auth.no_complement')"
@@ -85,7 +154,7 @@
         class="q-mb-md text-text"
       />
     </div>
-    <div class="">
+    <div>
       <template v-if="!form.no_complement">
         <div class="text-text">
           <span class="text-weight-medium">{{ $t('common.terms.complement') }}</span>
@@ -104,7 +173,8 @@
         />
       </template>
     </div>
-    <div class="">
+
+    <div>
       <div class="text-text">
         <span class="text-weight-medium">{{ $t('auth.address_nickname') }}</span>
       </div>
@@ -121,7 +191,7 @@
       />
     </div>
 
-    <div class="">
+    <div>
       <div class="text-text">
         <span class="text-weight-medium">{{ $t('auth.address_instructions') }}</span>
       </div>
@@ -139,7 +209,8 @@
         lazy-rules
       />
     </div>
-    <div class="">
+
+    <div>
       <div class="row q-gutter-sm q-mt-xs">
         <q-chip
           v-for="type in addressTypes"
@@ -147,7 +218,6 @@
           :selected="form.address_type === type.value"
           clickable
           color="primary"
-
           :outline="form.address_type !== type.value"
           text-color="surface"
           :icon="type.icon"
@@ -185,14 +255,21 @@ const fetchCep = async (rawCep) => {
   try {
     const { data } = await axios.get(`https://viacep.com.br/ws/${cleaned}/json/`);
     if (!data.erro) {
-      form.value.address = `${data.logradouro}, ${data.bairro} - ${data.localidade}/${data.uf}`;
-      form.value.city = data.localidade;
-      form.value.state = data.uf;
+      form.value.address = data.logradouro ?? '';
+      form.value.district = data.bairro ?? '';
+      form.value.city = data.localidade ?? '';
+      form.value.state = data.uf ?? '';
     } else {
       form.value.address = '';
+      form.value.district = '';
+      form.value.city = '';
+      form.value.state = '';
     }
   } catch {
     form.value.address = '';
+    form.value.district = '';
+    form.value.city = '';
+    form.value.state = '';
   } finally {
     loadingCep.value = false;
   }
@@ -204,6 +281,9 @@ const onCepChange = (val) => {
     fetchCep(val);
   } else {
     form.value.address = '';
+    form.value.district = '';
+    form.value.city = '';
+    form.value.state = '';
   }
 };
 </script>

+ 29 - 18
src/composables/useGeocodingApi.js

@@ -16,39 +16,50 @@ const parseAddressComponents = (components) => {
   };
 };
 
+const geocodeViaGoogleMaps = async (request) => {
+  const geocoder = new window.google.maps.Geocoder();
+  const { results } = await geocoder.geocode(request);
+  return results ?? [];
+};
+
 export const useGeocodingApi = () => {
   const geocodeCep = async (cep) => {
     const cleaned = cep.replace(/\D/g, '');
+
+    if (window.google?.maps?.Geocoder) {
+      const results = await geocodeViaGoogleMaps({ address: `${cleaned}, Brazil` });
+      if (!results.length) return null;
+      const result = results[0];
+      const parsed = parseAddressComponents(result.address_components);
+      return {
+        lat: result.geometry.location.lat(),
+        lng: result.geometry.location.lng(),
+        ...parsed,
+      };
+    }
+
     const { data } = await axios.get(GEOCODING_URL, {
-      params: {
-        address: `${cleaned}, Brazil`,
-        key: process.env.GOOGLE_MAPS_API_KEY,
-      },
+      params: { address: `${cleaned}, Brazil`, key: process.env.GOOGLE_MAPS_API_KEY },
     });
-
     if (data.status !== 'OK' || !data.results.length) return null;
-
     const result = data.results[0];
     const { lat, lng } = result.geometry.location;
-    const parsed = parseAddressComponents(result.address_components);
-
-    return { lat, lng, ...parsed };
+    return { lat, lng, ...parseAddressComponents(result.address_components) };
   };
 
   const reverseGeocode = async (lat, lng) => {
+    if (window.google?.maps?.Geocoder) {
+      const results = await geocodeViaGoogleMaps({ location: { lat, lng } });
+      if (!results.length) return null;
+      return { lat, lng, ...parseAddressComponents(results[0].address_components) };
+    }
+
     const { data } = await axios.get(GEOCODING_URL, {
-      params: {
-        latlng: `${lat},${lng}`,
-        key: process.env.GOOGLE_MAPS_API_KEY,
-      },
+      params: { latlng: `${lat},${lng}`, key: process.env.GOOGLE_MAPS_API_KEY },
     });
-
     if (data.status !== 'OK' || !data.results.length) return null;
-
     const result = data.results[0];
-    const parsed = parseAddressComponents(result.address_components);
-
-    return { lat, lng, ...parsed };
+    return { lat, lng, ...parseAddressComponents(result.address_components) };
   };
 
   return { geocodeCep, reverseGeocode };

+ 3 - 0
src/composables/useGeolocation.js

@@ -1,7 +1,10 @@
 import { Geolocation } from '@capacitor/geolocation';
+import { Capacitor } from '@capacitor/core';
 
 export const useGeolocation = () => {
   const requestPermission = async () => {
+    if (!Capacitor.isNativePlatform()) return true;
+
     const status = await Geolocation.requestPermissions();
     return status.location === 'granted' || status.location === 'limited';
   };

+ 26 - 1
src/css/quasar.variables.scss

@@ -15,6 +15,9 @@ $negative: #EF4444; // Red (errors, alerts)
 $info: #3B82F6; // Blue (informational)
 $warning: #F59E0B; // Amber (warnings)
 
+$card-incomplete-profile: #ede9fe;
+$card-incomplete-payments: #FFD3FF;
+
 // Extended Color System with Light/Dark Variants
 $colors: (
   // Primary Colors and Variants (Purple Theme)
@@ -87,10 +90,32 @@ $colors: (
   // Blue
   "info-light": #60A5FA,
   // Light Blue
-  "info-dark": #2563EB
+  "info-dark": #2563EB,
   // Dark Blue
+
+  // Pastel Background Variants (for chips/badges)
+  "success-bg": #D1FAE5,
+  // Light Green background
+  "warning-bg": #FEF3C7,
+  // Light Amber background
+  "info-bg": #DBEAFE,
+  // Light Blue background
+  "secondary-bg": #FCE7F3,
+  // Light Pink background
+  "neutral-bg": #F3F4F6,
+  // Light Grey background
+  "status-finished": #9CA3AF
+  // Grey text for finished status
 );
 
+// Standalone status chip variables (usable in component scoped SCSS)
+$status-bg-pending:   #FEF3C7;
+$status-bg-confirmed: #D1FAE5;
+$status-bg-started:   #DBEAFE;
+$status-bg-finished:  #F3F4F6;
+$status-bg-cancelled: #FCE7F3;
+$status-color-finished: #9CA3AF;
+
 // Dark Theme Color Overrides
 // $colors-dark: (
 //   // Primary Colors and Variants (Purple - adjusted for dark mode)

+ 5 - 5
src/css/table.scss

@@ -4,11 +4,11 @@
   padding-left: 16px !important;
   padding-right: 16px !important;
 
-  .body--dark & {
-    --table-bg-color: #{map.get($colors-dark, "surface")}; // Using our dark background
-    --table-border-color: #{map.get($colors-dark, "surface-light")}; // Darker border
-    --table-header-color: #{map.get($colors-dark, "text")}; // Light text for dark mode
-  }
+  // .body--dark & {
+  //   --table-bg-color: #{map.get($colors-dark, "surface")}; // Using our dark background
+  //   --table-border-color: #{map.get($colors-dark, "surface-light")}; // Darker border
+  //   --table-header-color: #{map.get($colors-dark, "text")}; // Light text for dark mode
+  // }
 
   .body--light & {
     --table-bg-color: #{map.get($colors, "surface")}; // Light background

+ 31 - 2
src/i18n/locales/en.json

@@ -34,6 +34,7 @@
       "country": "Country",
       "address": "Address",
       "address_number": "Address Number",
+      "district": "District",
       "complement": "Complement",
       "postal_code": "Postal Code",
       "phone": "Phone",
@@ -153,12 +154,16 @@
     "back_to_register": "Back to registration",
     "cep_not_found": "ZIP code not found. Please check and try again.",
     "location_permission_denied": "Location permission denied. Please enable it in device settings.",
+    "location_error": "Could not get your location. Please try again.",
     "confirm_location": "confirm location",
     "confirm_address": "confirm address",
     "complement": "Complement",
     "complement_placeholder": "Ex: Apartment, Suite, House.",
     "address_nickname_placeholder": "Ex: Apartment in Condominium",
-    "register_error": "Error completing registration. Please try again."
+    "complement_required": "Please fill in the complement or check 'Address without complement'.",
+    "register_error": "Error completing registration. Please try again.",
+    "geocoding_failed": "Address not identified. Drag the pin to adjust the location.",
+    "geocoding_failed_short": "Drag the pin to identify the address"
   },
   "business": {
     "advertise": "Advertise",
@@ -445,6 +450,24 @@
       "reschedule": "reschedule",
       "no_address": "No address"
     },
+    "agenda": {
+      "title": "Schedule",
+      "upcoming_title": "Upcoming services",
+      "completed_title": "Completed services",
+      "type_default": "Appointment",
+      "type_custom": "Custom",
+      "status_pending": "Pending",
+      "status_accepted": "Confirmed",
+      "status_paid": "Confirmed",
+      "status_started": "In progress",
+      "status_finished": "Completed",
+      "status_cancelled": "Cancelled",
+      "btn_view_details": "view details",
+      "btn_rate": "rate",
+      "btn_reschedule": "reschedule",
+      "empty_upcoming": "No scheduled services",
+      "empty_completed": "No completed services"
+    },
     "favorites": {
       "title": "Favorites",
       "view_schedule": "view schedule",
@@ -482,7 +505,11 @@
       "detail_total": "Total:",
       "btn_payment": "go to payment",
       "btn_cancel": "Cancel request"
-    }
+    },
+    "registration_incomplete_title": "Complete your profile information!",
+    "registration_incomplete_cta": "Resolve now",
+    "payment_incomplete_title": "Update your payment information!",
+    "payment_incomplete_cta": "Resolve now"
   },
   "profile": {
     "title": "Profile",
@@ -495,6 +522,8 @@
     "placeholder_email": "Enter your email",
     "phone": "Phone",
     "placeholder_phone": "(11) 99999-9999",
+    "document": "CPF (Tax ID)",
+    "placeholder_document": "000.000.000-00",
     "update": "Update",
     "language": "Language",
     "lang_pt": "PT-br",

+ 31 - 2
src/i18n/locales/es.json

@@ -34,6 +34,7 @@
       "country": "País",
       "address": "Dirección",
       "address_number": "Número",
+      "district": "Barrio",
       "complement": "Complemento",
       "postal_code": "Código postal",
       "phone": "Teléfono",
@@ -153,12 +154,16 @@
     "back_to_register": "Volver al registro",
     "cep_not_found": "Código postal no encontrado. Verifique e intente nuevamente.",
     "location_permission_denied": "Permiso de ubicación denegado. Habilítelo en la configuración del dispositivo.",
+    "location_error": "No se pudo obtener tu ubicación. Inténtalo de nuevo.",
     "confirm_location": "confirmar ubicación",
     "confirm_address": "confirmar dirección",
     "complement": "Complemento",
     "complement_placeholder": "Ej: Apartamento, Conjunto, Casa.",
     "address_nickname_placeholder": "Ej: Apartamento en el Condominio",
-    "register_error": "Error al finalizar el registro. Inténtelo de nuevo."
+    "complement_required": "Complete el complemento o marque 'Dirección sin complemento'.",
+    "register_error": "Error al finalizar el registro. Inténtelo de nuevo.",
+    "geocoding_failed": "Dirección no identificada. Arrastre el pin para ajustar la ubicación.",
+    "geocoding_failed_short": "Arrastre el pin para identificar la dirección"
   },
   "business": {
     "advertise": "Anunciar",
@@ -441,6 +446,24 @@
       "reschedule": "reagendar",
       "no_address": "Sin dirección"
     },
+    "agenda": {
+      "title": "Agenda",
+      "upcoming_title": "Próximos servicios",
+      "completed_title": "Servicios completados",
+      "type_default": "Agendamiento",
+      "type_custom": "A medida",
+      "status_pending": "Pendiente",
+      "status_accepted": "Confirmado",
+      "status_paid": "Confirmado",
+      "status_started": "En progreso",
+      "status_finished": "Completado",
+      "status_cancelled": "Cancelado",
+      "btn_view_details": "ver detalles",
+      "btn_rate": "evaluar",
+      "btn_reschedule": "reprogramar",
+      "empty_upcoming": "Sin servicios programados",
+      "empty_completed": "Sin servicios completados"
+    },
     "favorites": {
       "title": "Favoritos",
       "view_schedule": "ver agenda",
@@ -478,7 +501,11 @@
       "detail_total": "Total:",
       "btn_payment": "ir al pago",
       "btn_cancel": "Cancelar pedido"
-    }
+    },
+    "registration_incomplete_title": "¡Completa la información de tu perfil!",
+    "registration_incomplete_cta": "Resolver ahora",
+    "payment_incomplete_title": "¡Actualiza tus datos de pago!",
+    "payment_incomplete_cta": "Resolver ahora"
   },
   "profile": {
     "title": "Perfil",
@@ -491,6 +518,8 @@
     "placeholder_email": "Ingrese su correo electrónico",
     "phone": "Teléfono",
     "placeholder_phone": "(11) 99999-9999",
+    "document": "CPF (RUT)",
+    "placeholder_document": "000.000.000-00",
     "update": "Actualizar",
     "language": "Idioma",
     "lang_pt": "PT-br",

+ 31 - 2
src/i18n/locales/pt.json

@@ -34,6 +34,7 @@
       "country": "País",
       "address": "Endereço",
       "address_number": "Número",
+      "district": "Bairro",
       "complement": "Complemento",
       "postal_code": "Código Postal",
       "phone": "Telefone",
@@ -153,12 +154,16 @@
     "back_to_register": "Voltar para o cadastro",
     "cep_not_found": "CEP não encontrado. Verifique e tente novamente.",
     "location_permission_denied": "Permissão de localização negada. Habilite nas configurações do dispositivo.",
+    "location_error": "Não foi possível obter sua localização. Tente novamente.",
     "confirm_location": "confirmar localização",
     "confirm_address": "confirmar endereço",
     "complement": "Complemento",
     "complement_placeholder": "Ex: Apartamento, Conjunto, Casa.",
     "address_nickname_placeholder": "Ex: Apartamento no Condomínio",
-    "register_error": "Erro ao finalizar cadastro. Tente novamente."
+    "complement_required": "Informe o complemento ou marque 'Endereço sem complemento'.",
+    "register_error": "Erro ao finalizar cadastro. Tente novamente.",
+    "geocoding_failed": "Endereço não identificado. Arraste o pin para ajustar a localização.",
+    "geocoding_failed_short": "Arraste o pin para identificar o endereço"
   },
   "business": {
     "advertise": "Anunciar",
@@ -450,6 +455,24 @@
       "reschedule": "reagendar",
       "no_address": "Sem endereço"
     },
+    "agenda": {
+      "title": "Agenda",
+      "upcoming_title": "Próximos serviços",
+      "completed_title": "Serviços concluídos",
+      "type_default": "Agendamento",
+      "type_custom": "Sob Medida",
+      "status_pending": "Pendente",
+      "status_accepted": "Confirmado",
+      "status_paid": "Confirmado",
+      "status_started": "Em andamento",
+      "status_finished": "Concluído",
+      "status_cancelled": "Cancelado",
+      "btn_view_details": "ver detalhes",
+      "btn_rate": "avaliar",
+      "btn_reschedule": "reagendar",
+      "empty_upcoming": "Nenhum serviço agendado",
+      "empty_completed": "Nenhum serviço concluído"
+    },
     "favorites": {
       "title": "Favoritos",
       "view_schedule": "ver agenda",
@@ -499,7 +522,11 @@
       "help_link": "Ajuda",
       "already_reviewed": "Você já avaliou este serviço.",
       "reviewed_badge": "avaliado!"
-    }
+    },
+    "registration_incomplete_title": "Complete as informações do seu perfil!",
+    "registration_incomplete_cta": "Resolver agora",
+    "payment_incomplete_title": "Atualize seus dados de pagamento!",
+    "payment_incomplete_cta": "Resolver agora"
   },
   "profile": {
     "title": "Perfil",
@@ -512,6 +539,8 @@
     "placeholder_email": "Digite seu e-mail",
     "phone": "Telefone",
     "placeholder_phone": "(11) 99999-9999",
+    "document": "CPF",
+    "placeholder_document": "000.000.000-00",
     "update": "Atualizar",
     "language": "Idioma",
     "lang_pt": "PT-br",

+ 1 - 1
src/layouts/MainLayout.vue

@@ -75,7 +75,7 @@ const navItems = computed(() => [
     icon: "mdi-magnify",
   },
   {
-    name: "AgendaPage",
+    name: "CalendarPage",
     label: t('nav.agenda'),
     icon: "mdi-calendar-blank-outline",
   },

+ 2 - 0
src/pages/LoginPage.vue

@@ -108,6 +108,8 @@ const stepThreeForm = ref({
   document: '',
   zip_code: '',
   address: '',
+  number: '',
+  district: '',
   complement: '',
   nickname: '',
   instructions: '',

+ 0 - 52
src/pages/agenda/AgendaPage.vue

@@ -1,52 +0,0 @@
-<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
-<template>
-  <section class="mobile-placeholder">
-    <div class="mobile-placeholder__badge">
-      <q-icon name="mdi-calendar-blank-outline" />
-    </div>
-    <h1 class="mobile-placeholder__title">Agenda</h1>
-    <p class="mobile-placeholder__description">
-      Área reservada para exibir compromissos, confirmações e próximas diárias.
-    </p>
-  </section>
-</template>
-
-<style scoped>
-.mobile-placeholder {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  min-height: calc(100dvh - 240px);
-  padding: 32px 20px;
-  text-align: center;
-}
-
-.mobile-placeholder__badge {
-  display: grid;
-  place-items: center;
-  width: 88px;
-  height: 88px;
-  border-radius: 28px;
-  margin-bottom: 20px;
-  background: linear-gradient(180deg, rgba(255, 0, 234, 0.14), rgba(107, 17, 203, 0.08));
-  color: #ff00ea;
-  font-size: 44px;
-}
-
-.mobile-placeholder__title {
-  margin: 0 0 8px;
-  font-size: 28px;
-  font-weight: 700;
-  line-height: 1.1;
-  color: #4d4d4d;
-}
-
-.mobile-placeholder__description {
-  max-width: 280px;
-  margin: 0;
-  font-size: 16px;
-  line-height: 1.5;
-  color: #8d8d8d;
-}
-</style>

+ 392 - 0
src/pages/agenda/CalendarPage.vue

@@ -0,0 +1,392 @@
+<template>
+  <q-page class="bg-page q-pb-xl">
+    <div class="calendar-header row items-center bg-white">
+      <q-space />
+      <span class="text-subtitle1 text-weight-bold gradient-diarista">{{ $t('dashboard_client.agenda.title') }}</span>
+      <q-space />
+    </div>
+
+    <template v-if="loading">
+      <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">{{ $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"
+          >
+            <q-card-section class="q-pa-sm">
+              <div class="row no-wrap items-start q-gutter-x-sm">
+                <q-avatar size="44px">
+                  <img :src="item.provider_photo || defaultAvatar">
+                </q-avatar>
+
+                <div class="col column">
+                  <span class="text-name ellipsis">{{ item.provider_name }}</span>
+                  <div class="row items-center no-wrap">
+                    <span class="text-date-bold">{{ formatWeekday(item.date) }}</span>
+                    <span class="text-date-regular">{{ ', ' + formatDayMonth(item.date) }}</span>
+                  </div>
+                  <span class="text-date-regular">
+                    {{ $t('dashboard_client.next_schedules.from') }}
+                    <span class="text-date-bold">{{ item.start_time?.slice(0, 5) }}</span>
+                    {{ $t('dashboard_client.next_schedules.to') }}
+                    <span class="text-date-bold">{{ item.end_time?.slice(0, 5) }}</span>
+                  </span>
+                </div>
+
+                <div class="col-auto column items-end">
+                  <q-chip
+                    dense
+                    square
+                    :color="statusBgColor(item.status)"
+                    :text-color="statusTextColor(item.status)"
+                    :label="statusLabel(item.status)"
+                    class="status-chip"
+                  />
+                  <span class="text-price">{{ formatCurrency(item.total_amount) }}</span>
+                  <span class="text-period">{{ periodLabel(item.period_type) }}</span>
+                </div>
+              </div>
+
+              <div class="row items-center no-wrap q-mt-xs">
+                <span class="type-label" :class="item.schedule_type === 'custom' ? 'type-custom' : 'type-default'">
+                  {{ item.schedule_type === 'custom' ? $t('dashboard_client.agenda.type_custom') : $t('dashboard_client.agenda.type_default') }}
+                </span>
+                <q-space />
+                <q-btn
+                  flat
+                  no-caps
+                  color="primary"
+                  size="xs"
+                  class="btn-action"
+                  :label="$t('dashboard_client.agenda.btn_view_details')"
+                  @click="openDetailsDialog(item)"
+                />
+              </div>
+            </q-card-section>
+          </q-card>
+        </template>
+
+        <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">{{ $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"
+          >
+            <q-card-section class="q-pa-sm">
+              <div class="row no-wrap items-start q-gutter-x-sm">
+                <q-avatar size="44px">
+                  <img :src="item.provider_photo || defaultAvatar">
+                </q-avatar>
+
+                <div class="col column">
+                  <span class="text-name ellipsis">{{ item.provider_name }}</span>
+                  <div class="row items-center no-wrap">
+                    <span class="text-date-bold">{{ formatWeekday(item.date) }}</span>
+                    <span class="text-date-regular">{{ ', ' + formatDayMonth(item.date) }}</span>
+                  </div>
+                  <span class="text-date-regular">
+                    {{ $t('dashboard_client.next_schedules.from') }}
+                    <span class="text-date-bold">{{ item.start_time?.slice(0, 5) }}</span>
+                    {{ $t('dashboard_client.next_schedules.to') }}
+                    <span class="text-date-bold">{{ item.end_time?.slice(0, 5) }}</span>
+                  </span>
+                </div>
+
+                <div class="col-auto column items-end">
+                  <q-chip
+                    dense
+                    square
+                    :color="statusBgColor(item.status)"
+                    :text-color="statusTextColor(item.status)"
+                    :label="statusLabel(item.status)"
+                    class="status-chip"
+                  />
+                  <span class="text-price">{{ formatCurrency(item.total_amount) }}</span>
+                  <span class="text-period">{{ periodLabel(item.period_type) }}</span>
+                </div>
+              </div>
+
+              <div class="row items-center no-wrap q-mt-xs">
+                <span class="type-label" :class="item.schedule_type === 'custom' ? 'type-custom' : '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"
+                  color="amber"
+                  icon="mdi-star-outline"
+                  icon-selected="mdi-star"
+                  readonly
+                  class="q-mr-sm"
+                />
+                <q-btn
+                  v-if="item.client_reviewed"
+                  unelevated
+                  rounded
+                  no-caps
+                  color="secondary"
+                  size="xs"
+                  class="btn-rate"
+                  :label="$t('dashboard_client.agenda.btn_reschedule')"
+                  @click="openSchedulingDialog(item)"
+                />
+                <q-btn
+                  v-else
+                  unelevated
+                  rounded
+                  no-caps
+                  color="secondary"
+                  size="xs"
+                  class="btn-rate"
+                  :label="$t('dashboard_client.agenda.btn_rate')"
+                  @click="openRatingDialog(item)"
+                />
+              </div>
+            </q-card-section>
+          </q-card>
+        </template>
+
+        <div v-else class="text-center text-grey-5 q-py-lg text-body2">
+          {{ $t('dashboard_client.agenda.empty_completed') }}
+        </div>
+      </div>
+    </template>
+  </q-page>
+</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 } from 'src/helpers/utils';
+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 defaultAvatar = 'https://cdn.quasar.dev/img/avatar.png';
+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})/);
+  if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
+  const dmy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
+  if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
+  return null;
+};
+
+const formatWeekday = (dateStr) => {
+  const d = parseLocalDate(dateStr);
+  if (!d) return '';
+  const w = d.toLocaleDateString('pt-BR', { weekday: 'long' });
+  return w.charAt(0).toUpperCase() + w.slice(1);
+};
+
+const formatDayMonth = (dateStr) => {
+  const d = parseLocalDate(dateStr);
+  if (!d) return '';
+  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
+};
+
+const periodLabel = (periodType) => {
+  const key = `period_types.${periodType}`;
+  const translated = t(key);
+  return translated !== key ? translated : '';
+};
+
+const statusLabel = (status) => {
+  const map = {
+    pending: t('dashboard_client.agenda.status_pending'),
+    accepted: t('dashboard_client.agenda.status_accepted'),
+    paid: t('dashboard_client.agenda.status_paid'),
+    started: t('dashboard_client.agenda.status_started'),
+    finished: t('dashboard_client.agenda.status_finished'),
+    cancelled: t('dashboard_client.agenda.status_cancelled'),
+  };
+  return map[status] ?? status;
+};
+
+const statusBgColor = (status) => {
+  const map = {
+    pending: 'warning-bg',
+    accepted: 'success-bg',
+    paid: 'success-bg',
+    started: 'info-bg',
+    finished: 'neutral-bg',
+    cancelled: 'secondary-bg',
+  };
+  return map[status] ?? 'neutral-bg';
+};
+
+const statusTextColor = (status) => {
+  const map = {
+    pending: 'warning',
+    accepted: 'success',
+    paid: 'success',
+    started: 'info',
+    finished: 'status-finished',
+    cancelled: 'secondary',
+  };
+  return map[status] ?? 'text';
+};
+
+const loadCalendar = async () => {
+  const response = await getClientCalendar();
+  if (response) {
+    upcomingSchedules.value = response.upcomingSchedules ?? [];
+    completedSchedules.value = response.completedSchedules ?? [];
+  }
+};
+
+const openDetailsDialog = (schedule) => {
+  $q.dialog({
+    component: NextSchedulesDetailsDialog,
+    componentProps: { schedule },
+  }).onOk(async ({ action }) => {
+    if (action === 'cancelled') {
+      await loadCalendar();
+    }
+  });
+};
+
+const openRatingDialog = (schedule) => {
+  $q.dialog({
+    component: ScheduleRatingDialog,
+    componentProps: { schedule },
+  }).onOk(() => {
+    loadCalendar();
+  });
+};
+
+const openSchedulingDialog = (item) => {
+  $q.dialog({
+    component: SchedulingDialog,
+    componentProps: {
+      provider: {
+        provider_id: item.provider_id,
+        provider_name: item.provider_name,
+        average_rating: item.average_rating,
+        total_reviews: item.total_reviews,
+        total_services: item.total_services,
+      },
+    },
+  });
+};
+
+onMounted(async () => {
+  await loadCalendar();
+  loading.value = false;
+});
+</script>
+
+<style scoped lang="scss">
+.calendar-header {
+  padding-top: calc(env(safe-area-inset-top) + 12px);
+  padding-bottom: 12px;
+  box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.08);
+}
+
+.calendar-card {
+  border-radius: 12px;
+  width: 100%;
+}
+
+.type-label {
+  font-size: 10px;
+  font-weight: 600;
+  line-height: 1.2;
+}
+
+.type-default {
+  color: #8B5CF6;
+}
+
+.type-custom {
+  color: #EC4899;
+}
+
+.text-name {
+  font-size: 13px;
+  font-weight: 700;
+  color: #3a3a4a;
+  max-width: 130px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.text-date-bold {
+  font-family: 'Inter', sans-serif;
+  font-size: 11px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+
+.text-date-regular {
+  font-family: 'Inter', sans-serif;
+  font-size: 11px;
+  font-weight: 400;
+  color: #666;
+}
+
+.text-price {
+  font-size: 13px;
+  font-weight: 700;
+  color: #3a3a4a;
+  white-space: nowrap;
+}
+
+.text-period {
+  font-size: 10px;
+  color: #888;
+  text-align: right;
+  white-space: nowrap;
+}
+
+.status-chip {
+  font-size: 11px !important;
+  font-weight: 700;
+  height: auto;
+  padding: 2px 2px;
+}
+
+.btn-action {
+  font-size: 11px;
+  font-weight: 700;
+}
+
+.btn-rate {
+  font-size: 11px;
+  font-weight: 700;
+  padding: 3px 10px;
+}
+
+</style>

+ 16 - 7
src/pages/dashboard/DashboardPage.vue

@@ -7,7 +7,9 @@
     </template>
     <template v-else>
       <DashboardHeaderBar :data="headerBar" />
-      <DashboardSummaryInfos :data="summaryInfos" />
+      <DashboardRegistrationIncomplete v-if="!registrationComplete" />
+      <DashboardSummaryInfos v-else :data="summaryInfos" />
+      <DashboardPaymentIncomplete v-if="!hasPaymentMethods" />
       <DashboardPendingSchedules
         v-if="pendingSchedules.length > 0"
         :data="pendingSchedules"
@@ -29,6 +31,8 @@
 <script setup>
 import DashboardHeaderBar from 'src/components/dashboard/DashboardHeaderBar.vue';
 import DashboardSummaryInfos from 'src/components/dashboard/DashboardSummaryInfos.vue';
+import DashboardRegistrationIncomplete from 'src/components/dashboard/DashboardRegistrationIncomplete.vue';
+import DashboardPaymentIncomplete from 'src/components/dashboard/DashboardPaymentIncomplete.vue';
 import DashboardPendingSchedules from 'src/components/dashboard/DashboardPendingSchedules.vue';
 import ScheduleAcceptedDialog from 'src/components/dashboard/ScheduleAcceptedDialog.vue';
 import DashboardScrollAreaSchedules from 'src/components/dashboard/DashboardScrollAreaSchedules.vue';
@@ -41,14 +45,20 @@ import FinalSuccesModal from '../schedules/components/FinalSuccesModal.vue';
 import DashboardPendingCustomSchedules from 'src/pages/dashboard/components/DashboardPendingCustomSchedules.vue';
 import DashboardClientProposals from 'src/pages/dashboard/components/DashboardClientProposals.vue';
 import { useRouter } from 'vue-router'
-import { onMounted, ref } from 'vue';
+import { onMounted, ref, computed } from 'vue';
 import { useDialogPluginComponent, 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([]);
@@ -58,11 +68,10 @@ const lastDoneSchedules = ref([]);
 const favoriteProviders = ref([]);
 const providersClose = ref([]);
 const todaySchedules = ref([]);
-const $q = useQuasar();
 const loading = ref(true);
+const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSuccessModal') || false);
 
-const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSuccessModal') || 'true');
-const { onDialogOK } = useDialogPluginComponent();
+const registrationComplete = computed(() => store.user?.registration_complete ?? true);
 
 const openAcceptedDialog = (schedule) => {
   $q.dialog({
@@ -86,8 +95,9 @@ const reloadDashboard = async () => {
     providersClose.value = response.providersClose ?? [];
     clientProposals.value = response.schedulesProposals ?? [];
     todaySchedules.value = response.todaySchedules ?? [];
+    hasPaymentMethods.value = response.has_payment_methods ?? true;
   }
-  if( showSuccessModal.value ) {
+  if( showSuccessModal.value == true) {
     $q.dialog({
        component: FinalSuccesModal   
        })
@@ -96,7 +106,6 @@ const reloadDashboard = async () => {
     router.replace({ path: router.currentRoute.value.path, query: {} });
   }
 
-
   loading.value = false;
 };
 

+ 116 - 57
src/pages/location/AddressCompletionPage.vue

@@ -1,84 +1,150 @@
 <template>
-  <q-page class="address-completion-page bg-surface-dark">
+  <q-page class="address-completion-page bg-surface">
     <div class="address-completion-inner">
       <q-btn
         flat
         dense
         round
-        color="white"
+        color="primary"
         icon="mdi-arrow-left"
         class="address-completion-back"
         @click="router.back()"
       />
 
       <div class="address-completion-content">
-        <div class="address-completion-field-label">{{ $t('common.terms.address') }}</div>
+        <div class="text-text">
+          <span class="text-weight-medium">{{ $t('common.terms.address') }}</span>
+        </div>
         <q-input
-          :model-value="addressDisplay"
+          :model-value="flowStore.confirmedAddress"
           outlined
+          rounded
           readonly
-          dense
-          bg-color="white"
-          class="q-mb-lg"
+          class="bg-surface q-mt-sm q-mb-md"
           input-class="text-text"
         />
 
+        <div class="row q-col-gutter-sm">
+          <div class="col-4">
+            <div class="text-text">
+              <span class="text-weight-medium">{{ $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"
+              input-class="text-text"
+            />
+          </div>
+          <div class="col-8">
+            <div class="text-text">
+              <span class="text-weight-medium">{{ $t('common.terms.district') }}</span>
+            </div>
+            <q-input
+              :model-value="flowStore.confirmedDistrict"
+              outlined
+              rounded
+              readonly
+              class="bg-surface q-mt-sm q-mb-md"
+              input-class="text-text"
+            />
+          </div>
+        </div>
+
+        <div class="row q-col-gutter-sm">
+          <div class="col-8">
+            <div class="text-text">
+              <span class="text-weight-medium">{{ $t('common.terms.city') }}</span>
+            </div>
+            <q-input
+              :model-value="flowStore.confirmedCity"
+              outlined
+              rounded
+              readonly
+              class="bg-surface q-mt-sm q-mb-md"
+              input-class="text-text"
+            />
+          </div>
+          <div class="col-4">
+            <div class="text-text">
+              <span class="text-weight-medium">{{ $t('common.terms.state') }}</span>
+            </div>
+            <q-input
+              :model-value="flowStore.confirmedState"
+              outlined
+              rounded
+              readonly
+              class="bg-surface q-mt-sm q-mb-md"
+              input-class="text-text"
+            />
+          </div>
+        </div>
+
         <q-checkbox
           v-model="form.no_complement"
           :label="$t('auth.no_complement')"
           color="primary"
-          class="q-mb-sm"
+          keep-color
+          class="q-mb-md text-text"
         />
 
-        <q-input
-          v-if="!form.no_complement"
-          v-model="form.complement"
-          outlined
-          dense
-          bg-color="white"
-          :label="$t('auth.complement') + '*'"
-          :placeholder="$t('auth.complement_placeholder')"
-          class="q-mb-md"
-          input-class="text-text"
-        />
+        <template v-if="!form.no_complement">
+          <div class="text-text">
+            <span class="text-weight-medium">{{ $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>
 
-        <div class="address-completion-field-label">{{ $t('auth.address_nickname') }}</div>
+        <div class="text-text">
+          <span class="text-weight-medium">{{ $t('auth.address_nickname') }}</span>
+        </div>
         <q-input
           v-model="form.nickname"
           outlined
-          dense
-          bg-color="white"
+          rounded
+          class="bg-surface q-mt-sm q-mb-md"
           :placeholder="$t('auth.address_nickname_placeholder')"
-          class="q-mb-md"
           input-class="text-text"
         />
 
-        <div class="address-completion-field-label">{{ $t('auth.address_instructions') }}</div>
+        <div class="text-text">
+          <span class="text-weight-medium">{{ $t('auth.address_instructions') }}</span>
+        </div>
         <q-input
           v-model="form.instructions"
           outlined
+          rounded
+          class="bg-surface q-mt-sm q-mb-md"
           type="textarea"
-          bg-color="white"
-          class="q-mb-lg"
-          input-class="text-text"
           rows="3"
           autogrow
+          input-class="text-text"
         />
 
-        <div class="address-type-row q-mb-xl">
-          <q-btn
+        <div class="row q-gutter-sm q-mt-xs q-mb-xl">
+          <q-chip
             v-for="type in addressTypes"
             :key="type.value"
-            rounded
+            :selected="form.address_type === type.value"
+            clickable
+            color="primary"
             :outline="form.address_type !== type.value"
-            :color="form.address_type === type.value ? 'primary-button' : 'grey-4'"
-            :text-color="form.address_type === type.value ? 'white' : 'grey-8'"
+            text-color="surface"
             :icon="type.icon"
-            :label="type.label"
-            size="sm"
-            padding="8px 14px"
+            :icon-selected="type.icon"
             @click="form.address_type = type.value"
-          />
+          >
+            {{ type.label }}
+          </q-chip>
         </div>
       </div>
 
@@ -98,7 +164,7 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue';
+import { ref, computed, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
 import { useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
@@ -115,6 +181,7 @@ const { setAuthDataFromPayload } = useAuth();
 const submitting = ref(false);
 
 const form = ref({
+  number: '',
   no_complement: false,
   complement: '',
   nickname: '',
@@ -122,15 +189,6 @@ const form = ref({
   address_type: 'home',
 });
 
-const addressDisplay = computed(() => {
-  const parts = [
-    flowStore.confirmedAddress,
-    flowStore.confirmedNumber,
-    flowStore.confirmedDistrict,
-  ].filter(Boolean);
-  return parts.join(', ');
-});
-
 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' },
@@ -138,8 +196,8 @@ const addressTypes = computed(() => [
 ]);
 
 const handleConfirm = async () => {
-  if (!flowStore.hasConfirmedLocation() || !flowStore.hasCredentials()) {
-    router.replace({ name: 'LoginPage' });
+  if (!form.value.no_complement && !form.value.complement?.trim()) {
+    $q.notify({ type: 'warning', message: t('auth.complement_required') });
     return;
   }
 
@@ -151,7 +209,7 @@ const handleConfirm = async () => {
       code: flowStore.code,
       zip_code: flowStore.confirmedZipCode || undefined,
       address: flowStore.confirmedAddress || undefined,
-      number: flowStore.confirmedNumber || undefined,
+      number: form.value.number || undefined,
       district: flowStore.confirmedDistrict || undefined,
       city: flowStore.confirmedCity || undefined,
       state: flowStore.confirmedState || undefined,
@@ -165,7 +223,7 @@ const handleConfirm = async () => {
     };
 
     const response = await createUserAndClient(payload);
-    if (response.status === 200) {
+    if (response.status >= 200 && response.status < 300) {
       await setAuthDataFromPayload(response.data.payload);
       flowStore.clear();
       router.push({ name: 'DashboardPage' });
@@ -176,6 +234,14 @@ const handleConfirm = async () => {
     submitting.value = false;
   }
 };
+
+onMounted(() => {
+  if (!flowStore.hasConfirmedLocation() || !flowStore.hasCredentials()) {
+    router.replace({ name: 'LoginPage' });
+    return;
+  }
+  form.value.number = flowStore.confirmedNumber || '';
+});
 </script>
 
 <style lang="scss" scoped>
@@ -204,13 +270,6 @@ const handleConfirm = async () => {
   flex: 1;
 }
 
-.address-completion-field-label {
-  font-size: 14px;
-  font-weight: 600;
-  color: var(--q-text);
-  margin-bottom: 6px;
-}
-
 .address-type-row {
   display: flex;
   gap: 10px;

+ 36 - 3
src/pages/location/LocationMapPage.vue

@@ -20,7 +20,9 @@
         readonly
         class="location-map-address-input q-mb-md"
         bg-color="white"
+        input-class="text-text"
         :loading="reversing"
+        :placeholder="reversing ? '' : $t('auth.geocoding_failed_short')"
       />
       <q-btn
         color="primary-button"
@@ -29,6 +31,7 @@
         padding="14px 16px"
         class="full-width"
         :loading="reversing"
+        :disable="!currentGeoData"
         @click="handleConfirm"
       />
     </div>
@@ -38,11 +41,15 @@
 <script setup>
 import { ref, onMounted, onUnmounted } from 'vue';
 import { useRouter } from 'vue-router';
+import { useQuasar } from 'quasar';
+import { useI18n } from 'vue-i18n';
 import { GoogleMap } from '@capacitor/google-maps';
 import { useRegistrationFlowStore } from 'src/stores/registrationFlow';
 import { useGeocodingApi } from 'src/composables/useGeocodingApi';
 
 const router = useRouter();
+const $q = useQuasar();
+const { t } = useI18n();
 const flowStore = useRegistrationFlowStore();
 const { reverseGeocode } = useGeocodingApi();
 
@@ -81,14 +88,32 @@ const updateMarkerPosition = async (lat, lng) => {
     const geoData = await reverseGeocode(lat, lng);
     if (geoData) {
       currentGeoData.value = { ...geoData, lat, lng };
-      const parts = [geoData.address, geoData.number, geoData.district].filter(Boolean);
+      const parts = [geoData.address, geoData.number, geoData.district, geoData.city, geoData.state].filter(Boolean);
       currentAddress.value = parts.join(', ');
+    } else {
+      currentGeoData.value = { lat, lng, address: '', number: '', district: '', city: '', state: '', zip_code: '' };
+      currentAddress.value = '';
+      $q.notify({ type: 'warning', message: t('auth.geocoding_failed') });
     }
+  } catch {
+    currentGeoData.value = { lat, lng, address: '', number: '', district: '', city: '', state: '', zip_code: '' };
+    currentAddress.value = '';
+    $q.notify({ type: 'warning', message: t('auth.geocoding_failed') });
   } finally {
     reversing.value = false;
   }
 };
 
+const moveMarkerTo = async (lat, lng) => {
+  if (markerId.value) {
+    await googleMap.removeMarker(markerId.value);
+  }
+  markerId.value = await googleMap.addMarker({
+    coordinate: { lat, lng },
+    draggable: true,
+  });
+};
+
 onMounted(async () => {
   if (!flowStore.hasInitialLocation()) {
     router.replace({ name: 'LoginPage' });
@@ -100,8 +125,6 @@ onMounted(async () => {
   currentLat.value = lat;
   currentLng.value = lng;
 
-  await updateMarkerPosition(lat, lng);
-
   googleMap = await GoogleMap.create({
     id: 'location-map',
     element: mapRef.value,
@@ -117,12 +140,22 @@ onMounted(async () => {
     draggable: true,
   });
 
+  await updateMarkerPosition(lat, lng);
+
   await googleMap.setOnMarkerDragEndListener(async (event) => {
     const { latitude, longitude } = event;
     currentLat.value = latitude;
     currentLng.value = longitude;
     await updateMarkerPosition(latitude, longitude);
   });
+
+  await googleMap.setOnMapClickListener(async (event) => {
+    const { latitude, longitude } = event;
+    currentLat.value = latitude;
+    currentLng.value = longitude;
+    await moveMarkerTo(latitude, longitude);
+    await updateMarkerPosition(latitude, longitude);
+  });
 });
 
 onUnmounted(async () => {

+ 123 - 95
src/pages/profile/ProfileEditDialog.vue

@@ -22,68 +22,98 @@
             <q-btn flat no-caps color="grey-6" class="q-mt-sm" :label="$t('profile.change_photo')" />
           </div>
 
-          <div class="q-px-xl q-gutter-y-lg">
-            <div>
-              <div class="text-weight-bold text-grey-8 q-mb-sm">{{ $t('profile.full_name') }}</div>
-              <q-input
-                v-model="form.name"
-                outlined
-                dense
-                input-class="text-text"
-                :placeholder="$t('profile.placeholder_name')"
-              />
-            </div>
+          <q-form ref="formRef" @submit.prevent="onSubmit">
+            <div class="q-px-xl q-gutter-y-lg">
+              <div>
+                <div class="text-weight-bold text-grey-8 q-mb-sm">{{ $t('profile.full_name') }}</div>
+                <q-input
+                  v-model="form.name"
+                  outlined
+                  dense
+                  input-class="text-text"
+                  :placeholder="$t('profile.placeholder_name')"
+                  :rules="[inputRules.required]"
+                  :error="!!serverErrors.name"
+                  :error-message="serverErrors.name"
+                  @update:model-value="serverErrors.name = null"
+                />
+              </div>
 
-            <div>
-              <div class="text-weight-bold text-grey-8 q-mb-sm">{{ $t('profile.email') }}</div>
-              <q-input
-                v-model="form.email"
-                outlined
-                dense
-                input-class="text-text"
-                :placeholder="$t('profile.placeholder_email')"
-              />
-            </div>
+              <div>
+                <div class="text-weight-bold text-grey-8 q-mb-sm">{{ $t('profile.email') }}</div>
+                <q-input
+                  v-model="form.email"
+                  outlined
+                  dense
+                  input-class="text-text"
+                  :placeholder="$t('profile.placeholder_email')"
+                  :rules="[inputRules.email]"
+                  :error="!!serverErrors.email"
+                  :error-message="serverErrors.email"
+                  @update:model-value="serverErrors.email = null"
+                />
+              </div>
 
-            <div>
-              <div class="text-weight-bold text-grey-8 q-mb-sm">{{ $t('profile.phone') }}</div>
-              <q-input
-                v-model="form.phone"
-                outlined
-                dense
-                input-class="text-text"
-                mask="(##) #####-####"
-                unmasked-value
-                :placeholder="$t('profile.placeholder_phone')"
-              />
-            </div>
+              <div>
+                <div class="text-weight-bold text-grey-8 q-mb-sm">{{ $t('profile.phone') }}</div>
+                <q-input
+                  v-model="form.phone"
+                  outlined
+                  dense
+                  input-class="text-text"
+                  mask="(##) #####-####"
+                  unmasked-value
+                  :placeholder="$t('profile.placeholder_phone')"
+                  :error="!!serverErrors.phone"
+                  :error-message="serverErrors.phone"
+                  @update:model-value="serverErrors.phone = null"
+                />
+              </div>
 
-            <div>
-              <div class="text-weight-bold text-grey-8 q-mb-sm">{{ $t('profile.language') }}</div>
-              <div class="row">
-                <q-radio v-model="selectedLocale" val="pt" :label="$t('profile.lang_pt')" color="primary" class="text-text col-4" keep-color @update:model-value="onLocaleChange" />
-                <q-radio v-model="selectedLocale" val="en" :label="$t('profile.lang_en')" color="primary" class="text-text col-4" keep-color @update:model-value="onLocaleChange" />
-                <q-radio v-model="selectedLocale" val="es" :label="$t('profile.lang_es')" color="primary" class="text-text col-4" keep-color @update:model-value="onLocaleChange" />
+              <div>
+                <div class="text-weight-bold text-grey-8 q-mb-sm">{{ $t('profile.document') }}</div>
+                <q-input
+                  v-model="form.document"
+                  outlined
+                  dense
+                  input-class="text-text"
+                  mask="###.###.###-##"
+                  unmasked-value
+                  :placeholder="$t('profile.placeholder_document')"
+                  :rules="[inputRules.cpf]"
+                  :error="!!serverErrors.document"
+                  :error-message="serverErrors.document"
+                  @update:model-value="serverErrors.document = null"
+                />
+              </div>
+
+              <div>
+                <div class="text-weight-bold text-grey-8 q-mb-sm">{{ $t('profile.language') }}</div>
+                <div class="row">
+                  <q-radio v-model="selectedLocale" val="pt" :label="$t('profile.lang_pt')" color="primary" class="text-text col-4" keep-color @update:model-value="onLocaleChange" />
+                  <q-radio v-model="selectedLocale" val="en" :label="$t('profile.lang_en')" color="primary" class="text-text col-4" keep-color @update:model-value="onLocaleChange" />
+                  <q-radio v-model="selectedLocale" val="es" :label="$t('profile.lang_es')" color="primary" class="text-text col-4" keep-color @update:model-value="onLocaleChange" />
+                </div>
               </div>
             </div>
-          </div>
 
-          <q-space/>
-
-          <div class="q-pa-xl q-mt-md">
-            <q-btn
-              unelevated
-              rounded
-              no-caps
-              padding="8px 16px"
-              class="full-width q-py-md text-weight-bold"
-              :label="$t('profile.update')"
-              :color="hasUpdatedFields ? 'primary' : 'grey-4'"
-              :disable="!hasUpdatedFields"
-              :loading="submitting"
-              @click="submitUpdate"
-            />
-          </div>
+            <q-space />
+
+            <div class="q-pa-xl q-mt-md">
+              <q-btn
+                type="submit"
+                unelevated
+                rounded
+                no-caps
+                padding="8px 16px"
+                class="full-width q-py-md text-weight-bold"
+                :label="$t('profile.update')"
+                :color="hasUpdatedFields ? 'primary' : 'grey-4'"
+                :disable="!hasUpdatedFields"
+                :loading="submitting"
+              />
+            </div>
+          </q-form>
         </q-scroll-area>
       </template>
     </q-card>
@@ -93,8 +123,10 @@
 <script setup>
 import { ref, onMounted } from 'vue';
 import { useDialogPluginComponent, Cookies } from 'quasar';
-import { updateUser } from 'src/api/user';
+import { updateMe } from 'src/api/user';
 import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker';
+import { useSubmitHandler } from 'src/composables/useSubmitHandler';
+import { useInputRules } from 'src/composables/useInputRules';
 import { i18n } from 'src/boot/i18n';
 
 const props = defineProps({
@@ -106,58 +138,56 @@ const props = defineProps({
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
-const loading = ref(false);
-const submitting = ref(false);
-const userId = ref(null);
-
 const normalizeLocale = (loc) => {
-  if (!loc) return 'pt'
-  const l = String(loc).toLowerCase()
-  if (l.startsWith('pt')) return 'pt'
-  if (l.startsWith('en')) return 'en'
-  if (l.startsWith('es')) return 'es'
-  return 'pt'
-}
-const selectedLocale = ref(normalizeLocale(i18n.global.locale.value ?? i18n.global.locale))
-
-const onLocaleChange = (val) => {
-  i18n.global.locale.value = val   // troca em tempo real (composition API mode)
-  Cookies.set('locale', val, { expires: 365, path: '/' })
-}
+  if (!loc) return 'pt';
+  const l = String(loc).toLowerCase();
+  if (l.startsWith('pt')) return 'pt';
+  if (l.startsWith('en')) return 'en';
+  if (l.startsWith('es')) return 'es';
+  return 'pt';
+};
 
+const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
 const { form, hasUpdatedFields, setUpdateFormAsOriginal } = useFormUpdateTracker({
   name: '',
   email: '',
-  phone: ''
+  phone: '',
+  document: '',
 });
+const { loading: submitting, serverErrors, execute: submitForm } = useSubmitHandler((data) => {
+  setUpdateFormAsOriginal(data);
+  onDialogOK(data);
+});
+const { inputRules } = useInputRules();
 
-const submitUpdate = async () => {
-  if (!hasUpdatedFields.value) return;
+const formRef = ref(null);
+const loading = ref(false);
+const selectedLocale = ref(normalizeLocale(i18n.global.locale.value ?? i18n.global.locale));
 
-  submitting.value = true;
-  try {
-    const data = await updateUser({
-      name: form.name,
-      email: form.email,
-      phone: form.phone
-    }, userId.value);
-    setUpdateFormAsOriginal(data);
-    onDialogOK(data);
-  } catch (error) {
-    console.error('Erro ao atualizar perfil:', error);
-  } finally {
-    submitting.value = false;
-  }
+const onLocaleChange = (val) => {
+  i18n.global.locale.value = val;
+  Cookies.set('locale', val, { expires: 365, path: '/' });
+};
+
+const onSubmit = async () => {
+  const valid = await formRef.value.validate();
+  if (!valid) return;
+
+  await submitForm(() => updateMe({
+    name: form.name,
+    email: form.email,
+    phone: form.phone,
+    document: form.document || null,
+  }));
 };
 
 onMounted(async () => {
   if (props.userData) {
     const data = props.userData;
-    userId.value = data.id;
     form.name = data.name || '';
     form.email = data.email || '';
     form.phone = data.phone || '';
+    form.document = data.client_document || '';
     setUpdateFormAsOriginal(data);
     return;
   }
@@ -167,8 +197,6 @@ onMounted(async () => {
 </script>
 
 <style scoped lang="scss">
-
-
 :deep(.q-field--outlined .q-field__control) {
   border-radius: 8px;
   &::before { border: 1px solid #e0e0e0; }

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

@@ -101,6 +101,7 @@ import { ref, onMounted } from 'vue';
 import { useQuasar } from 'quasar';
 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';
@@ -110,6 +111,7 @@ import ProfilePrivacyDialog from 'src/components/profile/ProfilePrivacyDialog.vu
 import { useRouter } from 'vue-router';
 
 const $q = useQuasar();
+const store = userStore();
 
 const { logout } = useAuth();
 const router = useRouter();
@@ -127,6 +129,7 @@ const openEditProfile = () => {
     }
   }).onOk((updatedUser) => {
     user.value = { ...user.value, ...updatedUser };
+    store.setUser({ ...store.user, ...updatedUser });
   });
 };
 

+ 3 - 3
src/router/routes/navbar.route.js

@@ -20,8 +20,8 @@ export default [
   },
   {
     path: "agenda",
-    name: "AgendaPage",
-    component: () => import("src/pages/agenda/AgendaPage.vue"),
+    name: "CalendarPage",
+    component: () => import("src/pages/agenda/CalendarPage.vue"),
     meta: {
       title: "Agenda",
       requireAuth: true,
@@ -31,7 +31,7 @@ export default [
           title: "ui.navigation.dashboard",
         },
         {
-          name: "AgendaPage",
+          name: "CalendarPage",
           title: "Agenda",
         },
       ],