Просмотр исходного кода

Merge branch 'development' into feature/diaria-kay-notificação-chamativa

Gustavo Zanatta 7 часов назад
Родитель
Сommit
f8be6ec760

+ 2 - 2
package.json

@@ -1,8 +1,8 @@
 {
   "name": "diarista-prestador-app",
   "version": "0.0.1",
-  "description": "Aplicativo para o prestador do Diarista",
-  "productName": "Diarista Profissional App",
+  "description": "Aplicativo para o prestador do Diaria",
+  "productName": "Diaria Profissional App",
   "author": "zntt <zanattagg@gmail.com>",
   "type": "module",
   "private": true,

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

@@ -1,6 +1,6 @@
 {
   "appId": "br.com.diarista.provider",
-  "appName": "Diarista Profissional",
+  "appName": "Diaria Profissional",
   "webDir": "www",
   "plugins": {
     "Keyboard": {

+ 1 - 1
src-capacitor/package.json

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

+ 21 - 0
src/api/providerBankAccount.js

@@ -0,0 +1,21 @@
+import api from "src/api";
+
+export const getProviderBankAccounts = async () => {
+  const { data } = await api.get("/provider/bank-accounts");
+  return data.payload ?? [];
+};
+
+export const createProviderBankAccount = async (bankAccount) => {
+  const { data } = await api.post("/provider/bank-accounts", bankAccount);
+  return data.payload;
+};
+
+export const updateProviderBankAccount = async (id, bankAccount) => {
+  const { data } = await api.put(`/provider/bank-accounts/${id}`, bankAccount);
+  return data.payload;
+};
+
+export const deleteProviderBankAccount = async (id) => {
+  const { data } = await api.delete(`/provider/bank-accounts/${id}`);
+  return data.payload;
+};

+ 0 - 8
src/api/providerPaymentMethod.js

@@ -1,8 +0,0 @@
-import api from "src/api";
-
-export const updateProviderBankAccount = async (providerId, bankAccount) => {
-  const { data } = await api.patch(`/provider/${providerId}/bank-account`, {
-    recipient_default_bank_account: bankAccount,
-  });
-  return data.payload;
-};

+ 0 - 5
src/api/providerWithdrawal.js

@@ -5,11 +5,6 @@ export const getBalance = async () => {
   return data.payload
 }
 
-export const getWithdrawalConfiguration = async () => {
-  const { data } = await api.get('/provider/withdrawals/config')
-  return data.payload
-}
-
 export const getWithdrawalFees = async () => {
   const { data } = await api.get('/provider/withdrawals/fees')
   return data.payload

+ 28 - 23
src/boot/push-notifications.js

@@ -1,39 +1,44 @@
 import { defineBoot } from "#q-app/wrappers";
 import { Capacitor } from "@capacitor/core";
 import { PushNotifications } from "@capacitor/push-notifications";
-import { registerDeviceToken } from "src/api/deviceToken";
+import { useAuth } from "src/composables/useAuth";
+import { usePushNotifications } from "src/composables/usePushNotifications";
 
-const registerPushNotifications = async () => {
-  let permission = await PushNotifications.checkPermissions();
-
-  if (permission.receive === "prompt") {
-    permission = await PushNotifications.requestPermissions();
-  }
-
-  if (permission.receive !== "granted") {
-    return;
-  }
-
-  await PushNotifications.register();
-};
-
-export default defineBoot(() => {
+export default defineBoot(({ router }) => {
   if (!Capacitor.isNativePlatform()) {
     return;
   }
 
+  const { requestPermissionAndRegister, setDeviceToken, syncDeviceToken } =
+    usePushNotifications();
+
   PushNotifications.addListener("registration", async (token) => {
-    const platform = Capacitor.getPlatform();
-    try {
-      await registerDeviceToken(token.value, platform);
-    } catch {
-      // falha silenciosa — não bloqueia o uso do app
-    }
+    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();
   });
 
   PushNotifications.addListener("registrationError", () => {
     // falha silenciosa
   });
 
-  registerPushNotifications();
+  // 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("pushNotificationActionPerformed", async () => {
+    try {
+      const approved = await useAuth().refreshApprovalStatus();
+
+      if (!approved) {
+        return;
+      }
+
+      await router.replace({ name: "DashboardPage", query: { approved: "true" } });
+    } catch (error) {
+      console.error(error);
+    }
+  });
+
+  requestPermissionAndRegister();
 });

+ 4 - 0
src/components/dashboard/DashboardHeaderBar.vue

@@ -44,6 +44,7 @@
     <q-btn
       color="grey-7"
       dense
+      :disable="isPendingProvider"
       flat
       round
       size="sm"
@@ -70,12 +71,15 @@
 
 <script setup>
 import { computed } from 'vue'
+import { useProviderApproval } from 'src/composables/useProviderApproval'
 import { useRouter } from 'vue-router'
 
 import LogoDiariaColorida from 'src/assets/logo_diaria_colorido_sem_texto.svg'
 
 const router = useRouter()
 
+const { isPendingProvider } = useProviderApproval()
+
 const props = defineProps({
   data: {
     type: Object,

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

@@ -0,0 +1,54 @@
+<template>
+  <div class="q-mx-md q-mb-md">
+    <div class="font16 fontbold gradient-diarista q-mb-sm section-title">
+      {{ $t("provider.dashboard.pending_approval.title") }}
+    </div>
+
+    <div class="pending-alert column items-center text-center">
+      <q-icon
+        class="pending-alert__icon q-mb-sm"
+        name="mdi-alert-outline"
+        size="28px"
+      />
+
+      <div class="pending-alert__text font14 fontbold q-mb-md">
+        {{ $t("provider.dashboard.pending_approval.analyzing") }}
+      </div>
+
+      <div class="pending-alert__text font14 fontbold q-mb-md">
+        {{ $t("provider.dashboard.pending_approval.release") }}
+      </div>
+
+      <div class="pending-alert__text font14 fontbold q-mb-md">
+        {{ $t("provider.dashboard.pending_approval.email_notice") }}
+      </div>
+
+      <div class="pending-alert__text font14 fontregular">
+        {{ $t("provider.dashboard.pending_approval.come_back_later") }}
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+defineOptions({
+  name: "DashboardPendingApproval",
+});
+</script>
+
+<style lang="scss" scoped>
+.pending-alert {
+  background: #e3efff;
+  border-radius: 8px;
+  padding: 24px 20px;
+}
+
+.pending-alert__icon {
+  color: #6554d9;
+}
+
+.pending-alert__text {
+  color: #6554d9;
+  line-height: 1.3;
+}
+</style>

+ 53 - 50
src/components/dashboard/DashboardPriceSuggest.vue

@@ -1,60 +1,56 @@
 <template>
   <div class="q-mx-md q-mb-md">
     <q-card
-      class="price-suggest-card bg-surface shadow-card card-border"
+      class="price-suggest-card bg-surface shadow-card"
       :flat="false"
     >
-      <q-card-section class="q-pa-md">
-        <div class="row items-center justify-between q-mb-md">
-          <div class="column">
-            <span class="text-suggest-label font12">
-              {{ $t('provider.dashboard.price_suggest.region_label_1') }}
-            </span>
-
-            <span class="text-suggest-label font12 fontbold">
+      <q-card-section class="price-suggest-section">
+        <div class="row items-center justify-between no-wrap q-mb-sm">
+          <span class="text-suggest-label text-no-wrap">
+            {{ $t('provider.dashboard.price_suggest.region_label_1') }}
+            <span class="fontbold">
               {{ $t('provider.dashboard.price_suggest.region_label_2') }}
             </span>
-          </div>
+          </span>
 
           <q-badge
-            class="price-badge font12 fontbold q-px-md q-py-xs gradient-diarista-bg"
+            class="price-badge fontbold text-no-wrap gradient-diarista-bg"
             rounded
           >
             {{ formatCurrency(data?.average_price ?? 0) }}
           </q-badge>
         </div>
 
-        <div class="row items-center justify-between">
-          <div class="column">
-            <span class="text-suggest-label font12 fontbold">
-              {{ $t('provider.dashboard.price_suggest.my_price_label') }}
+        <div class="row items-center justify-between no-wrap">
+          <span class="text-suggest-label text-no-wrap">
+            {{ $t('provider.dashboard.price_suggest.my_price_label_1') }}
+            <span class="fontbold">
+              {{ $t('provider.dashboard.price_suggest.my_price_label_2') }}
+            </span>
+          </span>
+
+          <div class="my-price-chip row items-center no-wrap q-ml-xs">
+            <span class="text-my-price fontmedium text-no-wrap">
+              {{
+                showMyPrice
+                  ? formatCurrency(data?.your_price ?? 0)
+                  : $t('common.price_masked')
+              }}
             </span>
 
-            <q-chip
-              class="row items-center no-wrap bg-surface q-mt-xs"
-              color="text"
-              outline
-            >
-              <span class="text-my-price font10 fontsemibold q-mr-xs">
-                {{
-                  showMyPrice
-                    ? formatCurrency(data?.your_price ?? 0)
-                    : $t('common.price_masked')
-                }}
-              </span>
-
-              <q-btn
-                class="q-pa-none q-pl-sm"
-                color="grey-6"
-                flat
-                icon="mdi-eye-off-outline"
-                size="xs"
-                @click="showMyPrice = !showMyPrice"
-              />
-            </q-chip>
+            <q-btn
+              class="q-pa-none q-ml-xs"
+              color="grey-6"
+              dense
+              flat
+              :icon="showMyPrice ? 'mdi-eye-outline' : 'mdi-eye-off-outline'"
+              size="xs"
+              @click="showMyPrice = !showMyPrice"
+            />
           </div>
 
           <q-btn
+            class="q-ml-xs"
             color="primary"
             flat
             no-caps
@@ -62,14 +58,15 @@
             size="xs"
             @click="openServiceDataDialog"
           >
-            <div class="row items-center q-gutter-x-xs font10">
+            <div class="row items-center no-wrap alter-label">
               <span class="fontbold">
                 {{ $t('common.alter') }}
               </span>
 
               <q-icon
+                class="q-ml-xs"
                 name="mdi-pencil-outline"
-                size="xs"
+                size="14px"
               />
             </div>
           </q-btn>
@@ -107,32 +104,38 @@ const openServiceDataDialog = () => {
 
 <style scoped lang="scss">
 .price-suggest-card {
-  border-radius: 12px;
-}
-.text-suggest-label {
-  color: #3a3a4a;
-}
-.price-badge {
   border-radius: 20px;
 }
-.text-my-price {
-  color: #3a3a4a;
-}
 
-.price-suggest-card {
-  border-radius: 12px;
+.price-suggest-section {
+  padding: 14px 16px;
 }
 
 .text-suggest-label {
   color: #3a3a4a;
+  font-size: clamp(10px, 3.2vw, 13px);
   line-height: 1.2;
 }
 
 .price-badge {
   border-radius: 20px;
+  font-size: clamp(10px, 3.2vw, 13px);
+  padding: 4px 12px;
+}
+
+.my-price-chip {
+  border: 1px solid #b9b9c3;
+  border-radius: 20px;
+  padding: 2px 8px 2px 10px;
 }
 
 .text-my-price {
   color: #3a3a4a;
+  font-size: clamp(9px, 2.9vw, 12px);
+  letter-spacing: 0.5px;
+}
+
+.alter-label {
+  font-size: clamp(10px, 3.1vw, 13px);
 }
 </style>

+ 106 - 134
src/components/dashboard/DashboardSolicitations.vue

@@ -43,64 +43,54 @@
             'solicitation-card--multiple': visibleSolicitations.length > 1,
           }"
           :flat="false"
-          class="bg-surface card-border q-ma-md shadow-card solicitation-card"
+          class="bg-surface q-ma-md shadow-card solicitation-card"
         >
           <q-card-section class="q-pa-sm solicitation-card-section">
-            <div class="items-start no-wrap q-gutter-x-sm row text-text">
-              <div class="solicitation-avatar-column">
-                <q-avatar size="48px">
-                  <img
-                    v-if="item.customer_photo"
-                    :src="item.customer_photo"
-                    style="object-fit: cover"
-                  />
-
-                  <span
-                    v-else
-                    class="flex flex-center full-height full-width"
-                    style="border-radius: 50%"
-                    :style="avatarColors[item.id % avatarColors.length]"
-                  >
-                    {{
-                      getFirstName(item.client_name)
-                        ?.slice(0, 2)
-                        .toUpperCase() ?? "??"
-                    }}
-                  </span>
-                </q-avatar>
+            <div class="items-center no-wrap q-gutter-x-sm row text-text">
+              <q-avatar size="48px">
+                <img
+                  v-if="item.customer_photo"
+                  :src="item.customer_photo"
+                  style="object-fit: cover"
+                />
 
-              </div>
+                <span
+                  v-else
+                  class="flex flex-center full-height full-width"
+                  style="border-radius: 50%"
+                  :style="avatarColors[item.id % avatarColors.length]"
+                >
+                  {{
+                    getFirstName(item.client_name)
+                      ?.slice(0, 2)
+                      .toUpperCase() ?? "??"
+                  }}
+                </span>
+              </q-avatar>
 
               <div class="column flex-1 solicitation-info">
-                <div class="info-line solicitation-top-line no-wrap row">
-                  <div class="items-center no-wrap row info-line__main">
-                    <span class="ellipsis font12 fontbold">
-                      {{ getFirstName(item.client_name) || "—" }}
-                    </span>
+                <div class="items-center no-wrap q-gutter-x-xs row">
+                  <span class="ellipsis font12 fontmedium text-name">
+                    {{ getFirstName(item.client_name) || "—" }}
+                  </span>
 
+                  <div class="items-center no-wrap row">
                     <q-icon
-                      class="q-ml-xs"
                       color="warning"
                       name="mdi-star"
-                      size="12px"
+                      size="14px"
                     />
 
-                    <span class="font9 text-text no-wrap">
+                    <span class="font11 no-wrap text-text">
                       {{ item.average_rating }}
                     </span>
                   </div>
-
-                  <div class="column items-end no-wrap q-ml-xs solicitation-price">
-                    <span class="font12 fontbold no-wrap">
-                      {{ formatCurrency(packageTotal(item)) }}
-                    </span>
-                  </div>
                 </div>
 
-                <div class="schedule-list font9">
+                <div class="schedule-list">
                   <div
                     v-if="item.package_items.length > 1"
-                    class="package-summary font8"
+                    class="font8 package-summary"
                   >
                     {{ $t("provider.dashboard.solicitations.package_count", { count: item.package_items.length }) }}
                   </div>
@@ -110,49 +100,68 @@
                     :key="scheduleItem.id"
                     class="solicitation-date-time"
                   >
-                    <span class="schedule-weekday">
-                      {{ formatWeekday(scheduleItem.date) }}
-                    </span>
+                    <div class="items-center no-wrap row">
+                      <span class="font9 fontbold text-schedule-date-bold">
+                        {{ formatWeekday(scheduleItem.date) }}
+                      </span>
 
-                    <span class="fontbold schedule-day-month">
-                      {{ formatDayMonth(scheduleItem.date) }}
-                    </span>
+                      <span class="font9 text-schedule-date-regular">
+                        {{ ", " + formatDayMonth(scheduleItem.date) }}
+                      </span>
+                    </div>
 
-                    <span class="fontbold schedule-time">
+                    <div class="font9 text-schedule-date-regular">
                       {{ $t("common.from") }}
-                      {{ scheduleItem.start_time?.slice(0, 5) }}
+
+                      <span class="fontbold text-schedule-date-bold">
+                        {{ scheduleItem.start_time?.slice(0, 5) }}
+                      </span>
+
                       {{ $t("common.to") }}
-                      {{ scheduleItem.end_time?.slice(0, 5) }}
-                    </span>
+
+                      <span class="fontbold text-schedule-date-bold">
+                        {{ scheduleItem.end_time?.slice(0, 5) }}
+                      </span>
+                    </div>
                   </div>
                 </div>
+
+                <div class="font9 text-time-since">
+                  {{ formatTimeSinceRequest(item.time_since_request) }}
+                </div>
               </div>
-            </div>
 
-            <div class="font9 info-line solicitation-address-line row text-text">
-              {{ formatTimeSinceRequest(item.time_since_request) }} -
+              <div class="column items-end no-wrap solicitation-meta text-text">
+                <div class="font14 fontbold no-wrap text-price">
+                  {{ formatCurrency(packageTotal(item)) }}
+                </div>
+
+                <div class="font10 fontmedium text-type">
+                  {{ periodTypeLabel(item) }}
+                </div>
 
-              <span class="info-line__main solicitation-address">
-                {{ item.address?.district || "N/A" }}
+                <div class="ellipsis font10 text-region">
+                  {{ item.address?.district || "N/A" }}
+                </div>
 
-                <span class="fontbold no-wrap solicitation-distance">
+                <div class="font10 no-wrap text-distance">
                   {{ item.distance_km ?? 0 }} {{ $t("common.km") }}
-                </span>
-              </span>
+                </div>
+              </div>
             </div>
 
             <div class="items-center q-gutter-x-sm q-mt-sm row solicitation-actions">
-              <div class="solicitation-details">
-                <q-btn
-                  class="col-auto q-px-none solicitation-details-btn"
-                  color="primary"
-                  flat
-                  no-caps
-                  size="xs"
-                  :label="$t('common.details')"
-                  @click="emit('view-details', item)"
-                />
-              </div>
+              <q-btn
+                class="col-auto solicitation-details-btn"
+                color="primary"
+                flat
+                no-caps
+                padding="4px 8px"
+                size="xs"
+                unelevated
+                :label="$t('common.details')"
+                @click="emit('view-details', item)"
+              />
 
               <q-space />
 
@@ -186,6 +195,7 @@
 <script setup>
 import { avatarColors, formatCurrency, getFirstName } from "src/helpers/utils";
 import { computed } from "vue";
+import { labelsPeriodTypes } from "src/helpers/labelsPeriodTypes.js";
 import { useI18n } from "vue-i18n";
 
 const props = defineProps({
@@ -223,6 +233,14 @@ const visibleSolicitations = computed(() =>
   groupedSolicitations.value.slice(0, 3)
 );
 
+const periodTypeLabel = (item) => {
+  const label = labelsPeriodTypes.find(
+    (periodType) => periodType.value == item.period_type,
+  )?.label;
+
+  return label ? t(label) : "";
+};
+
 const packageTotal = (item) =>
   item.package_items.reduce(
     (total, packageItem) => total + (Number(packageItem.total_amount) || 0),
@@ -306,7 +324,7 @@ const formatTimeSinceRequest = (value) => {
 }
 
 .solicitation-card {
-  box-shadow: -4px 2px 12px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.06) !important;
+  border-radius: 12px;
   overflow: visible;
 }
 
@@ -338,93 +356,47 @@ const formatTimeSinceRequest = (value) => {
   min-width: 0;
 }
 
-.solicitation-avatar-column {
-  flex: 0 0 48px;
-  text-align: center;
-}
-
-.solicitation-time {
-  margin-bottom: 6px;
-  white-space: nowrap;
-}
-
 .solicitation-info {
-  align-self: stretch;
-  justify-content: space-around;
   gap: 2px;
-}
-
-.info-line {
-  align-items: baseline;
-  justify-content: space-between;
-  min-width: 0;
-}
-
-.solicitation-top-line {
-  align-items: flex-start;
-}
-
-.info-line__main {
   min-width: 0;
 }
 
-.solicitation-address {
-  max-width: 100%;
-  overflow-wrap: anywhere;
-  white-space: normal;
-}
-
-.solicitation-distance::before {
-  content: " - ";
+.solicitation-meta {
+  gap: 2px;
+  max-width: 42%;
+  text-align: right;
 }
 
 .schedule-list {
   display: flex;
   flex-direction: column;
   gap: 2px;
-  margin-bottom: 0;
 }
 
 .solicitation-date-time {
-  align-items: center;
-  display: grid;
-  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.4fr);
-  border-bottom: 1px dotted #d5d5df;
-  padding: 1px 0;
-  width: 100%;
   line-height: 1.35;
+  min-width: 0;
 }
 
-.schedule-weekday {
-  text-align: left;
-}
-
-.schedule-day-month {
-  text-align: center;
+.font11 {
+  font-size: 11px;
 }
 
-.schedule-time {
-  min-width: 0;
-  text-align: right;
-  white-space: nowrap;
+.text-name,
+.text-price,
+.text-schedule-date-bold {
+  color: #3a3a4a;
 }
 
-.solicitation-address-line {
-  display: block;
-  margin: 8px -8px 0;
-  padding: 8px 8px 6px;
-  text-align: center;
-
-  &::before {
-    color: var(--q-primary);
-    content: "●";
-    margin-right: 4px;
-  }
+.text-distance,
+.text-region,
+.text-schedule-date-regular,
+.text-type {
+  color: #666;
 }
 
-.solicitation-details {
-  align-items: center;
-  display: flex;
+.text-time-since {
+  color: #9a9aa5;
 }
 
 .solicitation-details-btn {

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

@@ -52,7 +52,7 @@
         :class="expanded ? 'items-start' : 'items-center no-wrap'"
       >
         <div
-          class="text-text col font8 fontmedium"
+          class="text-text col font12 fontmedium"
           :class="{ ellipsis: !expanded }"
         >
           {{ formatAddress(data.address) }}

+ 16 - 4
src/components/dashboard/NextSchedulesDetailsDialog.vue

@@ -116,15 +116,22 @@
           <div class="col-1 q-my-auto">
             <q-icon
               class="flex-shrink-0 q-mt-xxs"
-              color="primary"
-              name="mdi-map-marker-outline"
+              :color="isConfirmed ? 'primary' : 'grey-6'"
+              :name="isConfirmed ? 'mdi-map-marker-outline' : 'mdi-map-marker-off-outline'"
               size="20px"
             />
           </div>
 
           <div class="col">
-            <span class="font12 text-center text-grey-8">
-              {{ formattedAddress || "N/A" }}
+            <span
+              class="font12 text-center"
+              :class="isConfirmed ? 'text-grey-8' : 'text-grey-6'"
+            >
+              {{
+                isConfirmed
+                  ? formattedAddress || "N/A"
+                  : $t("provider.dashboard.next_schedules.address_hidden")
+              }}
             </span>
           </div>
         </div>
@@ -140,6 +147,7 @@
         </div>
 
         <q-btn
+          v-if="isConfirmed"
           class="q-mt-xs"
           color="primary"
           dense
@@ -229,6 +237,10 @@ const dayMonthLabel = computed(() => {
 
 const formattedAddress = computed(() => formatAddress(props.schedule.address));
 
+const isConfirmed = computed(() =>
+  ["accepted", "paid", "started", "finished"].includes(props.schedule.status),
+);
+
 const periodLabel = computed(() => {
   const period = labelsPeriodTypes.find(
     (label) => label.value == props.schedule.period_type,

+ 38 - 1
src/components/dashboard/ScheduleCancelDialog.vue

@@ -25,6 +25,24 @@
         </div>
       </q-card-section>
 
+      <q-card-section
+        v-if="packageWarning"
+        class="q-pt-none q-pb-sm q-px-lg"
+      >
+        <div class="package-warning-box row no-wrap q-gutter-x-sm q-pa-sm">
+          <q-icon
+            class="q-mt-xs flex-shrink-0"
+            color="negative"
+            name="mdi-package-variant"
+            size="22px"
+          />
+
+          <span class="font13 fontbold text-negative">
+            {{ packageWarning }}
+          </span>
+        </div>
+      </q-card-section>
+
       <q-card-section class="q-pt-none q-pb-sm q-px-lg">
         <div class="text-grey-8 font14 fontbold q-mb-xs">
           {{ $t('provider.dashboard.cancel_schedule.reason_label') }}
@@ -103,7 +121,7 @@
 import { cancelSchedule } from 'src/api/schedule'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import { useI18n } from 'vue-i18n'
-import { ref } from 'vue'
+import { computed, ref } from 'vue'
 
 const props = defineProps({
   schedule: {
@@ -119,6 +137,20 @@ const $q = useQuasar()
 const cancelText = ref('')
 const loading = ref(false)
 
+const packageWarning = computed(() => {
+  if (!props.schedule?.service_package_id) return null
+
+  const count = props.schedule?.package_items?.length
+
+  if (count > 1) {
+    return t('provider.dashboard.cancel_schedule.package_warning', { count })
+  }
+
+  if (count === 1) return null
+
+  return t('provider.dashboard.cancel_schedule.package_warning_generic')
+})
+
 const confirmCancel = async () => {
   if (!cancelText.value || cancelText.value.trim().length < 5) return
 
@@ -153,6 +185,11 @@ const confirmCancel = async () => {
   border-radius: 10px;
 }
 
+.package-warning-box {
+  background: #fdecec;
+  border-radius: 10px;
+}
+
 .btn-action {
   min-width: 110px;
 }

+ 494 - 0
src/components/profile/BankAccountForm.vue

@@ -0,0 +1,494 @@
+<template>
+  <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">
+      {{ isEditing ? $t('profile.bank_data.edit_account') : $t('profile.bank_data.bank_account_title') }}
+    </div>
+
+    <div class="col-12 field-label q-mb-xs">
+      {{ $t('profile.bank_data.bank_code') }}
+    </div>
+
+    <q-select
+      ref="bankSelectRef"
+      v-model="form.bank_code"
+      behavior="menu"
+      class="q-mb-sm col-12"
+      dense
+      emit-value
+      fill-input
+      hide-dropdown-icon
+      hide-selected
+      input-class="text-text"
+      input-debounce="0"
+      map-options
+      menu-anchor="bottom left"
+      menu-self="top left"
+      options-dense
+      use-input
+      outlined
+      popup-content-class="bank-select-menu"
+      :loading="loadingBanks"
+      :menu-offset="[0, 4]"
+      :options="bankOptions"
+      :options-dark="false"
+      :placeholder="$t('profile.bank_data.bank_code_placeholder')"
+      :popup-content-style="bankPopupStyle"
+      @blur="clearInvalidBankInput"
+      @filter="filterBanks"
+      @input-value="onBankInputValue"
+      @popup-show="updateBankSelectWidth"
+    >
+      <template #append>
+        <q-icon color="grey-7" name="mdi-magnify" />
+      </template>
+
+      <template #option="scope">
+        <q-item v-bind="scope.itemProps">
+          <q-item-section
+            class="bank-select-menu__code"
+            side
+          >
+            {{ scope.opt.code }}
+          </q-item-section>
+
+          <q-item-section class="bank-select-menu__name">
+            <q-item-label class="ellipsis">
+              {{ scope.opt.name }}
+            </q-item-label>
+          </q-item-section>
+        </q-item>
+      </template>
+
+      <template #no-option>
+        <q-item>
+          <q-item-section class="text-grey">
+            {{ $t("http.errors.no_records_found") }}
+          </q-item-section>
+        </q-item>
+      </template>
+    </q-select>
+
+    <div class="row q-mb-sm q-my-auto col-12 row items-center">
+      <q-radio
+        v-model="form.type"
+        class="q-mr-lg text-text"
+        color="primary"
+        keep-color
+        val="checking"
+        :label="$t('profile.bank_data.checking')"
+      />
+
+      <q-radio
+        v-model="form.type"
+        class="text-text"
+        color="primary"
+        keep-color
+        val="savings"
+        :label="$t('profile.bank_data.savings')"
+      />
+    </div>
+
+    <div class="row col-12 q-col-gutter-sm q-mb-sm">
+      <div class="col-8">
+        <div class="field-label q-mb-xs">
+          {{ $t('profile.bank_data.agency') }}
+        </div>
+
+        <q-input
+          v-model="form.branch_number"
+          dense
+          input-class="text-text"
+          inputmode="numeric"
+          maxlength="4"
+          outlined
+          :placeholder="$t('profile.bank_data.agency_placeholder')"
+        />
+      </div>
+
+      <div class="col-4">
+        <div class="field-label q-mb-xs">
+          {{ $t('profile.bank_data.agency_digit') }}
+        </div>
+
+        <q-input
+          v-model="form.branch_check_digit"
+          dense
+          input-class="text-text"
+          maxlength="2"
+          outlined
+          :placeholder="$t('profile.bank_data.digit_placeholder')"
+        />
+      </div>
+    </div>
+
+    <div class="row col-12 q-col-gutter-sm q-mb-sm">
+      <div class="col-8">
+        <div class="field-label q-mb-xs">
+          {{ $t('profile.bank_data.account') }}
+        </div>
+
+        <q-input
+          v-model="form.account_number"
+          dense
+          input-class="text-text"
+          inputmode="numeric"
+          maxlength="13"
+          outlined
+          :placeholder="$t('profile.bank_data.account_placeholder')"
+        />
+      </div>
+
+      <div class="col-4">
+        <div class="field-label q-mb-xs">
+          {{ $t('profile.bank_data.account_digit') }}
+        </div>
+
+        <q-input
+          v-model="form.account_check_digit"
+          dense
+          input-class="text-text"
+          maxlength="2"
+          outlined
+          :placeholder="$t('profile.bank_data.digit_placeholder')"
+        />
+      </div>
+    </div>
+
+    <q-checkbox
+      v-if="canBecomePrimary"
+      v-model="form.is_primary"
+      class="col-12 q-mt-sm text-text font12"
+      color="primary"
+      dense
+      keep-color
+      :label="$t('profile.bank_data.set_as_primary')"
+    />
+
+    <q-btn
+      class="full-width q-mt-lg"
+      color="primary"
+      no-caps
+      padding="8px 16px"
+      rounded
+      unelevated
+      :disable="!canSave"
+      :label="isEditing ? $t('profile.bank_data.att_bank') : $t('profile.bank_data.save_account')"
+      :loading="saving"
+      @click="save"
+    />
+
+    <q-btn
+      class="full-width q-mt-sm"
+      color="grey-7"
+      flat
+      no-caps
+      padding="4px 16px"
+      rounded
+      :disable="saving"
+      :label="$t('profile.bank_data.cancel')"
+      @click="$emit('cancel')"
+    />
+  </q-card>
+</template>
+
+<script setup>
+import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
+import { createProviderBankAccount, updateProviderBankAccount } from 'src/api/providerBankAccount';
+import { getBanks } from 'src/api/banks';
+import { useQuasar } from 'quasar';
+import { useI18n } from 'vue-i18n';
+
+const props = defineProps({
+  account: {
+    type: Object,
+    default: null,
+  },
+});
+
+const emit = defineEmits(['saved', 'cancel']);
+
+const { t } = useI18n();
+const $q = useQuasar();
+
+const banks = ref([]);
+const bankOptions = ref([]);
+const bankSelectRef = ref(null);
+const bankSelectWidth = ref(null);
+const loadingBanks = ref(false);
+const saving = ref(false);
+
+const isEditing = computed(() => !!props.account?.id);
+
+const form = reactive({
+  bank_code: '',
+  type: 'checking',
+  branch_number: '',
+  branch_check_digit: '',
+  account_number: '',
+  account_check_digit: '',
+  is_primary: false,
+});
+
+const canBecomePrimary = computed(() => isEditing.value && !props.account?.is_primary);
+
+const bankPopupStyle = computed(() => ({
+  maxHeight: '188px',
+  overflowY: 'auto',
+  width: bankSelectWidth.value ? `${bankSelectWidth.value}px` : undefined,
+  maxWidth: bankSelectWidth.value ? `${bankSelectWidth.value}px` : undefined,
+}));
+
+const isSelectedBankValid = (code) => {
+  if (!code) {
+    return false;
+  }
+
+  return banks.value.some((bank) => getBankCode(bank) === code);
+};
+
+const canSave = computed(
+  () =>
+    isSelectedBankValid(form.bank_code) &&
+    !!form.branch_number &&
+    !!form.account_number &&
+    !!form.account_check_digit,
+);
+
+const getBankCode = (bank) => {
+  const code = bank?.code ?? bank?.bank_code ?? bank?.number ?? bank?.value ?? '';
+  return String(code).padStart(3, '0');
+};
+
+const getBankName = (bank) => bank?.name ?? bank?.label ?? bank?.title ?? '';
+
+const getBankList = (response) => {
+  if (Array.isArray(response)) {
+    return response;
+  }
+
+  if (Array.isArray(response?.data)) {
+    return response.data;
+  }
+
+  if (Array.isArray(response?.payload)) {
+    return response.payload;
+  }
+
+  if (response && typeof response === 'object') {
+    return Object.entries(response).map(([code, name]) => ({
+      code,
+      name,
+    }));
+  }
+
+  return [];
+};
+
+const filterBanks = (val, update) => {
+  update(() => {
+    const needle = normalizeText(val).trim();
+
+    if (!needle) {
+      setBankOptions(banks.value);
+      return;
+    }
+
+    const filtered = banks.value.filter((bank) => {
+      const code = getBankCode(bank);
+      const name = normalizeText(getBankName(bank));
+
+      return code.includes(needle) || name.includes(needle);
+    });
+
+    setBankOptions(filtered);
+  });
+};
+
+const formatBankOption = (bank) => {
+  const code = getBankCode(bank);
+  const name = getBankName(bank);
+
+  return {
+    code,
+    name,
+    label: [code, name].filter(Boolean).join(' '),
+    value: code,
+  };
+};
+
+const bankLabel = (code) => {
+  const bank = banks.value.find((item) => getBankCode(item) === code);
+
+  return bank ? [getBankCode(bank), getBankName(bank)].filter(Boolean).join(' ') : '';
+};
+
+const showSelectedBankLabel = () => {
+  bankSelectRef.value?.updateInputValue(bankLabel(form.bank_code), true);
+};
+
+const normalizeText = (text) =>
+  String(text ?? '')
+    .normalize('NFD')
+    .replace(/[\u0300-\u036f]/g, '')
+    .toLowerCase();
+
+const setBankOptions = (list) => {
+  bankOptions.value = list.map(formatBankOption).filter((bank) => bank.code);
+};
+
+const updateBankSelectWidth = async () => {
+  await nextTick();
+
+  bankSelectWidth.value = bankSelectRef.value?.$el?.getBoundingClientRect()?.width ?? null;
+};
+
+const clearInvalidBankInput = () => {
+  if (!isSelectedBankValid(form.bank_code)) {
+    form.bank_code = '';
+
+    bankSelectRef.value?.updateInputValue('', true);
+
+    return;
+  }
+
+  showSelectedBankLabel();
+};
+
+const onBankInputValue = (value) => {
+  const typed = String(value ?? '');
+
+  if (typed !== form.bank_code && typed !== bankLabel(form.bank_code)) {
+    form.bank_code = '';
+  }
+};
+
+const fillForm = (account) => {
+  form.bank_code = account?.bank_code ? String(account.bank_code).padStart(3, '0') : '';
+  form.type = account?.type || 'checking';
+  form.branch_number = account?.branch_number || '';
+  form.branch_check_digit = account?.branch_check_digit || '';
+  form.account_number = account?.account_number || '';
+  form.account_check_digit = account?.account_check_digit || '';
+  form.is_primary = false;
+};
+
+watch(() => props.account, async (account) => {
+  fillForm(account);
+
+  await nextTick();
+
+  showSelectedBankLabel();
+});
+
+
+const save = async () => {
+  if (!canSave.value) {
+    $q.notify({ type: 'negative', message: t('profile.bank_data.bank_fields_required') });
+
+    return;
+  }
+
+  saving.value = true;
+
+  try {
+    const payload = {
+      bank_code: form.bank_code,
+      type: form.type,
+      branch_number: form.branch_number,
+      branch_check_digit: form.branch_check_digit || null,
+      account_number: form.account_number,
+      account_check_digit: form.account_check_digit,
+    };
+
+    if (canBecomePrimary.value && form.is_primary) {
+      payload.is_primary = true;
+    }
+
+    const account = isEditing.value
+      ? await updateProviderBankAccount(props.account.id, payload)
+      : await createProviderBankAccount(payload);
+
+    emit('saved', account);
+  } catch (error) {
+    console.log(error);
+  } finally {
+    saving.value = false;
+  }
+};
+
+const loadBanks = async () => {
+  loadingBanks.value = true;
+
+  try {
+    const response = await getBanks();
+
+    banks.value = getBankList(response);
+
+    setBankOptions(banks.value);
+  } catch (error) {
+    console.log(error);
+  } finally {
+    loadingBanks.value = false;
+  }
+};
+
+onMounted(async () => {
+  fillForm(props.account);
+
+  await loadBanks();
+
+  if (form.bank_code) {
+    await nextTick();
+
+    showSelectedBankLabel();
+  }
+
+  await updateBankSelectWidth();
+
+  window.addEventListener('resize', updateBankSelectWidth);
+});
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', updateBankSelectWidth);
+});
+</script>
+
+<style scoped>
+.field-label {
+  color: #8C8C8C;
+  font-size: 12px;
+  line-height: 1.2;
+}
+</style>
+
+<style>
+.bank-select-menu {
+  background: #FFFFFF !important;
+  color: #1F1F1F !important;
+  border: 1px solid #E7E7E7;
+  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.14);
+  max-height: 188px !important;
+  overflow-y: auto !important;
+}
+
+.bank-select-menu .q-item {
+  min-height: 32px;
+  padding: 6px 10px;
+  color: #1F1F1F;
+}
+
+.bank-select-menu__name {
+  min-width: 0;
+}
+
+.bank-select-menu__code {
+  min-width: 36px;
+  color: #8C8C8C;
+  font-family: monospace;
+  padding-right: 8px;
+}
+</style>

+ 141 - 414
src/components/profile/ProfileBankDataDialog.vue

@@ -35,222 +35,116 @@
           </div>
 
           <div class="text-grey-7 q-mb-md font12 fontregular">
-            {{ $t('profile.bank_data.subtitle') }}
+            {{ accounts.length ? $t('profile.bank_data.subtitle_manage') : $t('profile.bank_data.subtitle') }}
           </div>
 
-          <template v-if="pixEnabled">
-            <div class="pix-warning q-pa-md q-mb-md row items-start q-col-gutter-sm">
-              <q-icon
-                class="q-mt-xs"
-                color="warning"
-                name="mdi-alert-outline"
-                size="20px"
-              />
-
-              <div class="col font12">
-                <span class="fontbold">
-                  {{ $t('profile.bank_data.warning_title') }}
-                </span>
-
-                {{ $t('profile.bank_data.warning_message') }}
-              </div>
-            </div>
+          <div
+            v-if="!showForm"
+            class="column items-center q-mb-lg cursor-pointer new-account"
+            @click="openCreateForm"
+          >
+            <q-btn
+              class="new-account-btn shadow-card"
+              color="white"
+              icon="mdi-plus"
+              round
+              size="lg"
+              text-color="primary"
+              unelevated
+            />
 
-            <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
-                    class="q-ml-xs"
-                    color="grey-5"
-                    :label="$t('profile.bank_data.in_development')"
-                  />
-                </div>
-              </div>
+            <span class="font12 fontregular text-text q-mt-sm">
+              {{ $t('profile.bank_data.new_account') }}
+            </span>
+          </div>
 
-              <q-input
-                class="q-mb-sm"
-                dense
-                disable
-                input-class="text-text"
-                outlined
-                :placeholder="$t('profile.bank_data.pix_key_placeholder')"
-              />
+          <BankAccountForm
+            v-if="showForm"
+            :account="editingAccount"
+            @cancel="closeForm"
+            @saved="onSaved"
+          />
 
-              <q-btn
-                class="full-width"
-                color="grey-4"
-                disable
-                no-caps
-                padding="4px 12px"
-                rounded
-                text-color="grey-7"
-                unelevated
-                :label="$t('profile.bank_data.in_development')"
-              />
-            </q-card>
-          </template>
+          <div
+            v-if="loading"
+            class="row justify-center q-py-lg"
+          >
+            <q-spinner color="primary" size="32px" />
+          </div>
 
           <q-card
-            class="q-pa-md bg-white row shadow-card q-mb-md"
+            v-for="account in accounts"
+            :key="account.id"
+            class="q-pa-md bg-white shadow-card q-mb-md row items-center no-wrap account-card"
+            :class="{ 'account-card--primary': account.is_primary }"
             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-select
-              ref="bankSelectRef"
-              v-model="bankForm.bank"
-              behavior="menu"
-              class="q-mb-sm col-12"
-              dense
-              emit-value
-              fill-input
-              hide-dropdown-icon
-              hide-selected
-              input-class="text-text"
-              input-debounce="0"
-              map-options
-              menu-anchor="bottom left"
-              menu-self="top left"
-              options-dense
-              use-input
-              outlined
-              popup-content-class="bank-select-menu"
-              stack-label
-              :label="$t('profile.bank_data.bank_code')"
-              :loading="loadingBanks"
-              :menu-offset="[0, 4]"
-              :options="bankOptions"
-              :options-dark="false"
-              :placeholder="$t('profile.bank_data.bank_code_placeholder')"
-              :popup-content-style="bankPopupStyle"
-              @blur="clearInvalidBankInput"
-              @filter="filterBanks"
-              @input-value="onBankInputValue"
-              @popup-show="updateBankSelectWidth"
-            >
-              <template #append>
-                <q-icon color="grey-7" name="mdi-magnify" />
-              </template>
-
-              <template #option="scope">
-                <q-item v-bind="scope.itemProps">
-                  <q-item-section
-                    class="bank-select-menu__code"
-                    side
-                  >
-                    {{ scope.opt.code }}
-                  </q-item-section>
-
-                  <q-item-section class="bank-select-menu__name">
-                    <q-item-label class="ellipsis">
-                      {{ scope.opt.name }}
-                    </q-item-label>
-                  </q-item-section>
-                </q-item>
-              </template>
-
-              <template #no-option>
-                <q-item>
-                  <q-item-section class="text-grey">
-                    {{ $t("http.errors.no_records_found") }}
-                  </q-item-section>
-                </q-item>
-              </template>
-            </q-select>
-
-            <div class="row q-mb-sm q-my-auto col-12 row items-center">
-              <q-radio
-                v-model="bankForm.type"
-                class="q-mr-lg text-text"
-                color="primary"
-                keep-color
-                val="checking"
-                :label="$t('profile.bank_data.checking')"
-              />
+            <div class="relative-position q-mr-md">
+              <q-avatar
+                class="bg-white shadow-card"
+                size="44px"
+                text-color="primary"
+              >
+                <q-icon name="mdi-currency-usd" />
+              </q-avatar>
 
-              <q-radio
-                v-model="bankForm.type"
-                class="text-text"
+              <q-icon
+                v-if="account.is_primary"
+                class="account-card__check"
                 color="primary"
-                keep-color
-                val="savings"
-                :label="$t('profile.bank_data.savings')"
+                name="mdi-check-circle"
+                size="16px"
               />
             </div>
 
-            <div class="row col-12 q-col-gutter-sm q-mb-sm">
-              <q-input
-                v-model="bankForm.branch_number"
-                class="col-8"
-                dense
-                input-class="text-text"
-                inputmode="numeric"
-                outlined
-                stack-label
-                :label="$t('profile.bank_data.agency')"
-                :placeholder="$t('profile.bank_data.agency_placeholder')"
-              />
+            <div class="col">
+              <div class="font14 fontbold text-text">
+                {{ account.bank_name || account.bank_code }}
+              </div>
 
-              <q-input
-                v-model="bankForm.branch_check_digit"
-                class="col-4"
-                dense
-                input-class="text-text"
-                maxlength="2"
-                outlined
-                stack-label
-                :label="$t('profile.bank_data.agency_digit')"
-                :placeholder="$t('profile.bank_data.digit_placeholder')"
+              <div class="font12 fontregular text-grey-7">
+                {{ account.holder_name }}
+              </div>
+
+              <div class="font12 fontregular text-grey-7">
+                {{ $t('profile.bank_data.agency') }} {{ formatBranch(account) }}
+              </div>
+
+              <div class="font12 fontregular text-grey-7">
+                {{ $t('profile.bank_data.account') }} {{ formatAccount(account) }}
+              </div>
+
+              <q-badge
+                v-if="account.is_primary"
+                class="q-mt-xs"
+                color="primary"
+                :label="$t('profile.bank_data.primary_badge')"
               />
             </div>
 
-            <div class="row col-12 q-col-gutter-sm q-mb-sm">
-              <q-input
-                v-model="bankForm.account_number"
-                class="col-8"
+            <div class="column items-center">
+              <q-btn
+                color="grey-7"
                 dense
-                input-class="text-text"
-                inputmode="numeric"
-                outlined
-                stack-label
-                :label="$t('profile.bank_data.account')"
-                :placeholder="$t('profile.bank_data.account_placeholder')"
+                flat
+                icon="mdi-pencil-outline"
+                round
+                size="sm"
+                @click.stop="openEditForm(account)"
               />
 
-              <q-input
-                v-model="bankForm.account_check_digit"
-                class="col-4"
+              <q-btn
+                v-if="canDelete(account)"
+                color="grey-7"
                 dense
-                input-class="text-text"
-                maxlength="2"
-                outlined
-                stack-label
-                :label="$t('profile.bank_data.account_digit')"
-                :placeholder="$t('profile.bank_data.digit_placeholder')"
+                flat
+                icon="mdi-trash-can-outline"
+                round
+                size="sm"
+                @click.stop="confirmDelete(account)"
               />
             </div>
-
-            <q-btn
-              class="full-width q-mt-xl"
-              color="primary"
-              no-caps
-              padding="8px 16px"
-              rounded
-              unelevated
-              :disable="!canSaveBank"
-              :label="bankAlreadyRegistered ? $t('profile.bank_data.att_bank') : $t('profile.bank_data.save_account')"
-              :loading="savingBank"
-              @click="saveBank"
-            />
           </q-card>
         </div>
 
@@ -261,10 +155,9 @@
 </template>
 
 <script setup>
-import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
-import { getBanks } from 'src/api/banks';
-import { getWithdrawalConfiguration } from 'src/api/providerWithdrawal';
-import { updateProviderBankAccount } from 'src/api/providerPaymentMethod';
+import BankAccountForm from 'src/components/profile/BankAccountForm.vue';
+import { onMounted, ref } from 'vue';
+import { deleteProviderBankAccount, getProviderBankAccounts } from 'src/api/providerBankAccount';
 import { useDialogPluginComponent, useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
 import { userStore } from 'src/stores/user';
@@ -276,269 +169,103 @@ const { t } = useI18n();
 const $q = useQuasar();
 const user = userStore();
 
-const banks = ref([]);
-const bankAlreadyRegistered = ref(false);
-const bankOptions = ref([]);
-const bankSelectRef = ref(null);
-const bankSelectWidth = ref(null);
-const loadingBanks = ref(false);
-const pixEnabled = ref(false);
-const savingBank = ref(false);
-
-const bankForm = reactive({
-  bank: '',
-  type: 'checking',
-  branch_number: '',
-  branch_check_digit: '',
-  account_number: '',
-  account_check_digit: '',
-});
-
-const bankPopupStyle = computed(() => ({
-  maxHeight: '188px',
-  overflowY: 'auto',
-  width: bankSelectWidth.value ? `${bankSelectWidth.value}px` : undefined,
-  maxWidth: bankSelectWidth.value ? `${bankSelectWidth.value}px` : undefined,
-}));
-
-const providerId = computed(() => user.user?.provider?.id ?? user.user?.provider_id);
-
-const isSelectedBankValid = (code) => {
-  if (!code) {
-    return false;
-  }
-
-  return banks.value.some((bank) => getBankCode(bank) === code);
-};
+const accounts = ref([]);
+const editingAccount = ref(null);
+const loading = ref(false);
+const showForm = ref(false);
 
-const canSaveBank = computed(
-  () =>
-    !!providerId.value &&
-    isSelectedBankValid(bankForm.bank) &&
-    !!bankForm.branch_number &&
-    !!bankForm.account_number &&
-    !!bankForm.account_check_digit,
-);
-
-const getBankCode = (bank) => {
-  const code = bank?.code ?? bank?.bank_code ?? bank?.number ?? bank?.value ?? '';
-  return String(code).padStart(3, '0');
-};
+const canDelete = (account) => !account.is_primary && accounts.value.length > 1;
 
-const getBankName = (bank) => bank?.name ?? bank?.label ?? bank?.title ?? '';
+const formatBranch = (account) =>
+  [account.branch_number, account.branch_check_digit].filter(Boolean).join('-');
 
-const getBankList = (response) => {
-  if (Array.isArray(response)) {
-    return response;
-  }
+const formatAccount = (account) =>
+  [account.account_number, account.account_check_digit].filter(Boolean).join('-');
 
-  if (Array.isArray(response?.data)) {
-    return response.data;
-  }
-
-  if (Array.isArray(response?.payload)) {
-    return response.payload;
-  }
-
-  if (response && typeof response === 'object') {
-    return Object.entries(response).map(([code, name]) => ({
-      code,
-      name,
-    }));
-  }
-
-  return [];
-};
-
-//
-
-const filterBanks = (val, update) => {
-  update(() => {
-    const needle = normalizeText(val).trim();
-
-    if (!needle) {
-      setBankOptions(banks.value);
-      return;
-    }
-
-    const filtered = banks.value.filter((bank) => {
-      const code = getBankCode(bank);
-      const name = normalizeText(getBankName(bank));
-
-      return code.includes(needle) || name.includes(needle);
-    });
-
-    setBankOptions(filtered);
-  });
-};
-
-const formatBankOption = (bank) => {
-  const code = getBankCode(bank);
-  const name = getBankName(bank);
-
-  return {
-    code,
-    name,
-    label: code,
-    value: code,
-  };
+const openCreateForm = () => {
+  editingAccount.value = null;
+  showForm.value = true;
 };
 
-const normalizeText = (text) =>
-  String(text ?? '')
-    .normalize('NFD')
-    .replace(/[\u0300-\u036f]/g, '')
-    .toLowerCase();
-
-const setBankOptions = (list) => {
-  bankOptions.value = list.map(formatBankOption).filter((bank) => bank.code);
+const openEditForm = (account) => {
+  editingAccount.value = account;
+  showForm.value = true;
 };
 
-const updateBankSelectWidth = async () => {
-  await nextTick();
-
-  bankSelectWidth.value = bankSelectRef.value?.$el?.getBoundingClientRect()?.width ?? null;
+const closeForm = () => {
+  editingAccount.value = null;
+  showForm.value = false;
 };
 
-//
+const onSaved = async () => {
+  closeForm();
 
-const clearInvalidBankInput = () => {
-  if (!isSelectedBankValid(bankForm.bank)) {
-    bankForm.bank = '';
+  await loadAccounts();
 
-    bankSelectRef.value?.updateInputValue('', true);
-  }
+  await user.fetchUser();
 };
 
-const onBankInputValue = (value) => {
-  if (String(value ?? '') !== bankForm.bank) {
-    bankForm.bank = '';
-  }
+const confirmDelete = (account) => {
+  $q.dialog({
+    title: t('profile.bank_data.delete_title'),
+    message: t('profile.bank_data.delete_message'),
+    cancel: { flat: true, label: t('profile.bank_data.cancel'), noCaps: true },
+    ok: { color: 'negative', label: t('profile.bank_data.confirm'), noCaps: true, rounded: true, unelevated: true },
+    persistent: true,
+  }).onOk(() => removeAccount(account));
 };
 
-const saveBank = async () => {
-  if (!canSaveBank.value) {
-    $q.notify({ type: 'negative', message: t('profile.bank_data.bank_fields_required') });
-
-    return;
-  }
-
-  savingBank.value = true;
+const removeAccount = async (account) => {
+  loading.value = true;
 
   try {
-    // 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,
-    };
-
-    await updateProviderBankAccount(providerId.value, bankAccount);
+    await deleteProviderBankAccount(account.id);
 
-    await user.fetchUser();
-
-    bankAlreadyRegistered.value = true;
+    await loadAccounts();
 
-    $q.notify({ type: 'positive', message: t('profile.bank_data.account_saved') });
+    await user.fetchUser();
   } catch (error) {
-    $q.notify({ type: 'negative', message: t('profile.bank_data.save_error') });
-
     console.log(error);
   } finally {
-    savingBank.value = false;
+    loading.value = false;
   }
 };
 
 //
 
-const loadBanks = async () => {
-  loadingBanks.value = true;
+const loadAccounts = async () => {
+  loading.value = true;
 
   try {
-    const response = await getBanks();
-
-    banks.value = getBankList(response);
-
-    setBankOptions(banks.value);
+    accounts.value = await getProviderBankAccounts();
   } catch (error) {
     console.log(error);
   } finally {
-    loadingBanks.value = false;
+    loading.value = false;
   }
 };
 
-const loadWithdrawalConfiguration = async () => {
-  try {
-    const configuration = await getWithdrawalConfiguration();
-
-    pixEnabled.value = configuration?.pix_enabled === true;
-  } catch (error) {
-    pixEnabled.value = false;
-    console.log(error);
-  }
-};
-
-//
-
-onMounted(async () => {
-  const bank = user.user?.provider?.recipient_default_bank_account;
-
-  if (bank && Object.keys(bank).length) {
-    bankForm.bank = bank.bank ? String(bank.bank).padStart(3, '0') : '';
-    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;
-  }
-
-  await Promise.all([loadBanks(), loadWithdrawalConfiguration()]);
-
-  await updateBankSelectWidth();
-
-  window.addEventListener('resize', updateBankSelectWidth);
-});
-
-onBeforeUnmount(() => {
-  window.removeEventListener('resize', updateBankSelectWidth);
-});
+onMounted(loadAccounts);
 </script>
 
 <style scoped>
-.pix-warning {
-  background-color: #EEF0FF;
-  border-radius: 12px;
-}
-</style>
-
-<style>
-.bank-select-menu {
-  background: #FFFFFF !important;
-  color: #1F1F1F !important;
-  border: 1px solid #E7E7E7;
-  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.14);
-  max-height: 188px !important;
-  overflow-y: auto !important;
+.account-card--primary {
+  border: 1px solid var(--q-primary);
 }
 
-.bank-select-menu .q-item {
-  min-height: 32px;
-  padding: 6px 10px;
-  color: #1F1F1F;
+.account-card__check {
+  position: absolute;
+  top: -2px;
+  right: -2px;
+  background: #FFFFFF;
+  border-radius: 50%;
 }
 
-.bank-select-menu__name {
-  min-width: 0;
+.new-account {
+  width: fit-content;
 }
 
-.bank-select-menu__code {
-  min-width: 36px;
-  color: #8C8C8C;
-  font-family: monospace;
-  padding-right: 8px;
+.new-account-btn {
+  border-radius: 50%;
 }
 </style>

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

@@ -110,6 +110,7 @@
             input-class="text-text"
             :placeholder="$t('profile.help.message_placeholder')"
             :disable="loading"
+            :maxlength="MESSAGE_MAX_LENGTH"
             @keyup.enter="sendMessage()"
           />
           <q-btn
@@ -148,6 +149,8 @@ const $q = useQuasar();
 const { t } = useI18n();
 const store = chatbotStore();
 
+const MESSAGE_MAX_LENGTH = 4000;
+
 const messageInput = ref('');
 const loading = ref(false);
 const messagesContainer = ref(null);
@@ -192,7 +195,7 @@ const sendMessage = async (text) => {
 
   try {
     const history = store.messages
-      .slice(-6, -1)
+      .slice(0, -1)
       .map(({ role, text: msgText }) => ({ role, text: msgText }));
 
     const reply = await sendChatMessage({ message: content, history });

+ 55 - 4
src/composables/useAuth.js

@@ -1,6 +1,8 @@
 import api from "src/api";
 import { permissionStore } from "src/stores/permission";
 import { useAuthStorage } from "src/composables/useAuthStorage";
+import { APPROVAL_STATUS, useProviderApproval } from "src/composables/useProviderApproval";
+import { usePushNotifications } from "src/composables/usePushNotifications";
 import { userStore } from "src/stores/user";
 
 let refreshPromise = null;
@@ -25,14 +27,15 @@ export const useAuth = () => {
 
     await setRefreshToken(refresh_token);
 
+    // Fire-and-forget: o boot só consegue registrar o token quando o app abre
+    // já autenticado. Quem acabou de se cadastrar ou de entrar registra aqui.
+    registerPushDevice();
+
     if (String(user?.type).toUpperCase() === "PROVIDER" && !user?.provider) {
       await store.fetchUser();
     }
 
-    if (
-      String(store.user?.type).toUpperCase() === "PROVIDER" &&
-      store.user?.provider?.approval_status === "pending"
-    ) {
+    if (useProviderApproval().isPendingProvider.value) {
       permissionStore().resetScopes();
 
       return;
@@ -41,6 +44,50 @@ export const useAuth = () => {
     await permissionStore().fetchScopes();
   };
 
+  /**
+   * Reconsulta o cadastro do prestador em análise e, se ele já tiver sido
+   * aprovado, carrega os escopos para liberar o app sem exigir novo login.
+   *
+   * @return {Promise<boolean>} true quando o cadastro acabou de ser aprovado.
+   */
+  const refreshApprovalStatus = async () => {
+    const { approvalStatus, isPendingProvider } = useProviderApproval();
+
+    if (!isPendingProvider.value) {
+      return false;
+    }
+
+    await userStore().fetchUser();
+
+    if (approvalStatus.value !== APPROVAL_STATUS.ACCEPTED) {
+      return false;
+    }
+
+    await permissionStore().fetchScopes();
+
+    return true;
+  };
+
+  /**
+   * Garante que o dispositivo esteja apto a receber push depois do login:
+   * envia o token já capturado e (re)solicita o registro no FCM.
+   */
+  const registerPushDevice = async () => {
+    const { isSupported, requestPermissionAndRegister, syncDeviceToken } =
+      usePushNotifications();
+
+    if (!isSupported()) {
+      return;
+    }
+
+    try {
+      await syncDeviceToken();
+      await requestPermissionAndRegister();
+    } catch (error) {
+      console.error(error);
+    }
+  };
+
   //
 
   const login = async (email, password) => {
@@ -62,6 +109,9 @@ export const useAuth = () => {
 
   const logout = async () => {
     try {
+      // Precisa acontecer antes do /logout, enquanto o token ainda é válido.
+      await usePushNotifications().unregisterDeviceToken();
+
       const response = await api.post("/logout");
 
       if (response.status === 200) {
@@ -115,6 +165,7 @@ export const useAuth = () => {
     login,
     logout,
     refresh,
+    refreshApprovalStatus,
     setAuthDataFromPayload,
   };
 };

+ 33 - 0
src/composables/useProviderApproval.js

@@ -0,0 +1,33 @@
+import { computed } from "vue";
+import { userStore } from "src/stores/user";
+
+export const APPROVAL_STATUS = Object.freeze({
+  ACCEPTED: "accepted",
+  PENDING: "pending",
+  REJECTED: "rejected",
+});
+
+export const useProviderApproval = () => {
+  const user = userStore();
+
+  const isProvider = computed(
+    () => String(user.user?.type).toUpperCase() === "PROVIDER",
+  );
+
+  const approvalStatus = computed(() => user.user?.provider?.approval_status ?? null);
+
+  const isPendingProvider = computed(
+    () => isProvider.value && approvalStatus.value === APPROVAL_STATUS.PENDING,
+  );
+
+  const isRejectedProvider = computed(
+    () => isProvider.value && approvalStatus.value === APPROVAL_STATUS.REJECTED,
+  );
+
+  return {
+    approvalStatus,
+    isPendingProvider,
+    isProvider,
+    isRejectedProvider,
+  };
+};

+ 79 - 0
src/composables/usePushNotifications.js

@@ -0,0 +1,79 @@
+import { Capacitor } from "@capacitor/core";
+import { PushNotifications } from "@capacitor/push-notifications";
+import { registerDeviceToken, removeDeviceToken } from "src/api/deviceToken";
+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;
+
+export const usePushNotifications = () => {
+  const isSupported = () => Capacitor.isNativePlatform();
+
+  const setDeviceToken = (token) => {
+    deviceToken = token;
+  };
+
+  /**
+   * 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.
+   */
+  const syncDeviceToken = async () => {
+    if (!isSupported() || !deviceToken || !userStore().accessToken) {
+      return;
+    }
+
+    try {
+      await registerDeviceToken(deviceToken, Capacitor.getPlatform());
+    } catch {
+      // falha silenciosa — não bloqueia o uso do app
+    }
+  };
+
+  /**
+   * Desativa o token na API para o dispositivo parar de receber push
+   * depois que o usuário sai da conta.
+   */
+  const unregisterDeviceToken = async () => {
+    if (!isSupported() || !deviceToken || !userStore().accessToken) {
+      return;
+    }
+
+    try {
+      await removeDeviceToken(deviceToken);
+    } catch {
+      // falha silenciosa — não pode impedir o logout
+    }
+  };
+
+  /**
+   * Solicita a permissão de notificações e registra o app no FCM,
+   * o que dispara o listener "registration" configurado no boot.
+   */
+  const requestPermissionAndRegister = async () => {
+    if (!isSupported()) {
+      return;
+    }
+
+    let permission = await PushNotifications.checkPermissions();
+
+    if (permission.receive === "prompt") {
+      permission = await PushNotifications.requestPermissions();
+    }
+
+    if (permission.receive !== "granted") {
+      return;
+    }
+
+    await PushNotifications.register();
+  };
+
+  return {
+    isSupported,
+    requestPermissionAndRegister,
+    setDeviceToken,
+    syncDeviceToken,
+    unregisterDeviceToken,
+  };
+};

+ 30 - 24
src/i18n/locales/en.json

@@ -265,15 +265,23 @@
       }
     },
     "dashboard": {
+      "pending_approval": {
+        "title": "Profile under review",
+        "analyzing": "Your profile is being reviewed by the diária app team.",
+        "release": "Your access will be released shortly, if approved.",
+        "email_notice": "The approval confirmation will be sent to your registered email address.",
+        "come_back_later": "Please come back later.",
+        "approved": "Registration approved! Welcome to diária app."
+      },
       "summary": {
         "welcome": "Hello,",
         "my_schedules": "My schedules"
       },
       "price_suggest": {
-        "region_label": "Average daily rate in your area",
-        "region_label_1": "Suggested daily rate",
-        "region_label_2": "in the region",
-        "my_price_label": "My daily rate (8-hour rate)"
+        "region_label_1": "Average price of",
+        "region_label_2": "daily rates in the region:",
+        "my_price_label_1": "Value of",
+        "my_price_label_2": "my daily rate:"
       },
       "today_services": {
         "finished_label": "Service with",
@@ -378,6 +386,7 @@
         "place_unknown": "Address",
         "copy_address": "Copy address",
         "address_copied": "Address copied!",
+        "address_hidden": "Address available after the booking is confirmed",
         "btn_cancel_service": "Cancel service",
         "btn_need_help": "I need help",
         "empty_alert_bold": "You do not have scheduled services right now.",
@@ -392,6 +401,8 @@
         "warning_after": " of your profile in the app.",
         "btn_cancel": "Back",
         "btn_keep": "Cancel the service",
+        "package_warning": "This booking is part of a package with {count} bookings. Cancelling it will cancel ALL upcoming bookings in the package.",
+        "package_warning_generic": "This booking is part of a package. Cancelling it will cancel ALL upcoming bookings in the package.",
         "service_fee": "Service fee",
         "total": "Total to receive:",
         "close": "Close",
@@ -519,42 +530,37 @@
     "placeholder_phone": "Enter your phone",
     "bank_data": {
       "title": "Bank data",
-      "description": "Pix, agency and account",
-      "my_bank_data_title": "My bank data",
-      "subtitle": "Register your pix key and/or account to receive payments for your services.",
-      "warning_title": "Attention!",
-      "warning_message": " The pix key must be linked to a bank account of your CPF.",
-      "pix_title": "Pix key",
-      "pix_key": "Pix key",
-      "pix_key_placeholder": "Enter your pix key",
-      "pix_key_required": "Please enter your pix key",
-      "att_pix": "Update pix",
+      "description": "Accounts to receive your payments",
+      "my_bank_data_title": "Bank account",
+      "subtitle": "Register a bank account, under your CPF, to receive your payments.",
       "att_bank": "Update account",
-      "save_pix": "register pix",
-      "pix_saved": "Pix key saved successfully!",
       "bank_account_title": "Agency and account",
-      "account_type": "Account type",
       "checking": "Checking",
       "savings": "Savings",
       "agency": "Agency",
       "agency_placeholder": "Agency",
       "account": "Account",
       "account_placeholder": "Account",
-      "digit": "Digit",
       "digit_placeholder": "Digit",
       "bank_code": "Bank (code)",
       "bank_code_placeholder": "Enter the bank name or code",
       "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.",
       "missing_alert_title": "Register your bank data",
       "missing_alert_message": "You need to register your bank data to appear to platform clients.",
       "missing_alert_action": "Register bank data",
-      "missing_alert_later": "Not now"
+      "missing_alert_later": "Not now",
+      "subtitle_manage": "Manage your registered accounts or add a new one to receive your payments.",
+      "new_account": "New account",
+      "edit_account": "Edit account",
+      "primary_badge": "Primary account",
+      "delete_title": "Delete account",
+      "delete_message": "Do you want to delete this bank account?",
+      "confirm": "Confirm",
+      "cancel": "Cancel",
+      "set_as_primary": "Make this the primary account"
     },
     "availability": {
       "title": "Availability",
@@ -642,7 +648,7 @@
     "help": {
       "title": "Help",
       "description": "Questions and support",
-      "support_title": "Diarista Support",
+      "support_title": "Diaria Support",
       "online_status": "Online now",
       "ai_assistant_label": "AI Assistant",
       "quick_suggestions": "Quick suggestions:",
@@ -900,4 +906,4 @@
       "user_message_header": "--- User message ---"
     }
   }
-}
+}

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

@@ -265,15 +265,23 @@
       }
     },
     "dashboard": {
+      "pending_approval": {
+        "title": "Perfil en revisión",
+        "analyzing": "Tu perfil está siendo analizado por el equipo de diária app.",
+        "release": "En breve se liberará tu acceso, en caso de ser aprobado.",
+        "email_notice": "La confirmación de la aprobación será enviada al correo electrónico registrado.",
+        "come_back_later": "Por favor, vuelve más tarde.",
+        "approved": "¡Registro aprobado! Bienvenido a diária app."
+      },
       "summary": {
         "welcome": "Hola,",
         "my_schedules": "Mis citas"
       },
       "price_suggest": {
-        "region_label": "Precio promedio de las tarifas diarias en tu zona",
-        "region_label_1": "Precio de jornada sugerido",
-        "region_label_2": "en la región",
-        "my_price_label": "Mi tarifa diaria (jornada de 8 horas)"
+        "region_label_1": "Precio promedio de",
+        "region_label_2": "jornadas en la región:",
+        "my_price_label_1": "Valor de",
+        "my_price_label_2": "mi jornada:"
       },
       "today_services": {
         "finished_label": "Servicio con",
@@ -378,6 +386,7 @@
         "place_unknown": "Dirección",
         "copy_address": "Copiar dirección",
         "address_copied": "¡Dirección copiada!",
+        "address_hidden": "Dirección disponible tras la confirmación de la reserva",
         "btn_cancel_service": "Cancelar servicio",
         "btn_need_help": "Necesito ayuda",
         "empty_alert_bold": "En este momento todavía no tienes servicios programados.",
@@ -392,6 +401,8 @@
         "warning_after": " de su perfil en la aplicación.",
         "btn_cancel": "Cancelar el servicio",
         "btn_keep": "Volver",
+        "package_warning": "Esta reserva forma parte de un paquete con {count} reservas. Al cancelarla, se cancelarán TODAS las reservas futuras del paquete.",
+        "package_warning_generic": "Esta reserva forma parte de un paquete. Al cancelarla, se cancelarán TODAS las reservas futuras del paquete.",
         "service_fee": "Tarifa de servicio",
         "total": "Total a recibir:",
         "close": "Cerrar",
@@ -519,42 +530,37 @@
     "placeholder_phone": "Escriba su teléfono",
     "bank_data": {
       "title": "Datos bancarios",
-      "description": "Pix, agencia y cuenta",
-      "my_bank_data_title": "Mis datos bancarios",
-      "subtitle": "Registre su clave pix y/o cuenta para recibir los pagos por sus servicios.",
-      "warning_title": "¡Atención!",
-      "warning_message": " La clave pix debe estar vinculada a una cuenta bancaria de su CPF.",
-      "pix_title": "Clave pix",
-      "pix_key": "Clave pix",
-      "pix_key_placeholder": "Ingrese su clave pix",
-      "pix_key_required": "Por favor ingrese su clave pix",
-      "att_pix": "Actualizar pix",
+      "description": "Cuentas para recibir tus pagos",
+      "my_bank_data_title": "Cuenta bancaria",
+      "subtitle": "Registra una cuenta bancaria, con tu CPF, para recibir tus pagos.",
       "att_bank": "Actualizar cuenta",
-      "save_pix": "registrar pix",
-      "pix_saved": "¡Clave pix guardada con éxito!",
       "bank_account_title": "Agencia y cuenta",
-      "account_type": "Tipo de cuenta",
       "checking": "Corriente",
       "savings": "Ahorro",
       "agency": "Agencia",
       "agency_placeholder": "Agencia",
       "account": "Cuenta",
       "account_placeholder": "Cuenta",
-      "digit": "Dígito",
       "digit_placeholder": "Dígito",
       "bank_code": "Banco (código)",
       "bank_code_placeholder": "Ingrese el nombre o código del banco",
       "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.",
       "missing_alert_title": "Registre sus datos bancarios",
       "missing_alert_message": "Debe registrar sus datos bancarios para aparecer ante los clientes de la plataforma.",
       "missing_alert_action": "Registrar datos bancarios",
-      "missing_alert_later": "Ahora no"
+      "missing_alert_later": "Ahora no",
+      "subtitle_manage": "Gestiona las cuentas registradas o registra una nueva para recibir tus pagos.",
+      "new_account": "Nueva cuenta",
+      "edit_account": "Editar cuenta",
+      "primary_badge": "Cuenta principal",
+      "delete_title": "Eliminar cuenta",
+      "delete_message": "¿Deseas eliminar esta cuenta bancaria?",
+      "confirm": "Confirmar",
+      "cancel": "Cancelar",
+      "set_as_primary": "Convertir en cuenta principal"
     },
     "availability": {
       "title": "Disponibilidad",
@@ -642,7 +648,7 @@
     "help": {
       "title": "Ayuda",
       "description": "Dudas y soporte",
-      "support_title": "Soporte Diarista",
+      "support_title": "Soporte Diaria",
       "online_status": "En línea ahora",
       "ai_assistant_label": "Asistente IA",
       "quick_suggestions": "Sugerencias rápidas:",
@@ -900,4 +906,4 @@
       "user_message_header": "--- Mensaje del usuario ---"
     }
   }
-}
+}

+ 30 - 24
src/i18n/locales/pt.json

@@ -265,15 +265,23 @@
       }
     },
     "dashboard": {
+      "pending_approval": {
+        "title": "Perfil em análise",
+        "analyzing": "Seu perfil está sendo analisado pela equipe do diária app.",
+        "release": "Em breve será liberado seu acesso, caso seja aprovado.",
+        "email_notice": "A confirmação da aprovação será enviada para o e-mail cadastrado.",
+        "come_back_later": "Por favor, volte mais tarde.",
+        "approved": "Cadastro aprovado! Boas-vindas ao diária app."
+      },
       "summary": {
         "welcome": "Olá,",
         "my_schedules": "Minhas diárias"
       },
       "price_suggest": {
-        "region_label": "Preço médio de diárias na sua região",
-        "region_label_1": "Preço de diária sugerido",
-        "region_label_2": "na região",
-        "my_price_label": "Minha diária (valor de 8h)"
+        "region_label_1": "Preço médio de",
+        "region_label_2": "diárias na região:",
+        "my_price_label_1": "Valor da",
+        "my_price_label_2": "minha diária:"
       },
       "today_services": {
         "finished_label": "Serviço com",
@@ -378,6 +386,7 @@
         "place_unknown": "Endereço",
         "copy_address": "Copiar endereço",
         "address_copied": "Endereço copiado!",
+        "address_hidden": "Endereço disponível após a confirmação do agendamento",
         "btn_cancel_service": "Cancelar serviço",
         "btn_need_help": "Preciso de ajuda",
         "empty_alert_bold": "No momento, você ainda não possui serviços agendados.",
@@ -392,6 +401,8 @@
         "warning_after": " do seu perfil no aplicativo.",
         "btn_cancel": "Voltar",
         "btn_keep": "Cancelar o serviço",
+        "package_warning": "Este agendamento faz parte de um pacote com {count} agendamentos. Ao cancelar, TODOS os agendamentos futuros do pacote serão cancelados.",
+        "package_warning_generic": "Este agendamento faz parte de um pacote. Ao cancelar, TODOS os agendamentos futuros do pacote serão cancelados.",
         "service_fee": "Taxa de serviço",
         "total": "Total a receber:",
         "close": "Fechar",
@@ -524,42 +535,37 @@
     "push_notifications": "Receber notificações",
     "bank_data": {
       "title": "Dados bancários",
-      "description": "Pix, agência e conta",
-      "my_bank_data_title": "Meus dados bancários",
-      "subtitle": "Cadastre sua chave pix e/ou conta que deseja receber os pagamentos por seus serviços.",
-      "warning_title": "Atenção!",
-      "warning_message": " A chave pix precisa estar vinculada a uma conta bancária do seu CPF.",
-      "pix_title": "Chave pix",
-      "pix_key": "Chave pix",
-      "pix_key_placeholder": "Digite sua chave pix",
-      "pix_key_required": "Informe a chave pix",
-      "att_pix": "Atualizar pix",
+      "description": "Contas para receber seus pagamentos",
+      "my_bank_data_title": "Conta bancária",
+      "subtitle": "Cadastre uma conta bancária, em seu CPF, para receber seus pagamentos.",
       "att_bank": "Atualizar conta",
-      "save_pix": "cadastrar pix",
-      "pix_saved": "Chave pix salva com sucesso!",
       "bank_account_title": "Agência e conta",
-      "account_type": "Tipo de conta",
       "checking": "Corrente",
       "savings": "Poupança",
       "agency": "Agência",
       "agency_placeholder": "Agência",
       "account": "Conta",
       "account_placeholder": "Conta",
-      "digit": "Dígito",
       "digit_placeholder": "Dígito",
       "bank_code": "Banco (código)",
       "bank_code_placeholder": "Digite o nome ou código do banco",
       "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.",
       "missing_alert_title": "Cadastre seus dados bancários",
       "missing_alert_message": "Você precisa cadastrar seus dados bancários para aparecer para os clientes da plataforma.",
       "missing_alert_action": "Cadastrar dados bancários",
-      "missing_alert_later": "Agora não"
+      "missing_alert_later": "Agora não",
+      "subtitle_manage": "Gerencie as contas cadastradas ou cadastre uma nova conta para receber seus pagamentos.",
+      "new_account": "Nova conta",
+      "edit_account": "Editar conta",
+      "primary_badge": "Conta principal",
+      "delete_title": "Excluir conta",
+      "delete_message": "Deseja excluir esta conta bancária?",
+      "confirm": "Confirmar",
+      "cancel": "Cancelar",
+      "set_as_primary": "Tornar esta a conta principal"
     },
     "availability": {
       "title": "Disponibilidade",
@@ -647,7 +653,7 @@
     "help": {
       "title": "Ajuda",
       "description": "Dúvidas e suporte",
-      "support_title": "Suporte Diarista",
+      "support_title": "Suporte Diaria",
       "online_status": "Online agora",
       "ai_assistant_label": "Assistente IA",
       "quick_suggestions": "Sugestões rápidas:",
@@ -905,4 +911,4 @@
       "user_message_header": "--- Mensagem do usuário ---"
     }
   }
-}
+}

+ 26 - 20
src/layouts/MainLayout.vue

@@ -29,11 +29,15 @@
 
     <q-footer class="provider-bottom-nav bg-white">
       <nav class="provider-bottom-nav-inner">
-        <router-link
+        <component
+          :is="item.disabled ? 'div' : 'router-link'"
           v-for="item in navItems"
           :key="item.name"
-          :class="{ 'provider-bottom-nav-item--active': isNavItemActive(item) }"
-          :to="{ name: item.name }"
+          :class="{
+            'provider-bottom-nav-item--active': isNavItemActive(item),
+            'provider-bottom-nav-item--disabled': item.disabled,
+          }"
+          :to="item.disabled ? undefined : { name: item.name }"
           class="provider-bottom-nav-item"
         >
           <q-icon :name="item.icon" class="provider-bottom-nav-icon font30" />
@@ -44,7 +48,7 @@
           >
             {{ item.label }}
           </span>
-        </router-link>
+        </component>
       </nav>
 
       <div class="system-bar-spacer provider-bottom-nav-safe-area"></div>
@@ -57,6 +61,7 @@ import { computed, onMounted, useTemplateRef, watch } from "vue";
 import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
 import { useRoute } from "vue-router";
+import { useProviderApproval } from "src/composables/useProviderApproval";
 import { userStore } from "src/stores/user";
 import MissingBankDataDialog from "src/components/profile/MissingBankDataDialog.vue";
 import ProfileBankDataDialog from "src/components/profile/ProfileBankDataDialog.vue";
@@ -70,6 +75,7 @@ const scrollAreaRef = useTemplateRef("scrollAreaRef");
 const { t } = useI18n();
 const $q = useQuasar();
 const user = userStore();
+const { isPendingProvider } = useProviderApproval();
 
 let oldValue = route.path;
 
@@ -80,21 +86,25 @@ const navItems = computed(() => [
     name: "DashboardPage",
     label: t('nav.home'),
     icon: "mdi-home-outline",
+    disabled: false,
   },
   {
     name: "PaymentsPage",
     label: t('nav.payments'),
     icon: "mdi-credit-card-outline",
+    disabled: isPendingProvider.value,
   },
   {
     name: "CalendarPage",
     label: t('nav.agenda'),
     icon: "mdi-calendar-blank-outline",
+    disabled: isPendingProvider.value,
   },
   {
     name: "ProfilePage",
     label: t('nav.profile'),
     icon: "mdi-account-circle-outline",
+    disabled: false,
   },
 ]);
 
@@ -107,6 +117,11 @@ const checkMissingBankData = async () => {
     return;
   }
 
+  // Cadastro em análise não edita dados bancários.
+  if (isPendingProvider.value) {
+    return;
+  }
+
   try {
     await user.fetchUser();
   } catch (error) {
@@ -116,22 +131,7 @@ const checkMissingBankData = async () => {
   openMissingBankDataAlert();
 };
 
-const hasCompleteBankAccount = () => {
-  const provider = user.user?.provider;
-
-  const bankAccount = provider?.recipient_default_bank_account;
-
-  if (!provider || !bankAccount || !Object.keys(bankAccount).length) {
-    return false;
-  }
-
-  return Boolean(
-    bankAccount.bank
-    && bankAccount.branch_number
-    && bankAccount.account_number
-    && bankAccount.account_check_digit,
-  );
-};
+const hasCompleteBankAccount = () => Boolean(user.user?.provider?.has_primary_bank_account);
 
 const isNavItemActive = (item) => route.name === item.name;
 
@@ -248,6 +248,12 @@ onMounted(() => {
   color: #ff00ea;
 }
 
+.provider-bottom-nav-item--disabled {
+  color: #d4d4d4;
+  cursor: default;
+  pointer-events: none;
+}
+
 .provider-bottom-nav-icon {
   font-size: 24px;
   line-height: 1;

+ 19 - 160
src/pages/LoginPage.vue

@@ -25,53 +25,7 @@
       </div>
 
       <div v-else key="flow" class="flow-screen">
-        <div
-          v-if="registrationCompleted"
-          class="registration-flow"
-        >
-          <div class="flow-header row items-center">
-            <q-btn
-              class="flow-back-btn"
-              color="primary"
-              dense
-              flat
-              round
-              :aria-label="t('auth.logout')"
-              icon="mdi-logout"
-              @click="goBack"
-            />
-          </div>
-
-          <div class="flow-logo q-my-xl">
-            <q-img
-              style="max-width: 180px"
-              :src="LogoDiariaCampos"
-            />
-          </div>
-
-          <div class="registration-complete">
-            <div class="registration-complete__card row no-wrap items-start">
-              <q-icon
-                class="registration-complete__icon"
-                name="mdi-clock-time-four-outline"
-                size="32px"
-              />
-
-              <div class="registration-complete__content">
-                <div class="registration-complete__title fontbold">
-                  {{ t("provider.login.registration_complete.title") }}
-                </div>
-
-                <div class="registration-complete__message font12">
-                  {{ t("provider.login.registration_complete.message") }}
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-
         <q-form
-          v-else
           ref="loginForm"
           autocapitalize="off"
           autocomplete="off"
@@ -186,7 +140,6 @@ import { useQuasar } from "quasar";
 import { useRouter } from "vue-router";
 import { useScroll } from "src/composables/useScroll";
 import { useSubmitHandler } from "src/composables/useSubmitHandler";
-import { userStore } from "src/stores/user";
 
 import BackgroundLogin from "src/assets/background-login.svg";
 import FotoDiarista from "src/assets/foto_diarista_login.svg";
@@ -201,8 +154,7 @@ import LoginStep6Panel from "src/components/login/LoginStep6Panel.vue";
 
 const $q = useQuasar();
 const router = useRouter();
-const user = userStore();
-const { clearAuthData, logout, setAuthDataFromPayload } = useAuth();
+const { setAuthDataFromPayload } = useAuth();
 const { t } = useI18n();
 const { scrollToComponent } = useScroll();
 
@@ -263,16 +215,9 @@ const email = ref("");
 const isLogin = ref(false);
 const loginForm = ref(null);
 const phone = ref("");
-const registrationCompleted = ref(false);
 const showSubStep = ref(false);
 const smsEnabled = ref(false);
 
-const isPendingProvider = computed(
-  () =>
-    String(user.user?.type).toUpperCase() === "PROVIDER" &&
-    user.user?.provider?.approval_status === "pending",
-);
-
 const createInitialStepFiveForm = () => ({
   daily_price_2h: null,
   daily_price_4h: null,
@@ -294,7 +239,6 @@ const createInitialStepSixForm = () => ({
   bank: "",
   branch_check_digit: "",
   branch_number: "",
-  pix_key: "",
   working_days: {},
 });
 
@@ -341,46 +285,23 @@ const actionLabel = computed(() => {
   return t("provider.login.steps.step_3.action");
 });
 
-const showBackButton = computed(
-  () => registrationCompleted.value || steps.value >= 4,
-);
+const FIRST_NAVIGABLE_STEP = 3;
 
-//
+const showBackButton = computed(() => steps.value > FIRST_NAVIGABLE_STEP);
 
-const goBack = async () => {
-  if (registrationCompleted.value) {
-    await logout();
-    await resetToLoginStart();
-    router.replace({ name: "LoginPage" });
+const goToStep = async (target) => {
+  steps.value = target;
 
-    return;
-  }
+  await nextTick();
 
-  if (steps.value === 4) {
-    steps.value = 3;
-  } else if (steps.value === 5) {
-    steps.value = 4;
-  } else if (steps.value === 6) {
-    steps.value = 5;
-  }
+  loginForm.value?.resetValidation();
+  flowContentRef.value?.setScrollPosition("vertical", 0);
 };
 
-const resetToLoginStart = async () => {
-  await clearAuthData();
-
-  code.value = "";
-  email.value = "";
-  isLogin.value = false;
-  phone.value = "";
-  registrationCompleted.value = false;
-  showSubStep.value = false;
-  loading.value = false;
-  stepFiveForm.value = createInitialStepFiveForm();
-  stepFourForm.value = createInitialStepFourForm();
-  stepSixForm.value = createInitialStepSixForm();
-  stepThreeForm.value = createInitialStepThreeForm();
-  steps.value = 1;
-  loginForm.value?.resetValidation?.();
+const goBack = async () => {
+  if (steps.value <= FIRST_NAVIGABLE_STEP) return;
+
+  await goToStep(steps.value - 1);
 };
 
 //
@@ -431,7 +352,6 @@ const onValidationError = async (field) => {
 };
 
 const onSubmit = async () => {
-  if (registrationCompleted.value) return;
   if (showSubStep.value) return;
 
   const isValid = await loginForm.value.validate();
@@ -457,7 +377,7 @@ const onSubmit = async () => {
 
       case 3: {
         if (await validateCurrentStep()) {
-          steps.value = 4;
+          await goToStep(4);
         }
 
         break;
@@ -465,7 +385,7 @@ const onSubmit = async () => {
 
       case 4: {
         if (await validateCurrentStep()) {
-          steps.value = 5;
+          await goToStep(5);
         }
 
         break;
@@ -473,7 +393,7 @@ const onSubmit = async () => {
 
       case 5: {
         if (await validateCurrentStep()) {
-          steps.value = 6;
+          await goToStep(6);
         }
 
         break;
@@ -616,11 +536,6 @@ const registerUserAndProvider = async () => {
       stepSixForm.value.account_check_digit,
     );
 
-    appendIfFilled(
-      "recipient_default_bank_account[pix_key]",
-      stepSixForm.value.pix_key,
-    );
-
     append(
       "recipient_default_bank_account[holder_name]",
       stepThreeForm.value.name,
@@ -638,7 +553,8 @@ const registerUserAndProvider = async () => {
 
   if (response?.status >= 200 && response.status < 300) {
     await setAuthDataFromPayload(response.data.payload);
-    registrationCompleted.value = true;
+
+    router.push({ name: "DashboardPage" });
   }
 
   return response;
@@ -674,12 +590,6 @@ const validateCodeInput = async () => {
     if (isLogin.value === true) {
       await setAuthDataFromPayload(response.data.payload);
 
-      if (isPendingProvider.value) {
-        registrationCompleted.value = true;
-
-        return;
-      }
-
       router.push({ name: "DashboardPage" });
 
       return;
@@ -687,7 +597,8 @@ const validateCodeInput = async () => {
 
     stepThreeForm.value.email = email.value;
     stepThreeForm.value.phone = phone.value;
-    steps.value = 3;
+
+    await goToStep(3);
   }
 };
 
@@ -735,13 +646,6 @@ const validateCurrentStep = async () => {
 onMounted(() => {
   loadServiceConfiguration();
 
-  if (user.accessToken && isPendingProvider.value) {
-    registrationCompleted.value = true;
-    clicked.value = true;
-
-    return;
-  }
-
   setTimeout(() => {
     clicked.value = true;
   }, 1500);
@@ -879,49 +783,4 @@ onMounted(() => {
   background: var(--q-surface-dark);
 }
 
-.registration-flow {
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-  padding: 16px 20px;
-}
-
-.registration-complete {
-  flex: 1;
-  min-height: 0;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  padding: 24px 0 80px;
-  transform: translateY(-64px);
-
-  &__card {
-    width: 100%;
-    max-width: 420px;
-    gap: 14px;
-    padding: 18px;
-    background: #e3efff;
-    border-radius: 12px;
-    color: #6554d9;
-  }
-
-  &__icon {
-    flex: 0 0 auto;
-  }
-
-  &__content {
-    min-width: 0;
-  }
-
-  &__title {
-    font-size: 16px;
-    line-height: 1.25;
-    margin-bottom: 6px;
-  }
-
-  &__message {
-    line-height: 1.35;
-  }
-}
 </style>

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

@@ -216,6 +216,7 @@
                 <q-space />
 
                 <q-rating
+                  v-if="item.provider_reviewed || canReview(item)"
                   :model-value="item.provider_reviewed ? item.provider_stars : 0"
                   :max="5"
                   class="q-mr-sm"
@@ -227,7 +228,7 @@
                 />
 
                 <q-btn
-                  v-if="!item.provider_reviewed"
+                  v-if="!item.provider_reviewed && canReview(item)"
                   class="btn-rate"
                   color="secondary"
                   no-caps
@@ -376,7 +377,11 @@ const openDetailsDialog = (schedule) => {
   });
 };
 
+const canReview = (item) => item.status !== 'cancelled';
+
 const openRatingDialog = (schedule) => {
+  if (!canReview(schedule)) return;
+
   $q.dialog({
     component: ScheduleRatingDialog,
     componentProps: { schedule },

+ 132 - 33
src/pages/dashboard/DashboardPage.vue

@@ -15,37 +15,41 @@
 
         <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)"
-          @reject="(item) => openDetailsDialog(item, 'confirm-reject')"
-          @view-details="(item) => openDetailsDialog(item)"
-        />
-
-        <DashboardPendingConfirmation
-         v-if="pendingConfirmation.length > 0"
-        :data="pendingConfirmation"
-        @view-details="(item) => openNextScheduleDialog(item)" 
-        />
-
-        <DashboardNextSchedules
-           :data="nextSchedules"
-          @view-details="(item) => openNextScheduleDialog(item)"
-        />
-
-        <DashboardOpportunities :data="opportunities" />
+        <DashboardPendingApproval v-if="isPendingProvider" />
+
+        <template v-else>
+          <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)"
+            @reject="(item) => openDetailsDialog(item, 'confirm-reject')"
+            @view-details="(item) => openDetailsDialog(item)"
+          />
+
+          <DashboardPendingConfirmation
+            v-if="pendingConfirmation.length > 0"
+            :data="pendingConfirmation"
+            @view-details="(item) => openNextScheduleDialog(item)"
+          />
+
+          <DashboardNextSchedules
+            :data="nextSchedules"
+            @view-details="(item) => openNextScheduleDialog(item)"
+          />
+
+          <DashboardOpportunities :data="opportunities" />
+        </template>
       </q-pull-to-refresh>
     </template>
   </div>
@@ -53,14 +57,18 @@
 
 <script setup>
 import { dadosDashboard } from "src/api/dashboard";
-import { onMounted, ref, watch } from "vue";
+import { onMounted, onUnmounted, ref, watch } from "vue";
 import { acceptServicePackage, rejectServicePackage, updateScheduleStatus } from "src/api/schedule";
+import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
+import { useAuth } from "src/composables/useAuth";
+import { useProviderApproval } from "src/composables/useProviderApproval";
 import { useRoute, useRouter } from "vue-router";
 
 import DashboardHeaderBar from "src/components/dashboard/DashboardHeaderBar.vue";
 import DashboardNextSchedules from "src/components/dashboard/DashboardNextSchedules.vue";
 import DashboardOpportunities from "src/components/dashboard/DashboardOpportunities.vue";
+import DashboardPendingApproval from "src/components/dashboard/DashboardPendingApproval.vue";
 import DashboardPriceSuggest from "src/components/dashboard/DashboardPriceSuggest.vue";
 import DashboardScrollAreaSchedules from "src/components/dashboard/DashboardScrollAreaSchedules.vue";
 import DashboardSolicitations from "src/components/dashboard/DashboardSolicitations.vue";
@@ -76,6 +84,15 @@ import DashboardPendingConfirmation from "src/components/dashboard/DashboardPend
 const $q = useQuasar();
 const router = useRouter();
 const route = useRoute();
+const { t } = useI18n();
+const { refreshApprovalStatus } = useAuth();
+const { isPendingProvider } = useProviderApproval();
+
+// Enquanto o cadastro está em análise, reconsulta a aprovação periodicamente
+// para liberar a dashboard completa sem exigir novo login.
+const APPROVAL_POLL_INTERVAL_MS = 3 * 60 * 1000;
+
+let approvalPollId = null;
 
 const headerBar = ref({});
 const loading = ref(true);
@@ -124,9 +141,60 @@ const loadDashboard = async () => {
   }
 };
 
-const onRefresh = async (done) => {
+const checkApprovalStatus = async () => {
+  if (!isPendingProvider.value) {
+    return false;
+  }
+
   try {
+    const approved = await refreshApprovalStatus();
+
+    if (!approved) {
+      return false;
+    }
+
+    stopApprovalPolling();
+
+    $q.notify({
+      message: t("provider.dashboard.pending_approval.approved"),
+      type: "positive",
+    });
+
     await loadDashboard();
+
+    return true;
+  } catch (error) {
+    console.error(error);
+
+    return false;
+  }
+};
+
+const startApprovalPolling = () => {
+  if (approvalPollId || !isPendingProvider.value) {
+    return;
+  }
+
+  approvalPollId = setInterval(checkApprovalStatus, APPROVAL_POLL_INTERVAL_MS);
+};
+
+const stopApprovalPolling = () => {
+  if (!approvalPollId) {
+    return;
+  }
+
+  clearInterval(approvalPollId);
+
+  approvalPollId = null;
+};
+
+const onRefresh = async (done) => {
+  try {
+    const approved = await checkApprovalStatus();
+
+    if (!approved) {
+      await loadDashboard();
+    }
   } finally {
     done();
   }
@@ -172,6 +240,31 @@ 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(
+  () => route.query.approved,
+  async (val) => {
+    if (val !== "true") {
+      return;
+    }
+
+    stopApprovalPolling();
+
+    if (!loading.value) {
+      await loadDashboard();
+    }
+
+    $q.notify({
+      message: t("provider.dashboard.pending_approval.approved"),
+      type: "positive",
+    });
+
+    router.replace({ path: route.path, query: {} });
+  },
+  { immediate: true },
+);
+
 watch(
   () => route.query.showSuccessModal,
   async (val) => {
@@ -191,6 +284,12 @@ watch(
 onMounted(async () => {
   await loadDashboard();
   loading.value = false;
+
+  startApprovalPolling();
+});
+
+onUnmounted(() => {
+  stopApprovalPolling();
 });
 </script>
 

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

@@ -21,11 +21,12 @@
           <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> 
           
-          <q-btn 
-            outline 
-            no-caps 
+          <q-btn
+            v-if="!isPendingProvider"
+            outline
+            no-caps
             padding="6px 16px"
-            class="full-width q-mt-sm btn-edit-profile text-text" 
+            class="full-width q-mt-sm btn-edit-profile text-text"
             @click="openEditProfile"
           >
             <div class="row items-center q-gutter-x-sm">
@@ -38,7 +39,15 @@
     </div>
 
     <div class="q-mt-md q-px-md column">
-      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="openBankDataDialog">
+      <div v-if="isPendingProvider" class="pending-notice font12 fontregular q-mb-md">
+        {{ $t('provider.dashboard.pending_approval.analyzing') }}
+      </div>
+
+      <div
+        v-if="!isPendingProvider"
+        class="menu-item row items-center no-wrap cursor-pointer q-py-sm"
+        @click="openBankDataDialog"
+      >
         <div class="column">
           <span class="font16 fontbold menu-title gradient-diarista">{{ $t('profile.bank_data.title') }}</span>
           <span class="font12 fontregular text-text">{{ $t('profile.bank_data.description') }}</span>
@@ -47,7 +56,11 @@
         <q-icon name="mdi-chevron-right" color="primary" size="md" />
       </div>
 
-      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="openAvailabilityDialog">
+      <div
+        v-if="!isPendingProvider"
+        class="menu-item row items-center no-wrap cursor-pointer q-py-sm"
+        @click="openAvailabilityDialog"
+      >
         <div class="column">
           <span class="font16 fontbold menu-title gradient-diarista">{{ $t('profile.availability.title') }}</span>
           <span class="font12 fontregular text-text">{{ $t('profile.availability.description') }}</span>
@@ -56,7 +69,11 @@
         <q-icon name="mdi-chevron-right" color="primary" size="md" />
       </div>
 
-      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="openServiceDataDialog">
+      <div
+        v-if="!isPendingProvider"
+        class="menu-item row items-center no-wrap cursor-pointer q-py-sm"
+        @click="openServiceDataDialog"
+      >
         <div class="column">
           <span class="font16 fontbold menu-title gradient-diarista">{{ $t('profile.service_data.title') }}</span>
           <span class="font12 fontregular text-text">{{ $t('profile.service_data.description') }}</span>
@@ -65,7 +82,11 @@
         <q-icon name="mdi-chevron-right" color="primary" size="md" />
       </div>
 
-      <div class="menu-item row items-center no-wrap cursor-pointer q-py-sm" @click="openAddressDialog">
+      <div
+        v-if="!isPendingProvider"
+        class="menu-item row items-center no-wrap cursor-pointer q-py-sm"
+        @click="openAddressDialog"
+      >
         <div class="column">
           <span class="font16 fontbold menu-title gradient-diarista">{{ $t('profile.address.title') }}</span>
           <span class="font12 fontregular text-text">{{ $t('profile.address.description') }}</span>
@@ -129,6 +150,7 @@ import { ref, onMounted } from 'vue';
 import { useQuasar } from 'quasar';
 import { Browser } from '@capacitor/browser';
 import { useAuth } from 'src/composables/useAuth';
+import { useProviderApproval } from 'src/composables/useProviderApproval';
 import { getUser } from 'src/api/user';
 import ProfileEditDialog from './ProfileEditDialog.vue';
 import ProfileAddressDialog from 'src/components/profile/ProfileAddressDialog.vue';
@@ -145,6 +167,7 @@ const SUPPORT_PAGE_URL   = 'https://politicas.softpar.inf.br/suportes/suporteDia
 const $q = useQuasar();
 
 const { logout } = useAuth();
+const { isPendingProvider } = useProviderApproval();
 const router = useRouter();
 const user = ref({
   name: '',
@@ -231,6 +254,14 @@ onMounted(async () => {
   border-radius: 6px;
 }
 
+.pending-notice {
+  background: #e3efff;
+  border-radius: 8px;
+  color: #6554d9;
+  line-height: 1.3;
+  padding: 12px 14px;
+}
+
 .menu-item {
   transition: background 0.3s;
   &:active {

+ 12 - 10
src/router/index.js

@@ -11,9 +11,13 @@ import { permissionStore } from "src/stores/permission";
 import { route } from "quasar/wrappers";
 import { useAuth } from "src/composables/useAuth";
 import { userStore } from "src/stores/user";
+import { useProviderApproval } from "src/composables/useProviderApproval";
 
 import routes from "./routes";
 
+// Rotas liberadas para o prestador com cadastro em análise.
+const PENDING_PROVIDER_ROUTES = ["DashboardPage", "ProfilePage"];
+
 /*
  * If not building with SSR mode, you can
  * directly export the Router instantiation;
@@ -55,22 +59,20 @@ export default route(function (/* { store, ssrContext } */) {
 
     const user = userStore();
 
-    const isPendingProvider =
-      String(user.user?.type).toUpperCase() === "PROVIDER" &&
-      user.user?.provider?.approval_status === "pending";
+    const { isPendingProvider } = useProviderApproval();
 
-    if (user.accessToken && isPendingProvider) {
-      if (to.name !== "LoginPage") {
-        return next({ name: "LoginPage" });
+    if (user.accessToken) {
+      if (to.name == "LoginPage") {
+        return next({ name: "DashboardPage" });
       }
-
-      return next();
     }
 
-    if (user.accessToken) {
-      if (to.name == "LoginPage") {
+    if (user.accessToken && isPendingProvider.value) {
+      if (!PENDING_PROVIDER_ROUTES.includes(to.name)) {
         return next({ name: "DashboardPage" });
       }
+
+      return next();
     }
 
     if (to.meta.requiredPermission) {

+ 7 - 7
src/stores/permission.js

@@ -95,13 +95,13 @@ export const permissionStore = defineStore("permission", () => {
   const fetchScopes = async () => {
     try {
       const { accessToken } = userStore();
-      if (accessToken) {
-        const userPermissions = await getUserPermissions();
-        permissions.value = userPermissions;
-      } else {
-        const response = await getGuestPermissions();
-        permissions.value = response;
-      }
+      const response = accessToken
+        ? await getUserPermissions()
+        : await getGuestPermissions();
+
+      // Rotas bloqueadas para prestador em análise respondem 202 com outro
+      // formato de payload; nesse caso o app segue sem escopo algum.
+      permissions.value = Array.isArray(response) ? response : null;
     } catch (error) {
       console.error("Error fetching permissions:", error);
     }