Explorar el Código

refactor: tela de login fluxo

Gustavo Mantovani hace 2 semanas
padre
commit
8381c5cfb3
Se han modificado 88 ficheros con 1334 adiciones y 993 borrados
  1. 7 3
      .gitignore
  2. 10 0
      package-lock.json
  3. 2 1
      package.json
  4. 2 2
      src-capacitor/android/app/build.gradle
  5. 4 0
      src-capacitor/android/app/capacitor.build.gradle
  6. 7 0
      src-capacitor/android/app/src/main/AndroidManifest.xml
  7. 6 0
      src-capacitor/android/app/src/main/java/br/com/diarista/provider/MainActivity.java
  8. BIN
      src-capacitor/android/app/src/main/res/drawable-land-hdpi/splash.png
  9. BIN
      src-capacitor/android/app/src/main/res/drawable-land-mdpi/splash.png
  10. BIN
      src-capacitor/android/app/src/main/res/drawable-land-xhdpi/splash.png
  11. BIN
      src-capacitor/android/app/src/main/res/drawable-land-xxhdpi/splash.png
  12. BIN
      src-capacitor/android/app/src/main/res/drawable-land-xxxhdpi/splash.png
  13. BIN
      src-capacitor/android/app/src/main/res/drawable-port-hdpi/splash.png
  14. BIN
      src-capacitor/android/app/src/main/res/drawable-port-mdpi/splash.png
  15. BIN
      src-capacitor/android/app/src/main/res/drawable-port-xhdpi/splash.png
  16. BIN
      src-capacitor/android/app/src/main/res/drawable-port-xxhdpi/splash.png
  17. BIN
      src-capacitor/android/app/src/main/res/drawable-port-xxxhdpi/splash.png
  18. 0 34
      src-capacitor/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  19. 0 170
      src-capacitor/android/app/src/main/res/drawable/ic_launcher_background.xml
  20. BIN
      src-capacitor/android/app/src/main/res/drawable/splash.png
  21. 3 2
      src-capacitor/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  22. 3 2
      src-capacitor/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  23. BIN
      src-capacitor/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  24. BIN
      src-capacitor/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
  25. BIN
      src-capacitor/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
  26. BIN
      src-capacitor/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
  27. BIN
      src-capacitor/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  28. BIN
      src-capacitor/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  29. BIN
      src-capacitor/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
  30. BIN
      src-capacitor/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
  31. BIN
      src-capacitor/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
  32. BIN
      src-capacitor/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  33. BIN
      src-capacitor/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  34. BIN
      src-capacitor/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
  35. BIN
      src-capacitor/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
  36. BIN
      src-capacitor/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
  37. BIN
      src-capacitor/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  38. BIN
      src-capacitor/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  39. BIN
      src-capacitor/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
  40. BIN
      src-capacitor/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
  41. BIN
      src-capacitor/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
  42. BIN
      src-capacitor/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  43. BIN
      src-capacitor/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  44. BIN
      src-capacitor/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
  45. BIN
      src-capacitor/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
  46. BIN
      src-capacitor/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
  47. BIN
      src-capacitor/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  48. 0 4
      src-capacitor/android/app/src/main/res/values/ic_launcher_background.xml
  49. 2 2
      src-capacitor/android/app/src/main/res/values/strings.xml
  50. 1 1
      src-capacitor/android/build.gradle
  51. 12 0
      src-capacitor/android/capacitor.settings.gradle
  52. 1 1
      src-capacitor/android/variables.gradle
  53. 10 2
      src-capacitor/capacitor.config.json
  54. BIN
      src-capacitor/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png
  55. BIN
      src-capacitor/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png
  56. BIN
      src-capacitor/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png
  57. 181 106
      src-capacitor/package-lock.json
  58. 7 3
      src-capacitor/package.json
  59. 3 1
      src-capacitor/www/index.html
  60. 4 17
      src/api/providerPaymentMethod.js
  61. 6 0
      src/api/supportRequest.js
  62. 1 1
      src/components/dashboard/DashboardHeaderBar.vue
  63. 8 1
      src/components/dashboard/DashboardScrollAreaSchedules.vue
  64. 2 2
      src/components/login/LoginStepFivePanel.vue
  65. 184 290
      src/components/login/LoginStepFourPanel.vue
  66. 3 0
      src/components/login/LoginStepThreePanel.vue
  67. 4 3
      src/components/profile/ProfileAddressDialog.vue
  68. 1 1
      src/components/profile/ProfileAvailabilityDialog.vue
  69. 102 137
      src/components/profile/ProfileBankDataDialog.vue
  70. 37 10
      src/components/profile/ProfileHelpDialog.vue
  71. 1 1
      src/components/profile/ProfilePrivacyDialog.vue
  72. 2 2
      src/components/profile/ProfileServiceDataDialog.vue
  73. 241 0
      src/components/profile/SupportRequestDialog.vue
  74. 71 12
      src/components/shared/LocationMapDialog.vue
  75. 12 1
      src/composables/useAuth.js
  76. 13 6
      src/composables/useGeocodingApi.js
  77. 4 1
      src/css/app.scss
  78. 3 3
      src/css/table.scss
  79. 47 5
      src/i18n/locales/en.json
  80. 47 5
      src/i18n/locales/es.json
  81. 96 54
      src/i18n/locales/pt.json
  82. 6 19
      src/layouts/MainLayout.vue
  83. 147 68
      src/pages/LoginPage.vue
  84. 1 1
      src/pages/calendar/CalendarPage.vue
  85. 27 17
      src/pages/dashboard/DashboardPage.vue
  86. 1 0
      src/pages/opportunities/components/OpportunityDetailsDialog.vue
  87. 1 1
      src/pages/payments/PaymentsPage.vue
  88. 1 1
      src/pages/profile/ProfileEditDialog.vue

+ 7 - 3
.gitignore

@@ -23,9 +23,11 @@ Thumbs.db
 *.swo
 
 /src-capacitor/node_modules
-/src-cordova/platforms
-/src-cordova/plugins
-/src-cordova/www
+/src-capacitor/platforms
+/src-capacitor/plugins
+/src-capacitor/www
+/src-capacitor/www/
+/src-capacitor/www/assets
 
 /android
 /ios
@@ -39,3 +41,5 @@ Thumbs.db
 
 /dist-android
 /dist-ios
+/src-capacitor/www
+/src-capacitor/node_modules

+ 10 - 0
package-lock.json

@@ -10,6 +10,7 @@
       "dependencies": {
         "@bufbuild/protobuf": "^2.5.1",
         "@capacitor/browser": "^8.0.3",
+        "@capacitor/camera": "^7.0.5",
         "@capacitor/device": "^7.0.1",
         "@capacitor/geolocation": "^8.2.0",
         "@capacitor/google-maps": "^8.0.1",
@@ -323,6 +324,15 @@
         "@capacitor/core": ">=8.0.0"
       }
     },
+    "node_modules/@capacitor/camera": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/@capacitor/camera/-/camera-7.0.5.tgz",
+      "integrity": "sha512-+53f1xHq8on7O+ULXbgWp00ooWvn3fUkWxTP3p5zHi1nVriCxLbYnY0zTpZQqn52M96vclFKkoABbTVQ3x755w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@capacitor/core": ">=7.0.0"
+      }
+    },
     "node_modules/@capacitor/core": {
       "version": "8.3.4",
       "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.4.tgz",

+ 2 - 1
package.json

@@ -2,7 +2,7 @@
   "name": "diarista-prestador-app",
   "version": "0.0.1",
   "description": "Aplicativo para o prestador do Diarista",
-  "productName": "Diarista Prestador App",
+  "productName": "Diarista Profissional App",
   "author": "zntt <zanattagg@gmail.com>",
   "type": "module",
   "private": true,
@@ -24,6 +24,7 @@
   "dependencies": {
     "@bufbuild/protobuf": "^2.5.1",
     "@capacitor/browser": "^8.0.3",
+    "@capacitor/camera": "^7.0.5",
     "@capacitor/device": "^7.0.1",
     "@capacitor/geolocation": "^8.2.0",
     "@capacitor/google-maps": "^8.0.1",

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

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

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

@@ -11,10 +11,14 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
 dependencies {
     implementation project(':capacitor-app')
     implementation project(':capacitor-browser')
+    implementation project(':capacitor-camera')
     implementation project(':capacitor-device')
+    implementation project(':capacitor-geolocation')
+    implementation project(':capacitor-google-maps')
     implementation project(':capacitor-keyboard')
     implementation project(':capacitor-preferences')
     implementation project(':capacitor-push-notifications')
+    implementation project(':capacitor-splash-screen')
     implementation project(':capacitor-status-bar')
 
 }

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

@@ -9,6 +9,10 @@
         android:supportsRtl="true"
         android:theme="@style/AppTheme">
 
+        <meta-data
+            android:name="com.google.android.geo.API_KEY"
+            android:value="AIzaSyDrVs7amXuXKEu8sYMr-dl8VjNZbgS2frk" />
+
         <activity
             android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
             android:name=".MainActivity"
@@ -39,4 +43,7 @@
     <!-- Permissions -->
 
     <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-feature android:name="android.hardware.camera" android:required="false" />
+    <uses-feature android:name="android.hardware.camera.front" android:required="false" />
 </manifest>

+ 6 - 0
src-capacitor/android/app/src/main/java/br/com/diarista/provider/MainActivity.java

@@ -1,5 +1,6 @@
 package br.com.diarista.provider;
 
+import android.graphics.Color;
 import android.os.Bundle;
 import android.webkit.WebView;
 
@@ -9,6 +10,11 @@ public class MainActivity extends BridgeActivity {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        // Necessário para @capacitor/google-maps: o mapa nativo renderiza atrás do
+        // WebView, então o WebView precisa ser transparente onde o mapa é exibido.
+        // (O background do body cobre isso nas telas normais — só o LocationMapDialog
+        //  torna a cadeia html/body/#q-app transparente para revelar o mapa.)
+        getBridge().getWebView().setBackgroundColor(Color.TRANSPARENT);
     }
 
     @Override

BIN
src-capacitor/android/app/src/main/res/drawable-land-hdpi/splash.png


BIN
src-capacitor/android/app/src/main/res/drawable-land-mdpi/splash.png


BIN
src-capacitor/android/app/src/main/res/drawable-land-xhdpi/splash.png


BIN
src-capacitor/android/app/src/main/res/drawable-land-xxhdpi/splash.png


BIN
src-capacitor/android/app/src/main/res/drawable-land-xxxhdpi/splash.png


BIN
src-capacitor/android/app/src/main/res/drawable-port-hdpi/splash.png


BIN
src-capacitor/android/app/src/main/res/drawable-port-mdpi/splash.png


BIN
src-capacitor/android/app/src/main/res/drawable-port-xhdpi/splash.png


BIN
src-capacitor/android/app/src/main/res/drawable-port-xxhdpi/splash.png


BIN
src-capacitor/android/app/src/main/res/drawable-port-xxxhdpi/splash.png


+ 0 - 34
src-capacitor/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml

@@ -1,34 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:aapt="http://schemas.android.com/aapt"
-    android:width="108dp"
-    android:height="108dp"
-    android:viewportHeight="108"
-    android:viewportWidth="108">
-    <path
-        android:fillType="evenOdd"
-        android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
-        android:strokeColor="#00000000"
-        android:strokeWidth="1">
-        <aapt:attr name="android:fillColor">
-            <gradient
-                android:endX="78.5885"
-                android:endY="90.9159"
-                android:startX="48.7653"
-                android:startY="61.0927"
-                android:type="linear">
-                <item
-                    android:color="#44000000"
-                    android:offset="0.0" />
-                <item
-                    android:color="#00000000"
-                    android:offset="1.0" />
-            </gradient>
-        </aapt:attr>
-    </path>
-    <path
-        android:fillColor="#FFFFFF"
-        android:fillType="nonZero"
-        android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
-        android:strokeColor="#00000000"
-        android:strokeWidth="1" />
-</vector>

+ 0 - 170
src-capacitor/android/app/src/main/res/drawable/ic_launcher_background.xml

@@ -1,170 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="108dp"
-    android:height="108dp"
-    android:viewportHeight="108"
-    android:viewportWidth="108">
-    <path
-        android:fillColor="#26A69A"
-        android:pathData="M0,0h108v108h-108z" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M9,0L9,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,0L19,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M29,0L29,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M39,0L39,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M49,0L49,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M59,0L59,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M69,0L69,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M79,0L79,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M89,0L89,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M99,0L99,108"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,9L108,9"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,19L108,19"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,29L108,29"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,39L108,39"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,49L108,49"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,59L108,59"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,69L108,69"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,79L108,79"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,89L108,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,99L108,99"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,29L89,29"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,39L89,39"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,49L89,49"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,59L89,59"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,69L89,69"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,79L89,79"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M29,19L29,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M39,19L39,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M49,19L49,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M59,19L59,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M69,19L69,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M79,19L79,89"
-        android:strokeColor="#33FFFFFF"
-        android:strokeWidth="0.8" />
-</vector>

BIN
src-capacitor/android/app/src/main/res/drawable/splash.png


+ 3 - 2
src-capacitor/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/ic_launcher_background"/>
+    <background android:drawable="@mipmap/ic_launcher_background"/>
     <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
-</adaptive-icon>
+    <monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
+</adaptive-icon>

+ 3 - 2
src-capacitor/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@color/ic_launcher_background"/>
+    <background android:drawable="@mipmap/ic_launcher_background"/>
     <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
-</adaptive-icon>
+    <monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
+</adaptive-icon>

BIN
src-capacitor/android/app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
src-capacitor/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png


BIN
src-capacitor/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png


BIN
src-capacitor/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png


BIN
src-capacitor/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png


BIN
src-capacitor/android/app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
src-capacitor/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png


BIN
src-capacitor/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png


BIN
src-capacitor/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png


BIN
src-capacitor/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png


BIN
src-capacitor/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 0 - 4
src-capacitor/android/app/src/main/res/values/ic_launcher_background.xml

@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-    <color name="ic_launcher_background">#FFFFFF</color>
-</resources>

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

@@ -1,7 +1,7 @@
 <?xml version='1.0' encoding='utf-8'?>
 <resources>
-    <string name="app_name">Diarista Prestador</string>
-    <string name="title_activity_main">Diarista Prestador</string>
+    <string name="app_name">Diária Profissionais</string>
+    <string name="title_activity_main">Diária Profissionais</string>
     <string name="package_name">br.com.diarista.provider</string>
     <string name="custom_url_scheme">br.com.diarista.provider</string>
 </resources>

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

@@ -7,7 +7,7 @@ buildscript {
         mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:8.7.2'
+        classpath 'com.android.tools.build:gradle:8.9.1'
         classpath 'com.google.gms:google-services:4.4.2'
 
         // NOTE: Do not place your application dependencies here; they belong

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

@@ -8,9 +8,18 @@ project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/
 include ':capacitor-browser'
 project(':capacitor-browser').projectDir = new File('../node_modules/@capacitor/browser/android')
 
+include ':capacitor-camera'
+project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android')
+
 include ':capacitor-device'
 project(':capacitor-device').projectDir = new File('../node_modules/@capacitor/device/android')
 
+include ':capacitor-geolocation'
+project(':capacitor-geolocation').projectDir = new File('../node_modules/@capacitor/geolocation/android')
+
+include ':capacitor-google-maps'
+project(':capacitor-google-maps').projectDir = new File('../node_modules/@capacitor/google-maps/android')
+
 include ':capacitor-keyboard'
 project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android')
 
@@ -20,5 +29,8 @@ project(':capacitor-preferences').projectDir = new File('../node_modules/@capaci
 include ':capacitor-push-notifications'
 project(':capacitor-push-notifications').projectDir = new File('../node_modules/@capacitor/push-notifications/android')
 
+include ':capacitor-splash-screen'
+project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capacitor/splash-screen/android')
+
 include ':capacitor-status-bar'
 project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')

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

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

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

@@ -1,6 +1,6 @@
 {
   "appId": "br.com.diarista.provider",
-  "appName": "Diarista Prestador",
+  "appName": "Diarista Profissional",
   "webDir": "www",
   "plugins": {
     "Keyboard": {
@@ -10,11 +10,19 @@
     "StatusBar": {
       "overlaysWebView": true
     },
+    "SplashScreen": {
+      "launchAutoHide": false,
+      "backgroundColor": "#8B5CF6",
+      "androidScaleType": "CENTER_CROP",
+      "showSpinner": false,
+      "splashFullScreen": true,
+      "splashImmersive": true
+    },
     "CapacitorCookies": {
       "enabled": true
     },
     "CapacitorHttp": {
-      "enabled": true
+      "enabled": false
     }
   }
 }

BIN
src-capacitor/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png


BIN
src-capacitor/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png


BIN
src-capacitor/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png


+ 181 - 106
src-capacitor/package-lock.json

@@ -1,23 +1,27 @@
 {
-  "name": "quasar-skeleton",
+  "name": "diarista-prestador-app",
   "version": "0.0.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
-      "name": "quasar-skeleton",
+      "name": "diarista-prestador-app",
       "version": "0.0.1",
       "dependencies": {
         "@capacitor/android": "^7.2.0",
         "@capacitor/app": "^7.0.0",
         "@capacitor/browser": "^8.0.3",
-        "@capacitor/cli": "^7.0.0",
+        "@capacitor/camera": "^7.0.5",
+        "@capacitor/cli": "^8.3.3",
         "@capacitor/core": "^7.0.0",
         "@capacitor/device": "^7.0.1",
+        "@capacitor/geolocation": "^8.2.0",
+        "@capacitor/google-maps": "^8.0.1",
         "@capacitor/ios": "^7.2.0",
         "@capacitor/keyboard": "^7.0.1",
         "@capacitor/preferences": "^7.0.1",
-        "@capacitor/push-notifications": "^7.0.6",
+        "@capacitor/push-notifications": "^8.1.1",
+        "@capacitor/splash-screen": "^7.0.5",
         "@capacitor/status-bar": "^7.0.1"
       }
     },
@@ -47,10 +51,19 @@
         "@capacitor/core": ">=8.0.0"
       }
     },
+    "node_modules/@capacitor/camera": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/@capacitor/camera/-/camera-7.0.5.tgz",
+      "integrity": "sha512-+53f1xHq8on7O+ULXbgWp00ooWvn3fUkWxTP3p5zHi1nVriCxLbYnY0zTpZQqn52M96vclFKkoABbTVQ3x755w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@capacitor/core": ">=7.0.0"
+      }
+    },
     "node_modules/@capacitor/cli": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.2.0.tgz",
-      "integrity": "sha512-RNW9vtYYYSDmOdguYBSW0VpRnG/d6lGydlc9DLrJ7qbSPxFrotTz9IjkM48O+SruUma61DyuSqJttdbay2xSxg==",
+      "version": "8.4.1",
+      "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.4.1.tgz",
+      "integrity": "sha512-t7F2s7fFHCq113xgrggrmK6ctV0/8E5YfLNVLfPHp4GCTDO+tly9fZvWPf2/sOI8lMm18dLT43qbXLRTz/OZgw==",
       "license": "MIT",
       "dependencies": {
         "@ionic/cli-framework-output": "^2.2.8",
@@ -61,13 +74,13 @@
         "env-paths": "^2.2.0",
         "fs-extra": "^11.2.0",
         "kleur": "^4.1.5",
-        "native-run": "^2.0.1",
+        "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": "^6.1.11",
+        "tar": "^7.5.3",
         "tslib": "^2.8.1",
         "xml2js": "^0.6.2"
       },
@@ -76,7 +89,7 @@
         "capacitor": "bin/capacitor"
       },
       "engines": {
-        "node": ">=20.0.0"
+        "node": ">=22.0.0"
       }
     },
     "node_modules/@capacitor/core": {
@@ -96,6 +109,32 @@
         "@capacitor/core": ">=7.0.0"
       }
     },
+    "node_modules/@capacitor/geolocation": {
+      "version": "8.2.0",
+      "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-8.2.0.tgz",
+      "integrity": "sha512-N29QcoIPmme0xSxRkm7+3hjoHp6mBAOarxecvtCCZKyOBeKiJsFUq981cezg2XWBa6fhCXJMCCjQPngKK/dIag==",
+      "license": "MIT",
+      "dependencies": {
+        "@capacitor/synapse": "^1.0.4"
+      },
+      "peerDependencies": {
+        "@capacitor/core": ">=8.0.0"
+      }
+    },
+    "node_modules/@capacitor/google-maps": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/@capacitor/google-maps/-/google-maps-8.0.1.tgz",
+      "integrity": "sha512-XSOyanbtOeO5KrSfoJOpbcSW4EXvfdxp+SUy6umdIGFeR2WJKNnPC6isXn+zjJgJH8kblT2X4fF0dxhZV8wLBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@googlemaps/js-api-loader": "^2.0.2",
+        "@googlemaps/markerclusterer": "^2.6.2",
+        "@types/google.maps": "^3.58.1"
+      },
+      "peerDependencies": {
+        "@capacitor/core": ">=8.0.0"
+      }
+    },
     "node_modules/@capacitor/ios": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-7.2.0.tgz",
@@ -123,9 +162,18 @@
       }
     },
     "node_modules/@capacitor/push-notifications": {
-      "version": "7.0.6",
-      "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-7.0.6.tgz",
-      "integrity": "sha512-zAhbHpdbc15ImuVGgoFwUZsKI+jjGxy/oO5mgfYKUx8Xl2OskndzhL79PYsCPjyGLbqUR9NRUZJthV9auVT3nw==",
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/@capacitor/push-notifications/-/push-notifications-8.1.1.tgz",
+      "integrity": "sha512-WqzjPKIbYbARMN+GC0XMAJcxJpUUzqgzS/Ny8RODLrro38pQhm3GXYwX2Mwd+LZlLY39rGImkCkrKyQSNfuikA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@capacitor/core": ">=8.0.0"
+      }
+    },
+    "node_modules/@capacitor/splash-screen": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/@capacitor/splash-screen/-/splash-screen-7.0.5.tgz",
+      "integrity": "sha512-bPG2SamFL7VT5I3XsgsGgJhkiyxq3OXCOVKEDupSieVdtEiG7j2vLbSD0xL+91zteF/qLuxsjbugGy5LD8mS2A==",
       "license": "MIT",
       "peerDependencies": {
         "@capacitor/core": ">=7.0.0"
@@ -140,6 +188,32 @@
         "@capacitor/core": ">=7.0.0"
       }
     },
+    "node_modules/@capacitor/synapse": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.4.tgz",
+      "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==",
+      "license": "ISC"
+    },
+    "node_modules/@googlemaps/js-api-loader": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-2.1.1.tgz",
+      "integrity": "sha512-yUpAwksbHrlZIWD49JmveNSfBG4oAK0AwMknfSaPMnP5N7UT8oFRVCqwjGb1XQovi//7KLbPQKZpbofiLGzpDw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/google.maps": "^3.53.1"
+      }
+    },
+    "node_modules/@googlemaps/markerclusterer": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.6.2.tgz",
+      "integrity": "sha512-U6uVhq8iWhiIckA89sgRu8OK35mjd6/3CuoZKWakKEf0QmRRWpatlsPb3kqXkoWSmbcZkopRiI4dnW6DQSd7bQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/supercluster": "^7.1.3",
+        "fast-equals": "^5.2.2",
+        "supercluster": "^8.0.1"
+      }
+    },
     "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",
@@ -375,6 +449,18 @@
         "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==",
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.0.4"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
     "node_modules/@types/fs-extra": {
       "version": "8.1.5",
       "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz",
@@ -384,6 +470,18 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/geojson": {
+      "version": "7946.0.16",
+      "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+      "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+      "license": "MIT"
+    },
+    "node_modules/@types/google.maps": {
+      "version": "3.65.2",
+      "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.65.2.tgz",
+      "integrity": "sha512-e52bmOhGCQSNabFpL48iQlwJybq6rfns8NUVJ20MR7CdPlHQ2RmSCnPbJfrUYJfogrE4OiHQTZ4LXpop+eer1w==",
+      "license": "MIT"
+    },
     "node_modules/@types/node": {
       "version": "22.14.1",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
@@ -399,13 +497,22 @@
       "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==",
       "license": "MIT"
     },
+    "node_modules/@types/supercluster": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
+      "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/geojson": "*"
+      }
+    },
     "node_modules/@xmldom/xmldom": {
-      "version": "0.8.10",
-      "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
-      "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
+      "version": "0.9.10",
+      "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",
+      "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==",
       "license": "MIT",
       "engines": {
-        "node": ">=10.0.0"
+        "node": ">=14.6"
       }
     },
     "node_modules/ansi-regex": {
@@ -516,12 +623,12 @@
       }
     },
     "node_modules/chownr": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
-      "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
-      "license": "ISC",
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+      "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+      "license": "BlueOak-1.0.0",
       "engines": {
-        "node": ">=10"
+        "node": ">=18"
       }
     },
     "node_modules/color-convert": {
@@ -624,6 +731,15 @@
         "node": ">=6"
       }
     },
+    "node_modules/fast-equals": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
+      "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
     "node_modules/fd-slicer": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
@@ -675,30 +791,6 @@
         "node": ">=14.14"
       }
     },
-    "node_modules/fs-minipass": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
-      "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
-      "license": "ISC",
-      "dependencies": {
-        "minipass": "^3.0.0"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/fs-minipass/node_modules/minipass": {
-      "version": "3.3.6",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
-      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
-      "license": "ISC",
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
     "node_modules/glob": {
       "version": "11.0.1",
       "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz",
@@ -812,6 +904,12 @@
         "graceful-fs": "^4.1.6"
       }
     },
+    "node_modules/kdbush": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz",
+      "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==",
+      "license": "ISC"
+    },
     "node_modules/kleur": {
       "version": "4.1.5",
       "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -855,40 +953,15 @@
       }
     },
     "node_modules/minizlib": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
-      "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+      "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
       "license": "MIT",
       "dependencies": {
-        "minipass": "^3.0.0",
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">= 8"
-      }
-    },
-    "node_modules/minizlib/node_modules/minipass": {
-      "version": "3.3.6",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
-      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
-      "license": "ISC",
-      "dependencies": {
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=8"
-      }
-    },
-    "node_modules/mkdirp": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
-      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
-      "license": "MIT",
-      "bin": {
-        "mkdirp": "bin/cmd.js"
+        "minipass": "^7.1.2"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">= 18"
       }
     },
     "node_modules/ms": {
@@ -898,9 +971,9 @@
       "license": "MIT"
     },
     "node_modules/native-run": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.1.tgz",
-      "integrity": "sha512-XfG1FBZLM50J10xH9361whJRC9SHZ0Bub4iNRhhI61C8Jv0e1ud19muex6sNKB51ibQNUJNuYn25MuYET/rE6w==",
+      "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==",
       "license": "MIT",
       "dependencies": {
         "@ionic/utils-fs": "^3.1.7",
@@ -977,12 +1050,12 @@
       "license": "MIT"
     },
     "node_modules/plist": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
-      "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz",
+      "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==",
       "license": "MIT",
       "dependencies": {
-        "@xmldom/xmldom": "^0.8.8",
+        "@xmldom/xmldom": "^0.9.10",
         "base64-js": "^1.5.1",
         "xmlbuilder": "^15.1.1"
       },
@@ -1205,30 +1278,29 @@
         "node": ">=8"
       }
     },
-    "node_modules/tar": {
-      "version": "6.2.1",
-      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
-      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+    "node_modules/supercluster": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
+      "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
       "license": "ISC",
       "dependencies": {
-        "chownr": "^2.0.0",
-        "fs-minipass": "^2.0.0",
-        "minipass": "^5.0.0",
-        "minizlib": "^2.1.1",
-        "mkdirp": "^1.0.3",
-        "yallist": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=10"
+        "kdbush": "^4.0.2"
       }
     },
-    "node_modules/tar/node_modules/minipass": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
-      "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
-      "license": "ISC",
+    "node_modules/tar": {
+      "version": "7.5.19",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz",
+      "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==",
+      "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": ">=8"
+        "node": ">=18"
       }
     },
     "node_modules/through2": {
@@ -1367,10 +1439,13 @@
       }
     },
     "node_modules/yallist": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
-      "license": "ISC"
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+      "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=18"
+      }
     },
     "node_modules/yauzl": {
       "version": "2.10.0",

+ 7 - 3
src-capacitor/package.json

@@ -1,20 +1,24 @@
 {
-  "name": "quasar-skeleton",
+  "name": "diarista-prestador-app",
   "version": "0.0.1",
-  "description": "A skeleton for future projects",
-  "author": "Denis <denis.gnl@gmail.com>",
+  "description": "Aplicativo para o prestador do Diarista",
+  "author": "zntt <zanattagg@gmail.com>",
   "private": true,
   "dependencies": {
     "@capacitor/android": "^7.2.0",
     "@capacitor/app": "^7.0.0",
     "@capacitor/browser": "^8.0.3",
+    "@capacitor/camera": "^7.0.5",
     "@capacitor/cli": "^8.3.3",
     "@capacitor/core": "^7.0.0",
     "@capacitor/device": "^7.0.1",
+    "@capacitor/geolocation": "^8.2.0",
+    "@capacitor/google-maps": "^8.0.1",
     "@capacitor/ios": "^7.2.0",
     "@capacitor/keyboard": "^7.0.1",
     "@capacitor/preferences": "^7.0.1",
     "@capacitor/push-notifications": "^8.1.1",
+    "@capacitor/splash-screen": "^7.0.5",
     "@capacitor/status-bar": "^7.0.1"
   }
 }

+ 3 - 1
src-capacitor/www/index.html

@@ -1 +1,3 @@
-<!DOCTYPE html><html><head></head><body></body></html>
+<!DOCTYPE html><html><head><title>Diarista Profissional App</title><meta charset=utf-8><meta name=description content="Aplicativo para o prestador do Diarista"><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width,viewport-fit=cover"><link rel=icon type=image/png sizes=128x128 href=icons/favicon-128x128.png><link rel=icon type=image/png sizes=96x96 href=icons/favicon-96x96.png><link rel=icon type=image/png sizes=32x32 href=icons/favicon-32x32.png><link rel=icon type=image/png sizes=16x16 href=icons/favicon-16x16.png><link rel=icon type=image/ico href=favicon.ico>  <script type="module" crossorigin src="./assets/index-BFdxlQrK.js"></script>
+  <link rel="stylesheet" crossorigin href="./assets/index-DNrVWvRO.css">
+</head><body><div id=q-app></div></body></html>

+ 4 - 17
src/api/providerPaymentMethod.js

@@ -1,21 +1,8 @@
 import api from "src/api";
 
-export const getProviderPaymentMethods = async (providerId) => {
-  const { data } = await api.get(`/provider/payment-methods/${providerId}`);
-  return data.payload;
-};
-
-export const createProviderPaymentMethod = async (info) => {
-  const { data } = await api.post("/provider/payment-method", info);
-  return data.payload;
-};
-
-export const updateProviderPaymentMethod = async (id, info) => {
-  const { data } = await api.put(`/provider/payment-method/${id}`, info);
-  return data.payload;
-};
-
-export const deleteProviderPaymentMethod = async (id) => {
-  const { data } = await api.delete(`/provider/payment-method/${id}`);
+export const updateProviderBankAccount = async (providerId, bankAccount) => {
+  const { data } = await api.patch(`/provider/${providerId}/bank-account`, {
+    recipient_default_bank_account: bankAccount,
+  });
   return data.payload;
 };

+ 6 - 0
src/api/supportRequest.js

@@ -0,0 +1,6 @@
+import api from 'src/api';
+
+export const createSupportRequest = async (payload) => {
+  const { data } = await api.post('/support-request', payload);
+  return data.payload;
+};

+ 1 - 1
src/components/dashboard/DashboardHeaderBar.vue

@@ -84,7 +84,7 @@ const goToNotifications = () => {
 
 <style scoped lang="scss">
 .dashboard-header {
-  padding-top: calc(env(safe-area-inset-top) + 8px);
+  padding-top: 8px;
   width: 100%;
   box-sizing: border-box;
 }

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

@@ -17,15 +17,22 @@ import Banner1 from 'src/assets/banner_1.svg';
 import Banner2 from 'src/assets/banner_2.svg';
 
 import { useRouter } from 'vue-router';
+import { useQuasar } from 'quasar';
+import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue';
 
 const router = useRouter();
+const $q = useQuasar();
 
 const cards = [
   { id: 1, image: Banner1, alt: 'Diária sob medida', routeName: 'OpportunitiesPage' },
-  { id: 2, image: Banner2, alt: 'Escolha profissionais' },
+  { id: 2, image: Banner2, alt: 'Como aceitar uma diária', action: 'help' },
 ];
 
 const goToCard = (card) => {
+  if (card.action === 'help') {
+    $q.dialog({ component: ProfileHelpDialog })
+    return
+  }
   if (card.routeName) {
     router.push({ name: card.routeName })
   }

+ 2 - 2
src/components/login/LoginStepFivePanel.vue

@@ -90,8 +90,8 @@ const priceError = computed(() => {
     return 'Valor inválido';
   }
 
-  if (numericValue < 100) {
-    return 'Valor mínimo R$ 100,00';
+  if (numericValue < 3) {
+    return 'Valor mínimo R$ 3,00';
   }
 
   if (numericValue > 500) {

+ 184 - 290
src/components/login/LoginStepFourPanel.vue

@@ -1,31 +1,43 @@
 <template>
   <q-card-section class="no-padding">
     <div class="column items-center q-gutter-y-xl q-pt-md">
-
       <div class="column items-center q-gutter-y-sm">
         <div
           class="photo-circle"
           :class="selfieDone ? 'photo-circle--done' : 'photo-circle--pending'"
-          @click="subStep = 'selfie_camera'"
+          @click="takeSelfie"
         >
-          <q-icon name="photo_camera" size="52px" color="white" />
+          <q-icon
+            color="white"
+            name="photo_camera" 
+            size="52px"
+          />
         </div>
 
         <template v-if="!selfieDone">
           <q-btn
             color="primary-button"
-            :label="t('provider.login.steps.step_4.btn_selfie')"
-            rounded
             no-caps
             padding="10px 28px"
-            @click="subStep = 'selfie_camera'"
+            rounded
+            :label="t('provider.login.steps.step_4.btn_selfie')"
+            :loading="loadingSelfie"
+            @click="takeSelfie"
           />
           <div class="text-text text-center font12">{{ t('provider.login.steps.step_4.selfie_hint') }}</div>
         </template>
+
         <template v-else>
           <div class="row items-center q-gutter-x-xs">
-            <q-icon name="check_circle" color="positive" size="18px" />
-            <span class="text-primary">{{ t('provider.login.steps.step_4.selfie_sent') }}</span>
+            <q-icon
+              color="positive"
+              name="check_circle"
+              size="18px"
+            />
+
+            <span class="text-primary">
+              {{ t('provider.login.steps.step_4.selfie_sent') }}
+            </span>
           </div>
         </template>
       </div>
@@ -34,140 +46,109 @@
         <div
           class="photo-circle"
           :class="docDone ? 'photo-circle--done' : 'photo-circle--pending'"
-          @click="subStep = 'doc_front_camera'"
+          @click="takeDocFront"
         >
-          <q-icon name="mdi-card-account-details-outline" size="52px" color="white" />
+          <q-icon
+            color="white"
+            name="mdi-card-account-details-outline"
+            size="52px"
+          />
         </div>
 
         <template v-if="!docDone">
           <q-btn
             color="primary-button"
-            :label="t('provider.login.steps.step_4.btn_document')"
-            rounded
             no-caps
             padding="10px 28px"
-            @click="subStep = 'doc_front_camera'"
+            rounded
+            :label="t('provider.login.steps.step_4.btn_document')"
+            :loading="loadingDoc"
+            @click="takeDocFront"
           />
-          <div class="text-text text-center font12">{{ t('provider.login.steps.step_4.document_hint') }}</div>
+
+          <div class="text-text text-center font12">
+            {{ t('provider.login.steps.step_4.document_hint') }}
+          </div>
         </template>
+
         <template v-else>
           <div class="row items-center q-gutter-x-xs">
-            <q-icon name="check_circle" color="positive" size="18px" />
-            <span class="text-primary">{{ t('provider.login.steps.step_4.document_sent') }}</span>
+            <q-icon
+              color="positive"
+              name="check_circle"
+              size="18px"
+            />
+
+            <span class="text-primary">
+              {{ t('provider.login.steps.step_4.document_sent') }}
+            </span>
           </div>
         </template>
       </div>
-
     </div>
 
     <Teleport to="body">
-      <div v-if="subStep !== 'main'" class="fs-overlay">
-        <div v-if="isCameraState" class="fs-camera">
-          <div class="fs-header">
-            <q-btn flat dense round icon="chevron_left" color="black" @click="goBack" />
-            <span class="font14">{{ t('common.actions.back') }}</span>
-          </div>
-          <div class="fs-viewport">
-            <video ref="videoRef" autoplay playsinline muted class="fs-video" />
-            <div v-if="subStep === 'selfie_camera'" class="guide-oval" />
-            <div v-else class="guide-doc" />
-          </div>
-          <div class="fs-footer">
-            <q-btn
-              color="primary-button"
-              :label="captureLabel"
-              rounded
-              no-caps
-              padding="14px 32px"
-              class="full-width"
-              @click="capturePhoto"
-            />
-          </div>
-        </div>
-
-        <div v-else-if="isPreviewState" class="fs-camera">
-          <div class="fs-header">
-            <q-btn flat dense round icon="chevron_left" color="black" @click="goBack" />
-            <span class="font14">{{ t('common.actions.back') }}</span>
-          </div>
-          <div class="fs-viewport">
-            <img :src="currentPreview" class="fs-video fs-preview-img" />
-            <div v-if="subStep === 'selfie_preview'" class="guide-oval" />
-            <div v-else class="guide-doc" />
-          </div>
-          <div class="fs-footer column q-gutter-y-sm">
-            <q-btn
-              outline
-              color="primary-button"
-              :label="t('provider.login.steps.step_4.btn_retake')"
-              rounded
-              no-caps
-              padding="12px 32px"
-              class="full-width"
-              @click="retakePhoto"
-            />
-            <q-btn
-              color="primary-button"
-              :label="subStep === 'selfie_preview' ? t('provider.login.steps.step_4.btn_confirm') : t('provider.login.steps.step_4.btn_use_photo')"
-              rounded
-              no-caps
-              padding="12px 32px"
-              class="full-width"
-              @click="usePhoto"
-            />
-          </div>
-        </div>
-
-        <div v-else class="fs-result">
-          <q-img :src="LogoDiaria" class="q-mb-xl" style="max-width: 150px" />
+      <div v-if="showResult" class="fs-overlay">
+        <div class="fs-result">
+          <q-img
+            class="q-mb-xl"
+            style="max-width: 150px"
+            :src="LogoDiaria"
+          />
 
           <div
             class="result-circle"
-            :class="docResult === 'error' ? 'result-circle--error' : 'result-circle--done'"
+            :class="resultError ? 'result-circle--error' : 'result-circle--done'"
           >
             <q-icon
-              :name="subStep === 'selfie_result' ? 'photo_camera' : 'mdi-card-account-details-outline'"
-              size="56px"
               color="white"
+              size="56px"
+              :name="resultIcon"
             />
           </div>
 
-          <template v-if="docResult === 'error'">
-            <q-icon name="cancel" color="negative" size="28px" class="q-mt-md" />
+          <template v-if="resultError">
+            <q-icon
+              class="q-mt-md" 
+              color="negative" 
+              name="cancel"
+              size="28px"
+            />
+
             <div class="text-primary text-center q-mt-xs q-px-lg font14">
               {{ t('provider.login.steps.step_4.error_message') }}
             </div>
           </template>
+
           <div v-else class="text-primary text-center q-mt-md q-px-lg font14">
-            {{ subStep === 'selfie_result' ? t('provider.login.steps.step_4.selfie_success') : t('provider.login.steps.step_4.document_success') }}
+            {{ resultMessage }}
           </div>
 
           <q-btn
+            class="q-mt-xl"
             color="primary-button"
-            :label="docResult === 'error' ? t('provider.login.steps.step_4.btn_retry') : t('provider.login.steps.step_4.btn_continue')"
-            rounded
             no-caps
             padding="14px 48px"
-            class="q-mt-xl"
+            rounded
             style="min-width: 200px"
-            @click="handleResult"
+            :label="resultError ? t('provider.login.steps.step_4.btn_retry') : t('provider.login.steps.step_4.btn_continue')"
+            @click="closeResult"
           />
 
-          <div v-if="subStep === 'doc_result' && docResult !== 'error'" class="text-grey-6 text-center q-mt-sm font12">
+          <div v-if="!resultError && showDocHint" class="text-grey-6 text-center q-mt-sm font12">
             {{ t('provider.login.steps.step_4.document_hint_result') }}
           </div>
         </div>
-
       </div>
     </Teleport>
-
-    <canvas ref="canvasRef" style="display: none" />
   </q-card-section>
 </template>
 
 <script setup>
-import { ref, computed, watch, onUnmounted, nextTick } from 'vue';
+import { Camera, CameraDirection, CameraResultType, CameraSource } from '@capacitor/camera';
+import { computed, ref } from 'vue';
 import { useI18n } from 'vue-i18n';
+
 import LogoDiaria from 'src/assets/logo_diaria_campos_login.svg';
 
 const { t } = useI18n();
@@ -175,168 +156,144 @@ const { t } = useI18n();
 const form = defineModel({ type: Object, required: true });
 const emit = defineEmits(['update:show-sub-step']);
 
-const subStep = ref('main');
-const docResult = ref(null); // null | 'success' | 'error'
+const loadingSelfie = ref(false);
+const loadingDoc = ref(false);
 
-const tempDocFront = ref(null);
-const tempDocBack = ref(null);
-const tempSelfie = ref(null);
+const showResult = ref(false);
+const resultError = ref(false);
+const resultIcon = ref('photo_camera');
+const resultMessage = ref('');
+const showDocHint = ref(false);
 
-const tempSelfieFile = ref(null);
+// foto de documento frente — aguarda verso antes de fechar
 const tempDocFrontFile = ref(null);
-const tempDocBackFile = ref(null);
 
-const stream = ref(null);
-const videoRef = ref(null);
-const canvasRef = ref(null);
+const docDone = computed(() => !!form.value.document_front && !!form.value.document_back);
+const selfieDone = computed(() => !!form.value.selfie);
 
-const CAMERA_STATES = ['selfie_camera', 'doc_front_camera', 'doc_back_camera'];
-const PREVIEW_STATES = ['selfie_preview', 'doc_front_preview', 'doc_back_preview'];
+const base64ToFile = (base64, filename, mimeType = 'image/jpeg') => {
+  const byteString = atob(base64);
 
-const isCameraState = computed(() => CAMERA_STATES.includes(subStep.value));
-const isPreviewState = computed(() => PREVIEW_STATES.includes(subStep.value));
+  const ab = new ArrayBuffer(byteString.length);
 
-const selfieDone = computed(() => !!form.value.selfie);
-const docDone = computed(() => !!form.value.document_front && !!form.value.document_back);
+  const ia = new Uint8Array(ab);
 
-const currentPreview = computed(() => {
-  if (subStep.value === 'selfie_preview') return tempSelfie.value;
-  if (subStep.value === 'doc_front_preview') return tempDocFront.value;
-  if (subStep.value === 'doc_back_preview') return tempDocBack.value;
-  return null;
-});
-
-const captureLabel = computed(() => {
-  if (subStep.value === 'selfie_camera') return t('provider.login.steps.step_4.btn_capture_selfie');
-  if (subStep.value === 'doc_front_camera') return t('provider.login.steps.step_4.btn_capture_front');
-  return t('provider.login.steps.step_4.btn_capture_back');
-});
-
-const stopCamera = () => {
-  if (stream.value) {
-    stream.value.getTracks().forEach((t) => t.stop());
-    stream.value = null;
-  }
-  if (videoRef.value) {
-    videoRef.value.srcObject = null;
+  for (let i = 0; i < byteString.length; i++) {
+    ia[i] = byteString.charCodeAt(i);
   }
+
+  return new File([ab], filename, { type: mimeType });
 };
 
-const startCameraStream = async (facingMode) => {
-  try {
-    stopCamera();
-    stream.value = await navigator.mediaDevices.getUserMedia({
-      video: { facingMode, width: { ideal: 1280 }, height: { ideal: 720 } },
-    });
-    await nextTick();
-    if (videoRef.value) {
-      videoRef.value.srcObject = stream.value;
-      await videoRef.value.play().catch(() => {});
-    }
-    return true;
-  } catch {
-    stopCamera();
-    return false;
+const closeResult = () => {
+  showResult.value = false;
+
+  emit('update:show-sub-step', false);
+
+  if (resultError.value) {
+    resultError.value = false;
   }
 };
 
-watch(subStep, async (newStep) => {
-  emit('update:show-sub-step', newStep !== 'main');
+const openCamera = async (direction) => {
+  const photo = await Camera.getPhoto({
+    resultType: CameraResultType.Base64,
+    source: CameraSource.Camera,
+    quality: 85,
+    direction: direction === 'front' ? CameraDirection.Front : CameraDirection.Rear,
+    allowEditing: false,
+    saveToGallery: false,
+  });
+
+  return base64ToFile(photo.base64String.replace(/\s/g, ''), 'photo.jpg', `image/${photo.format}`);
+};
 
-  if (!CAMERA_STATES.includes(newStep)) {
-    stopCamera();
-  }
+const takeDocFront = async () => {
+  if (loadingDoc.value) return;
+
+  loadingDoc.value = true;
+
+  try {
+    const file = await openCamera('rear');
 
-  if (newStep === 'selfie_camera') {
-    const ok = await startCameraStream('user');
-    if (!ok) subStep.value = 'main';
-  } else if (newStep === 'doc_front_camera' || newStep === 'doc_back_camera') {
-    const ok = await startCameraStream('environment');
-    if (!ok) {
-      docResult.value = 'error';
-      subStep.value = 'doc_result';
+    tempDocFrontFile.value = file;
+
+    await takeDocBack();
+  } catch (err) {
+    const msg = (err?.message || '').toLowerCase();
+
+    if (!msg.includes('cancel') && !msg.includes('dismiss') && !msg.includes('no image')) {
+      resultIcon.value = 'mdi-card-account-details-outline';
+      resultError.value = true;
+      showDocHint.value = false;
+      showResult.value = true;
+
+      emit('update:show-sub-step', true);
     }
+  } finally {
+    loadingDoc.value = false;
   }
-});
-
-onUnmounted(stopCamera);
-
-const capturePhoto = () => {
-  const video = videoRef.value;
-  const canvas = canvasRef.value;
-  if (!video || !canvas) return;
-
-  canvas.width = video.videoWidth || 1280;
-  canvas.height = video.videoHeight || 720;
-  canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
-
-  const capturing = subStep.value;
-  canvas.toBlob((blob) => {
-    const file = new File([blob], 'photo.jpg', { type: 'image/jpeg' });
-    const preview = URL.createObjectURL(blob);
-
-    if (capturing === 'selfie_camera') {
-      if (tempSelfie.value) URL.revokeObjectURL(tempSelfie.value);
-      tempSelfie.value = preview;
-      tempSelfieFile.value = file;
-      subStep.value = 'selfie_preview';
-    } else if (capturing === 'doc_front_camera') {
-      if (tempDocFront.value) URL.revokeObjectURL(tempDocFront.value);
-      tempDocFront.value = preview;
-      tempDocFrontFile.value = file;
-      subStep.value = 'doc_front_preview';
-    } else if (capturing === 'doc_back_camera') {
-      if (tempDocBack.value) URL.revokeObjectURL(tempDocBack.value);
-      tempDocBack.value = preview;
-      tempDocBackFile.value = file;
-      subStep.value = 'doc_back_preview';
-    }
-  }, 'image/jpeg', 0.85);
 };
 
-const usePhoto = () => {
-  if (subStep.value === 'selfie_preview') {
-    form.value.selfie = tempSelfieFile.value;
-    subStep.value = 'selfie_result';
-  } else if (subStep.value === 'doc_front_preview') {
-    subStep.value = 'doc_back_camera';
-  } else if (subStep.value === 'doc_back_preview') {
+const takeDocBack = async () => {
+  try {
+    const file = await openCamera('rear');
+
     form.value.document_front = tempDocFrontFile.value;
-    form.value.document_back = tempDocBackFile.value;
-    docResult.value = 'success';
-    subStep.value = 'doc_result';
+    form.value.document_back = file;
+    tempDocFrontFile.value = null;
+    resultIcon.value = 'mdi-card-account-details-outline';
+    resultMessage.value = t('provider.login.steps.step_4.document_success');
+    resultError.value = false;
+    showDocHint.value = true;
+    showResult.value = true;
+
+    emit('update:show-sub-step', true);
+  } catch (err) {
+    const msg = (err?.message || '').toLowerCase();
+
+    if (!msg.includes('cancel') && !msg.includes('dismiss') && !msg.includes('no image')) {
+      resultIcon.value = 'mdi-card-account-details-outline';
+      resultError.value = true;
+      showDocHint.value = false;
+      showResult.value = true;
+
+      emit('update:show-sub-step', true);
+    }
+
+    tempDocFrontFile.value = null;
   }
 };
 
-const retakePhoto = () => {
-  const retakeMap = {
-    selfie_preview: 'selfie_camera',
-    doc_front_preview: 'doc_front_camera',
-    doc_back_preview: 'doc_back_camera',
-  };
-  subStep.value = retakeMap[subStep.value] || 'main';
-};
+const takeSelfie = async () => {
+  if (loadingSelfie.value) return;
 
-const goBack = () => {
-  const backMap = {
-    selfie_camera: 'main',
-    selfie_preview: 'selfie_camera',
-    doc_front_camera: 'main',
-    doc_front_preview: 'doc_front_camera',
-    doc_back_camera: 'doc_front_preview',
-    doc_back_preview: 'doc_back_camera',
-  };
-  subStep.value = backMap[subStep.value] || 'main';
-};
+  loadingSelfie.value = true;
 
-const handleResult = () => {
-  if (docResult.value === 'error') {
-    docResult.value = null;
-    tempDocFront.value = null;
-    tempDocBack.value = null;
-    subStep.value = 'doc_front_camera';
-  } else {
-    subStep.value = 'main';
+  try {
+    const file = await openCamera('front');
+
+    form.value.selfie = file;
+    resultIcon.value = 'photo_camera';
+    resultMessage.value = t('provider.login.steps.step_4.selfie_success');
+    resultError.value = false;
+    showDocHint.value = false;
+    showResult.value = true;
+
+    emit('update:show-sub-step', true);
+  } catch (err) {
+    const msg = (err?.message || '').toLowerCase();
+
+    if (!msg.includes('cancel') && !msg.includes('dismiss') && !msg.includes('no image')) {
+      resultIcon.value = 'photo_camera';
+      resultError.value = true;
+      showDocHint.value = false;
+      showResult.value = true;
+
+      emit('update:show-sub-step', true);
+    }
+  } finally {
+    loadingSelfie.value = false;
   }
 };
 </script>
@@ -370,69 +327,6 @@ const handleResult = () => {
   background: white;
 }
 
-.fs-camera {
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-}
-
-.fs-header {
-  display: flex;
-  align-items: center;
-  gap: 4px;
-  padding: 8px 12px;
-  background: white;
-  flex-shrink: 0;
-}
-
-.fs-viewport {
-  flex: 1;
-  background: #000;
-  position: relative;
-  overflow: hidden;
-}
-
-.fs-video {
-  width: 100%;
-  height: 100%;
-  object-fit: cover;
-  display: block;
-}
-
-.fs-preview-img {
-  object-fit: contain;
-}
-
-.fs-footer {
-  padding: 16px 20px;
-  background: white;
-  flex-shrink: 0;
-}
-
-.guide-oval {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  width: 62%;
-  height: 70%;
-  border: 3px solid #8b5cf6;
-  border-radius: 50%;
-  pointer-events: none;
-}
-
-.guide-doc {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
-  width: 86%;
-  height: 52%;
-  border: 2px solid rgba(255, 255, 255, 0.65);
-  border-radius: 14px;
-  pointer-events: none;
-}
-
 .fs-result {
   height: 100%;
   display: flex;

+ 3 - 0
src/components/login/LoginStepThreePanel.vue

@@ -66,6 +66,7 @@
       :rules="[inputRules.required, validateRG]"
       lazy-rules
       mask="##.###.###-#"
+      type="tel"
     />
 
     <div class="text-text">
@@ -83,6 +84,7 @@
       :rules="[inputRules.required, inputRules.cpf]"
       lazy-rules
       mask="###.###.###-##"
+      type="tel"
     />
 
     <div class="text-text">
@@ -117,6 +119,7 @@
       :rules="[inputRules.required, inputRules.cep]"
       lazy-rules
       mask="#####-###"
+      type="tel"
       :loading="loadingCep"
       @update:model-value="onCepChange"
     />

+ 4 - 3
src/components/profile/ProfileAddressDialog.vue

@@ -1,7 +1,7 @@
 <template>
   <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
     <div class="bg-page full-height no-shadow">
-      <div class="row items-center q-pa-md q-pb-sm bg-white shadow-profile bg-surface">
+      <div class="row items-center q-px-md q-pb-sm bg-white shadow-profile bg-surface dialog-safe-header">
         <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
         <span class="font16 fontbold gradient-diarista">{{ $t('profile.address.title') }}</span>
@@ -197,6 +197,7 @@ const openMapDialog = async () => {
   let initialLng = null;
 
   const hasAddress = form.address || form.zip_code;
+
   if (hasAddress) {
     geocodingCep.value = true;
     try {
@@ -212,8 +213,8 @@ const openMapDialog = async () => {
         initialLat = geo.lat;
         initialLng = geo.lng;
       }
-    } catch {
-      // fallback to default
+    } catch (err) {
+      console.error('geocodeFullAddress falhou, usando coordenada default:', err);
     } finally {
       geocodingCep.value = false;
     }

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

@@ -2,7 +2,7 @@
   <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
     <div class="bg-page full-height column no-shadow">
 
-      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
+      <div class="row items-center q-px-md q-pb-sm bg-white shadow-profile bg-surface dialog-safe-header">
         <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
         <span class="gradient-diarista font16 fontbold">{{ $t('profile.availability.title') }}</span>

+ 102 - 137
src/components/profile/ProfileBankDataDialog.vue

@@ -1,7 +1,7 @@
 <template>
   <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
     <div class="bg-page full-height no-shadow">
-      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
+      <div class="row items-center q-px-md q-pb-sm bg-white shadow-profile bg-surface dialog-safe-header">
         <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
         <span class="gradient-diarista font16 fontbold">{{ $t('profile.bank_data.title') }}</span>
@@ -22,18 +22,17 @@
             </div>
           </div>
 
-
-
           <q-card class="q-pa-md bg-white shadow-card q-mb-md" style="border-radius: 20px;" :flat="false">
             <div class="text-center text-text q-mb-sm font14 fontbold">
               <div>
                 {{ $t('profile.bank_data.pix_title') }}
+                <q-badge color="grey-5" class="q-ml-xs" :label="$t('profile.bank_data.in_development')" />
               </div>
             </div>
             <q-input
-              v-model="pixForm.pix_key"
               outlined
               dense
+              disable
               input-class="text-text"
               :placeholder="$t('profile.bank_data.pix_key_placeholder')"
               class="q-mb-sm"
@@ -43,24 +42,35 @@
               unelevated
               rounded
               no-caps
-              color="primary"
+              color="grey-4"
+              text-color="grey-7"
               class="full-width"
               padding="4px 12px"
-              :label="pixAlreadyRegistered ? $t('profile.bank_data.att_pix') : $t('profile.bank_data.save_pix')"
-              :loading="savingPix"
-              :disable="!hasPixUpdatedFields"
-              @click="savePix"
+              disable
+              :label="$t('profile.bank_data.in_development')"
             />
           </q-card>
 
-
           <q-card class="q-pa-md bg-white row shadow-card q-mb-md" style="border-radius: 20px;" :flat="false">
             <div class="text-center text-text q-mb-sm col-12 font14 fontbold">
               {{ $t('profile.bank_data.bank_account_title') }}
             </div>
+
+            <q-input
+              v-model="bankForm.bank"
+              outlined
+              dense
+              inputmode="numeric"
+              maxlength="3"
+              input-class="text-text"
+              :label="$t('profile.bank_data.bank_code')"
+              :placeholder="$t('profile.bank_data.bank_code_placeholder')"
+              class="q-mb-sm col-12"
+            />
+
             <div class="row q-mb-sm q-my-auto col-12 row items-center">
               <q-radio
-                v-model="bankForm.bank_account_type"
+                v-model="bankForm.type"
                 val="checking"
                 :label="$t('profile.bank_data.checking')"
                 color="primary"
@@ -68,7 +78,7 @@
                 class="q-mr-lg text-text"
               />
               <q-radio
-                v-model="bankForm.bank_account_type"
+                v-model="bankForm.type"
                 val="savings"
                 :label="$t('profile.bank_data.savings')"
                 color="primary"
@@ -77,32 +87,47 @@
               />
             </div>
 
-            <q-input
-              v-model="bankForm.agency"
-              outlined
-              dense
-              input-class="text-text"
-              :placeholder="$t('profile.bank_data.agency')"
-              class="q-mb-sm col-12"
-            />
-
-            <q-input
-              v-model="bankForm.account"
-              outlined
-              dense
-              input-class="text-text"
-              :placeholder="$t('profile.bank_data.account')"
-              class="q-mb-sm col-12"
-            />
+            <div class="row col-12 q-col-gutter-sm q-mb-sm">
+              <q-input
+                v-model="bankForm.branch_number"
+                outlined
+                dense
+                inputmode="numeric"
+                input-class="text-text"
+                :label="$t('profile.bank_data.agency')"
+                class="col-8"
+              />
+              <q-input
+                v-model="bankForm.branch_check_digit"
+                outlined
+                dense
+                maxlength="2"
+                input-class="text-text"
+                :label="$t('profile.bank_data.agency_digit')"
+                class="col-4"
+              />
+            </div>
 
-            <q-input
-              v-model="bankForm.digit"
-              outlined
-              dense
-              input-class="text-text"
-              :placeholder="$t('profile.bank_data.digit')"
-              class="q-mb-sm col-3"
-            />
+            <div class="row col-12 q-col-gutter-sm q-mb-sm">
+              <q-input
+                v-model="bankForm.account_number"
+                outlined
+                dense
+                inputmode="numeric"
+                input-class="text-text"
+                :label="$t('profile.bank_data.account')"
+                class="col-8"
+              />
+              <q-input
+                v-model="bankForm.account_check_digit"
+                outlined
+                dense
+                maxlength="2"
+                input-class="text-text"
+                :label="$t('profile.bank_data.account_digit')"
+                class="col-4"
+              />
+            </div>
 
             <q-btn
               unelevated
@@ -113,7 +138,7 @@
               class="full-width"
               :label="bankAlreadyRegistered ? $t('profile.bank_data.att_bank') : $t('profile.bank_data.save_account')"
               :loading="savingBank"
-              :disable="!hasBankUpdatedFields"
+              :disable="!canSaveBank"
               @click="saveBank"
             />
           </q-card>
@@ -125,15 +150,10 @@
 </template>
 
 <script setup>
-import { ref, onMounted } from 'vue';
+import { ref, reactive, computed, onMounted } from 'vue';
 import { useDialogPluginComponent, useQuasar } from 'quasar';
-import {
-  getProviderPaymentMethods,
-  createProviderPaymentMethod,
-  updateProviderPaymentMethod,
-} from 'src/api/providerPaymentMethod';
+import { updateProviderBankAccount } from 'src/api/providerPaymentMethod';
 import { userStore } from 'src/stores/user';
-import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker';
 import { useI18n } from 'vue-i18n';
 
 defineEmits([...useDialogPluginComponent.emits]);
@@ -143,89 +163,48 @@ const $q = useQuasar();
 const { t } = useI18n();
 const user = userStore();
 
-const pixId = ref(null);
-const bankId = ref(null);
-const savingPix = ref(false);
 const savingBank = ref(false);
-const pixAlreadyRegistered = ref(false);
 const bankAlreadyRegistered = ref(false);
-const {
-  form: pixForm,
-  hasUpdatedFields: hasPixUpdatedFields,
-  getUpdatedFields: getPixUpdatedFields,
-  setUpdateFormAsOriginal: setPixOriginal,
-} = useFormUpdateTracker({
-  pix_key: '',
-});
 
-const {
-  form: bankForm,
-  hasUpdatedFields: hasBankUpdatedFields,
-  getUpdatedFields: getBankUpdatedFields,
-  setUpdateFormAsOriginal: setBankOriginal,
-} = useFormUpdateTracker({
-  bank_account_type: 'checking',
-  agency: '',
-  account: '',
-  digit: '',
+const bankForm = reactive({
+  bank: '',
+  type: 'checking',
+  branch_number: '',
+  branch_check_digit: '',
+  account_number: '',
+  account_check_digit: '',
 });
 
-const savePix = async () => {
-  if (!pixForm.pix_key) {
-    $q.notify({ type: 'negative', message: t('profile.bank_data.pix_key_required') });
-    return;
-  }
-
-  savingPix.value = true;
-  try {
-    const payload = {
-      provider_id: user.user.provider.id,
-      account_type: 'pix',
-      pix_key: pixForm.pix_key,
-    };
-
-    if (pixId.value) {
-      await updateProviderPaymentMethod(pixId.value, getPixUpdatedFields.value);
-    } else {
-      const created = await createProviderPaymentMethod(payload);
-      pixId.value = created.id;
-    }
-
-    setPixOriginal();
-    $q.notify({ type: 'positive', message: t('profile.bank_data.pix_saved') });
-  } catch (error) {
-    $q.notify({ type: 'negative', message: t('profile.bank_data.save_error') });
-    console.log(error);
-  } finally {
-    savingPix.value = false;
-  }
-};
+const canSaveBank = computed(
+  () =>
+    !!bankForm.bank &&
+    !!bankForm.branch_number &&
+    !!bankForm.account_number &&
+    !!bankForm.account_check_digit,
+);
 
 const saveBank = async () => {
-  if (!bankForm.agency || !bankForm.account) {
+  if (!canSaveBank.value) {
     $q.notify({ type: 'negative', message: t('profile.bank_data.bank_fields_required') });
     return;
   }
 
   savingBank.value = true;
   try {
-    const payload = {
-      provider_id: user.user.provider.id,
-      account_type: 'bank_account',
-      bank_account_type: bankForm.bank_account_type,
-      agency: bankForm.agency,
-      account: bankForm.account,
-      digit: bankForm.digit,
+    // Holder (nome/CPF) é derivado do prestador no backend; aqui só os campos digitados.
+    const bankAccount = {
+      bank: bankForm.bank,
+      type: bankForm.type,
+      branch_number: bankForm.branch_number,
+      branch_check_digit: bankForm.branch_check_digit || null,
+      account_number: bankForm.account_number,
+      account_check_digit: bankForm.account_check_digit,
     };
 
-    if (bankId.value) {
-      await updateProviderPaymentMethod(bankId.value, getBankUpdatedFields.value);
-    } else {
-      const created = await createProviderPaymentMethod(payload);
-      bankId.value = created.id;
-    }
+    await updateProviderBankAccount(user.user.provider.id, bankAccount);
+    await user.fetchUser();
 
-    setBankOriginal();
+    bankAlreadyRegistered.value = true;
     $q.notify({ type: 'positive', message: t('profile.bank_data.account_saved') });
   } catch (error) {
     $q.notify({ type: 'negative', message: t('profile.bank_data.save_error') });
@@ -235,31 +214,17 @@ const saveBank = async () => {
   }
 };
 
-onMounted(async () => {
-  try {
-    const methods = await getProviderPaymentMethods(user.user.provider.id);
-    if (!methods) return;
-
-    const pix = methods.find((m) => m.account_type === 'pix');
-    if (pix) {
-      pixId.value = pix.id;
-      pixForm.pix_key = pix.pix_key || '';
-      setPixOriginal();
-      pixAlreadyRegistered.value = true;
-    }
-
-    const bank = methods.find((m) => m.account_type === 'bank_account');
-    if (bank) {
-      bankId.value = bank.id;
-      bankForm.bank_account_type = bank.bank_account_type || 'checking';
-      bankForm.agency = bank.agency || '';
-      bankForm.account = bank.account || '';
-      bankForm.digit = bank.digit || '';
-      setBankOriginal();
-      bankAlreadyRegistered.value = true;
-    }
-  } catch (error) {
-    console.error('Erro ao carregar dados bancários:', error);
+onMounted(() => {
+  const bank = user.user?.provider?.recipient_default_bank_account;
+
+  if (bank && Object.keys(bank).length) {
+    bankForm.bank = bank.bank || '';
+    bankForm.type = bank.type || 'checking';
+    bankForm.branch_number = bank.branch_number || '';
+    bankForm.branch_check_digit = bank.branch_check_digit || '';
+    bankForm.account_number = bank.account_number || '';
+    bankForm.account_check_digit = bank.account_check_digit || '';
+    bankAlreadyRegistered.value = true;
   }
 });
 </script>

+ 37 - 10
src/components/profile/ProfileHelpDialog.vue

@@ -2,11 +2,22 @@
   <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
     <div class="bg-page full-height column no-shadow">
 
-      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
+      <div class="row items-center q-px-md q-pb-sm bg-white shadow-profile bg-surface dialog-safe-header">
         <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
         <span class="text-primary font16 fontbold gradient-diarista">{{ $t('profile.help.title') }}</span>
         <q-space />
+        <q-btn
+          flat
+          round
+          dense
+          icon="mdi-face-agent"
+          color="primary"
+          size="sm"
+          @click="openHumanSupport"
+        >
+          <q-tooltip>{{ $t('profile.help.suggestion_human') }}</q-tooltip>
+        </q-btn>
         <q-btn
           v-if="store.messages.length > 1"
           flat
@@ -17,7 +28,6 @@
           size="sm"
           @click="clearChat"
         />
-        <div v-else style="width: 32px"></div>
       </div>
 
       <div class="col overflow-auto">
@@ -79,11 +89,11 @@
             <div class="col-12 text-grey-6 q-mb-sm">{{ $t('profile.help.quick_suggestions') }}</div>
             <div
               v-for="suggestion in suggestions"
-              :key="suggestion"
+              :key="suggestion.value"
               class="row col-12 q-py-xs"
-              @click="sendMessage(suggestion)"
+              @click="onSuggestionClick(suggestion)"
             >
-              <span class="text-text bg-surface suggestion-btn font12 fontregular q-py-sm q-px-md cursor-pointer">{{ suggestion }}</span>
+              <span class="text-text bg-surface suggestion-btn font12 fontregular q-py-sm q-px-md cursor-pointer">{{ suggestion.label }}</span>
             </div>
           </div>
 
@@ -124,15 +134,17 @@
 
 <script setup>
 import { ref, computed, nextTick, onMounted } from 'vue';
-import { useDialogPluginComponent } from 'quasar';
+import { useDialogPluginComponent, useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
 import { chatbotStore } from 'src/stores/chatbot';
 import { sendChatMessage } from 'src/api/chatbot';
+import SupportRequestDialog from 'src/components/profile/SupportRequestDialog.vue';
 import diarinho_suporte from 'src/assets/diarinho_suporte.svg';
 
 defineEmits([...useDialogPluginComponent.emits]);
 
 const { dialogRef } = useDialogPluginComponent();
+const $q = useQuasar();
 const { t } = useI18n();
 const store = chatbotStore();
 
@@ -141,12 +153,26 @@ const loading = ref(false);
 const messagesContainer = ref(null);
 
 const suggestions = computed(() => [
-  t('profile.help.suggestion_cancel'),
-  t('profile.help.suggestion_data'),
-  t('profile.help.suggestion_payment'),
-  t('profile.help.suggestion_human'),
+  { label: t('profile.help.suggestion_cancel'), value: 'cancel' },
+  { label: t('profile.help.suggestion_data'), value: 'data' },
+  { label: t('profile.help.suggestion_payment'), value: 'payment' },
+  { label: t('profile.help.suggestion_human'), value: 'human' },
 ]);
 
+const onSuggestionClick = (suggestion) => {
+  if (suggestion.value === 'human') {
+    openHumanSupport();
+    return;
+  }
+  sendMessage(suggestion.label);
+};
+
+const openHumanSupport = () => {
+  $q.dialog({
+    component: SupportRequestDialog,
+  });
+};
+
 const scrollToBottom = async () => {
   await nextTick();
   if (messagesContainer.value) {
@@ -275,6 +301,7 @@ onMounted(() => {
 .chat-footer {
   border-top: 1px solid rgba(0, 0, 0, 0.06);
   flex-shrink: 0;
+  padding-bottom: env(safe-area-inset-bottom, 0px);
 }
 
 .shadow-up {

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

@@ -2,7 +2,7 @@
   <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
     <div class="bg-page full-height column no-shadow">
 
-      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
+      <div class="row items-center q-px-md q-pb-sm bg-white shadow-profile bg-surface dialog-safe-header">
         <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
         <span class="text-primary font16 fontbold gradient-diarista">{{ $t('profile.privacy.title') }}</span>

+ 2 - 2
src/components/profile/ProfileServiceDataDialog.vue

@@ -1,7 +1,7 @@
 <template>
   <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
     <div class="bg-page full-height column no-shadow">
-      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
+      <div class="row items-center q-px-md q-pb-sm bg-white shadow-profile bg-surface dialog-safe-header">
         <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
         <q-space />
         <span class="gradient-diarista font16 fontbold">{{ $t('profile.service_data.title') }}</span>
@@ -174,7 +174,7 @@ const priceError = computed(() => {
   const value = Number(form.value.daily_price_8h);
   if (!form.value.daily_price_8h && form.value.daily_price_8h !== 0) return '';
   if (Number.isNaN(value)) return t('validation.rules.required');
-  if (value < 100) return t('profile.service_data.price_min_error');
+  if (value < 3) return t('profile.service_data.price_min_error');
   if (value > 500) return t('profile.service_data.price_max_error');
   return '';
 });

+ 241 - 0
src/components/profile/SupportRequestDialog.vue

@@ -0,0 +1,241 @@
+<template>
+  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
+    <div class="bg-page full-height column no-shadow support-dialog">
+
+      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile bg-surface">
+        <q-btn v-close-popup icon="mdi-chevron-left" flat round dense color="primary" />
+        <q-space />
+        <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('support_request.title') }}</span>
+        <q-space />
+        <div style="width: 32px"></div>
+      </div>
+
+      <div class="col column no-wrap q-px-md q-pt-lg overflow-auto">
+        <span class="text-grey-7 q-mb-lg font12 fontregular" style="line-height: 1.4;">
+          {{ $t('support_request.subtitle') }}
+        </span>
+
+        <q-form ref="formRef" class="column no-wrap q-gutter-y-md support-form" @submit="onSubmit">
+          <div>
+            <div class="text-text font12 fontbold q-mb-xs">{{ $t('support_request.fields.category') }}</div>
+            <q-select
+              v-model="form.category"
+              :options="categoryOptions"
+              :rules="[inputRules.required]"
+              emit-value
+              map-options
+              outlined
+              dense
+              input-class="text-text"
+              popup-content-class="text-text"
+            />
+          </div>
+
+          <div>
+            <div class="text-text font12 fontbold q-mb-xs">{{ $t('support_request.fields.title') }}</div>
+            <q-input
+              v-model="form.title"
+              :rules="[inputRules.required, inputRules.max(150)]"
+              outlined
+              dense
+              counter
+              maxlength="150"
+              input-class="text-text"
+            />
+          </div>
+
+          <div>
+            <div class="text-text font12 fontbold q-mb-xs">{{ $t('support_request.fields.message') }}</div>
+            <q-input
+              v-model="form.message"
+              type="textarea"
+              :rules="[inputRules.required]"
+              outlined
+              autogrow
+              input-class="text-text"
+              :input-style="{ minHeight: '90px' }"
+            />
+          </div>
+
+          <div class="text-grey-6 font10 fontregular row items-center q-gutter-x-xs">
+            <q-icon name="mdi-information-outline" size="14px" />
+            <span>{{ $t('support_request.chat_attached_note') }}</span>
+          </div>
+
+          <q-separator class="q-my-sm" />
+
+          <span class="text-dark font12 fontbold">{{ $t('support_request.contact_title') }}</span>
+          <span class="text-grey-6 font10 fontregular" style="line-height: 1.4;">
+            {{ $t('support_request.contact_subtitle') }}
+          </span>
+
+          <div>
+            <div class="text-text font12 fontbold q-mb-xs">{{ $t('support_request.fields.name') }}</div>
+            <q-input
+              v-model="form.name"
+              outlined
+              dense
+              input-class="text-text"
+            />
+          </div>
+          <div>
+            <div class="text-text font12 fontbold q-mb-xs">{{ $t('support_request.fields.email') }}</div>
+            <q-input
+              v-model="form.email"
+              type="email"
+              :rules="form.email ? [inputRules.email] : []"
+              outlined
+              dense
+              input-class="text-text"
+            />
+          </div>
+          <div>
+            <div class="text-text font12 fontbold q-mb-xs">{{ $t('support_request.fields.phone') }}</div>
+            <q-input
+              v-model="form.phone"
+              outlined
+              dense
+              input-class="text-text"
+            />
+          </div>
+
+          <q-btn
+            type="submit"
+            unelevated
+            rounded
+            no-caps
+            color="primary"
+            class="full-width save-btn q-mt-md"
+            padding="10px 12px"
+            :label="$t('support_request.actions.send')"
+            :loading="loading"
+          />
+        </q-form>
+      </div>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { useDialogPluginComponent, useQuasar } from 'quasar';
+import { useI18n } from 'vue-i18n';
+import { useInputRules } from 'src/composables/useInputRules';
+import { userStore } from 'src/stores/user';
+import { chatbotStore } from 'src/stores/chatbot';
+import { createSupportRequest } from 'src/api/supportRequest';
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogOK } = useDialogPluginComponent();
+const $q = useQuasar();
+const { t } = useI18n();
+const { inputRules } = useInputRules();
+const user = userStore();
+const chatbot = chatbotStore();
+
+const formRef = ref(null);
+const loading = ref(false);
+
+const form = reactive({
+  category: null,
+  title: '',
+  message: '',
+  name: user.user?.name || '',
+  email: user.user?.email || '',
+  phone: user.user?.phone || '',
+});
+
+const categoryOptions = [
+  { label: t('support_request.categories.payment'), value: 'payment' },
+  { label: t('support_request.categories.schedule'), value: 'schedule' },
+  { label: t('support_request.categories.account'), value: 'account' },
+  { label: t('support_request.categories.other'), value: 'other' },
+];
+
+const buildTranscript = () => {
+  const lines = chatbot.messages.map((msg) => {
+    const who = msg.role === 'user' ? t('support_request.transcript.you') : t('support_request.transcript.bot');
+    return `${who}: ${msg.text}`;
+  });
+  return lines.join('\n');
+};
+
+const buildMessage = () => {
+  const transcript = buildTranscript();
+  const header = t('support_request.transcript.chat_header');
+  const userHeader = t('support_request.transcript.user_message_header');
+  const parts = [];
+  if (transcript.trim()) {
+    parts.push(`${header}\n${transcript}`);
+  }
+  parts.push(`${userHeader}\n${form.message.trim()}`);
+  return parts.join('\n\n');
+};
+
+const onSubmit = async () => {
+  loading.value = true;
+  try {
+    await createSupportRequest({
+      category: form.category,
+      title: form.title.trim(),
+      message: buildMessage(),
+      name: form.name?.trim() || null,
+      email: form.email?.trim() || null,
+      phone: form.phone?.trim() || null,
+    });
+    $q.notify({ type: 'positive', message: t('support_request.success') });
+    onDialogOK(true);
+  } catch (error) {
+    console.error(error);
+    $q.notify({ type: 'negative', message: t('support_request.error') });
+  } finally {
+    loading.value = false;
+  }
+};
+</script>
+
+<style scoped lang="scss">
+// Fonte consistente entre os dois apps e alinhada às telas (o dialog é teleportado
+// para fora do .q-page, então não herda a fonte definida lá).
+.support-dialog {
+  font-family: "Inter", sans-serif;
+
+  // Inputs, textareas e o texto interno dos campos NÃO herdam font-family do navegador
+  // por padrão. Sem isso, os valores dos campos caem na fonte do body (Roboto) e ficam
+  // diferentes dos labels — e diferentes entre os apps. Aqui travamos tudo na mesma fonte.
+  :deep(.q-field),
+  :deep(.q-field__native),
+  :deep(.q-field__input),
+  :deep(.q-field__label),
+  :deep(.q-field__prefix),
+  :deep(.q-field__suffix),
+  :deep(input),
+  :deep(textarea),
+  :deep(.q-item__label),
+  :deep(.q-btn),
+  :deep(.q-btn__content) {
+    font-family: "Inter", sans-serif;
+  }
+}
+
+.support-form {
+  // A seta do select herda cor clara/branca no tema dos apps e fica invisível;
+  // forçamos uma cor visível para deixar explícito que é um campo de seleção.
+  :deep(.q-select__dropdown-icon) {
+    color: #555555 !important;
+  }
+
+  :deep(.q-field__native),
+  :deep(.q-field__native span),
+  :deep(.q-field__input),
+  :deep(textarea.q-field__native) {
+    color: #555555 !important;
+  }
+
+  :deep(.q-field__native::placeholder),
+  :deep(.q-field__input::placeholder) {
+    color: #9e9e9e !important;
+  }
+}
+</style>

+ 71 - 12
src/components/shared/LocationMapDialog.vue

@@ -1,5 +1,5 @@
 <template>
-  <q-dialog ref="dialogRef" persistent maximized @show="initMap">
+  <q-dialog ref="dialogRef" persistent maximized class="map-dialog-transparent" @show="initMap" @hide="onDialogHide">
     <div class="location-map-dialog">
       <div ref="mapRef" class="location-map-container" />
 
@@ -41,7 +41,7 @@
 </template>
 
 <script setup>
-import { ref, onUnmounted } from 'vue';
+import { ref, nextTick, onUnmounted } from 'vue';
 import { useDialogPluginComponent, useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
 import { GoogleMap } from '@capacitor/google-maps';
@@ -73,13 +73,45 @@ const currentGeoData = ref(null);
 let googleMap = null;
 const markerId = ref(null);
 
+let hiddenEls = [];
+const applyMapTransparency = () => {
+  document.documentElement.style.setProperty('background', 'transparent', 'important');
+  document.body.style.setProperty('background', 'transparent', 'important');
+  const qApp = document.getElementById('q-app');
+  if (qApp) qApp.style.setProperty('background', 'transparent', 'important');
+
+  hiddenEls = [];
+  document
+    .querySelectorAll('.q-layout, .q-dialog:not(.map-dialog-transparent)')
+    .forEach((el) => {
+      el.style.setProperty('visibility', 'hidden', 'important');
+      hiddenEls.push(el);
+    });
+};
+
+const removeMapTransparency = () => {
+  document.documentElement.style.removeProperty('background');
+  document.body.style.removeProperty('background');
+  const qApp = document.getElementById('q-app');
+  if (qApp) qApp.style.removeProperty('background');
+
+  hiddenEls.forEach((el) => el.style.removeProperty('visibility'));
+  hiddenEls = [];
+};
+
+const onDialogHide = () => {
+  removeMapTransparency();
+};
+
 const handleCancel = async () => {
+  removeMapTransparency();
   await destroyMap();
   onDialogCancel();
 };
 
 const handleConfirm = async () => {
   if (!currentGeoData.value) return;
+  removeMapTransparency();
   await destroyMap();
   onDialogOK(currentGeoData.value);
 };
@@ -124,15 +156,31 @@ const moveMarkerTo = async (lat, lng) => {
 };
 
 const initMap = async () => {
-  googleMap = await GoogleMap.create({
-    id: 'location-map-dialog',
-    element: mapRef.value,
-    apiKey: process.env.GOOGLE_MAPS_API_KEY,
-    config: {
-      center: { lat: props.initialLat, lng: props.initialLng },
-      zoom: 17,
-    },
-  });
+  applyMapTransparency();
+  await nextTick();
+  await new Promise(resolve => setTimeout(resolve, 100));
+
+  const apiKey = process.env.GOOGLE_MAPS_API_KEY;
+
+  if (!apiKey) {
+    $q.notify({ type: 'negative', message: 'API key do mapa não configurada.' });
+    return;
+  }
+
+  try {
+    googleMap = await GoogleMap.create({
+      id: 'location-map-dialog',
+      element: mapRef.value,
+      apiKey,
+      config: {
+        center: { lat: props.initialLat, lng: props.initialLng },
+        zoom: 17,
+      },
+    });
+  } catch (err) {
+    $q.notify({ type: 'negative', message: `Erro ao abrir o mapa: ${err?.message ?? err}` });
+    return;
+  }
 
   markerId.value = await googleMap.addMarker({
     coordinate: { lat: props.initialLat, lng: props.initialLng },
@@ -154,22 +202,33 @@ const initMap = async () => {
 };
 
 onUnmounted(async () => {
+  removeMapTransparency();
   await destroyMap();
 });
 </script>
 
+<style lang="scss">
+.map-dialog-transparent .q-dialog__inner {
+  background: transparent !important;
+}
+.map-dialog-transparent .q-dialog__backdrop {
+  display: none !important;
+}
+</style>
+
 <style lang="scss" scoped>
 .location-map-dialog {
   position: relative;
   width: 100vw;
   height: 100dvh;
   overflow: hidden;
-  background: #e5e3df;
+  background: transparent;
 }
 
 .location-map-container {
   width: 100%;
   height: 100%;
+  background: transparent;
 }
 
 .location-map-back-btn {

+ 12 - 1
src/composables/useAuth.js

@@ -3,6 +3,8 @@ import { permissionStore } from "src/stores/permission";
 import { userStore } from "src/stores/user";
 import { useAuthStorage } from "src/composables/useAuthStorage";
 
+let refreshPromise = null;
+
 export const useAuth = () => {
   const { setRefreshToken, getRefreshToken, clearRefreshToken } = useAuthStorage();
 
@@ -48,7 +50,7 @@ export const useAuth = () => {
     }
   };
 
-  const refresh = async () => {
+  const doRefresh = async () => {
     try {
       const refreshToken = await getRefreshToken();
 
@@ -70,6 +72,15 @@ export const useAuth = () => {
     }
   };
 
+  const refresh = () => {
+    if (!refreshPromise) {
+      refreshPromise = doRefresh().finally(() => {
+        refreshPromise = null;
+      });
+    }
+    return refreshPromise;
+  };
+
   return {
     setAuthDataFromPayload,
     clearAuthData,

+ 13 - 6
src/composables/useGeocodingApi.js

@@ -25,8 +25,10 @@ const geocodeViaGoogleMaps = async (request) => {
 export const useGeocodingApi = () => {
   const geocodeCep = async (cep) => {
     const cleaned = cep.replace(/\D/g, '');
+    const apiKey = process.env.GOOGLE_MAPS_API_KEY;
+    const usandoGeocoderNativo = !!window.google?.maps?.Geocoder;
 
-    if (window.google?.maps?.Geocoder) {
+    if (usandoGeocoderNativo) {
       const results = await geocodeViaGoogleMaps({ address: `${cleaned}, Brazil` });
       if (!results.length) return null;
       const result = results[0];
@@ -39,7 +41,7 @@ export const useGeocodingApi = () => {
     }
 
     const { data } = await axios.get(GEOCODING_URL, {
-      params: { address: `${cleaned}, Brazil`, key: process.env.GOOGLE_MAPS_API_KEY },
+      params: { address: `${cleaned}, Brazil`, key: apiKey },
     });
     if (data.status !== 'OK' || !data.results.length) return null;
     const result = data.results[0];
@@ -48,14 +50,17 @@ export const useGeocodingApi = () => {
   };
 
   const reverseGeocode = async (lat, lng) => {
-    if (window.google?.maps?.Geocoder) {
+    const apiKey = process.env.GOOGLE_MAPS_API_KEY;
+    const usandoGeocoderNativo = !!window.google?.maps?.Geocoder;
+
+    if (usandoGeocoderNativo) {
       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: apiKey },
     });
     if (data.status !== 'OK' || !data.results.length) return null;
     const result = data.results[0];
@@ -65,8 +70,10 @@ export const useGeocodingApi = () => {
   const geocodeFullAddress = async ({ address, number, district, zip_code, city, state }) => {
     const parts = [address, number, district, zip_code, city, state, 'Brazil'].filter(Boolean);
     const query = parts.join(', ');
+    const apiKey = process.env.GOOGLE_MAPS_API_KEY;
+    const usandoGeocoderNativo = !!window.google?.maps?.Geocoder;
 
-    if (window.google?.maps?.Geocoder) {
+    if (usandoGeocoderNativo) {
       const results = await geocodeViaGoogleMaps({ address: query });
       if (!results.length) return null;
       const result = results[0];
@@ -78,7 +85,7 @@ export const useGeocodingApi = () => {
     }
 
     const { data } = await axios.get(GEOCODING_URL, {
-      params: { address: query, key: process.env.GOOGLE_MAPS_API_KEY },
+      params: { address: query, key: apiKey },
     });
     if (data.status !== 'OK' || !data.results.length) return null;
     const result = data.results[0];

+ 4 - 1
src/css/app.scss

@@ -180,11 +180,14 @@ box-shadow: 1px 4px 4px 0px rgba(0,0,0,0.2);
   font-weight: 300;
 }
 
-// ─── Layout utilities ──────────────────────────────────────────────────────────
 .card-border { border-radius: 18px; }
 
 .dashboard-section-title { line-height: 1.05; }
 
 .section-title {
   line-height: 1.05;
+}
+.dialog-safe-header {
+  padding-top: calc(env(safe-area-inset-top, 24px) + 12px) !important;
+  min-height: calc(50px + env(safe-area-inset-top, 24px));
 }

+ 3 - 3
src/css/table.scss

@@ -5,9 +5,9 @@
   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
+    --table-bg-color: #{map.get($colors, "surface")};
+    --table-border-color: #{map.get($colors, "surface-light")};
+    --table-header-color: #{map.get($colors, "text")};
   }
 
   .body--light & {

+ 47 - 5
src/i18n/locales/en.json

@@ -209,7 +209,7 @@
         },
         "step_5": {
           "daily_price_title": "What is the value of your daily rate for up to 8 hours?",
-          "daily_price_min_max": "Minimum value $ 100.00. Maximum value $ 500.00.",
+          "daily_price_min_max": "Minimum value $ 3.00. Maximum value $ 500.00.",
           "dont_worry": "Don't worry!",
           "change_anytime": "You can change your daily rate value whenever you want ;)",
           "shorter_services": "Based on the value above, the values for shorter services will be:",
@@ -238,6 +238,11 @@
       "registration_complete": {
         "title": "Registration sent!",
         "message": "Your registration was received and is under review. Please wait for approval before entering the platform."
+      },
+      "pending_approval": {
+        "title": "Registration under review",
+        "message": "Registration submitted successfully! Your profile is under review and you will be able to access the app once it is approved.",
+        "ok": "Got it"
       }
     },
     "dashboard": {
@@ -478,7 +483,12 @@
       "agency": "Agency",
       "account": "Account",
       "digit": "Digit",
-      "bank_fields_required": "Please enter agency and account",
+      "bank_code": "Bank (code)",
+      "bank_code_placeholder": "e.g. 341",
+      "agency_digit": "Digit",
+      "account_digit": "Digit",
+      "in_development": "Under development",
+      "bank_fields_required": "Please enter bank, agency and account",
       "save_account": "register account",
       "account_saved": "Bank account saved successfully!",
       "save_error": "Error saving. Please try again."
@@ -531,8 +541,8 @@
       "pricing_title": "My rates",
       "pricing_subtitle": "Set your 8-hour daily rate. The others are calculated automatically.",
       "price_8h_label": "What is your rate for up to 8 hours?",
-      "price_min_max": "Minimum R$100.00. Maximum R$500.00.",
-      "price_min_error": "Minimum value R$ 100.00",
+      "price_min_max": "Minimum R$3.00. Maximum R$500.00.",
+      "price_min_error": "Minimum value R$ 3.00",
       "price_max_error": "Maximum value R$ 500.00",
       "shorter_services": "Based on the value above, the rates for shorter services will be:",
       "up_to_6h": "up to 6 hours",
@@ -791,5 +801,37 @@
         }
       }
     }
+  },
+  "support_request": {
+    "title": "Talk to an agent",
+    "subtitle": "Fill in the fields below and a human agent will get in touch with you.",
+    "contact_title": "How can we reach you?",
+    "contact_subtitle": "We prefilled it with your account details. Change it if you prefer to be contacted through another channel.",
+    "chat_attached_note": "Your recent conversation with the assistant will be attached to give the agent context.",
+    "success": "Request sent! An agent will contact you soon.",
+    "error": "We could not send your request. Please try again.",
+    "fields": {
+      "category": "Category",
+      "title": "Subject",
+      "message": "Your message",
+      "name": "Name",
+      "email": "Email",
+      "phone": "Phone"
+    },
+    "categories": {
+      "payment": "Payment",
+      "schedule": "Schedule",
+      "account": "Account",
+      "other": "Other"
+    },
+    "actions": {
+      "send": "Send request"
+    },
+    "transcript": {
+      "you": "You",
+      "bot": "Assistant",
+      "chat_header": "--- Conversation with the assistant ---",
+      "user_message_header": "--- User message ---"
+    }
   }
-}
+}

+ 47 - 5
src/i18n/locales/es.json

@@ -209,7 +209,7 @@
         },
         "step_5": {
           "daily_price_title": "¿Cuál es el valor de su jornada de hasta 8 horas?",
-          "daily_price_min_max": "Valor mínimo R$ 100,00. Valor máximo R$ 500,00.",
+          "daily_price_min_max": "Valor mínimo R$ 3,00. Valor máximo R$ 500,00.",
           "dont_worry": "¡No se preocupe!",
           "change_anytime": "Puede cambiar el valor de su jornada cuando quiera ;)",
           "shorter_services": "Basado en el valor anterior, los valores de servicios más cortos serán:",
@@ -238,6 +238,11 @@
       "registration_complete": {
         "title": "¡Registro enviado!",
         "message": "Su registro fue recibido y está en análisis. Espere la aprobación para entrar en la plataforma."
+      },
+      "pending_approval": {
+        "title": "Registro en revisión",
+        "message": "¡Registro enviado con éxito! Tu perfil está en revisión y podrás acceder a la app en cuanto sea aprobado.",
+        "ok": "Entendido"
       }
     },
     "dashboard": {
@@ -478,7 +483,12 @@
       "agency": "Agencia",
       "account": "Cuenta",
       "digit": "Dígito",
-      "bank_fields_required": "Por favor ingrese agencia y cuenta",
+      "bank_code": "Banco (código)",
+      "bank_code_placeholder": "Ej.: 341",
+      "agency_digit": "Dígito",
+      "account_digit": "Dígito",
+      "in_development": "En desarrollo",
+      "bank_fields_required": "Por favor ingrese banco, agencia y cuenta",
       "save_account": "registrar cuenta",
       "account_saved": "¡Cuenta bancaria guardada con éxito!",
       "save_error": "Error al guardar. Inténtelo de nuevo."
@@ -531,8 +541,8 @@
       "pricing_title": "Mis tarifas",
       "pricing_subtitle": "Define el valor de tu jornada de 8 horas. Los demás se calculan automáticamente.",
       "price_8h_label": "¿Cuál es tu tarifa por hasta 8 horas?",
-      "price_min_max": "Mínimo R$100,00. Máximo R$500,00.",
-      "price_min_error": "Valor mínimo R$ 100,00",
+      "price_min_max": "Mínimo R$3,00. Máximo R$500,00.",
+      "price_min_error": "Valor mínimo R$ 3,00",
       "price_max_error": "Valor máximo R$ 500,00",
       "shorter_services": "Basado en el valor anterior, las tarifas para servicios más cortos serán:",
       "up_to_6h": "hasta 6 horas",
@@ -791,5 +801,37 @@
         }
       }
     }
+  },
+  "support_request": {
+    "title": "Hablar con un agente",
+    "subtitle": "Completa los campos a continuación y un agente humano se pondrá en contacto contigo.",
+    "contact_title": "¿Cómo podemos contactarte?",
+    "contact_subtitle": "Lo completamos con tus datos de cuenta. Cámbialo si prefieres que te contacten por otro canal.",
+    "chat_attached_note": "Tu conversación reciente con el asistente se adjuntará para dar contexto al agente.",
+    "success": "¡Solicitud enviada! Un agente se pondrá en contacto pronto.",
+    "error": "No pudimos enviar tu solicitud. Inténtalo de nuevo.",
+    "fields": {
+      "category": "Categoría",
+      "title": "Asunto",
+      "message": "Tu mensaje",
+      "name": "Nombre",
+      "email": "Correo",
+      "phone": "Teléfono"
+    },
+    "categories": {
+      "payment": "Pago",
+      "schedule": "Agenda",
+      "account": "Cuenta",
+      "other": "Otro"
+    },
+    "actions": {
+      "send": "Enviar solicitud"
+    },
+    "transcript": {
+      "you": "Tú",
+      "bot": "Asistente",
+      "chat_header": "--- Conversación con el asistente ---",
+      "user_message_header": "--- Mensaje del usuario ---"
+    }
   }
-}
+}

+ 96 - 54
src/i18n/locales/pt.json

@@ -179,65 +179,70 @@
           "rg": "RG"
         },
         "step_4": {
-            "document_front": "Lado 1 (Frente)",
-            "document_front_desc": "Foto da frente do seu documento (RG ou CNH).",
-            "document_back": "Lado 2 (Verso)",
-            "document_back_desc": "Foto do verso do seu documento (RG ou CNH).",
-            "selfie": "Selfie com documento",
-            "selfie_desc": "Tire uma selfie segurando o seu documento próximo ao rosto.",
-            "photo_captured": "Foto capturada!",
-            "action": "enviar documentos",
-            "btn_selfie": "Tirar selfie",
-            "selfie_hint": "Escolha um ambiente iluminado",
-            "selfie_sent": "Selfie enviada!",
-            "btn_document": "Fotografar documento",
-            "document_hint": "RG com CPF, frente e verso",
-            "document_sent": "Documento enviado!",
-            "btn_retake": "Tirar outra foto",
-            "btn_confirm": "Usar esta selfie",
-            "btn_use_photo": "Usar esta foto",
-            "error_message": "Não foi possível processar a imagem. Tente novamente.",
-            "selfie_success": "Selfie enviada com sucesso!",
-            "document_success": "Documento enviado com sucesso!",
-            "btn_retry": "Tentar novamente",
-            "btn_continue": "Continuar",
-            "document_hint_result": "Continue para enviar o verso do documento",
-            "btn_capture_selfie": "Tirar selfie",
-            "btn_capture_front": "Fotografar frente",
-            "btn_capture_back": "Fotografar verso",
-            "upload_all_photos": "Anexe todas as fotos necessárias!"
+          "document_front": "Lado 1 (Frente)",
+          "document_front_desc": "Foto da frente do seu documento (RG ou CNH).",
+          "document_back": "Lado 2 (Verso)",
+          "document_back_desc": "Foto do verso do seu documento (RG ou CNH).",
+          "selfie": "Selfie com documento",
+          "selfie_desc": "Tire uma selfie segurando o seu documento próximo ao rosto.",
+          "photo_captured": "Foto capturada!",
+          "action": "enviar documentos",
+          "btn_selfie": "Tirar selfie",
+          "selfie_hint": "Escolha um ambiente iluminado",
+          "selfie_sent": "Selfie enviada!",
+          "btn_document": "Fotografar documento",
+          "document_hint": "RG com CPF, frente e verso",
+          "document_sent": "Documento enviado!",
+          "btn_retake": "Tirar outra foto",
+          "btn_confirm": "Usar esta selfie",
+          "btn_use_photo": "Usar esta foto",
+          "error_message": "Não foi possível processar a imagem. Tente novamente.",
+          "selfie_success": "Selfie enviada com sucesso!",
+          "document_success": "Documento enviado com sucesso!",
+          "btn_retry": "Tentar novamente",
+          "btn_continue": "Continuar",
+          "document_hint_result": "Continue para enviar o verso do documento",
+          "btn_capture_selfie": "Tirar selfie",
+          "btn_capture_front": "Fotografar frente",
+          "btn_capture_back": "Fotografar verso",
+          "upload_all_photos": "Anexe todas as fotos necessárias!"
         },
         "step_5": {
-            "daily_price_title": "Qual valor da sua diária de até 8 horas?",
-            "daily_price_min_max": "Valor mínimo R$ 100,00. Valor máximo R$ 500,00.",
-            "dont_worry": "Não se preocupe!",
-            "change_anytime": "Você pode alterar o valor da sua diária quando quiser ;)",
-            "shorter_services": "Baseado no valor acima, os valores de serviços mais curtos serão:",
-            "up_to_6h": "até 6 horas",
-            "up_to_4h": "até 4 horas",
-            "up_to_2h": "até 2 horas",
-            "other_services": "Além da limpeza básica, quais serviços você também realiza?",
-            "change_in_profile": "(poderá ser alterado no perfil)",
-            "no_services": "Nenhum tipo de serviço disponível no momento.",
-            "search_visibility": "Essas informações aparecerão para quem buscar o seu perfil na busca.",
-            "action": "continuar"
+          "daily_price_title": "Qual valor da sua diária de até 8 horas?",
+          "daily_price_min_max": "Valor mínimo R$ 3,00. Valor máximo R$ 500,00.",
+          "dont_worry": "Não se preocupe!",
+          "change_anytime": "Você pode alterar o valor da sua diária quando quiser ;)",
+          "shorter_services": "Baseado no valor acima, os valores de serviços mais curtos serão:",
+          "up_to_6h": "até 6 horas",
+          "up_to_4h": "até 4 horas",
+          "up_to_2h": "até 2 horas",
+          "other_services": "Além da limpeza básica, quais serviços você também realiza?",
+          "change_in_profile": "(poderá ser alterado no perfil)",
+          "no_services": "Nenhum tipo de serviço disponível no momento.",
+          "search_visibility": "Essas informações aparecerão para quem buscar o seu perfil na busca.",
+          "action": "continuar"
         },
         "step_6": {
-            "title": "Agora vamos escolher quais melhor horários para você receber solicitações de serviços!",
-            "lock_hint": "Toque e bloqueie os dias ou períodos",
-            "lock_description": "se você não deseja receber solicitações de serviços neste período.",
-            "morning": "manhã",
-            "afternoon": "tarde",
-            "instruction": "Para bloquear dias individuais, clique no dia, para bloquear apenas períodos, clique em manhã ou tarde.",
-            "dont_worry": "Não se preocupe!",
-            "change_anytime": "Depois você poderá alterar a disponibilidade de sua preferência a qualquer momento no app ;)",
-            "select_at_least_one": "Selecione ao menos um período de trabalho.",
-            "action": "concluir cadastro"
+          "title": "Agora vamos escolher quais melhor horários para você receber solicitações de serviços!",
+          "lock_hint": "Toque e bloqueie os dias ou períodos",
+          "lock_description": "se você não deseja receber solicitações de serviços neste período.",
+          "morning": "manhã",
+          "afternoon": "tarde",
+          "instruction": "Para bloquear dias individuais, clique no dia, para bloquear apenas períodos, clique em manhã ou tarde.",
+          "dont_worry": "Não se preocupe!",
+          "change_anytime": "Depois você poderá alterar a disponibilidade de sua preferência a qualquer momento no app ;)",
+          "select_at_least_one": "Selecione ao menos um período de trabalho.",
+          "action": "concluir cadastro"
         }
       },
       "registration_complete": {
         "title": "Cadastro enviado!",
         "message": "Seu cadastro foi recebido e está em análise. Aguarde a aprovação para entrar na plataforma."
+      },
+      "pending_approval": {
+        "title": "Cadastro em análise",
+        "message": "Cadastro enviado com sucesso! Seu perfil está em análise e você poderá acessar o app assim que for aprovado.",
+        "ok": "Entendi"
       }
     },
     "dashboard": {
@@ -481,7 +486,12 @@
       "agency": "Agência",
       "account": "Conta",
       "digit": "Dígito",
-      "bank_fields_required": "Informe agência e conta",
+      "bank_code": "Banco (código)",
+      "bank_code_placeholder": "Ex.: 341",
+      "agency_digit": "Dígito",
+      "account_digit": "Dígito",
+      "in_development": "Em desenvolvimento",
+      "bank_fields_required": "Informe banco, agência e conta",
       "save_account": "cadastrar conta",
       "account_saved": "Conta bancária salva com sucesso!",
       "save_error": "Erro ao salvar. Tente novamente."
@@ -534,8 +544,8 @@
       "pricing_title": "Meus valores",
       "pricing_subtitle": "Defina o valor da sua diária de 8 horas. Os demais são calculados automaticamente.",
       "price_8h_label": "Qual valor da sua diária de até 8 horas?",
-      "price_min_max": "Valor mínimo R$100,00. Valor máximo R$ 500,00.",
-      "price_min_error": "Valor mínimo R$ 100,00",
+      "price_min_max": "Valor mínimo R$3,00. Valor máximo R$ 500,00.",
+      "price_min_error": "Valor mínimo R$ 3,00",
       "price_max_error": "Valor máximo R$ 500,00",
       "shorter_services": "Baseado no valor acima, os valores de serviços mais curtos serão:",
       "up_to_6h": "até 6 horas",
@@ -794,5 +804,37 @@
         }
       }
     }
+  },
+  "support_request": {
+    "title": "Falar com atendente",
+    "subtitle": "Preencha os campos abaixo e um atendente humano entrará em contato com você.",
+    "contact_title": "Como podemos te contatar?",
+    "contact_subtitle": "Já preenchemos com seus dados de cadastro. Altere se preferir ser contatado por outro canal.",
+    "chat_attached_note": "Sua conversa recente com o assistente será anexada para dar contexto ao atendente.",
+    "success": "Solicitação enviada! Em breve um atendente entrará em contato.",
+    "error": "Não foi possível enviar sua solicitação. Tente novamente.",
+    "fields": {
+      "category": "Categoria",
+      "title": "Assunto",
+      "message": "Sua mensagem",
+      "name": "Nome",
+      "email": "E-mail",
+      "phone": "Telefone"
+    },
+    "categories": {
+      "payment": "Pagamento",
+      "schedule": "Agendamento",
+      "account": "Conta",
+      "other": "Outro"
+    },
+    "actions": {
+      "send": "Enviar solicitação"
+    },
+    "transcript": {
+      "you": "Você",
+      "bot": "Assistente",
+      "chat_header": "--- Conversa com o assistente ---",
+      "user_message_header": "--- Mensagem do usuário ---"
+    }
   }
-}
+}

+ 6 - 19
src/layouts/MainLayout.vue

@@ -1,11 +1,8 @@
 <template>
   <q-layout class="main-layout relative" view="hHh lpR fFf">
     <q-header
-      v-if="$q.screen.lt.sm"
-      class="bg-surface column justify-end"
-      :style="{
-        height: `calc(50px + env(safe-area-inset-top))`,
-      }"
+      class="bg-surface"
+      :style="{ height: 'env(safe-area-inset-top)' }"
     >
     </q-header>
     <q-page-container>
@@ -22,14 +19,13 @@
               <component
                 :is="Component"
                 class="main-layout-view"
-                :class="{ 'main-layout-view--mobile': $q.screen.lt.sm }"
               />
             </Transition>
           </router-view>
         </q-scroll-area>
       </q-page>
     </q-page-container>
-    <q-footer v-if="$q.screen.lt.sm" class="provider-bottom-nav bg-white">
+    <q-footer class="provider-bottom-nav bg-white">
       <nav class="provider-bottom-nav-inner">
         <router-link
           v-for="item in navItems"
@@ -48,7 +44,6 @@
 
 <script setup>
 import { computed, useTemplateRef, watch } from "vue";
-import { useQuasar } from "quasar";
 import { useRoute } from "vue-router";
 import { useI18n } from "vue-i18n";
 
@@ -56,7 +51,6 @@ defineOptions({
   name: "MainLayout",
 });
 
-const $q = useQuasar();
 const route = useRoute();
 const scrollAreaRef = useTemplateRef("scrollAreaRef");
 const { t } = useI18n();
@@ -89,10 +83,7 @@ const navItems = computed(() => [
 const isNavItemActive = (item) => route.name === item.name;
 
 const scrollAreaHeight = computed(() => {
-  if ($q.screen.lt.sm) {
-    return 'height: calc(100dvh - 50px - env(safe-area-inset-top) - 80px - env(safe-area-inset-bottom)) !important;';
-  }
-  return 'height: 100dvh !important;';
+  return 'height: calc(100dvh - env(safe-area-inset-top) - 60px - env(safe-area-inset-bottom)) !important;';
 });
 
 watch(
@@ -134,10 +125,6 @@ watch(
   overflow-x: hidden;
 }
 
-.main-layout-view--mobile {
-  padding: 0 !important;
-}
-
 .v-enter-active {
   opacity: 1;
   transition: all 0.15s ease-in;
@@ -167,8 +154,8 @@ watch(
   display: grid;
   grid-template-columns: repeat(4, minmax(0, 1fr));
   align-items: end;
-  height: 60px;
-  padding: 0px 0px calc(12px + env(safe-area-inset-bottom));
+  height: calc(60px + env(safe-area-inset-bottom));
+  padding: 0 0 env(safe-area-inset-bottom);
 }
 
 .provider-bottom-nav-item {

+ 147 - 68
src/pages/LoginPage.vue

@@ -3,47 +3,85 @@
   <q-page class="login-page bg-surface-dark">
     <Transition name="fade-slide" mode="out-in">
       <div v-if="!clicked" key="splash" class="splash-screen" @click="clicked = true">
-        <img :src="BackgroundLogin" class="splash-layer splash-layer--bg" />
-        <img :src="FotoDiarista" class="splash-layer splash-layer--photo" />
-        <img :src="LogoLogin" class="splash-layer splash-layer--logo" />
+        <img
+          :src="BackgroundLogin"
+          class="splash-layer splash-layer--bg"
+        />
+
+        <img
+          :src="FotoDiarista"
+          class="splash-layer splash-layer--photo"
+        />
+
+        <img
+          :src="LogoLogin"
+          class="splash-layer splash-layer--logo"
+        />
       </div>
 
       <div v-else key="flow" class="flow-screen">
         <div v-if="!showSubStep" class="flow-logo q-my-xl">
-          <q-img :src="LogoDiariaCampos" style="max-width: 180px" />
+          <q-img
+            :src="LogoDiariaCampos"
+            style="max-width: 180px"
+          />
         </div>
 
         <q-form
           v-if="!registrationCompleted"
           ref="loginForm"
-          class="flow-form"
-          autocorrect="off"
           autocapitalize="off"
           autocomplete="off"
+          autocorrect="off"
+          class="flow-form"
           spellcheck="false"
           @submit="onSubmit"
         >
-          <div class="flow-content" :class="{ 'flow-content--centered': steps <= 2 && !showSubStep }">
-            <LoginStepOnePanel v-if="steps === 1" v-model:email="email" v-model:phone="phone" />
-            <LoginStepTwoPanel v-else-if="steps === 2" v-model:code="code" />
-            <LoginStepThreePanel v-else-if="steps === 3" v-model="stepThreeForm" />
+          <div
+            class="flow-content"
+            :class="{ 'flow-content--centered': steps <= 2 && !showSubStep }"
+          >
+            <LoginStepOnePanel
+              v-if="steps === 1"
+              v-model:email="email"
+              v-model:phone="phone"
+            />
+
+            <LoginStepTwoPanel
+              v-else-if="steps === 2"
+              v-model:code="code"
+            />
+
+            <LoginStepThreePanel
+              v-else-if="steps === 3"
+              v-model="stepThreeForm"
+            />
+
             <LoginStepFourPanel
               v-else-if="steps === 4"
               v-model="stepFourForm"
               @update:show-sub-step="showSubStep = $event"
             />
-            <LoginStepFivePanel v-else-if="steps === 5" v-model="stepFiveForm" />
-            <LoginStepSixPanel v-else-if="steps === 6" v-model="stepSixForm" />
+
+            <LoginStepFivePanel
+              v-else-if="steps === 5"
+              v-model="stepFiveForm"
+            />
+
+            <LoginStepSixPanel
+              v-else-if="steps === 6"
+              v-model="stepSixForm"
+            />
           </div>
 
           <div v-if="!showSubStep" class="flow-footer">
             <q-btn
+              class="full-width"
               color="primary-button"
-              :label="actionLabel"
-              rounded
               padding="8px 16px"
+              rounded
               type="submit"
-              class="full-width"
+              :label="actionLabel"
               :loading="submitting"
             >
               <template #loading>
@@ -54,10 +92,17 @@
         </q-form>
 
         <div v-else class="registration-complete">
-          <q-icon name="mdi-check-circle-outline" size="52px" color="positive" class="q-mb-md" />
+          <q-icon
+            class="q-mb-md"
+            color="positive"
+            name="mdi-check-circle-outline"
+            size="52px"
+          />
+
           <div class="registration-complete__title text-text fontbold">
             {{ t('provider.login.registration_complete.title') }}
           </div>
+
           <div class="registration-complete__message text-grey-7 font12">
             {{ t('provider.login.registration_complete.message') }}
           </div>
@@ -68,12 +113,12 @@
 </template>
 
 <script setup>
-import { computed, ref } from 'vue';
-import { useQuasar } from 'quasar';
-import { useRouter } from 'vue-router';
-import { useI18n } from 'vue-i18n';
+import { computed, onMounted, ref } from 'vue';
 import { createUserAndProvider, sendCode, validateCode } from 'src/api/user';
 import { useAuth } from 'src/composables/useAuth';
+import { useI18n } from 'vue-i18n';
+import { useQuasar } from 'quasar';
+import { useRouter } from 'vue-router';
 
 import BackgroundLogin from 'src/assets/background-login.svg';
 import FotoDiarista from 'src/assets/foto_diarista_login.svg';
@@ -93,6 +138,7 @@ const router = useRouter();
 const { setAuthDataFromPayload } = useAuth();
 
 const clicked = ref(false);
+
 const showSubStep = ref(false);
 const registrationCompleted = ref(false);
 const steps = ref(1);
@@ -122,12 +168,9 @@ const stepThreeForm = ref({
 });
 
 const stepFourForm = ref({
-  selfie_file: null,
-  selfie_base64: '',
-  document_front_file: null,
-  document_front_base64: '',
-  document_back_file: null,
-  document_back_base64: '',
+  selfie: null,
+  document_front: null,
+  document_back: null,
 });
 
 const stepFiveForm = ref({
@@ -152,25 +195,21 @@ const stepSixForm = ref({
 const actionLabel = computed(() => {
   if (steps.value === 1) return t('provider.login.steps.step_1.action');
   if (steps.value === 2) return t('provider.login.steps.step_2.action');
-  if (steps.value === 7) return t('provider.login.steps.step_6.action');
+  if (steps.value === 6) return t('provider.login.steps.step_6.action');
+
   return t('provider.login.steps.step_3.action');
 });
 
-const toISODate = (value) => {
-  const matches = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value || '');
-  if (!matches) return null;
-
-  return `${matches[3]}-${matches[2]}-${matches[1]}`;
-};
-
 const mapWorkingDays = () => {
   const mapped = [];
+
   const workingDays = stepSixForm.value.working_days || {};
 
   Object.entries(workingDays).forEach(([dayKey, periods]) => {
     if (periods?.morning) {
       mapped.push({ day: Number(dayKey), period: 'morning' });
     }
+
     if (periods?.afternoon) {
       mapped.push({ day: Number(dayKey), period: 'afternoon' });
     }
@@ -183,6 +222,43 @@ const hasWorkingDaySelected = () => {
   return mapWorkingDays().length > 0;
 };
 
+const sendValidationCode = async () => {
+  const response = await sendCode(email.value, phone.value);
+
+  if (response.status === 201) {
+    steps.value = 2;
+  }
+
+  return response;
+};
+
+const toISODate = (value) => {
+  const matches = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value || '');
+
+  if (!matches) return null;
+
+  return `${matches[3]}-${matches[2]}-${matches[1]}`;
+};
+
+const validateCodeInput = async () => {
+  const response = await validateCode(email.value, phone.value, code.value, isLogin.value);
+
+  if (response.status === 200) {
+    if (isLogin.value === true) {
+      await setAuthDataFromPayload(response.data.payload);
+
+      router.push({ name: 'DashboardPage' });
+
+      return;
+    }
+
+    stepThreeForm.value.email = email.value;
+    stepThreeForm.value.phone = phone.value;
+
+    steps.value = 3;
+  }
+};
+
 const validateCurrentStep = async () => {
   const isValid = await loginForm.value?.validate();
 
@@ -190,9 +266,11 @@ const validateCurrentStep = async () => {
     return false;
   }
 
-    if(steps.value === 4) {
+  if(steps.value === 4) {
     const hasSelfie = !!stepFourForm.value.selfie;
+
     const hasDocumentFront = !!stepFourForm.value.document_front;
+
     const hasDocumentBack = !!stepFourForm.value.document_back;
 
     if (!hasSelfie || !hasDocumentFront || !hasDocumentBack) {
@@ -200,57 +278,39 @@ const validateCurrentStep = async () => {
         type: 'negative',
         message: t('provider.login.steps.step_4.upload_all_photos'),
       });
+
       return false;
     }
   }
 
-  if (steps.value === 7 && !hasWorkingDaySelected()) {
+  if (steps.value === 6 && !hasWorkingDaySelected()) {
     $q.notify({
       type: 'negative',
       message: t('provider.login.steps.step_6.select_at_least_one'),
     });
+
     return false;
   }
 
   return true;
 };
 
-const sendValidationCode = async () => {
-  const response = await sendCode(email.value, phone.value);
-  if (response.status === 201) {
-    steps.value = 2;
-  }
-  return response;
-};
-
-const validateCodeInput = async () => {
-  const response = await validateCode(email.value, phone.value, code.value, isLogin.value);
-
-  if (response.status === 200) {
-    if (isLogin.value === true) {
-      await setAuthDataFromPayload(response.data.payload);
-      router.push({ name: 'DashboardPage' });
-      return;
-    }
-
-    stepThreeForm.value.email = email.value;
-    stepThreeForm.value.phone = phone.value;
-    steps.value = 3;
-  }
-};
-
 const registerUserAndProvider = async () => {
   const workingDays = mapWorkingDays();
+
   const form = new FormData();
 
   const hasValue = (val) => val !== null && val !== undefined && String(val).trim() !== '';
+
   const append = (key, val) => {
-    if (val === null || val === undefined) return;
+    if (val === null || val === undefined || val === '') return;
     if (typeof val === 'boolean') form.append(key, val ? '1' : '0');
     else form.append(key, val);
   };
+
   const appendIfFilled = (key, val) => {
     if (!hasValue(val)) return;
+
     form.append(key, val);
   };
 
@@ -314,6 +374,7 @@ const registerUserAndProvider = async () => {
   }
 
   const response = await createUserAndProvider(form);
+
   if (response.status >= 200 && response.status < 300) {
     registrationCompleted.value = true;
   }
@@ -324,6 +385,7 @@ const onSubmit = async () => {
   if (showSubStep.value) return; // Não submete o form principal se estiver em um sub-passo
 
   const isValid = await loginForm.value.validate();
+
   if (!isValid) return;
 
   submitting.value = true;
@@ -332,51 +394,68 @@ const onSubmit = async () => {
     switch (steps.value) {
       case 1: {
         const response = await sendValidationCode();
+
         isLogin.value = response?.data?.payload?.isLogin === true;
+
         break;
       }
+
       case 2:
         await validateCodeInput();
+
         break;
+
       case 3: {
         if (await validateCurrentStep()) {
           steps.value = 4;
         }
+
         break;
       }
+
       case 4: {
         if (await validateCurrentStep()) {
           steps.value = 5;
         }
+
         break;
       }
+
       case 5: {
         if (await validateCurrentStep()) {
           steps.value = 6;
         }
+
         break;
       }
+
       case 6: {
-        if (await validateCurrentStep()) {
-          steps.value = 7;
-        }
-        break;
-      }
-      case 7: {
         if (await validateCurrentStep()) {
           await registerUserAndProvider();
         }
+
         break;
       }
+
       default:
         break;
     }
   } catch (error) {
     console.error(error);
+
+    const msg = error?.response?.data?.message ?? error?.message ?? 'Erro ao concluir cadastro. Tente novamente.';
+
+    $q.notify({ type: 'negative', message: msg });
   } finally {
     submitting.value = false;
   }
 };
+
+onMounted(() => {
+  setTimeout(() => {
+    clicked.value = true;
+  }, 1500);
+});
 </script>
 
 <style lang="scss" scoped>
@@ -475,7 +554,7 @@ const onSubmit = async () => {
 }
 
 .flow-footer {
-  padding: 20px 0 12px;
+  padding: 20px 0 calc(12px + env(safe-area-inset-bottom));
 }
 
 .registration-complete {

+ 1 - 1
src/pages/calendar/CalendarPage.vue

@@ -291,7 +291,7 @@ onMounted(async () => {
 
 <style scoped lang="scss">
 .calendar-header {
-  padding-top: calc(env(safe-area-inset-top) + 12px);
+  padding-top: 12px;
   padding-bottom: 12px;
   box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.08);
 }

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

@@ -6,23 +6,25 @@
       </div>
     </template>
     <template v-else>
-      <DashboardHeaderBar
-  :data="headerBar"
-  :notifications="notifications"
-/>
-      <DashboardSummaryInfos :data="summaryInfos" />
-      <DashboardPriceSuggest :data="priceSuggestion"/>
-      <DashboardTodayServices v-if="todayServices?.length > 0" :data="todayServices" @refresh="loadDashboard" @rate="openRatingDialog" />
-      <DashboardScrollAreaSchedules />
-      <DashboardSolicitations
-        v-if="solicitations?.length > 0"
-        :data="solicitations"
-        @accept="(item) => openDetailsDialog(item, 'confirm-accept')"
-        @reject="(item) => openDetailsDialog(item, 'confirm-reject')"
-        @view-details="(item) => openDetailsDialog(item)"
-      />
-      <DashboardNextSchedules v-if="nextSchedules?.length > 0" :data="nextSchedules" @view-details="(item) => openNextScheduleDialog(item)" />
-      <DashboardOpportunities v-if="opportunities?.length > 0" :data="opportunities"/>
+      <q-pull-to-refresh color="primary" @refresh="onRefresh">
+        <DashboardHeaderBar
+          :data="headerBar"
+          :notifications="notifications"
+        />
+        <DashboardSummaryInfos :data="summaryInfos" />
+        <DashboardPriceSuggest :data="priceSuggestion"/>
+        <DashboardTodayServices v-if="todayServices?.length > 0" :data="todayServices" @refresh="loadDashboard" @rate="openRatingDialog" />
+        <DashboardScrollAreaSchedules />
+        <DashboardSolicitations
+          v-if="solicitations?.length > 0"
+          :data="solicitations"
+          @accept="(item) => openDetailsDialog(item, 'confirm-accept')"
+          @reject="(item) => openDetailsDialog(item, 'confirm-reject')"
+          @view-details="(item) => openDetailsDialog(item)"
+        />
+        <DashboardNextSchedules v-if="nextSchedules?.length > 0" :data="nextSchedules" @view-details="(item) => openNextScheduleDialog(item)" />
+        <DashboardOpportunities v-if="opportunities?.length > 0" :data="opportunities"/>
+      </q-pull-to-refresh>
     </template>
   </div>
 </template>
@@ -86,6 +88,14 @@ const loadDashboard = async () => {
 
 }
 
+const onRefresh = async (done) => {
+  try {
+    await loadDashboard();
+  } finally {
+    done();
+  }
+};
+
 const handleScheduleAction = async (id, status) => {
   try {
     await updateScheduleStatus(id, status);

+ 1 - 0
src/pages/opportunities/components/OpportunityDetailsDialog.vue

@@ -316,6 +316,7 @@ onMounted(async () => {
   position: relative;
 
   padding: 14px 16px;
+  padding-top: calc(env(safe-area-inset-top, 24px) + 14px);
   background: #fff;
 
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);

+ 1 - 1
src/pages/payments/PaymentsPage.vue

@@ -433,7 +433,7 @@ onMounted(() => {
 
 <style scoped lang="scss">
 .payments-header {
-  padding-top: calc(env(safe-area-inset-top) + 12px);
+  padding-top: 12px;
   padding-bottom: 12px;
   box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.08);
 }

+ 1 - 1
src/pages/profile/ProfileEditDialog.vue

@@ -1,7 +1,7 @@
 <template>
   <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
     <q-card class="bg-white full-width full-height">
-      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile">
+      <div class="row items-center q-px-md q-pb-sm bg-white shadow-profile dialog-safe-header">
         <q-btn flat round dense icon="mdi-chevron-left" color="primary" @click="onDialogCancel" />
         <q-space />
         <span class="font16 fontbold gradient-diarista">{{ $t('profile.edit_data') }}</span>