14 Commits 7ae6a4f056 ... a212464172

Autor SHA1 Mensagem Data
  Gustavo Zanatta a212464172 fix pushs 1 semana atrás
  Gustavo Zanatta c52c326ed4 padronizacao indicador de multiplos cards 1 semana atrás
  Gustavo Zanatta 2b9b51c4c7 feat: listagem propostas enviadas pelo prestador 1 semana atrás
  Gustavo Zanatta 1a65929fa3 ajustes avaliacao: max 5 files, so permite anexar foto em avaliaca negativa, nao tava exibindo foto do avaliado, pré-seleciona favorito se ja era antes 1 semana atrás
  Gustavo Zanatta 2673badec4 feat: redireciona e abre dealhes do agendamento no bloco servicos do dia 1 semana atrás
  Gustavo Zanatta 83b8010689 feat: modal de confirmacao de saque + card indicando saque em processamento 1 semana atrás
  Gustavo Zanatta 00b0b7f4a8 ajuste no compartilhamento de codigo unico para favorito 2 semanas atrás
  Gustavo Zanatta 3301c5e3c6 fix light dark 2 semanas atrás
  Gustavo Zanatta c679d460b8 feat: codigo unico para favorito 2 semanas atrás
  Gustavo Zanatta a5ce3bf1d8 fix botoes cancelar 2 semanas atrás
  Gustavo Zanatta 20f7a10a69 chore version 2.4 2 semanas atrás
  Gustavo Zanatta 60218dc788 fix aba pagamentos: fotos aparecendo, e movimentacoes apenas que deram sucesso 2 semanas atrás
  Gustavo Zanatta c71a8f64cc share 2 semanas atrás
  Gustavo Zanatta 411abceb69 chore version 2.3 2 semanas atrás
36 arquivos alterados com 854 adições e 145 exclusões
  1. 12 2
      package-lock.json
  2. 1 1
      package.json
  3. 1 1
      quasar.config.js
  4. 2 2
      src-capacitor/android/app/build.gradle
  5. 3 0
      src-capacitor/android/app/src/main/res/values/styles.xml
  6. 2 1
      src-capacitor/capacitor.config.json
  7. 2 2
      src-capacitor/package-lock.json
  8. 1 1
      src-capacitor/package.json
  9. 5 21
      src/App.vue
  10. 2 2
      src/api/paymentSplit.js
  11. 6 0
      src/assets/diarinho-saque.svg
  12. 27 6
      src/boot/push-notifications.js
  13. 15 1
      src/components/dashboard/DashboardNextSchedules.vue
  14. 42 11
      src/components/dashboard/DashboardOpportunities.vue
  15. 15 1
      src/components/dashboard/DashboardPendingConfirmation.vue
  16. 29 14
      src/components/dashboard/DashboardSolicitations.vue
  17. 17 3
      src/components/dashboard/DashboardTodayServices.vue
  18. 33 4
      src/components/dashboard/ScheduleRatingDialog.vue
  19. 6 0
      src/components/dashboard/SolicitationDetailsDialog.vue
  20. 72 0
      src/components/defaults/DefaultCarouselIndicator.vue
  21. 4 2
      src/components/login/LoginStep1Panel.vue
  22. 30 15
      src/components/login/LoginStep3Panel.vue
  23. 5 4
      src/components/login/LoginStep5Panel.vue
  24. 4 2
      src/components/payments/MovimentacoesDialog.vue
  25. 65 0
      src/components/payments/WithdrawSuccessDialog.vue
  26. 121 0
      src/composables/useCarouselIndicator.js
  27. 48 12
      src/composables/usePushNotifications.js
  28. 16 2
      src/i18n/locales/en.json
  29. 16 2
      src/i18n/locales/es.json
  30. 16 2
      src/i18n/locales/pt.json
  31. 21 1
      src/pages/calendar/CalendarPage.vue
  32. 28 16
      src/pages/dashboard/DashboardPage.vue
  33. 13 0
      src/pages/opportunities/OpportunitiesPage.vue
  34. 10 2
      src/pages/opportunities/components/OpportunityDetailsDialog.vue
  35. 75 4
      src/pages/payments/PaymentsPage.vue
  36. 89 8
      src/pages/profile/ProfilePage.vue

+ 12 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
 {
   "name": "diarista-prestador-app",
   "name": "diarista-prestador-app",
-  "version": "2.2",
+  "version": "2.5",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "diarista-prestador-app",
       "name": "diarista-prestador-app",
-      "version": "2.2",
+      "version": "2.5",
       "dependencies": {
       "dependencies": {
         "@bufbuild/protobuf": "^2.5.1",
         "@bufbuild/protobuf": "^2.5.1",
         "@capacitor/browser": "^7.0.5",
         "@capacitor/browser": "^7.0.5",
@@ -16,6 +16,7 @@
         "@capacitor/geolocation": "^7.1.8",
         "@capacitor/geolocation": "^7.1.8",
         "@capacitor/google-maps": "^7.2.2",
         "@capacitor/google-maps": "^7.2.2",
         "@capacitor/push-notifications": "^7.0.7",
         "@capacitor/push-notifications": "^7.0.7",
+        "@capacitor/share": "^7.0.4",
         "@ionic/pwa-elements": "^3.4.0",
         "@ionic/pwa-elements": "^3.4.0",
         "@quasar/cli": "^2.5.0",
         "@quasar/cli": "^2.5.0",
         "@quasar/extras": "^1.17.0",
         "@quasar/extras": "^1.17.0",
@@ -510,6 +511,15 @@
         "@capacitor/core": ">=7.0.0"
         "@capacitor/core": ">=7.0.0"
       }
       }
     },
     },
+    "node_modules/@capacitor/share": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/@capacitor/share/-/share-7.0.4.tgz",
+      "integrity": "sha512-wNXxmXUcChrtbZ5jQv8scFIbIGKo3rk6B7qpWySxfUhYJP5NANgAqQ9Z69m0k/zYsucdh66SBU7XW1ykd9aUug==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@capacitor/core": ">=7.0.0"
+      }
+    },
     "node_modules/@capacitor/status-bar": {
     "node_modules/@capacitor/status-bar": {
       "version": "7.0.1",
       "version": "7.0.1",
       "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.1.tgz",
       "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.1.tgz",

+ 1 - 1
package.json

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

+ 1 - 1
quasar.config.js

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

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

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

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

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

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

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

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

@@ -1,12 +1,12 @@
 {
 {
   "name": "diarista-prestador-app",
   "name": "diarista-prestador-app",
-  "version": "2.2",
+  "version": "2.5",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "diarista-prestador-app",
       "name": "diarista-prestador-app",
-      "version": "2.2",
+      "version": "2.5",
       "dependencies": {
       "dependencies": {
         "@capacitor/android": "^7.6.8",
         "@capacitor/android": "^7.6.8",
         "@capacitor/app": "^7.0.0",
         "@capacitor/app": "^7.0.0",

+ 1 - 1
src-capacitor/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "diarista-prestador-app",
   "name": "diarista-prestador-app",
-  "version": "2.2",
+  "version": "2.5",
   "description": "Aplicativo para o prestador do Diaria",
   "description": "Aplicativo para o prestador do Diaria",
   "author": "zntt <zanattagg@gmail.com>",
   "author": "zntt <zanattagg@gmail.com>",
   "private": true,
   "private": true,

+ 5 - 21
src/App.vue

@@ -18,27 +18,15 @@ const { locale } = useI18n();
 
 
 const $q = useQuasar();
 const $q = useQuasar();
 
 
-const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
-  ? "dark"
-  : "light";
-  
-const updateStatusBar = async (isDark) => {
+const updateStatusBar = async () => {
   if (!Capacitor.isNativePlatform()) return;
   if (!Capacitor.isNativePlatform()) return;
 
 
   await StatusBar.show();
   await StatusBar.show();
   await StatusBar.setStyle({
   await StatusBar.setStyle({
-    style: isDark ? Style.Dark : Style.Light,
+    style: Style.Light,
   });
   });
 };
 };
 
 
-watch(
-  () => $q.dark.isActive,
-  async (value) => {
-    await Preferences.set({ key: "theme", value: value ? "dark" : "light" });
-    await updateStatusBar(value);
-  },
-);
-
 watch(
 watch(
   () => locale.value,
   () => locale.value,
   async (value) => {
   async (value) => {
@@ -47,16 +35,12 @@ watch(
 );
 );
 
 
 onBeforeMount(async () => {
 onBeforeMount(async () => {
-  const { value: themePref } = await Preferences.get({ key: "theme" });
-  const theme = themePref || systemTheme;
-  const isDark = theme === "dark";
-
-  $q.dark.set(isDark);
-  await updateStatusBar(isDark);
+  $q.dark.set(false);
+  await updateStatusBar();
 
 
   const { value: localePref } = await Preferences.get({ key: "locale" });
   const { value: localePref } = await Preferences.get({ key: "locale" });
   const userLocale = localePref || window.navigator.language;
   const userLocale = localePref || window.navigator.language;
 
 
-  console.log(theme, userLocale);
+  console.log(userLocale);
 });
 });
 </script>
 </script>

+ 2 - 2
src/api/paymentSplit.js

@@ -1,6 +1,6 @@
 import api from 'src/api'
 import api from 'src/api'
 
 
-export const getPaymentSplits = async () => {
-  const { data } = await api.get('/provider/payment-splits')
+export const getPaymentSplits = async (params = {}) => {
+  const { data } = await api.get('/provider/payment-splits', { params })
   return data.payload
   return data.payload
 }
 }

Diferenças do arquivo suprimidas por serem muito extensas
+ 6 - 0
src/assets/diarinho-saque.svg


+ 27 - 6
src/boot/push-notifications.js

@@ -1,10 +1,13 @@
 import { defineBoot } from "#q-app/wrappers";
 import { defineBoot } from "#q-app/wrappers";
 import { Capacitor } from "@capacitor/core";
 import { Capacitor } from "@capacitor/core";
+import { Notify } from "quasar";
 import { PushNotifications } from "@capacitor/push-notifications";
 import { PushNotifications } from "@capacitor/push-notifications";
 import { useAuth } from "src/composables/useAuth";
 import { useAuth } from "src/composables/useAuth";
 import { usePushNotifications } from "src/composables/usePushNotifications";
 import { usePushNotifications } from "src/composables/usePushNotifications";
 
 
-export default defineBoot(({ router }) => {
+const CHANNEL_ID = "diaria";
+
+export default defineBoot(async ({ router }) => {
   if (!Capacitor.isNativePlatform()) {
   if (!Capacitor.isNativePlatform()) {
     return;
     return;
   }
   }
@@ -12,11 +15,20 @@ export default defineBoot(({ router }) => {
   const { requestPermissionAndRegister, setDeviceToken, syncDeviceToken } =
   const { requestPermissionAndRegister, setDeviceToken, syncDeviceToken } =
     usePushNotifications();
     usePushNotifications();
 
 
+  if (Capacitor.getPlatform() === "android") {
+    await PushNotifications.createChannel({
+      id: CHANNEL_ID,
+      name: "Notificações",
+      description: "Canal padrão de notificações",
+      importance: 4, // HIGH
+      visibility: 1, // PUBLIC
+      vibration: true,
+    }).catch(() => {});
+  }
+
   PushNotifications.addListener("registration", async (token) => {
   PushNotifications.addListener("registration", async (token) => {
-    setDeviceToken(token.value);
+    await setDeviceToken(token.value);
 
 
-    // Se o app abriu já autenticado, envia agora; caso contrário o envio
-    // acontece no login (useAuth), quando o access token existir.
     await syncDeviceToken();
     await syncDeviceToken();
   });
   });
 
 
@@ -24,8 +36,17 @@ export default defineBoot(({ router }) => {
     // falha silenciosa
     // falha silenciosa
   });
   });
 
 
-  // Ao tocar em uma push, reconsulta a aprovação: se o cadastro acabou de ser
-  // aprovado, a dashboard completa é liberada sem exigir novo login.
+  PushNotifications.addListener("pushNotificationReceived", (notification) => {
+    Notify.create({
+      message: notification.title || "Nova notificação",
+      caption: notification.body,
+      type: "info",
+      position: "top",
+      timeout: 6000,
+      actions: [{ icon: "close", color: "white", round: true }],
+    });
+  });
+
   PushNotifications.addListener("pushNotificationActionPerformed", async () => {
   PushNotifications.addListener("pushNotificationActionPerformed", async () => {
     try {
     try {
       const approved = await useAuth().refreshApprovalStatus();
       const approved = await useAuth().refreshApprovalStatus();

+ 15 - 1
src/components/dashboard/DashboardNextSchedules.vue

@@ -25,8 +25,10 @@
       class="scroll-wrapper"
       class="scroll-wrapper"
     >
     >
       <div
       <div
+        ref="trackRef"
         class="scroll-track"
         class="scroll-track"
         :class="{ 'scroll-track--single': data.length === 1 }"
         :class="{ 'scroll-track--single': data.length === 1 }"
+        @scroll="onTrackScroll"
       >
       >
         <q-card
         <q-card
           v-for="item in data"
           v-for="item in data"
@@ -170,6 +172,11 @@
         </q-card>
         </q-card>
       </div>
       </div>
     </div>
     </div>
+
+    <DefaultCarouselIndicator
+      :active="activeIndex"
+      :count="pageCount"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -177,10 +184,13 @@
 import { avatarColors, formatAddress, formatCurrency, getFirstName } from "src/helpers/utils";
 import { avatarColors, formatAddress, formatCurrency, getFirstName } from "src/helpers/utils";
 import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
 import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
 import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
+import { useCarouselIndicator } from "src/composables/useCarouselIndicator";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 
 
-defineProps({
+import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
+
+const props = defineProps({
   data: {
   data: {
     default: () => [],
     default: () => [],
     type: Array,
     type: Array,
@@ -189,6 +199,9 @@ defineProps({
 
 
 const emit = defineEmits(["view-details"]);
 const emit = defineEmits(["view-details"]);
 
 
+const { activeIndex, onTrackScroll, pageCount, trackRef } =
+  useCarouselIndicator(() => props.data);
+
 const $q = useQuasar();
 const $q = useQuasar();
 
 
 const { t } = useI18n();
 const { t } = useI18n();
@@ -250,6 +263,7 @@ const copyAddress = (address) => {
 .schedule-card {
 .schedule-card {
   border-radius: 12px;
   border-radius: 12px;
   min-width: 82vw;
   min-width: 82vw;
+  scroll-snap-align: start;
 }
 }
 .schedule-card--single {
 .schedule-card--single {
   min-width: 100%;
   min-width: 100%;

+ 42 - 11
src/components/dashboard/DashboardOpportunities.vue

@@ -48,8 +48,9 @@
 
 
     <div v-else class="scroll-wrapper">
     <div v-else class="scroll-wrapper">
       <div
       <div
+        ref="trackRef"
         class="scroll-track row no-wrap"
         class="scroll-track row no-wrap"
-        :class="{ 'q-gutter-x-md': data.length > 1 }"
+        @scroll="onTrackScroll"
       >
       >
         <q-card
         <q-card
           v-for="item in data"
           v-for="item in data"
@@ -153,20 +154,38 @@
                 {{ item.custom_schedule?.address_type }}
                 {{ item.custom_schedule?.address_type }}
               </div>
               </div>
 
 
-              <q-btn
-                color="primary"
-                no-caps
-                rounded
-                size="xs"
-                unelevated
-                :label="$t('common.details')"
-                @click="openOpportunityDetails(item)"
-              />
+              <div class="row items-center q-gutter-x-sm">
+                <q-btn
+                  v-if="item.proposal_sent"
+                  color="grey-5"
+                  disable
+                  no-caps
+                  rounded
+                  size="xs"
+                  unelevated
+                  :label="$t('provider.dashboard.opportunities.requested')"
+                />
+
+                <q-btn
+                  color="primary"
+                  no-caps
+                  rounded
+                  size="xs"
+                  unelevated
+                  :label="$t('common.details')"
+                  @click="openOpportunityDetails(item)"
+                />
+              </div>
             </div>
             </div>
           </q-card-section>
           </q-card-section>
         </q-card>
         </q-card>
       </div>
       </div>
     </div>
     </div>
+
+    <DefaultCarouselIndicator
+      :active="activeIndex"
+      :count="pageCount"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -174,12 +193,14 @@
 import { avatarColors, formatCurrency, getFirstName } from "src/helpers/utils";
 import { avatarColors, formatCurrency, getFirstName } from "src/helpers/utils";
 import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
 import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
 import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
+import { useCarouselIndicator } from "src/composables/useCarouselIndicator";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 import { useRouter } from "vue-router";
 import { useRouter } from "vue-router";
 
 
+import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
 import OpportunityDetailsDialog from "src/pages/opportunities/components/OpportunityDetailsDialog.vue";
 import OpportunityDetailsDialog from "src/pages/opportunities/components/OpportunityDetailsDialog.vue";
 
 
-defineProps({
+const props = defineProps({
   data: {
   data: {
     default: () => [],
     default: () => [],
     type: Array,
     type: Array,
@@ -190,6 +211,9 @@ const $q = useQuasar();
 
 
 const router = useRouter();
 const router = useRouter();
 
 
+const { activeIndex, onTrackScroll, pageCount, trackRef } =
+  useCarouselIndicator(() => props.data);
+
 
 
 
 
 
 
@@ -227,8 +251,14 @@ const openOpportunityDetails = (item) => {
   padding-top: 1px;
   padding-top: 1px;
 }
 }
 .scroll-wrapper {
 .scroll-wrapper {
+  overflow: hidden;
+}
+.scroll-track {
+  gap: 12px;
   overflow-x: auto;
   overflow-x: auto;
+  overscroll-behavior-x: contain;
   padding-bottom: 8px;
   padding-bottom: 8px;
+  scroll-snap-type: x proximity;
 
 
   &::-webkit-scrollbar {
   &::-webkit-scrollbar {
     display: none;
     display: none;
@@ -237,6 +267,7 @@ const openOpportunityDetails = (item) => {
 .opportunity-card {
 .opportunity-card {
   border-radius: 12px;
   border-radius: 12px;
   min-width: 82vw;
   min-width: 82vw;
+  scroll-snap-align: start;
 }
 }
 .opportunity-card--single {
 .opportunity-card--single {
   min-width: 100%;
   min-width: 100%;

+ 15 - 1
src/components/dashboard/DashboardPendingConfirmation.vue

@@ -25,8 +25,10 @@
       class="scroll-wrapper"
       class="scroll-wrapper"
     >
     >
       <div
       <div
+        ref="trackRef"
         class="scroll-track"
         class="scroll-track"
         :class="{ 'scroll-track--single': data.length === 1 }"
         :class="{ 'scroll-track--single': data.length === 1 }"
+        @scroll="onTrackScroll"
       >
       >
         <q-card
         <q-card
           v-for="item in data"
           v-for="item in data"
@@ -170,6 +172,11 @@
         </q-card>
         </q-card>
       </div>
       </div>
     </div>
     </div>
+
+    <DefaultCarouselIndicator
+      :active="activeIndex"
+      :count="pageCount"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -177,10 +184,13 @@
 import { avatarColors, formatAddress, formatCurrency, getFirstName } from "src/helpers/utils";
 import { avatarColors, formatAddress, formatCurrency, getFirstName } from "src/helpers/utils";
 import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
 import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
 import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
+import { useCarouselIndicator } from "src/composables/useCarouselIndicator";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 
 
-defineProps({
+import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
+
+const props = defineProps({
   data: {
   data: {
     default: () => [],
     default: () => [],
     type: Array,
     type: Array,
@@ -189,6 +199,9 @@ defineProps({
 
 
 const emit = defineEmits(["view-details"]);
 const emit = defineEmits(["view-details"]);
 
 
+const { activeIndex, onTrackScroll, pageCount, trackRef } =
+  useCarouselIndicator(() => props.data);
+
 const $q = useQuasar();
 const $q = useQuasar();
 
 
 const { t } = useI18n();
 const { t } = useI18n();
@@ -250,6 +263,7 @@ const copyAddress = (address) => {
 .schedule-card {
 .schedule-card {
   border-radius: 12px;
   border-radius: 12px;
   min-width: 82vw;
   min-width: 82vw;
+  scroll-snap-align: start;
 }
 }
 .schedule-card--single {
 .schedule-card--single {
   min-width: 100%;
   min-width: 100%;

+ 29 - 14
src/components/dashboard/DashboardSolicitations.vue

@@ -29,11 +29,10 @@
       :class="{ 'scroll-wrapper--single': visibleSolicitations.length === 1 }"
       :class="{ 'scroll-wrapper--single': visibleSolicitations.length === 1 }"
     >
     >
       <div
       <div
+        ref="trackRef"
         class="no-wrap row scroll-track"
         class="no-wrap row scroll-track"
-        :class="{
-          'q-gutter-x-md': visibleSolicitations.length > 1,
-          'scroll-track--single': visibleSolicitations.length === 1,
-        }"
+        :class="{ 'scroll-track--single': visibleSolicitations.length === 1 }"
+        @scroll="onTrackScroll"
       >
       >
         <q-card
         <q-card
           v-for="item in visibleSolicitations"
           v-for="item in visibleSolicitations"
@@ -189,6 +188,11 @@
         </q-card>
         </q-card>
       </div>
       </div>
     </div>
     </div>
+
+    <DefaultCarouselIndicator
+      :active="activeIndex"
+      :count="pageCount"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -196,8 +200,11 @@
 import { avatarColors, formatCurrency, getFirstName } from "src/helpers/utils";
 import { avatarColors, formatCurrency, getFirstName } from "src/helpers/utils";
 import { computed } from "vue";
 import { computed } from "vue";
 import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
 import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
+import { useCarouselIndicator } from "src/composables/useCarouselIndicator";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 
 
+import DefaultCarouselIndicator from "src/components/defaults/DefaultCarouselIndicator.vue";
+
 const props = defineProps({
 const props = defineProps({
   data: {
   data: {
     default: () => [],
     default: () => [],
@@ -233,6 +240,9 @@ const visibleSolicitations = computed(() =>
   groupedSolicitations.value.slice(0, 3)
   groupedSolicitations.value.slice(0, 3)
 );
 );
 
 
+const { activeIndex, onTrackScroll, pageCount, trackRef } =
+  useCarouselIndicator(() => visibleSolicitations.value);
+
 const periodTypeLabel = (item) => {
 const periodTypeLabel = (item) => {
   const label = labelsPeriodTypes.find(
   const label = labelsPeriodTypes.find(
     (periodType) => periodType.value == item.period_type,
     (periodType) => periodType.value == item.period_type,
@@ -306,21 +316,24 @@ const formatTimeSinceRequest = (value) => {
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .scroll-wrapper {
 .scroll-wrapper {
-  overflow-x: auto;
-  padding: 0 0 8px 8px;
-
-  &::-webkit-scrollbar {
-    display: none;
-  }
+  overflow: hidden;
 }
 }
 
 
 .scroll-wrapper--single {
 .scroll-wrapper--single {
-  overflow-x: visible;
-  padding: 0 0 8px 0;
+  overflow: visible;
 }
 }
 
 
 .scroll-track {
 .scroll-track {
   align-items: flex-start;
   align-items: flex-start;
+  gap: 12px;
+  overflow-x: auto;
+  overscroll-behavior-x: contain;
+  padding: 0 0 8px 0;
+  scroll-snap-type: x mandatory;
+
+  &::-webkit-scrollbar {
+    display: none;
+  }
 }
 }
 
 
 .solicitation-card {
 .solicitation-card {
@@ -335,15 +348,17 @@ const formatTimeSinceRequest = (value) => {
 }
 }
 
 
 .solicitation-card--multiple {
 .solicitation-card--multiple {
+  scroll-snap-align: start;
   flex: 0 0 82vw;
   flex: 0 0 82vw;
-  margin-left: 8px !important;
-  margin-right: 8px !important;
+  margin-left: 0 !important;
+  margin-right: 0 !important;
   max-width: 82vw;
   max-width: 82vw;
   min-width: 82vw;
   min-width: 82vw;
   width: 82vw;
   width: 82vw;
 }
 }
 
 
 .scroll-track--single {
 .scroll-track--single {
+  overflow-x: visible;
   width: 100%;
   width: 100%;
 }
 }
 
 

+ 17 - 3
src/components/dashboard/DashboardTodayServices.vue

@@ -15,15 +15,18 @@
 
 
     <div class="scroll-wrapper">
     <div class="scroll-wrapper">
       <div
       <div
+        ref="trackRef"
         class="scroll-track"
         class="scroll-track"
         :class="{ 'scroll-track--single': visibleItems.length === 1 }"
         :class="{ 'scroll-track--single': visibleItems.length === 1 }"
+        @scroll="onTrackScroll"
       >
       >
         <q-card
         <q-card
           v-for="item in visibleItems"
           v-for="item in visibleItems"
           :key="item.id"
           :key="item.id"
           :class="{ 'today-card--single': visibleItems.length === 1 }"
           :class="{ 'today-card--single': visibleItems.length === 1 }"
           :flat="false"
           :flat="false"
-          class="today-card card-border shadow-card bg-surface"
+          class="today-card card-border shadow-card bg-surface cursor-pointer"
+          @click="emit('view-details', item)"
         >
         >
           <q-card-section class="column justify-between q-pa-sm full-height">
           <q-card-section class="column justify-between q-pa-sm full-height">
             <div class="row no-wrap items-center q-mb-xs">
             <div class="row no-wrap items-center q-mb-xs">
@@ -108,6 +111,7 @@
                     inputmode="numeric"
                     inputmode="numeric"
                     maxlength="1"
                     maxlength="1"
                     type="tel"
                     type="tel"
+                    @click.stop
                     @input="onCodeInput(item, idx)"
                     @input="onCodeInput(item, idx)"
                     @keydown.delete="onCodeDelete(item.id, idx)"
                     @keydown.delete="onCodeDelete(item.id, idx)"
                     @paste.prevent="onCodePaste(item, $event)"
                     @paste.prevent="onCodePaste(item, $event)"
@@ -219,7 +223,7 @@
                 color="primary"
                 color="primary"
                 name="mdi-content-copy"
                 name="mdi-content-copy"
                 size="16px"
                 size="16px"
-                @click="copyAddress(item.address)"
+                @click.stop="copyAddress(item.address)"
               />
               />
             </div>
             </div>
           </template>
           </template>
@@ -255,20 +259,27 @@
         </q-card>
         </q-card>
       </div>
       </div>
     </div>
     </div>
+
+    <DefaultCarouselIndicator
+      :active="activeIndex"
+      :count="pageCount"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
 import { avatarColors, formatAddress, getFirstName } from 'src/helpers/utils';
 import { avatarColors, formatAddress, getFirstName } from 'src/helpers/utils';
 import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
 import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
+import { useCarouselIndicator } from 'src/composables/useCarouselIndicator'
 import { useI18n } from 'vue-i18n'
 import { useI18n } from 'vue-i18n'
 import { useQuasar } from 'quasar'
 import { useQuasar } from 'quasar'
 import { verifyScheduleCode } from 'src/api/schedule'
 import { verifyScheduleCode } from 'src/api/schedule'
 
 
+import DefaultCarouselIndicator from 'src/components/defaults/DefaultCarouselIndicator.vue'
 import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
 import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
 
 
 const props = defineProps({ data: { type: Array, default: () => [] } })
 const props = defineProps({ data: { type: Array, default: () => [] } })
-const emit  = defineEmits(['rate', 'refresh'])
+const emit  = defineEmits(['rate', 'refresh', 'view-details'])
 
 
 const { t } = useI18n()
 const { t } = useI18n()
 const $q    = useQuasar()
 const $q    = useQuasar()
@@ -419,6 +430,9 @@ const visibleItems = computed(() =>
   props.data.filter((item) => !item.provider_reviewed)
   props.data.filter((item) => !item.provider_reviewed)
 )
 )
 
 
+const { activeIndex, onTrackScroll, pageCount, trackRef } =
+  useCarouselIndicator(() => visibleItems.value)
+
 const copyAddress = (address) => {
 const copyAddress = (address) => {
   const formatted = formatAddress(address);
   const formatted = formatAddress(address);
 
 

+ 33 - 4
src/components/dashboard/ScheduleRatingDialog.vue

@@ -24,7 +24,14 @@
           class="q-mb-sm"
           class="q-mb-sm"
           size="64px"
           size="64px"
         >
         >
+          <img
+            v-if="photoUrl"
+            style="object-fit: cover"
+            :src="photoUrl"
+          />
+
           <span
           <span
+            v-else
             class="full-width full-height flex flex-center"
             class="full-width full-height flex flex-center"
             style="font-size: 20px; border-radius: 50%;"
             style="font-size: 20px; border-radius: 50%;"
             :style="avatarStyle"
             :style="avatarStyle"
@@ -102,7 +109,10 @@
         />
         />
       </q-card-section>
       </q-card-section>
 
 
-      <q-card-section class="q-pt-xs q-pb-xs q-px-lg">
+      <q-card-section
+        v-if="isNegative"
+        class="q-pt-xs q-pb-xs q-px-lg"
+      >
         <input
         <input
           ref="photoInputRef"
           ref="photoInputRef"
           accept="image/jpeg,image/png,image/webp"
           accept="image/jpeg,image/png,image/webp"
@@ -133,7 +143,7 @@
           </div>
           </div>
 
 
           <div
           <div
-            v-if="photos.length < 5"
+            v-if="photos.length < MAX_PHOTOS"
             class="photo-add"
             class="photo-add"
             @click="photoInputRef.click()"
             @click="photoInputRef.click()"
           >
           >
@@ -217,6 +227,8 @@ const { t } = useI18n()
 const $q = useQuasar()
 const $q = useQuasar()
 const store = userStore()
 const store = userStore()
 
 
+const MAX_PHOTOS = 5
+
 const stars = ref(0)
 const stars = ref(0)
 const selectedTagIds = ref([])
 const selectedTagIds = ref([])
 const comment = ref(null)
 const comment = ref(null)
@@ -235,6 +247,13 @@ const avatarStyle = computed(() => {
 
 
 const isNegative = computed(() => stars.value > 0 && stars.value <= 2)
 const isNegative = computed(() => stars.value > 0 && stars.value <= 2)
 
 
+const photoUrl = computed(() =>
+  props.schedule.customer_photo
+  ?? props.schedule.client_photo
+  ?? props.schedule.client?.profile_media?.url
+  ?? null
+)
+
 const initials = computed(() =>
 const initials = computed(() =>
   getFirstName(props.schedule.client_name)?.slice(0, 2).toUpperCase() ?? '??'
   getFirstName(props.schedule.client_name)?.slice(0, 2).toUpperCase() ?? '??'
 )
 )
@@ -242,7 +261,7 @@ const initials = computed(() =>
 const onPhotosSelected = (event) => {
 const onPhotosSelected = (event) => {
   const files = Array.from(event.target.files)
   const files = Array.from(event.target.files)
 
 
-  const remaining = 5 - photos.value.length
+  const remaining = MAX_PHOTOS - photos.value.length
 
 
   files.slice(0, remaining).forEach(file => {
   files.slice(0, remaining).forEach(file => {
     photos.value.push(file)
     photos.value.push(file)
@@ -256,6 +275,16 @@ const onStarsChange = () => {
   selectedTagIds.value = []
   selectedTagIds.value = []
 
 
   blockClient.value = false
   blockClient.value = false
+
+  if (!isNegative.value) clearPhotos()
+}
+
+const clearPhotos = () => {
+  photoPreviews.value.forEach(preview => URL.revokeObjectURL(preview))
+
+  photos.value = []
+
+  photoPreviews.value = []
 }
 }
 
 
 const removePhoto = (idx) => {
 const removePhoto = (idx) => {
@@ -292,7 +321,7 @@ const submit = async () => {
       block_provider: false,
       block_provider: false,
       block_client: isNegative.value && blockClient.value,
       block_client: isNegative.value && blockClient.value,
       favorite_provider: false,
       favorite_provider: false,
-      photos: photos.value,
+      photos: isNegative.value ? photos.value : [],
     })
     })
 
 
     onDialogOK(true)
     onDialogOK(true)

+ 6 - 0
src/components/dashboard/SolicitationDetailsDialog.vue

@@ -167,6 +167,7 @@
               no-caps
               no-caps
               size="sm"
               size="sm"
               :label="$t('provider.dashboard.cancel_schedule.help')"
               :label="$t('provider.dashboard.cancel_schedule.help')"
+              @click="openHelp"
             />
             />
           </div>
           </div>
         </q-card-section>
         </q-card-section>
@@ -403,6 +404,7 @@ import { computed, ref } from "vue";
 import { useDialogPluginComponent, useQuasar } from "quasar";
 import { useDialogPluginComponent, useQuasar } from "quasar";
 import { useI18n } from "vue-i18n";
 import { useI18n } from "vue-i18n";
 
 
+import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 import ScheduleCancelDialog from "./ScheduleCancelDialog.vue";
 import ScheduleCancelDialog from "./ScheduleCancelDialog.vue";
 
 
 const props = defineProps({
 const props = defineProps({
@@ -609,6 +611,10 @@ const parseLocalDate = (dateString) => {
 
 
 //
 //
 
 
+const openHelp = () => {
+  $q.dialog({ component: ProfileHelpDialog });
+};
+
 const openCancelDialog = () => {
 const openCancelDialog = () => {
   $q.dialog({
   $q.dialog({
     component: ScheduleCancelDialog,
     component: ScheduleCancelDialog,

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

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

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

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

+ 30 - 15
src/components/login/LoginStep3Panel.vue

@@ -9,7 +9,8 @@
     <q-input
     <q-input
       :ref="fieldRefs.name"
       :ref="fieldRefs.name"
       v-model="form.name"
       v-model="form.name"
-      class="bg-surface q-mb-md q-mt-sm"
+      bg-color="surface"
+      class="q-mb-md q-mt-sm"
       hide-bottom-space
       hide-bottom-space
       input-class="font12 fontmedium text-text"
       input-class="font12 fontmedium text-text"
       lazy-rules
       lazy-rules
@@ -32,7 +33,8 @@
     <q-input
     <q-input
       :ref="fieldRefs.phone"
       :ref="fieldRefs.phone"
       v-model="form.phone"
       v-model="form.phone"
-      class="bg-surface q-mb-md q-mt-sm"
+      bg-color="surface"
+      class="q-mb-md q-mt-sm"
       hide-bottom-space
       hide-bottom-space
       input-class="font12 fontmedium text-text"
       input-class="font12 fontmedium text-text"
       lazy-rules
       lazy-rules
@@ -56,7 +58,8 @@
     <q-input
     <q-input
       :ref="fieldRefs.email"
       :ref="fieldRefs.email"
       v-model="form.email"
       v-model="form.email"
-      class="bg-surface q-mb-md q-mt-sm"
+      bg-color="surface"
+      class="q-mb-md q-mt-sm"
       hide-bottom-space
       hide-bottom-space
       input-class="font12 fontmedium text-text"
       input-class="font12 fontmedium text-text"
       lazy-rules
       lazy-rules
@@ -81,7 +84,8 @@
     <q-input
     <q-input
       :ref="fieldRefs.rg"
       :ref="fieldRefs.rg"
       v-model="form.rg"
       v-model="form.rg"
-      class="bg-surface q-mb-md q-mt-sm"
+      bg-color="surface"
+      class="q-mb-md q-mt-sm"
       hide-bottom-space
       hide-bottom-space
       input-class="font12 fontmedium text-text"
       input-class="font12 fontmedium text-text"
       lazy-rules
       lazy-rules
@@ -106,7 +110,8 @@
     <q-input
     <q-input
       :ref="fieldRefs.document"
       :ref="fieldRefs.document"
       v-model="form.document"
       v-model="form.document"
-      class="bg-surface q-mb-md q-mt-sm"
+      bg-color="surface"
+      class="q-mb-md q-mt-sm"
       hide-bottom-space
       hide-bottom-space
       input-class="font12 fontmedium text-text"
       input-class="font12 fontmedium text-text"
       lazy-rules
       lazy-rules
@@ -131,7 +136,8 @@
     <q-input
     <q-input
       :ref="fieldRefs.birth_date"
       :ref="fieldRefs.birth_date"
       v-model="form.birth_date"
       v-model="form.birth_date"
-      class="bg-surface q-mb-md q-mt-sm"
+      bg-color="surface"
+      class="q-mb-md q-mt-sm"
       hide-bottom-space
       hide-bottom-space
       input-class="font12 fontmedium text-text"
       input-class="font12 fontmedium text-text"
       lazy-rules
       lazy-rules
@@ -170,7 +176,8 @@
     <q-input
     <q-input
       :ref="fieldRefs.zip_code"
       :ref="fieldRefs.zip_code"
       v-model="form.zip_code"
       v-model="form.zip_code"
-      class="bg-surface q-mb-md q-mt-sm"
+      bg-color="surface"
+      class="q-mb-md q-mt-sm"
       hide-bottom-space
       hide-bottom-space
       input-class="font12 fontmedium text-text"
       input-class="font12 fontmedium text-text"
       lazy-rules
       lazy-rules
@@ -206,7 +213,8 @@
     <q-input
     <q-input
       :ref="fieldRefs.address"
       :ref="fieldRefs.address"
       v-model="form.address"
       v-model="form.address"
-      class="bg-surface q-mb-md q-mt-sm"
+      bg-color="surface"
+      class="q-mb-md q-mt-sm"
       hide-bottom-space
       hide-bottom-space
       input-class="font12 fontmedium text-text"
       input-class="font12 fontmedium text-text"
       lazy-rules
       lazy-rules
@@ -232,7 +240,8 @@
         <q-input
         <q-input
           :ref="fieldRefs.number"
           :ref="fieldRefs.number"
           v-model="form.number"
           v-model="form.number"
-          class="bg-surface q-mb-md q-mt-sm"
+          bg-color="surface"
+          class="q-mb-md q-mt-sm"
           hide-bottom-space
           hide-bottom-space
           input-class="font12 fontmedium text-text"
           input-class="font12 fontmedium text-text"
           lazy-rules
           lazy-rules
@@ -257,7 +266,8 @@
         <q-input
         <q-input
           :ref="fieldRefs.district"
           :ref="fieldRefs.district"
           v-model="form.district"
           v-model="form.district"
-          class="bg-surface q-mb-md q-mt-sm"
+          bg-color="surface"
+          class="q-mb-md q-mt-sm"
           hide-bottom-space
           hide-bottom-space
           input-class="font12 fontmedium text-text"
           input-class="font12 fontmedium text-text"
           lazy-rules
           lazy-rules
@@ -285,7 +295,8 @@
         <q-input
         <q-input
           :ref="fieldRefs.city"
           :ref="fieldRefs.city"
           v-model="form.city"
           v-model="form.city"
-          class="bg-surface q-mb-md q-mt-sm"
+          bg-color="surface"
+          class="q-mb-md q-mt-sm"
           hide-bottom-space
           hide-bottom-space
           input-class="font12 fontmedium text-text"
           input-class="font12 fontmedium text-text"
           lazy-rules
           lazy-rules
@@ -310,7 +321,8 @@
         <q-input
         <q-input
           :ref="fieldRefs.state"
           :ref="fieldRefs.state"
           v-model="form.state"
           v-model="form.state"
-          class="bg-surface q-mb-md q-mt-sm"
+          bg-color="surface"
+          class="q-mb-md q-mt-sm"
           hide-bottom-space
           hide-bottom-space
           input-class="font12 fontmedium text-text"
           input-class="font12 fontmedium text-text"
           lazy-rules
           lazy-rules
@@ -335,7 +347,8 @@
     <q-input
     <q-input
       :ref="fieldRefs.complement"
       :ref="fieldRefs.complement"
       v-model="form.complement"
       v-model="form.complement"
-      class="bg-surface q-mb-md q-mt-sm"
+      bg-color="surface"
+      class="q-mb-md q-mt-sm"
       hide-bottom-space
       hide-bottom-space
       input-class="font12 fontmedium text-text"
       input-class="font12 fontmedium text-text"
       lazy-rules
       lazy-rules
@@ -358,7 +371,8 @@
     <q-input
     <q-input
       :ref="fieldRefs.nickname"
       :ref="fieldRefs.nickname"
       v-model="form.nickname"
       v-model="form.nickname"
-      class="bg-surface q-mb-md q-mt-sm"
+      bg-color="surface"
+      class="q-mb-md q-mt-sm"
       hide-bottom-space
       hide-bottom-space
       input-class="font12 fontmedium text-text"
       input-class="font12 fontmedium text-text"
       lazy-rules
       lazy-rules
@@ -381,7 +395,8 @@
       :ref="fieldRefs.instructions"
       :ref="fieldRefs.instructions"
       v-model="form.instructions"
       v-model="form.instructions"
       autogrow
       autogrow
-      class="bg-surface q-mb-md q-mt-sm"
+      bg-color="surface"
+      class="q-mb-md q-mt-sm"
       hide-bottom-space
       hide-bottom-space
       input-class="font12 fontmedium text-text"
       input-class="font12 fontmedium text-text"
       lazy-rules
       lazy-rules

+ 5 - 4
src/components/login/LoginStep5Panel.vue

@@ -7,7 +7,8 @@
     <DefaultCurrencyInput
     <DefaultCurrencyInput
       :ref="fieldRefs.daily_price_8h"
       :ref="fieldRefs.daily_price_8h"
       v-model="form.daily_price_8h"
       v-model="form.daily_price_8h"
-      class="bg-surface q-mb-sm custom-currency-input"
+      bg-color="surface"
+      class="q-mb-sm custom-currency-input"
       hide-bottom-space
       hide-bottom-space
       input-class="text-text font12 fontmedium"
       input-class="text-text font12 fontmedium"
       label=""
       label=""
@@ -50,7 +51,7 @@
 
 
         <q-input
         <q-input
           :model-value="formatCurrency(form.daily_price_6h)"
           :model-value="formatCurrency(form.daily_price_6h)"
-          class="bg-surface"
+          bg-color="surface"
           input-class="font12 fontmedium text-text"
           input-class="font12 fontmedium text-text"
           readonly
           readonly
         />
         />
@@ -63,7 +64,7 @@
 
 
         <q-input
         <q-input
           :model-value="formatCurrency(form.daily_price_4h)"
           :model-value="formatCurrency(form.daily_price_4h)"
-          class="bg-surface"
+          bg-color="surface"
           input-class="font12 fontmedium text-text"
           input-class="font12 fontmedium text-text"
           readonly
           readonly
         />
         />
@@ -76,7 +77,7 @@
 
 
         <q-input
         <q-input
           :model-value="formatCurrency(form.daily_price_2h)"
           :model-value="formatCurrency(form.daily_price_2h)"
-          class="bg-surface"
+          bg-color="surface"
           input-class="font12 fontmedium text-text"
           input-class="font12 fontmedium text-text"
           readonly
           readonly
         />
         />

+ 4 - 2
src/components/payments/MovimentacoesDialog.vue

@@ -24,7 +24,8 @@
             <q-card-section class="q-pa-sm">
             <q-card-section class="q-pa-sm">
               <div class="row items-center no-wrap q-gutter-x-sm">
               <div class="row items-center no-wrap q-gutter-x-sm">
                 <q-avatar size="40px" :color="movBgColor(item.type)" :text-color="movIconColor(item.type)">
                 <q-avatar size="40px" :color="movBgColor(item.type)" :text-color="movIconColor(item.type)">
-                  <q-icon :name="movIcon(item.type)" size="20px" />
+                  <img v-if="item.client_photo" :src="item.client_photo" style="object-fit: cover" />
+                  <q-icon v-else :name="movIcon(item.type)" size="20px" />
                 </q-avatar>
                 </q-avatar>
                 <div class="col column">
                 <div class="col column">
                   <span class="font13 fontsemibold text-dark">{{ movLabel(item) }}</span>
                   <span class="font13 fontsemibold text-dark">{{ movLabel(item) }}</span>
@@ -68,7 +69,7 @@ const loadMovements = async () => {
   loading.value = true;
   loading.value = true;
   try {
   try {
     const [splits, withdrawals] = await Promise.all([
     const [splits, withdrawals] = await Promise.all([
-      getPaymentSplits(),
+      getPaymentSplits({ payment_status: 'paid' }),
       getWithdrawals(),
       getWithdrawals(),
     ]);
     ]);
 
 
@@ -76,6 +77,7 @@ const loadMovements = async () => {
       id: `split-${s.id}`,
       id: `split-${s.id}`,
       type: 'servico',
       type: 'servico',
       client_name: s.client_name,
       client_name: s.client_name,
+      client_photo: s.client_photo,
       gross_amount: parseFloat(s.gross_amount || 0),
       gross_amount: parseFloat(s.gross_amount || 0),
       created_at: s.created_at,
       created_at: s.created_at,
     }));
     }));

+ 65 - 0
src/components/payments/WithdrawSuccessDialog.vue

@@ -0,0 +1,65 @@
+<template>
+  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right" @hide="onDialogHide">
+    <div class="bg-page full-height column no-shadow">
+
+      <div class="row items-center q-px-md q-pb-sm bg-white shadow-profile bg-surface dialog-safe-header">
+        <q-btn icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogOK" />
+        <q-space />
+        <span class="text-primary font16 fontbold gradient-diarista">{{ $t('provider.payments.withdraw_success_header') }}</span>
+        <q-space />
+        <div style="width: 32px"></div>
+      </div>
+
+      <div class="col column items-center justify-center q-px-xl q-pb-xl">
+        <img
+          alt="mascote"
+          class="success-mascot q-mb-lg"
+          :src="LogoDiariaSaque"
+        />
+
+        <div class="success-title fontbold text-primary text-center q-mb-sm">
+          {{ $t('provider.payments.withdraw_success_title') }}
+        </div>
+
+        <i18n-t class="success-message text-grey-6 text-center font14" keypath="provider.payments.withdraw_success_message" tag="div">
+          <template #deposited>
+            <strong class="text-text">{{ $t('provider.payments.withdraw_success_deposited') }}</strong>
+          </template>
+
+          <template #deadline>
+            <strong class="text-text">{{ $t('provider.payments.withdraw_success_deadline') }}</strong>
+          </template>
+        </i18n-t>
+      </div>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { useDialogPluginComponent } from 'quasar';
+
+import LogoDiariaSaque from 'src/assets/diarinho-saque.svg';
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
+</script>
+
+<style scoped>
+.success-mascot {
+  width: 260px;
+  max-width: 75vw;
+  height: auto;
+}
+
+.success-title {
+  font-size: 22px;
+  line-height: 1.3;
+  max-width: 280px;
+}
+
+.success-message {
+  line-height: 1.6;
+  max-width: 280px;
+}
+</style>

+ 121 - 0
src/composables/useCarouselIndicator.js

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

+ 48 - 12
src/composables/usePushNotifications.js

@@ -1,33 +1,62 @@
 import { Capacitor } from "@capacitor/core";
 import { Capacitor } from "@capacitor/core";
+import { Preferences } from "@capacitor/preferences";
 import { PushNotifications } from "@capacitor/push-notifications";
 import { PushNotifications } from "@capacitor/push-notifications";
 import { registerDeviceToken, removeDeviceToken } from "src/api/deviceToken";
 import { registerDeviceToken, removeDeviceToken } from "src/api/deviceToken";
 import { userStore } from "src/stores/user";
 import { userStore } from "src/stores/user";
 
 
-// Token FCM do dispositivo, capturado pelo listener "registration" do boot.
-// Fica em escopo de módulo para continuar acessível no logout.
-let deviceToken = null;
+const PUSH_TOKEN_KEY = "fcm_device_token";
 
 
 export const usePushNotifications = () => {
 export const usePushNotifications = () => {
   const isSupported = () => Capacitor.isNativePlatform();
   const isSupported = () => Capacitor.isNativePlatform();
 
 
-  const setDeviceToken = (token) => {
-    deviceToken = token;
+  const setDeviceToken = async (token) => {
+    if (!isSupported()) {
+      return;
+    }
+
+    await Preferences.set({
+      key: PUSH_TOKEN_KEY,
+      value: JSON.stringify({ token, platform: Capacitor.getPlatform() }),
+    });
+  };
+
+  const getDeviceToken = async () => {
+    if (!isSupported()) {
+      return null;
+    }
+
+    const { value } = await Preferences.get({ key: PUSH_TOKEN_KEY });
+
+    if (!value) {
+      return null;
+    }
+
+    try {
+      return JSON.parse(value);
+    } catch {
+      return null;
+    }
   };
   };
 
 
   /**
   /**
    * Envia o token do dispositivo para a API.
    * Envia o token do dispositivo para a API.
-   * Só faz sentido autenticado: sem access token o POST volta 401, o
-   * dispositivo fica sem push e o interceptor ainda tenta um refresh à toa.
+   * O token continua guardado depois do envio — o logout precisa dele.
    */
    */
   const syncDeviceToken = async () => {
   const syncDeviceToken = async () => {
-    if (!isSupported() || !deviceToken || !userStore().accessToken) {
+    if (!isSupported() || !userStore().accessToken) {
+      return;
+    }
+
+    const stored = await getDeviceToken();
+
+    if (!stored?.token) {
       return;
       return;
     }
     }
 
 
     try {
     try {
-      await registerDeviceToken(deviceToken, Capacitor.getPlatform());
+      await registerDeviceToken(stored.token, stored.platform);
     } catch {
     } catch {
-      // falha silenciosa — não bloqueia o uso do app
+      // falha silenciosa — o token segue guardado para a próxima tentativa
     }
     }
   };
   };
 
 
@@ -36,12 +65,18 @@ export const usePushNotifications = () => {
    * depois que o usuário sai da conta.
    * depois que o usuário sai da conta.
    */
    */
   const unregisterDeviceToken = async () => {
   const unregisterDeviceToken = async () => {
-    if (!isSupported() || !deviceToken || !userStore().accessToken) {
+    if (!isSupported() || !userStore().accessToken) {
+      return;
+    }
+
+    const stored = await getDeviceToken();
+
+    if (!stored?.token) {
       return;
       return;
     }
     }
 
 
     try {
     try {
-      await removeDeviceToken(deviceToken);
+      await removeDeviceToken(stored.token);
     } catch {
     } catch {
       // falha silenciosa — não pode impedir o logout
       // falha silenciosa — não pode impedir o logout
     }
     }
@@ -73,6 +108,7 @@ export const usePushNotifications = () => {
     isSupported,
     isSupported,
     requestPermissionAndRegister,
     requestPermissionAndRegister,
     setDeviceToken,
     setDeviceToken,
+    getDeviceToken,
     syncDeviceToken,
     syncDeviceToken,
     unregisterDeviceToken,
     unregisterDeviceToken,
   };
   };

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

@@ -337,6 +337,7 @@
         "empty_alert_text": "Wait for a client request and keep an eye out in case opportunities appear here.",
         "empty_alert_text": "Wait for a client request and keep an eye out in case opportunities appear here.",
         "distance_km": "{distance} km from you",
         "distance_km": "{distance} km from you",
         "details": "View details",
         "details": "View details",
+        "requested": "Requested",
         "currency": "R$ {value}"
         "currency": "R$ {value}"
       },
       },
       "opportunity_details": {
       "opportunity_details": {
@@ -506,7 +507,14 @@
       "withdraw_confirm_message": "Do you want to confirm the withdrawal to your account?",
       "withdraw_confirm_message": "Do you want to confirm the withdrawal to your account?",
       "withdraw_gross_amount_label": "Available balance",
       "withdraw_gross_amount_label": "Available balance",
       "withdraw_fee_label": "Transfer fee",
       "withdraw_fee_label": "Transfer fee",
-      "withdraw_net_amount_label": "Amount you will receive"
+      "withdraw_net_amount_label": "Amount you will receive",
+      "withdraw_success_header": "Withdrawal",
+      "withdraw_success_title": "Withdrawal requested and processing..",
+      "withdraw_success_message": "The amount {deposited} into the registered account {deadline}.",
+      "withdraw_success_deposited": "will be deposited",
+      "withdraw_success_deadline": "within two business days",
+      "withdrawal_in_progress_title": "Withdrawal in progress",
+      "withdrawal_in_progress_desc": "Withdrawal of {amount} requested on {date} is being processed."
     }
     }
   },
   },
   "business": {
   "business": {
@@ -519,7 +527,13 @@
   "profile": {
   "profile": {
     "title": "Profile",
     "title": "Profile",
     "share": {
     "share": {
-      "message": "Download the Diária App and hire a service with {name}."
+      "message": "Download the Diária App and book a service with {name}. Use the code {code} in the app's Favorites tab to add me to your favorites automatically \n{link}",
+      "title": "Diária App",
+      "dialog_title": "Share profile",
+      "code_label": "My code:",
+      "code_copied": "Code copied!",
+      "copied": "Message copied! Paste it on WhatsApp to share.",
+      "error": "Could not share the profile. Please try again."
     },
     },
     "edit_profile": "Edit profile",
     "edit_profile": "Edit profile",
     "edit_data": "My data",
     "edit_data": "My data",

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

@@ -337,6 +337,7 @@
         "empty_alert_text": "Espera la solicitud de un cliente y mantente atento por si aparecen oportunidades aquí.",
         "empty_alert_text": "Espera la solicitud de un cliente y mantente atento por si aparecen oportunidades aquí.",
         "distance_km": "{distance} km de ti",
         "distance_km": "{distance} km de ti",
         "details": "Ver detalles",
         "details": "Ver detalles",
+        "requested": "Solicitado",
         "currency": "R$ {value}"
         "currency": "R$ {value}"
       },
       },
       "opportunity_details": {
       "opportunity_details": {
@@ -506,7 +507,14 @@
       "withdraw_confirm_message": "¿Desea confirmar el retiro a su cuenta?",
       "withdraw_confirm_message": "¿Desea confirmar el retiro a su cuenta?",
       "withdraw_gross_amount_label": "Saldo disponible",
       "withdraw_gross_amount_label": "Saldo disponible",
       "withdraw_fee_label": "Tarifa de transferencia",
       "withdraw_fee_label": "Tarifa de transferencia",
-      "withdraw_net_amount_label": "Monto que recibirá"
+      "withdraw_net_amount_label": "Monto que recibirá",
+      "withdraw_success_header": "Retiro",
+      "withdraw_success_title": "Retiro solicitado en procesamiento..",
+      "withdraw_success_message": "El monto {deposited} en la cuenta registrada {deadline}.",
+      "withdraw_success_deposited": "será depositado",
+      "withdraw_success_deadline": "en hasta dos días hábiles",
+      "withdrawal_in_progress_title": "Retiro en curso",
+      "withdrawal_in_progress_desc": "Retiro de {amount} solicitado el {date} está en procesamiento."
     }
     }
   },
   },
   "business": {
   "business": {
@@ -519,7 +527,13 @@
   "profile": {
   "profile": {
     "title": "Perfil",
     "title": "Perfil",
     "share": {
     "share": {
-      "message": "Descarga la app Diária y contrata un servicio con {name}."
+      "message": "Descarga la app Diária y contrata un servicio con {name}. Usa el código {code} en la pestaña Favoritos de la app para añadirme automáticamente a tus favoritos \n{link}",
+      "title": "Diária App",
+      "dialog_title": "Compartir perfil",
+      "code_label": "Mi código:",
+      "code_copied": "¡Código copiado!",
+      "copied": "¡Mensaje copiado! Pégalo en WhatsApp para compartir.",
+      "error": "No se pudo compartir el perfil. Inténtalo de nuevo."
     },
     },
     "edit_profile": "Editar perfil",
     "edit_profile": "Editar perfil",
     "edit_data": "Mis datos",
     "edit_data": "Mis datos",

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

@@ -337,6 +337,7 @@
         "empty_alert_text": "Aguarde a solicitação de um cliente e fique atento caso apareçam oportunidades aqui.",
         "empty_alert_text": "Aguarde a solicitação de um cliente e fique atento caso apareçam oportunidades aqui.",
         "distance_km": "{distance} km de você",
         "distance_km": "{distance} km de você",
         "details": "Ver detalhes",
         "details": "Ver detalhes",
+        "requested": "Solicitado",
         "currency": "R$ {value}"
         "currency": "R$ {value}"
       },
       },
       "opportunity_details": {
       "opportunity_details": {
@@ -509,7 +510,14 @@
       "withdraw_confirm_message": "Deseja confirmar o saque para sua conta?",
       "withdraw_confirm_message": "Deseja confirmar o saque para sua conta?",
       "withdraw_gross_amount_label": "Saldo disponível",
       "withdraw_gross_amount_label": "Saldo disponível",
       "withdraw_fee_label": "Taxa de transferência",
       "withdraw_fee_label": "Taxa de transferência",
-      "withdraw_net_amount_label": "Valor que você receberá"
+      "withdraw_net_amount_label": "Valor que você receberá",
+      "withdraw_success_header": "Saque",
+      "withdraw_success_title": "Saque solicitado em processamento..",
+      "withdraw_success_message": "O valor {deposited} na conta cadastrada {deadline}.",
+      "withdraw_success_deposited": "será depositado",
+      "withdraw_success_deadline": "em até dois dias úteis",
+      "withdrawal_in_progress_title": "Saque em andamento",
+      "withdrawal_in_progress_desc": "Saque de {amount} solicitado em {date} está em processamento."
     }
     }
   },
   },
   "business": {
   "business": {
@@ -522,7 +530,13 @@
   "profile": {
   "profile": {
     "title": "Perfil",
     "title": "Perfil",
     "share": {
     "share": {
-      "message": "Baixe o Diária App e contrate um serviço com {name}."
+      "message": "Baixe o Diária App e contrate um serviço com {name}. Use o código {code} na aba Favoritos do app para me adicionar automaticamente aos seus favoritos\n{link}",
+      "title": "Diária App",
+      "dialog_title": "Compartilhar perfil",
+      "code_label": "Meu código:",
+      "code_copied": "Código copiado!",
+      "copied": "Mensagem copiada! Cole no WhatsApp para compartilhar.",
+      "error": "Não foi possível compartilhar o perfil. Tente novamente."
     },
     },
     "edit_profile": "Editar perfil",
     "edit_profile": "Editar perfil",
     "edit_data": "Meu dados",
     "edit_data": "Meu dados",

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

@@ -257,14 +257,17 @@
 import { avatarColors, formatCurrency, getFirstName } from 'src/helpers/utils';
 import { avatarColors, formatCurrency, getFirstName } from 'src/helpers/utils';
 import { getProviderCalendar } from 'src/api/providerCalendar';
 import { getProviderCalendar } from 'src/api/providerCalendar';
 import { labelsPeriodTypes } from 'src/helpers/labelsPeriodTypes.js';
 import { labelsPeriodTypes } from 'src/helpers/labelsPeriodTypes.js';
-import { onMounted, ref } from 'vue';
+import { nextTick, onMounted, ref } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import { useQuasar } from 'quasar';
 import { useQuasar } from 'quasar';
+import { useRoute, useRouter } from 'vue-router';
 
 
 import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
 import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
 import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
 import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
 
 
 const $q = useQuasar();
 const $q = useQuasar();
+const route = useRoute();
+const router = useRouter();
 const { t } = useI18n();
 const { t } = useI18n();
 
 
 
 
@@ -390,9 +393,26 @@ const openRatingDialog = (schedule) => {
   });
   });
 };
 };
 
 
+const openScheduleFromQuery = async () => {
+  const scheduleId = Number(route.query.scheduleId);
+
+  if (!scheduleId) return;
+
+  await router.replace({ path: route.path, query: {} });
+  await nextTick();
+
+  const schedule = [...upcomingSchedules.value, ...completedSchedules.value].find(
+    (item) => item.id === scheduleId,
+  );
+
+  if (schedule) openDetailsDialog(schedule);
+};
+
 onMounted(async () => {
 onMounted(async () => {
   await loadCalendar();
   await loadCalendar();
   loading.value = false;
   loading.value = false;
+
+  await openScheduleFromQuery();
 });
 });
 </script>
 </script>
 
 

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

@@ -25,6 +25,7 @@
             :data="todayServices"
             :data="todayServices"
             @refresh="loadDashboard"
             @refresh="loadDashboard"
             @rate="openRatingDialog"
             @rate="openRatingDialog"
+            @view-details="openScheduleOnCalendar"
           />
           />
 
 
           <DashboardScrollAreaSchedules />
           <DashboardScrollAreaSchedules />
@@ -204,20 +205,26 @@ const openDetailsDialog = (solicitation, initialView = "details") => {
   $q.dialog({
   $q.dialog({
     component: SolicitationDetailsDialog,
     component: SolicitationDetailsDialog,
     componentProps: { initialView, solicitation },
     componentProps: { initialView, solicitation },
-  }).onOk(async ({ action, id, ids, service_package_id }) => {
-  await handleScheduleAction(
-    id,
-    action === "accept" ? "accepted" : "rejected",
-    ids,
-    service_package_id,
-  );
-
-  if (action === "accept") {
-    $q.dialog({
-      component: ProposalAcceptedDialog,
-    });
-  }
-});
+  }).onOk(async ({ action, id, ids, service_package_id } = {}) => {
+    if (action === "cancelled") {
+      await loadDashboard();
+
+      return;
+    }
+
+    await handleScheduleAction(
+      id,
+      action === "accept" ? "accepted" : "rejected",
+      ids,
+      service_package_id,
+    );
+
+    if (action === "accept") {
+      $q.dialog({
+        component: ProposalAcceptedDialog,
+      });
+    }
+  });
 };
 };
 
 
 const openNextScheduleDialog = (schedule) => {
 const openNextScheduleDialog = (schedule) => {
@@ -231,6 +238,13 @@ const openNextScheduleDialog = (schedule) => {
   });
   });
 };
 };
 
 
+const openScheduleOnCalendar = (schedule) => {
+  router.push({
+    name: "CalendarPage",
+    query: { scheduleId: schedule.id },
+  });
+};
+
 const openRatingDialog = (schedule) => {
 const openRatingDialog = (schedule) => {
   $q.dialog({
   $q.dialog({
     component: ScheduleRatingDialog,
     component: ScheduleRatingDialog,
@@ -240,8 +254,6 @@ const openRatingDialog = (schedule) => {
   });
   });
 };
 };
 
 
-// Chegada pela push de cadastro aprovado: o status já foi revalidado no boot,
-// aqui só recarregamos a dashboard liberada e avisamos o prestador.
 watch(
 watch(
   () => route.query.approved,
   () => route.query.approved,
   async (val) => {
   async (val) => {

+ 13 - 0
src/pages/opportunities/OpportunitiesPage.vue

@@ -137,6 +137,19 @@
 
 
           <q-space />
           <q-space />
 
 
+          <q-btn
+            v-if="item.proposal_sent"
+            class="font11 fontsemibold q-mr-sm"
+            color="grey-5"
+            disable
+            no-caps
+            padding="4px 12px"
+            rounded
+            size="xs"
+            unelevated
+            :label="$t('provider.dashboard.opportunities.requested')"
+          />
+
           <q-btn
           <q-btn
             class="font11 fontsemibold"
             class="font11 fontsemibold"
             color="secondary"
             color="secondary"

+ 10 - 2
src/pages/opportunities/components/OpportunityDetailsDialog.vue

@@ -193,12 +193,15 @@
       <div class="q-px-md text-center">
       <div class="q-px-md text-center">
         <q-btn
         <q-btn
           class="font11 fontsemibold full-width"
           class="font11 fontsemibold full-width"
-          color="primary"
           no-caps
           no-caps
           rounded
           rounded
           unelevated
           unelevated
+          :color="details.proposal_sent ? 'grey-5' : 'primary'"
+          :disable="details.proposal_sent"
           :label="
           :label="
-            $t('provider.dashboard.opportunity_details.button_accept')
+            details.proposal_sent
+              ? $t('provider.dashboard.opportunities.requested')
+              : $t('provider.dashboard.opportunity_details.button_accept')
           "
           "
           @click="goToProposalFlow"
           @click="goToProposalFlow"
         />
         />
@@ -206,6 +209,7 @@
 
 
       <div class="q-px-md">
       <div class="q-px-md">
         <q-btn
         <q-btn
+          v-if="!details.proposal_sent"
           class="refuse-btn font11 fontsemibold full-width"
           class="refuse-btn font11 fontsemibold full-width"
           flat
           flat
           label="Recusar oportunidade"
           label="Recusar oportunidade"
@@ -398,6 +402,10 @@ const goBack = () => {
 }
 }
 
 
 const goToProposalFlow = async () => {
 const goToProposalFlow = async () => {
+  if (details.value?.proposal_sent) {
+    return
+  }
+
   await proposalOpportunity(
   await proposalOpportunity(
     details.value.schedule_id,
     details.value.schedule_id,
     user.user.provider.id
     user.user.provider.id

+ 75 - 4
src/pages/payments/PaymentsPage.vue

@@ -139,6 +139,39 @@
       </q-card>
       </q-card>
     </div>
     </div>
 
 
+    <div v-if="withdrawalInProgress" class="q-mx-md q-mt-sm">
+      <q-card
+        class="balance-card withdrawal-progress-card bg-surface"
+        flat
+      >
+        <q-card-section class="row items-center no-wrap q-pa-md">
+          <q-icon
+            class="q-mr-sm"
+            color="primary"
+            name="mdi-bank-transfer-out"
+            size="26px"
+          />
+
+          <div class="col">
+            <div class="text-primary font12 fontbold">
+              {{ $t("provider.payments.withdrawal_in_progress_title") }}
+            </div>
+
+            <div class="text-grey-6 font12 q-mt-xs">
+              {{
+                $t("provider.payments.withdrawal_in_progress_desc", {
+                  amount: formatCurrency(parseAmount(withdrawalInProgress.net_amount)),
+                  date: formatIsoDate(withdrawalInProgress.created_at),
+                })
+              }}
+            </div>
+          </div>
+
+          <q-spinner-dots color="primary" size="24px" />
+        </q-card-section>
+      </q-card>
+    </div>
+
     <div class="q-mx-md q-mt-md">
     <div class="q-mx-md q-mt-md">
       <q-card
       <q-card
         class="balance-card bg-secondary shadow-card"
         class="balance-card bg-secondary shadow-card"
@@ -244,10 +277,10 @@
                 </div>
                 </div>
 
 
                 <div class="text-text fontmedium font12">
                 <div class="text-text fontmedium font12">
-                  <span>
+                  <span class="text-text">
                     {{ $t("provider.payments.services_date_payment") }}
                     {{ $t("provider.payments.services_date_payment") }}
+                    {{ " " + item.payment_date }}
                   </span>
                   </span>
-                  {{ " " + item.payment_date }}
                 </div>
                 </div>
               </div>
               </div>
 
 
@@ -286,6 +319,7 @@ import { avatarColors, formatCurrency } from "src/helpers/utils";
 import {
 import {
   getBalance,
   getBalance,
   getWithdrawalFees,
   getWithdrawalFees,
+  getWithdrawals,
   requestWithdrawal,
   requestWithdrawal,
 } from "src/api/providerWithdrawal";
 } from "src/api/providerWithdrawal";
 
 
@@ -296,6 +330,7 @@ import { useQuasar } from "quasar";
 
 
 import MovimentacoesDialog from "src/components/payments/MovimentacoesDialog.vue";
 import MovimentacoesDialog from "src/components/payments/MovimentacoesDialog.vue";
 import WithdrawConfirmDialog from "src/components/payments/WithdrawConfirmDialog.vue";
 import WithdrawConfirmDialog from "src/components/payments/WithdrawConfirmDialog.vue";
+import WithdrawSuccessDialog from "src/components/payments/WithdrawSuccessDialog.vue";
 
 
 const $q = useQuasar();
 const $q = useQuasar();
 
 
@@ -309,6 +344,8 @@ const withdrawLoading  = ref(false);
 const saldoDisponivel = ref(0);
 const saldoDisponivel = ref(0);
 const saldoALiberar   = ref(0);
 const saldoALiberar   = ref(0);
 
 
+const withdrawalInProgress = ref(null);
+
 const paymentTransferFeeAmount = ref(3.67);
 const paymentTransferFeeAmount = ref(3.67);
 
 
 const services = ref([]);
 const services = ref([]);
@@ -341,6 +378,7 @@ const canWithdraw = computed(
   () =>
   () =>
     saldoDisponivel.value > 0 &&
     saldoDisponivel.value > 0 &&
     withdrawalNetAmount.value > 0 &&
     withdrawalNetAmount.value > 0 &&
+    !withdrawalInProgress.value &&
     !withdrawLoading.value,
     !withdrawLoading.value,
 );
 );
 
 
@@ -367,16 +405,24 @@ const periodDays = {
   year: 365,
   year: 365,
 };
 };
 
 
+const withdrawalInProgressStatuses = [
+  "pending_transfer",
+  "processing",
+];
+
 const loadData = async () => {
 const loadData = async () => {
   servicesLoading.value = true;
   servicesLoading.value = true;
 
 
   try {
   try {
-    const [balance, splits, fees] = await Promise.all([
+    const [balance, splits, fees, withdrawals] = await Promise.all([
       getBalance(),
       getBalance(),
       getPaymentSplits(),
       getPaymentSplits(),
       loadWithdrawalFees(),
       loadWithdrawalFees(),
+      loadWithdrawals(),
     ]);
     ]);
 
 
+    withdrawalInProgress.value = findWithdrawalInProgress(withdrawals);
+
     const paymentSplits = splits || [];
     const paymentSplits = splits || [];
 
 
     const availableFromSplits = sumSplits(
     const availableFromSplits = sumSplits(
@@ -410,7 +456,7 @@ const loadData = async () => {
       client_name:
       client_name:
         split.client_name ??
         split.client_name ??
         t("provider.payments.default_client_name"),
         t("provider.payments.default_client_name"),
-      client_photo: null,
+      client_photo: split.client_photo ?? null,
       date: formatServiceDate(split.schedule_date),
       date: formatServiceDate(split.schedule_date),
       payment_date: formatIsoDate(split.payment_paid_at),
       payment_date: formatIsoDate(split.payment_paid_at),
       total_amount: splitAmount(split),
       total_amount: splitAmount(split),
@@ -436,6 +482,19 @@ const loadData = async () => {
   }
   }
 };
 };
 
 
+const loadWithdrawals = async () => {
+  try {
+    return await getWithdrawals();
+  } catch {
+    return [];
+  }
+};
+
+const findWithdrawalInProgress = (withdrawals) =>
+  (withdrawals || []).find((withdrawal) =>
+    withdrawalInProgressStatuses.includes(withdrawal.status),
+  ) ?? null;
+
 const loadWithdrawalFees = async () => {
 const loadWithdrawalFees = async () => {
   try {
   try {
     return await getWithdrawalFees();
     return await getWithdrawalFees();
@@ -474,6 +533,14 @@ const onSacar = () => {
       });
       });
 
 
       saldoDisponivel.value = 0;
       saldoDisponivel.value = 0;
+
+      withdrawalInProgress.value = findWithdrawalInProgress(
+        await loadWithdrawals(),
+      );
+
+      $q.dialog({
+        component: WithdrawSuccessDialog,
+      });
     } catch (error) {
     } catch (error) {
       $q.notify({
       $q.notify({
         type: "negative",
         type: "negative",
@@ -697,6 +764,10 @@ onMounted(loadData);
   border-radius: 8px !important;
   border-radius: 8px !important;
 }
 }
 
 
+.withdrawal-progress-card {
+  border: 1px dashed var(--q-primary);
+}
+
 .text-name {
 .text-name {
   max-width: 130px;
   max-width: 130px;
   overflow: hidden;
   overflow: hidden;

+ 89 - 8
src/pages/profile/ProfilePage.vue

@@ -9,7 +9,7 @@
 
 
     <div class="q-px-md q-mt-md">
     <div class="q-px-md q-mt-md">
       <q-card class="profile-card bg-surface shadow-card card-border" :flat="false">
       <q-card class="profile-card bg-surface shadow-card card-border" :flat="false">
-        <q-btn flat round dense icon="mdi-share-variant-outline" color="grey-6" class="absolute-top-right q-ma-sm" @click="shareProfile" />
+        <q-btn flat round dense icon="mdi-share-variant-outline" color="grey-6" class="absolute-top-right q-ma-sm share-btn" @click="shareProfile" />
         
         
         <q-card-section class="column items-center q-pb-md">
         <q-card-section class="column items-center q-pb-md">
           <q-avatar size="70px" class="shadow-card" :style="!user.profile_photo ? avatarStyle : undefined">
           <q-avatar size="70px" class="shadow-card" :style="!user.profile_photo ? avatarStyle : undefined">
@@ -20,7 +20,27 @@
           <div class="fonte-nome-profile font16 fontbold q-mt-md text-text">{{ user.name || '—' }}</div>
           <div class="fonte-nome-profile font16 fontbold q-mt-md text-text">{{ user.name || '—' }}</div>
           <div class="fonte-email-profile font9 fontmedium text-grey-6 q-my-sm">{{ user.email || '—' }}</div>
           <div class="fonte-email-profile font9 fontmedium text-grey-6 q-my-sm">{{ user.email || '—' }}</div>
           <div class="fonte-telefone-profile font9 fontmedium text-grey-7">{{ user.phone || '—' }}</div> 
           <div class="fonte-telefone-profile font9 fontmedium text-grey-7">{{ user.phone || '—' }}</div> 
-          
+
+          <div
+            v-if="shareCode"
+            class="row no-wrap q-gutter-x-xs q-mt-sm share-code cursor-pointer"
+            @click="copyShareCode"
+          >
+            <div class="font9 fontmedium text-grey-6 q-my-auto">
+              <span>
+                {{ $t('profile.share.code_label') }}
+              </span>
+            </div>
+
+            <div class="font12 fontbold gradient-diarista q-my-auto">
+              <span>
+                {{ shareCode }}
+              </span>
+            
+            </div>
+            <q-icon class="q-my-auto" name="mdi-content-copy" size="14px" color="grey-6" />
+          </div>
+
           <q-btn
           <q-btn
             v-if="!isPendingProvider"
             v-if="!isPendingProvider"
             outline
             outline
@@ -150,7 +170,7 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, onMounted } from 'vue';
+import { computed, ref, onMounted } from 'vue';
 import { useQuasar } from 'quasar';
 import { useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import { Browser } from '@capacitor/browser';
 import { Browser } from '@capacitor/browser';
@@ -169,6 +189,7 @@ import { useRouter } from 'vue-router';
 
 
 const PRIVACY_POLICY_URL = 'https://politicas.softpar.inf.br/politicas/politicaDiaristaPrestador.html';
 const PRIVACY_POLICY_URL = 'https://politicas.softpar.inf.br/politicas/politicaDiaristaPrestador.html';
 const SUPPORT_PAGE_URL   = 'https://politicas.softpar.inf.br/suportes/suporteDiaristaPrestador.html';
 const SUPPORT_PAGE_URL   = 'https://politicas.softpar.inf.br/suportes/suporteDiaristaPrestador.html';
+const CLIENT_APP_STORE_URL = 'https://play.google.com/store/apps/details?id=br.com.diarista.client';
 
 
 const appVersion = process.env.APP_VERSION;
 const appVersion = process.env.APP_VERSION;
 
 
@@ -184,25 +205,78 @@ const user = ref({
   phone: ''
   phone: ''
 });
 });
 
 
-// compartilha perfil 
+const shareCode = computed(() => user.value.provider_share_code || '');
+
+const shareMessage = computed(() => t('profile.share.message', {
+  name: user.value.name,
+  code: shareCode.value,
+  link: CLIENT_APP_STORE_URL,
+}));
+
+const isShareCanceled = (error) => {
+  const message = String(error?.message ?? '').toLowerCase();
+
+  return error?.name === 'AbortError' || message.includes('canceled') || message.includes('cancelled');
+};
+
+const copyToClipboard = async (text) => {
+  try {
+    if (navigator?.clipboard?.writeText) {
+      await navigator.clipboard.writeText(text);
+
+      return true;
+    }
+  } catch (error) {
+    console.error('Erro ao copiar mensagem:', error);
+  }
+
+  return false;
+};
+
 const shareProfile = async () => {
 const shareProfile = async () => {
-  const message = t('profile.share.message', { name: user.value.name });
+  const message = shareMessage.value;
 
 
   try {
   try {
+    const { value: canShare } = await Share.canShare();
+
+    if (!canShare) {
+      throw new Error('Share unavailable');
+    }
+
     await Share.share({
     await Share.share({
-      title: 'Diária App',
+      title: t('profile.share.title'),
       text: message,
       text: message,
-      dialogTitle: 'Compartilhar perfil',
+      dialogTitle: t('profile.share.dialog_title'),
     });
     });
   } catch (error) {
   } catch (error) {
-    if (error?.name === 'AbortError') {
+    if (isShareCanceled(error)) {
       return;
       return;
     }
     }
 
 
     console.error('Erro ao compartilhar perfil:', error);
     console.error('Erro ao compartilhar perfil:', error);
+
+    const copied = await copyToClipboard(message);
+
+    $q.notify({
+      message: copied ? t('profile.share.copied') : t('profile.share.error'),
+      color: copied ? 'positive' : 'negative',
+    });
   }
   }
 };
 };
 
 
+const copyShareCode = async () => {
+  if (!shareCode.value) {
+    return;
+  }
+
+  const copied = await copyToClipboard(shareCode.value);
+
+  $q.notify({
+    message: copied ? t('profile.share.code_copied') : t('profile.share.error'),
+    color: copied ? 'positive' : 'negative',
+  });
+};
+
 const openEditProfile = () => {
 const openEditProfile = () => {
   $q.dialog({
   $q.dialog({
     component: ProfileEditDialog,
     component: ProfileEditDialog,
@@ -277,6 +351,13 @@ onMounted(async () => {
 .profile-card {
 .profile-card {
   position: relative;
   position: relative;
 }
 }
+.share-btn {
+  z-index: 1;
+}
+
+.share-code {
+  letter-spacing: 0.5px;
+}
 
 
 .btn-edit-profile {
 .btn-edit-profile {
   border-radius: 6px;
   border-radius: 6px;

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff