Kaynağa Gözat

refactor: secao de pagamento

Gustavo Mantovani 2 hafta önce
ebeveyn
işleme
c868e260df

+ 43 - 39
src/components/dashboard/DashboardHeaderBar.vue

@@ -3,59 +3,60 @@
   <div class="dashboard-header shadow-card bg-white row items-center no-wrap q-px-md q-pb-sm">
     <div class="col column q-gutter-y-xs">
       <div class="row items-center q-gutter-x-xs">
-        <q-icon name="mdi-star" color="warning" size="14px" />
-        <span class="dashboard-metric-value font10 fontregular">{{ data?.rating != null ? Number(data.rating).toFixed(1).replace('.', ',') : '-' }}</span>
+        <q-icon color="warning" name="mdi-star" size="14px" />
+
+        <span class="dashboard-metric-value font10 fontregular">{{ formatRating(data?.rating, '-') }}</span>
+
         <span class="dashboard-metric-meta font10 fontregular">({{ data?.total_ratings ?? 0 }})</span>
       </div>
+
       <div class="row items-center q-gutter-x-xs">
-        <q-icon name="mdi-broom" color="secondary" size="14px" />
+        <q-icon color="secondary" name="mdi-broom" size="14px" />
+
         <span class="dashboard-metric-value font10 fontregular">{{ data?.total_services ?? 0 }}</span>
       </div>
     </div>
 
     <div class="col-auto row justify-center">
-      <img :src="LogoDiariaColorida" alt="Diária" class="dashboard-logo" />
+      <img alt="Diária" class="dashboard-logo" :src="LogoDiariaColorida" />
     </div>
 
     <div class="col row justify-end items-center">
 
-  <q-btn
-    flat
-    round
-    dense
-    color="grey-7"
-    size="sm"
-    @click="goToNotifications"
-  >
-
-    <q-icon
-      name="mdi-bell-outline"
-      size="20px"
-    />
-
-    <q-badge
-      v-if="unreadNotifications > 0"
-      floating
-      rounded
-      color="pink"
-      class="notification-badge font10 fontregular"
-    >
-      {{ unreadNotifications }}
-    </q-badge>
-
-  </q-btn>
-
-</div>
+      <q-btn
+        color="grey-7"
+        dense
+        flat
+        round
+        size="sm"
+        @click="goToNotifications"
+      >
+        <q-icon
+          name="mdi-bell-outline"
+          size="20px"
+        />
+
+        <q-badge
+          v-if="unreadNotifications > 0"
+          class="notification-badge font10 fontregular"
+          color="pink"
+          floating
+          rounded
+        >
+          {{ unreadNotifications }}
+        </q-badge>
+      </q-btn>
+
+    </div>
   </div>
 </template>
 
 <script setup>
-import LogoDiariaColorida from 'src/assets/logo_diaria_colorido_sem_texto.svg'
-
 import { computed } from 'vue'
+import { formatRating } from 'src/helpers/utils'
 import { useRouter } from 'vue-router'
 
-const router = useRouter()
+import LogoDiariaColorida from 'src/assets/logo_diaria_colorido_sem_texto.svg'
 
 const props = defineProps({
   data: {
@@ -69,6 +70,8 @@ const props = defineProps({
   }
 })
 
+const router = useRouter()
+
 //vai para dashboard as notificações tem que ser mocada no backend
 const unreadNotifications = computed(() => {
   return props.notifications.filter((notification) => !notification.read).length
@@ -76,12 +79,13 @@ const unreadNotifications = computed(() => {
 
 const goToNotifications = () => {
   router.push({
-  name: 'NotificationsPage',
-  query: {
-    notifications: JSON.stringify(props.notifications)
-  }
-})
+    name: 'NotificationsPage',
+    query: {
+      notifications: JSON.stringify(props.notifications)
+    }
+  })
 }
+
 </script>
 
 <style scoped lang="scss">

+ 14 - 28
src/components/dashboard/DashboardNextSchedules.vue

@@ -11,8 +11,8 @@
         <q-card
           v-for="item in data"
           :key="item.id"
-          :flat="false"
           class="schedule-card card-border shadow-card bg-surface"
+          :flat="false"
         >
           <q-card-section
             class="q-pa-md row col-12 no-wrap schedule-card-section"
@@ -25,8 +25,8 @@
                 >
                   <img
                     v-if="item.provider_photo"
-                    :src="item.provider_photo"
                     style="object-fit: cover; border-radius: 50%"
+                    :src="item.provider_photo"
                   />
 
                   <span v-else>
@@ -56,7 +56,7 @@
                   >
                     {{
                       getFirstName(item.provider_name) ||
-                      $t("dashboard_client.next_schedules.no_provider")
+                        $t("dashboard_client.next_schedules.no_provider")
                     }}
                   </span>
                 </div>
@@ -112,7 +112,13 @@
             <div class="column text-text schedule-price">
               <div class="column schedule-price-info">
                 <span class="text-price-main font16 fontbold">
-                  {{ formatScheduleTotal(item) }}
+                  {{
+                    formatScheduleTotal(
+                      item,
+                      platformFees,
+                      t("dashboard_client.next_schedules.to_combine"),
+                    )
+                  }}
                 </span>
 
                 <span
@@ -146,16 +152,10 @@
 <script setup>
 import { avatarColors } from "src/helpers/avatarColors";
 import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
+import { formatLabelByPeriodType, getFirstName } from "src/helpers/utils";
 
 import {
-  formatCurrency,
-  formatLabelByPeriodType,
-  getFirstName,
-} from "src/helpers/utils";
-
-import {
-  getSchedulePaymentType,
-  getScheduleTotalWithPlatformFee,
+  formatScheduleTotal,
 } from "src/helpers/paymentPlatformFees";
 
 import { onMounted } from "vue";
@@ -163,11 +163,10 @@ import { useI18n } from "vue-i18n";
 import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 
 defineProps({ data: { type: Array, default: () => [] } });
-
 const emit = defineEmits(["view-details"]);
 
-const { t } = useI18n();
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
+const { t } = useI18n();
 
 const addressIcon = (type) =>
   type === "home" ? "mdi-home-outline" : "mdi-office-building-outline";
@@ -183,23 +182,10 @@ const addressLabel = (type) => {
   return t("dashboard_client.next_schedules.place_unknown");
 };
 
-const formatScheduleTotal = (item) => {
-  if (!item.total_amount || item.total_amount === "0.00") {
-    return t("dashboard_client.next_schedules.to_combine");
-  }
-
-  return formatCurrency(
-    getScheduleTotalWithPlatformFee(
-      item,
-      getSchedulePaymentType(item),
-      platformFees.value,
-    ),
-  );
-};
-
 onMounted(() => {
   loadPlatformFees().catch(() => {});
 });
+
 </script>
 
 <style scoped lang="scss">

+ 60 - 76
src/components/dashboard/DashboardPendingSchedules.vue

@@ -9,8 +9,8 @@
         <q-card
           v-for="item in data"
           :key="item.id"
-          :flat="false"
           class="pending-card card-border shadow-card bg-surface"
+          :flat="false"
           @click="seeDetails(item)"
         >
           <q-card-section class="card-body q-pa-md">
@@ -22,8 +22,8 @@
               >
                 <img
                   v-if="providerPhoto(item)"
-                  :src="providerPhoto(item)"
                   style="object-fit: cover; border-radius: 50%"
+                  :src="providerPhoto(item)"
                 />
 
                 <span v-else>
@@ -40,6 +40,7 @@
                       $t("dashboard_client.pending_schedules.pay_to_provider")
                     }}
                   </span>
+
                   <span v-else-if="item.status == 'pending'">
                     {{
                       $t("dashboard_client.pending_schedules.requesting_with")
@@ -66,7 +67,7 @@
                   />
 
                   <span class="font9 fontmedium text-grey-5 meta-text ellipsis">
-                    {{ displayTime(item) }}
+                    {{ requestStatusText(item) }}
                   </span>
                 </div>
               </div>
@@ -89,8 +90,8 @@
                     type === 'servicePackage'
                       ? $t("dashboard_client.pending_schedules.status.accepted")
                       : $t(
-                          `dashboard_client.pending_schedules.status.${item.status ?? "pending"}`,
-                        )
+                        `dashboard_client.pending_schedules.status.${item.status ?? "pending"}`,
+                      )
                   }}
                 </span>
               </div>
@@ -104,22 +105,31 @@
             </div>
 
             <div class="schedule-info">
-  <div class="schedule-info-item">
-    <q-icon
-      name="mdi-calendar-outline"
-      size="14px"
-    />
-    <span>{{ displayDate(item) }}</span>
-  </div>
+              <div class="schedule-info-item">
+                <q-icon
+                  name="mdi-calendar-outline"
+                  size="14px"
+                />
 
-  <div class="schedule-info-item">
-    <q-icon
-      name="mdi-clock-outline"
-      size="14px"
-    />
-    <span>{{ displayScheduleTime(item) }}</span>
-  </div>
-</div>
+                <span>{{ formatNumericDate(scheduleFor(item)?.date) || "—" }}</span>
+              </div>
+
+              <div class="schedule-info-item">
+                <q-icon
+                  name="mdi-clock-outline"
+                  size="14px"
+                />
+
+                <span>
+                  {{
+                    formatTimeRange(
+                      scheduleFor(item)?.start_time,
+                      scheduleFor(item)?.end_time,
+                    ) || "—"
+                  }}
+                </span>
+              </div>
+            </div>
 
             <div class="card-footer row items-center no-wrap">
               <q-btn
@@ -147,7 +157,7 @@
                 />
 
                 {{
-                  displayAddress(item)
+                  formatAddress(addressFor(item)) || "—"
                 }}
               </span>
             </div>
@@ -171,12 +181,14 @@
 </template>
 
 <script setup>
-import { computed, ref } from "vue";
 import { avatarColors } from "src/helpers/avatarColors";
-import { getFirstName } from "src/helpers/utils";
-import { parseLocalDate } from "src/helpers/scheduleDate";
+import { computed, ref } from "vue";
+import { formatAddress, getFirstName } from "src/helpers/utils";
+import { formatNumericDate, formatTimeRange } from "src/helpers/scheduleDate";
 import { useI18n } from "vue-i18n";
 
+const emit = defineEmits(["view-details", "cancel"]);
+
 const props = defineProps({
   data: { type: Array, default: () => [] },
   type: { type: String, default: 'schedule' },
@@ -184,12 +196,26 @@ const props = defineProps({
   progressClass: { type: String, default: '' },
 });
 
-const emit = defineEmits(["view-details", "cancel"]);
-
 const { t } = useI18n();
 
-const trackRef = ref(null);
 const activeIndex = ref(0);
+const trackRef = ref(null);
+
+const isServicePackage = computed(() => props.type === 'servicePackage');
+
+const addressFor = (item) => {
+  const address = isServicePackage.value
+    ? item.schedules?.[0]?.address
+    : item.address;
+
+  if (!address) return null;
+
+  return {
+    address: address.address,
+    district: address.district,
+    number: address.number,
+  };
+};
 
 const onTrackScroll = () => {
   const track = trackRef.value;
@@ -199,15 +225,6 @@ const onTrackScroll = () => {
   activeIndex.value = Math.round(track.scrollLeft / track.clientWidth);
 };
 
-const isServicePackage = computed(() => props.type === 'servicePackage');
-
-const providerPhoto = (item) => {
-  if (isServicePackage.value) {
-    return item.provider?.profile_media?.url ?? item.provider_photo ?? null;
-  }
-  return item.provider_photo;
-};
-
 const providerName = (item) => {
   if (isServicePackage.value) {
     return item.provider?.user?.name ?? item.provider_name ?? '';
@@ -215,26 +232,17 @@ const providerName = (item) => {
   return item.provider_name ?? '';
 };
 
-const displayAddress = (item) => {
+const providerPhoto = (item) => {
   if (isServicePackage.value) {
-    const firstSchedule = item.schedules?.[0];
-    const addr = firstSchedule?.address;
-    if (addr) {
-      return [addr.address, addr.number, addr.district].filter(Boolean).join(', ') || '—';
-    }
-    return '—';
+    return item.provider?.profile_media?.url ?? item.provider_photo ?? null;
   }
-  return [
-    item.address?.address,
-    item.address?.number,
-    item.address?.district,
-  ].filter(Boolean).join(', ') || '—';
+  return item.provider_photo;
 };
 
 const scheduleCountLabel = (count) =>
   t("dashboard_client.pending_schedules.schedule_count", { count });
 
-const displayTime = (item) => {
+const requestStatusText = (item) => {
   if (isServicePackage.value) {
     return item.schedules?.length ? scheduleCountLabel(item.schedules.length) : "—";
   }
@@ -252,39 +260,15 @@ const displayTime = (item) => {
     : timeAgo;
 };
 
-const displayDate = (item) => {
-  const schedule = isServicePackage.value
-    ? item.schedules?.[0]
-    : item;
-
-  if (!schedule?.date) return "—";
-
-  const date = parseLocalDate(schedule.date);
-
-  return date.toLocaleDateString("pt-BR", {
-    day: "2-digit",
-    month: "2-digit",
-    year: "numeric",
-  });
-};
-
-const displayScheduleTime = (item) => {
-  const schedule = isServicePackage.value
-    ? item.schedules?.[0]
-    : item;
-
-  if (!schedule?.start_time || !schedule?.end_time) {
-    return "—";
-  }
-
-  return `${schedule.start_time.slice(0, 5)} às ${schedule.end_time.slice(0, 5)}`;
-};
+const scheduleFor = (item) =>
+  isServicePackage.value ? item.schedules?.[0] : item;
 
 const seeDetails = (item) => {
   if (isServicePackage.value || item.status === "accepted") {
     emit("view-details", item);
   }
 };
+
 </script>
 
 <style scoped lang="scss">

+ 24 - 29
src/components/dashboard/DashboardProvidersClose.vue

@@ -57,8 +57,8 @@
       <q-card
         v-for="p in data"
         :key="p.provider_id"
-        :flat="false"
         class="card-border bg-page text-text q-mb-sm"
+        :flat="false"
       >
         <q-card-section class="row no-wrap q-pa-sm">
           <div class="row no-wrap full-width">
@@ -69,8 +69,8 @@
               >
                 <img
                   v-if="p.provider_photo"
-                  :src="p.provider_photo"
                   style="object-fit: cover; border-radius: 50%"
+                  :src="p.provider_photo"
                 />
 
                 <span v-else>
@@ -100,7 +100,7 @@
                     <span class="text-provider-close-rating font9 fontmedium">
                       {{
                         p.average_rating != null
-                          ? Number(p.average_rating).toFixed(1) +
+                          ? formatRating(p.average_rating) +
                             " (" +
                             (p.total_reviews ?? 0) +
                             ")"
@@ -166,7 +166,14 @@
 
 <script setup>
 import { avatarColors } from "src/helpers/avatarColors";
-import { formatCurrency, getFirstName } from "src/helpers/utils";
+
+import {
+  formatCurrency,
+  formatDistance,
+  formatRating,
+  getFirstName,
+} from "src/helpers/utils";
+
 import { ref } from "vue";
 import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
@@ -175,9 +182,8 @@ import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 
 defineProps({ data: { type: Array, default: () => [] } });
 
-const { t } = useI18n();
-
 const $q = useQuasar();
+const { t } = useI18n();
 
 const currentPeriodType = ref(6);
 
@@ -188,15 +194,20 @@ const periodTypeMap = ref({
   8: "daily_price_8h",
 });
 
-const formatDistance = (distance) => {
-  if (distance === null || distance === undefined || distance === "")
-    return "—";
-
-  const numericDistance = Number(distance);
+//
+const goToScheduling = (provider) => {
+  $q.dialog({
+    component: SchedulingDialog,
+    componentProps: { provider },
+  });
+};
 
-  if (!Number.isFinite(numericDistance)) return "—";
+const setPeriodTypeNext = () => {
+  const nextPeriod = currentPeriodType.value + 2;
 
-  return `${numericDistance.toLocaleString("pt-BR", { maximumFractionDigits: 1 })} km`;
+  if (periodTypeMap.value[nextPeriod]) {
+    currentPeriodType.value = nextPeriod;
+  }
 };
 
 const setPeriodTypePrevious = () => {
@@ -207,14 +218,6 @@ const setPeriodTypePrevious = () => {
   }
 };
 
-const setPeriodTypeNext = () => {
-  const nextPeriod = currentPeriodType.value + 2;
-
-  if (periodTypeMap.value[nextPeriod]) {
-    currentPeriodType.value = nextPeriod;
-  }
-};
-
 const showCorrectLabels = () => {
   switch (currentPeriodType.value) {
     case 8:
@@ -240,14 +243,6 @@ const showCorrectValues = (p) => {
     : t("dashboard_client.providers_close.no_price");
 };
 
-//
-
-const goToScheduling = (provider) => {
-  $q.dialog({
-    component: SchedulingDialog,
-    componentProps: { provider },
-  });
-};
 </script>
 
 <style scoped lang="scss">

+ 14 - 0
src/helpers/paymentPlatformFees.js

@@ -1,3 +1,5 @@
+import { formatCurrency } from './utils'
+
 export const scheduleUsesServicePackageDiscount = (item) => {
   const count = Number(
     item?.schedules?.[0]?.service_package_items_count
@@ -47,3 +49,15 @@ export const getScheduleTotalWithPlatformFee = (schedule, paymentType, platformF
 
   return parseFloat((base * (1 + (feeRate ?? 0))).toFixed(2))
 }
+
+export const formatScheduleTotal = (schedule, platformFees, emptyLabel = '') => {
+  if (!schedule?.total_amount || schedule.total_amount === '0.00') return emptyLabel
+
+  return formatCurrency(
+    getScheduleTotalWithPlatformFee(
+      schedule,
+      getSchedulePaymentType(schedule),
+      platformFees,
+    ),
+  )
+}

+ 43 - 1
src/helpers/scheduleDate.js

@@ -8,6 +8,13 @@ const parseLocalDate = (dateStr) => {
   return null;
 };
 
+const parseUtcDate = (dateStr) => {
+  if (!dateStr) return null;
+
+  const hasTimezone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(dateStr);
+  return new Date(hasTimezone ? dateStr : `${dateStr}Z`);
+};
+
 const formatWeekday = (dateStr) => {
   const d = parseLocalDate(dateStr);
   if (!d) return "";
@@ -21,4 +28,39 @@ const formatDayMonth = (dateStr) => {
   return d.toLocaleDateString("pt-BR", { day: "2-digit", month: "2-digit" });
 };
 
-export { parseLocalDate, formatWeekday, formatDayMonth };
+const formatLongDate = (dateStr) => {
+  const date = parseLocalDate(dateStr);
+  if (!date) return dateStr ?? "";
+
+  return date.toLocaleDateString("pt-BR", {
+    day: "2-digit",
+    month: "long",
+    year: "numeric",
+  });
+};
+
+const formatNumericDate = (dateStr) => {
+  const date = parseLocalDate(dateStr);
+  if (!date) return dateStr ?? "";
+
+  return date.toLocaleDateString("pt-BR", {
+    day: "2-digit",
+    month: "2-digit",
+    year: "numeric",
+  });
+};
+
+const formatTimeRange = (start, end, separator = "às") => {
+  if (!start || !end) return "";
+  return `${start.slice(0, 5)} ${separator} ${end.slice(0, 5)}`;
+};
+
+export {
+  formatDayMonth,
+  formatLongDate,
+  formatNumericDate,
+  formatTimeRange,
+  formatWeekday,
+  parseLocalDate,
+  parseUtcDate,
+};

+ 33 - 1
src/helpers/utils.js

@@ -296,6 +296,35 @@ const formatAddress = (address) => {
   return parts.join(', ');
 };
 
+const formatCardBrand = (brand) => {
+  if (!brand) return '';
+
+  const brands = {
+    diners: 'Diners',
+    discover: 'Discover',
+    elo: 'Elo',
+    hipercard: 'Hipercard',
+    mastercard: 'Mastercard',
+    visa: 'VISA',
+  };
+
+  return brands[brand] ?? brand.toUpperCase();
+};
+
+const formatDistance = (distance) => {
+  if (distance === null || distance === undefined || distance === '') return '—';
+
+  const value = Number(distance);
+  if (!Number.isFinite(value)) return '—';
+
+  return `${value.toLocaleString('pt-BR', { maximumFractionDigits: 1 })} km`;
+};
+
+const formatRating = (rating, fallback = '—') => {
+  if (rating === null || rating === undefined || rating === '') return fallback;
+  return Number(rating).toFixed(1).replace('.', ',');
+};
+
 const getFirstName = (fullName) => {
   if (!fullName) return '';
 
@@ -367,8 +396,11 @@ export {
   detectCardBrand,
   validateCardExpiration,
   formatAddress,
+  formatCardBrand,
   getFirstName,
   calculateDailyPrices,
   chooseprice,
-  formatLabelByPeriodType
+  formatDistance,
+  formatLabelByPeriodType,
+  formatRating,
 };

+ 55 - 56
src/pages/agenda/CalendarPage.vue

@@ -41,16 +41,16 @@
           <q-card
             v-for="item in upcomingSchedules"
             :key="item.id"
-            :flat="false"
             class="calendar-card bg-surface shadow-card q-mb-sm"
+            :flat="false"
           >
             <q-card-section class="q-pa-sm">
               <div class="row no-wrap items-start q-gutter-x-sm">
                 <q-avatar size="44px">
                   <img
                     v-if="item.provider_photo"
-                    :src="item.provider_photo"
                     style="object-fit: cover"
+                    :src="item.provider_photo"
                   />
 
                   <span
@@ -71,7 +71,7 @@
                   <span class="font12 fontbold">
                     {{
                       getFirstName(item.provider_name) ||
-                      $t("dashboard_client.agenda.waiting_proposals")
+                        $t("dashboard_client.agenda.waiting_proposals")
                     }}
                   </span>
 
@@ -161,16 +161,16 @@
           <q-card
             v-for="item in completedSchedules"
             :key="item.id"
-            :flat="false"
             class="calendar-card bg-surface shadow-card q-mb-sm"
+            :flat="false"
           >
             <q-card-section class="q-pa-sm">
               <div class="row no-wrap items-start q-gutter-x-sm">
                 <q-avatar size="44px">
                   <img
                     v-if="item.provider_photo"
-                    :src="item.provider_photo"
                     style="object-fit: cover"
+                    :src="item.provider_photo"
                   />
 
                   <span
@@ -191,7 +191,7 @@
                   <span class="font12 fontbold">
                     {{
                       getFirstName(item.provider_name) ||
-                      $t("dashboard_client.agenda.waiting_proposals")
+                        $t("dashboard_client.agenda.waiting_proposals")
                     }}
                   </span>
 
@@ -319,27 +319,22 @@ import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 import { useQuasar } from "quasar";
 import { useRouter } from "vue-router";
 
-import NextSchedulesDetailsDialog from "src/components/dashboard/NextSchedulesDetailsDialog.vue";
-import ScheduleRatingDialog from "src/components/dashboard/ScheduleRatingDialog.vue";
+import NextSchedulesDetailsDialog from "src/pages/dashboard/components/schedule/NextSchedulesDetailsDialog.vue";
+import ScheduleRatingDialog from "src/pages/dashboard/components/schedule/ScheduleRatingDialog.vue";
 import SchedulingDialog from "src/pages/search/components/SchedulingDialog.vue";
 
 const $q = useQuasar();
-const { t } = useI18n();
-const router = useRouter();
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
+const router = useRouter();
+const { t } = useI18n();
 
+const completedSchedules = ref([]);
 const loading = ref(true);
 const upcomingSchedules = ref([]);
-const completedSchedules = ref([]);
 
-const periodLabel = (periodType) => {
-  const key = `period_types.${periodType}`;
-  const translated = t(key);
-  return translated !== key ? translated : "";
-};
+const canReview = (item) => item.status !== "cancelled";
 
 //
-
 const formatScheduleTotal = (item) =>
   formatCurrency(
     getScheduleTotalWithPlatformFee(
@@ -349,44 +344,7 @@ const formatScheduleTotal = (item) =>
     ),
   );
 
-const statusLabel = (status) => {
-  const map = {
-    pending: t("dashboard_client.agenda.status_pending"),
-    accepted: t("dashboard_client.agenda.status_accepted"),
-    paid: t("dashboard_client.agenda.status_paid"),
-    started: t("dashboard_client.agenda.status_started"),
-    finished: t("dashboard_client.agenda.status_finished"),
-    cancelled: t("dashboard_client.agenda.status_cancelled"),
-  };
-  return map[status] ?? status;
-};
-
-const statusBgColor = (status) => {
-  const map = {
-    pending: "warning-bg",
-    accepted: "success-bg",
-    paid: "success-bg",
-    started: "info-bg",
-    finished: "neutral-bg",
-    cancelled: "secondary-bg",
-  };
-  return map[status] ?? "neutral-bg";
-};
-
-const statusTextColor = (status) => {
-  const map = {
-    pending: "warning",
-    accepted: "success",
-    paid: "success",
-    started: "info",
-    finished: "status-finished",
-    cancelled: "secondary",
-  };
-  return map[status] ?? "text";
-};
-
 //
-
 const loadCalendar = async () => {
   const response = await getClientCalendar();
   if (response) {
@@ -406,8 +364,6 @@ const openDetailsDialog = (schedule) => {
   });
 };
 
-const canReview = (item) => item.status !== "cancelled";
-
 const openRatingDialog = (schedule) => {
   if (!canReview(schedule)) return;
 
@@ -434,10 +390,53 @@ const openSchedulingDialog = (item) => {
   });
 };
 
+const periodLabel = (periodType) => {
+  const key = `period_types.${periodType}`;
+  const translated = t(key);
+  return translated !== key ? translated : "";
+};
+
+const statusBgColor = (status) => {
+  const map = {
+    pending: "warning-bg",
+    accepted: "success-bg",
+    paid: "success-bg",
+    started: "info-bg",
+    finished: "neutral-bg",
+    cancelled: "secondary-bg",
+  };
+  return map[status] ?? "neutral-bg";
+};
+
+const statusLabel = (status) => {
+  const map = {
+    pending: t("dashboard_client.agenda.status_pending"),
+    accepted: t("dashboard_client.agenda.status_accepted"),
+    paid: t("dashboard_client.agenda.status_paid"),
+    started: t("dashboard_client.agenda.status_started"),
+    finished: t("dashboard_client.agenda.status_finished"),
+    cancelled: t("dashboard_client.agenda.status_cancelled"),
+  };
+  return map[status] ?? status;
+};
+
+const statusTextColor = (status) => {
+  const map = {
+    pending: "warning",
+    accepted: "success",
+    paid: "success",
+    started: "info",
+    finished: "status-finished",
+    cancelled: "secondary",
+  };
+  return map[status] ?? "text";
+};
+
 onMounted(async () => {
   await Promise.all([loadCalendar(), loadPlatformFees().catch(() => {})]);
   loading.value = false;
 });
+
 </script>
 
 <style scoped lang="scss">

+ 96 - 70
src/pages/dashboard/DashboardPage.vue

@@ -5,36 +5,51 @@
         <q-spinner-dots color="primary" />
       </div>
     </template>
+
     <template v-else>
       <q-pull-to-refresh color="primary" @refresh="onRefresh">
         <DashboardHeaderBar
           :data="headerBar"
           :notifications="notifications"
         />
+
         <DashboardRegistrationIncomplete v-if="!registrationComplete" />
+
         <DashboardSummaryInfos v-else :data="summaryInfos" />
+
         <DashboardPaymentIncomplete v-if="showPaymentBanner" />
+
         <AddressIncompleteBanner v-if="!hasLocation" @resolved="reloadDashboard" />
+
         <DashboardPendingCustomSchedules v-if="customSchedulesNoProposals.length > 0" />
+
         <DashboardClientProposals v-if="clientProposals.length > 0" :data="clientProposals" @refresh-data="reloadDashboard" />
+
         <DashboardPendingSchedules
           v-if="pendingServicePackages.length > 0"
+          progress-class="progress-fill--urgent"
           :data="pendingServicePackages"
           :type="'servicePackage'"
-          progress-class="progress-fill--urgent"
           @view-details="openServicePackagePaymentDialog"
         />
+
         <DashboardPendingSchedules
           v-if="pendingRequests.length > 0"
-          :data="pendingRequests"
           progress-class="progress-fill--loop"
+          :data="pendingRequests"
           @cancel="openCancelRequestDialog"
         />
+
         <DashboardTodaySchedules v-if="todaySchedules.length > 0" :data="todaySchedules" @rate="openRatingDialog" />
+
         <DashboardScrollAreaSchedules />
+
         <DashboardNextSchedules v-if="nextSchedules.length > 0" :data="nextSchedules" @view-details="openNextScheduleDialog" />
+
         <DashboardLastDoneSchedules v-if="lastDoneSchedules.length > 0" :data="lastDoneSchedules" />
+
         <DashboardFavoriteProviders v-if="favoriteProviders.length > 0" :data="favoriteProviders" />
+
         <DashboardProvidersClose v-if="hasLocation" :data="providersClose" />
       </q-pull-to-refresh>
     </template>
@@ -42,62 +57,85 @@
 </template>
 
 <script setup>
+import { computed, onMounted, ref } from 'vue';
+import { dadosDashboard } from 'src/api/dashboard';
+import { LocalStorage, useQuasar } from 'quasar';
+import { updateMe } from 'src/api/user';
+import { useI18n } from 'vue-i18n';
+import { usePaymentStore } from 'src/stores/payment';
+import { useRoute, useRouter } from 'vue-router'
+import { userStore } from 'src/stores/user';
+
+import AddressIncompleteBanner from 'src/components/shared/AddressIncompleteBanner.vue';
+import DashboardClientProposals from 'src/pages/dashboard/components/DashboardClientProposals.vue';
+import DashboardFavoriteProviders from 'src/components/dashboard/DashboardFavoriteProviders.vue';
 import DashboardHeaderBar from 'src/components/dashboard/DashboardHeaderBar.vue';
-import DashboardSummaryInfos from 'src/components/dashboard/DashboardSummaryInfos.vue';
-import DashboardRegistrationIncomplete from 'src/components/dashboard/DashboardRegistrationIncomplete.vue';
+import DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
+import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedules.vue';
 import DashboardPaymentIncomplete from 'src/components/dashboard/DashboardPaymentIncomplete.vue';
+import DashboardPendingCustomSchedules from 'src/pages/dashboard/components/DashboardPendingCustomSchedules.vue';
 import DashboardPendingSchedules from 'src/components/dashboard/DashboardPendingSchedules.vue';
-import ScheduleAcceptedDialog from 'src/components/dashboard/ScheduleAcceptedDialog.vue';
-import ScheduleCancelDialog from 'src/components/dashboard/ScheduleCancelDialog.vue';
-import DashboardScrollAreaSchedules from 'src/components/dashboard/DashboardScrollAreaSchedules.vue';
-import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedules.vue';
-import DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
-import DashboardFavoriteProviders from 'src/components/dashboard/DashboardFavoriteProviders.vue';
 import DashboardProvidersClose from 'src/components/dashboard/DashboardProvidersClose.vue';
-import AddressIncompleteBanner from 'src/components/shared/AddressIncompleteBanner.vue';
+import DashboardRegistrationIncomplete from 'src/components/dashboard/DashboardRegistrationIncomplete.vue';
+import DashboardScrollAreaSchedules from 'src/components/dashboard/DashboardScrollAreaSchedules.vue';
+import DashboardSummaryInfos from 'src/components/dashboard/DashboardSummaryInfos.vue';
 import DashboardTodaySchedules from 'src/components/dashboard/DashboardTodaySchedules.vue';
 import FinalSuccesModal from '../schedules/components/FinalSuccesModal.vue';
-import DashboardPendingCustomSchedules from 'src/pages/dashboard/components/DashboardPendingCustomSchedules.vue';
-import DashboardClientProposals from 'src/pages/dashboard/components/DashboardClientProposals.vue';
-import { useRoute, useRouter } from 'vue-router'
-import { onMounted, ref, computed } from 'vue';
-import { useI18n } from 'vue-i18n';
-import { LocalStorage, useQuasar } from 'quasar';
-import { dadosDashboard } from 'src/api/dashboard';
-import { updateMe } from 'src/api/user';
-import { userStore } from 'src/stores/user';
-import NextSchedulesDetailsDialog from 'src/components/dashboard/NextSchedulesDetailsDialog.vue';
-import ScheduleRatingDialog from 'src/components/dashboard/ScheduleRatingDialog.vue';
+import NextSchedulesDetailsDialog from 'src/pages/dashboard/components/schedule/NextSchedulesDetailsDialog.vue';
+import ScheduleAcceptedDialog from 'src/pages/dashboard/components/schedule/ScheduleAcceptedDialog.vue';
+import ScheduleCancelDialog from 'src/pages/dashboard/components/schedule/ScheduleCancelDialog.vue';
+import ScheduleRatingDialog from 'src/pages/dashboard/components/schedule/ScheduleRatingDialog.vue';
 
-const router = useRouter()
-const route = useRoute()
-const { t } = useI18n();
 const $q = useQuasar();
+const paymentStore = usePaymentStore();
+const route = useRoute()
+const router = useRouter()
 const store = userStore();
+const { t } = useI18n();
 
+const clientProposals = ref([]);
+const customSchedulesNoProposals = ref([]);
+const favoriteProviders = ref([]);
+const hasLocation = ref(true);
 const hasPaymentMethods = ref(true);
 const headerBar = ref({});
-const summaryInfos = ref({});
+const lastDoneSchedules = ref([]);
+const loading = ref(true);
+const nextSchedules = ref([]);
+const notifications = ref([]);
 const pendingSchedules = ref([]);
 const pendingServicePackages = ref([]);
-const nextSchedules = ref([]);
-const clientProposals = ref([]);
-const customSchedulesNoProposals = ref([]);
-const lastDoneSchedules = ref([]);
-const favoriteProviders = ref([]);
 const providersClose = ref([]);
-const todaySchedules = ref([]);
-const notifications = ref([]);
-const loading = ref(true);
-const registrationComplete = computed(() => store.user?.registration_complete ?? true);
 const showPaymentBanner = ref(false);
-const hasLocation = ref(true);
 
 const successModalKey = ref(
   route.query.success ??
     (router.currentRoute.value.fullPath.includes('showSuccessModal') ? 'custom_schedule' : null),
 );
 
+const summaryInfos = ref({});
+const todaySchedules = ref([]);
+
+const pendingRequests = computed(() => {
+  const seenPackages = new Set();
+
+  return pendingSchedules.value.filter((schedule) => {
+    if (schedule.status !== 'pending') return false;
+
+    if (!schedule.service_package_id) return true;
+
+    if (seenPackages.has(schedule.service_package_id)) return false;
+
+    seenPackages.add(schedule.service_package_id);
+
+    return true;
+  });
+});
+
+const registrationComplete = computed(() => store.user?.registration_complete ?? true);
+
+const AUTO_SHOWN_PACKAGES_KEY = "sp_payment_popup_shown_ids";
+
 const successModalContents = {
   custom_schedule: null,
   order_sent: () => ({
@@ -109,21 +147,27 @@ const successModalContents = {
   }),
 };
 
-const pendingRequests = computed(() => {
-  const seenPackages = new Set();
+let autoOpeningPackageDialog = false;
 
-  return pendingSchedules.value.filter((schedule) => {
-    if (schedule.status !== 'pending') return false;
+const maybeOpenSuccessModal = () => {
+  if (!successModalKey.value) return;
 
-    if (!schedule.service_package_id) return true;
+  if (!(successModalKey.value in successModalContents)) {
+    successModalKey.value = null;
+    return;
+  }
 
-    if (seenPackages.has(schedule.service_package_id)) return false;
+  const buildContent = successModalContents[successModalKey.value];
 
-    seenPackages.add(schedule.service_package_id);
+  successModalKey.value = null;
 
-    return true;
+  $q.dialog({
+    component: FinalSuccesModal,
+    componentProps: { content: buildContent ? buildContent() : null },
+  }).onDismiss(() => {
+    router.replace({ path: route.path, query: {} });
   });
-});
+};
 
 const openCancelRequestDialog = (schedule) => {
   $q.dialog({
@@ -135,17 +179,17 @@ const openCancelRequestDialog = (schedule) => {
 };
 
 const openServicePackagePaymentDialog = (servicePackage) => {
+  paymentStore.setPackage(servicePackage);
+
   $q.dialog({
     component: ScheduleAcceptedDialog,
-    componentProps: { servicePackage }
   }).onOk(() => {
     reloadDashboard();
+  }).onDismiss(() => {
+    paymentStore.clearPackage();
   });
 };
 
-const AUTO_SHOWN_PACKAGES_KEY = "sp_payment_popup_shown_ids";
-let autoOpeningPackageDialog = false;
-
 const maybeAutoOpenServicePackagePayment = () => {
   if (autoOpeningPackageDialog) return;
 
@@ -159,16 +203,17 @@ const maybeAutoOpenServicePackagePayment = () => {
 
   autoOpeningPackageDialog = true;
   LocalStorage.set(AUTO_SHOWN_PACKAGES_KEY, [...shownIds, packageToShow.id]);
+  paymentStore.setPackage(packageToShow);
 
   $q.dialog({
     component: ScheduleAcceptedDialog,
-    componentProps: { servicePackage: packageToShow },
   })
     .onOk(() => {
       reloadDashboard();
     })
     .onDismiss(() => {
       autoOpeningPackageDialog = false;
+      paymentStore.clearPackage();
     });
 };
 
@@ -196,26 +241,6 @@ const reloadDashboard = async (showLoader = true) => {
   maybeAutoOpenServicePackagePayment();
 };
 
-const maybeOpenSuccessModal = () => {
-  if (!successModalKey.value) return;
-
-  if (!(successModalKey.value in successModalContents)) {
-    successModalKey.value = null;
-    return;
-  }
-
-  const buildContent = successModalContents[successModalKey.value];
-
-  successModalKey.value = null;
-
-  $q.dialog({
-    component: FinalSuccesModal,
-    componentProps: { content: buildContent ? buildContent() : null },
-  }).onDismiss(() => {
-    router.replace({ path: route.path, query: {} });
-  });
-};
-
 const onRefresh = async (done) => {
   try {
     await reloadDashboard(false);
@@ -254,6 +279,7 @@ onMounted(async () => {
     store.markFirstAccessSeen();
   }
 });
+
 </script>
 
 <style scoped>

+ 18 - 33
src/pages/dashboard/components/DashboardClientProposals.vue

@@ -10,8 +10,8 @@
       <q-card
         v-for="item in data"
         :key="item.id"
-        :flat="false"
         class="proposal-card shadow-card"
+        :flat="false"
       >
         <div class="row no-wrap">
           <div class="column">
@@ -21,8 +21,8 @@
             >
               <img
                 v-if="item.provider_photo"
-                :src="item.provider_photo"
                 style="object-fit: cover; border-radius: 50%"
+                :src="item.provider_photo"
               />
 
               <span v-else>
@@ -56,6 +56,7 @@
                       })
                     }}
                   </span>
+
                   <span class="font10 fontmedium">
                     <q-icon
                       class="font9 fontregular"
@@ -104,7 +105,6 @@
               </div>
             </div>
 
-            <!-- PREÇO -->
             <div class="price text-text">
               <span class="font9 fontmedium distance">
                 <span class="fontbold font10">
@@ -155,11 +155,7 @@
 </template>
 
 <script setup>
-import { refuseProposal } from "src/api/customSchedules";
 import { avatarColors } from "src/helpers/avatarColors";
-import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
-import { useQuasar } from "quasar";
-import SchedulePaymentDialog from "src/components/dashboard/SchedulePaymentDialog.vue";
 
 import {
   chooseprice,
@@ -168,10 +164,12 @@ import {
   getFirstName,
 } from "src/helpers/utils";
 
-// import { getProviderGenderLabel } from "src/helpers/provider";
-// import { useI18n } from "vue-i18n";
+import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
+import { refuseProposal } from "src/api/customSchedules";
+import { usePaymentStore } from "src/stores/payment";
+import { useQuasar } from "quasar";
 
-const emit = defineEmits(["refreshData"]);
+import SchedulePaymentDialog from "./schedule/SchedulePaymentDialog.vue";
 
 defineProps({
   data: {
@@ -180,30 +178,13 @@ defineProps({
   },
 });
 
+// import { getProviderGenderLabel } from "src/helpers/provider";
+// import { useI18n } from "vue-i18n";
+const emit = defineEmits(["refreshData"]);
+
 // const { t } = useI18n();
 const $q = useQuasar();
-
-// const formatTime = (time) => {
-//   if (!time) return '';
-
-//   const [hour, minute] = time.split(':');
-//   return `${hour}:${minute}`;
-// };
-
-// const formatDate = (date) => {
-//   if (!date) return '';
-
-//   const d = new Date(date);
-
-//   const weekday = d.toLocaleDateString('pt-BR', {
-//     weekday: 'long'
-//   });
-
-//   const day = String(d.getDate()).padStart(2, '0');
-//   const month = String(d.getMonth() + 1).padStart(2, '0');
-
-//   return `${weekday}, ${day}-${month}`;
-// };
+const paymentStore = usePaymentStore();
 
 const handleAcceptProposal = (item) => {
   const servicePackage = {
@@ -212,14 +193,17 @@ const handleAcceptProposal = (item) => {
     address: item.address ?? null,
   };
 
+  paymentStore.setPackage(servicePackage);
+
   $q.dialog({
     component: SchedulePaymentDialog,
     componentProps: {
-      servicePackage,
       targetType: "schedule_proposal",
     },
   }).onOk(() => {
     emit("refreshData");
+  }).onDismiss(() => {
+    paymentStore.clearPackage();
   });
 };
 
@@ -236,6 +220,7 @@ const handleRefuseProposal = async (proposalId) => {
     // isLoading.value = false
   }
 };
+
 </script>
 
 <style scoped lang="scss">

+ 16 - 42
src/components/dashboard/NextSchedulesDetailsDialog.vue → src/pages/dashboard/components/schedule/NextSchedulesDetailsDialog.vue

@@ -32,10 +32,10 @@
           >
             {{
               "(" +
-              providerAge +
-              " " +
-              $t("dashboard_client.next_schedules.provider_age_unit") +
-              ")"
+                providerAge +
+                " " +
+                $t("dashboard_client.next_schedules.provider_age_unit") +
+                ")"
             }}
           </span>
         </div>
@@ -184,6 +184,7 @@
 import { avatarColors } from "src/helpers/avatarColors";
 import { computed, onMounted, ref } from "vue";
 import { formatAddress, formatCurrency, getFirstName } from "src/helpers/utils";
+import { formatLongDate } from "src/helpers/scheduleDate";
 import { getScheduleClienteDetails } from "src/api/dashboard";
 
 import {
@@ -198,18 +199,16 @@ import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
 import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 import ScheduleCancelDialog from "./ScheduleCancelDialog.vue";
 
+defineEmits([...useDialogPluginComponent.emits]);
+
 const props = defineProps({
   schedule: { type: Object, required: true },
 });
 
-defineEmits([...useDialogPluginComponent.emits]);
-
-const { t } = useI18n();
 const $q = useQuasar();
-
 const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
-
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
+const { t } = useI18n();
 
 const details = ref(null);
 const loadingDetails = ref(true);
@@ -218,34 +217,17 @@ const avatarStyle = computed(
   () => avatarColors[props.schedule.id % avatarColors.length],
 );
 
-const parseLocalDate = (dateStr) => {
-  if (!dateStr) return null;
-
-  const s = String(dateStr);
-
-  const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
-
-  if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]);
-
-  const dmy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
-
-  if (dmy) return new Date(+dmy[3], +dmy[2] - 1, +dmy[1]);
-
-  return null;
-};
-
 const fullDateLabel = computed(() => {
-  if (props.schedule.formatted_date) return props.schedule.formatted_date;
+  return props.schedule.formatted_date || formatLongDate(props.schedule.date);
+});
 
-  const d = parseLocalDate(props.schedule.date);
+const priceRangeLabel = computed(() => {
+  const minPrice = details.value?.min_price;
+  const maxPrice = details.value?.max_price;
 
-  if (!d) return props.schedule.date ?? "";
+  if (minPrice == null || maxPrice == null) return null;
 
-  return d.toLocaleDateString("pt-BR", {
-    day: "2-digit",
-    month: "long",
-    year: "numeric",
-  });
+  return `${formatCurrency(minPrice)} - ${formatCurrency(maxPrice)}`;
 });
 
 const providerAge = computed(() => {
@@ -287,15 +269,6 @@ const total = computed(() =>
   ),
 );
 
-const priceRangeLabel = computed(() => {
-  const minPrice = details.value?.min_price;
-  const maxPrice = details.value?.max_price;
-
-  if (minPrice == null || maxPrice == null) return null;
-
-  return `${formatCurrency(minPrice)} - ${formatCurrency(maxPrice)}`;
-});
-
 const openCancelDialog = () => {
   $q.dialog({
     component: ScheduleCancelDialog,
@@ -318,6 +291,7 @@ onMounted(async () => {
     loadingDetails.value = false;
   }
 });
+
 </script>
 
 <style scoped lang="scss">

+ 28 - 47
src/components/dashboard/ScheduleAcceptedDialog.vue → src/pages/dashboard/components/schedule/ScheduleAcceptedDialog.vue

@@ -15,8 +15,8 @@
         >
           <img
             v-if="getProfileMediaUrl(item)"
-            :src="getProfileMediaUrl(item)"
             style="object-fit: cover"
+            :src="getProfileMediaUrl(item)"
           />
 
           <span v-else>
@@ -55,7 +55,7 @@
           class="detail-row schedule-row"
         >
           <span class="detail-value font13">
-            {{ formatScheduleDate(schedule) }}
+            {{ schedule.formatted_date || formatLongDate(schedule.date) }}
           </span>
 
           <span class="detail-valued text-text font13">
@@ -90,8 +90,8 @@
         <div class="detail-row">
           <span class="text-primary font14 fontmedium">
             {{ hasServicePackageDiscount
-                ? $t("dashboard_client.pending_schedules.detail_package_total")
-                : $t("dashboard_client.pending_schedules.detail_pix_total") }}
+              ? $t("dashboard_client.pending_schedules.detail_package_total")
+              : $t("dashboard_client.pending_schedules.detail_pix_total") }}
           </span>
 
           <span class="total-value font14 fontbold">
@@ -167,6 +167,7 @@
 import { avatarColors } from "src/helpers/avatarColors";
 import { computed, onMounted } from "vue";
 import { formatCurrency, getFirstName } from "src/helpers/utils";
+import { formatLongDate } from "src/helpers/scheduleDate";
 import { getProfileMediaUrl } from "src/helpers/profileMedia";
 
 import {
@@ -181,25 +182,14 @@ import { usePaymentStore } from "src/stores/payment";
 import SchedulePaymentDialog from "./SchedulePaymentDialog.vue";
 import SchedulePaymentPixDialog from "./SchedulePaymentPixDialog.vue";
 
-const props = defineProps({
-  servicePackage: { type: Object, required: true },
-});
-
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
-
 const $q = useQuasar();
-
+const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();
 const paymentStore = usePaymentStore();
-
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
 
-const item = computed(() => props.servicePackage);
-
-const displayProviderName = computed(() => {
-  return item.value.provider?.user?.name ?? item.value.provider_name ?? '';
-});
+const item = computed(() => paymentStore.currentPackage);
 
 const activeSchedules = computed(() =>
   (item.value.schedules ?? []).filter(
@@ -207,24 +197,6 @@ const activeSchedules = computed(() =>
   ),
 );
 
-const displayDistrict = computed(() => {
-  return activeSchedules.value[0]?.address?.district ?? item.value.address?.district ?? '';
-});
-
-const formatScheduleDate = (schedule) => {
-  if (!schedule) return '';
-  if (schedule.formatted_date) return schedule.formatted_date;
-  const raw = String(schedule.date || '');
-  const m = raw.match(/^(\d{4})-(\d{2})-(\d{2})/);
-  if (!m) return raw;
-  const d = new Date(+m[1], +m[2] - 1, +m[3]);
-  return d.toLocaleDateString('pt-BR', {
-    day: '2-digit',
-    month: 'long',
-    year: 'numeric',
-  });
-};
-
 const avatarStyle = computed(
   () => avatarColors[item.value.id % avatarColors.length],
 );
@@ -240,21 +212,18 @@ const baseAmount = computed(() => {
   );
 });
 
-const creditCardTotal = computed(() =>
-  parseFloat((baseAmount.value + platformFee("credit_card")).toFixed(2)),
-);
+const displayDistrict = computed(() => {
+  return activeSchedules.value[0]?.address?.district ?? item.value.address?.district ?? '';
+});
+
+const displayProviderName = computed(() => {
+  return item.value.provider?.user?.name ?? item.value.provider_name ?? '';
+});
 
 const hasServicePackageDiscount = computed(() =>
   scheduleUsesServicePackageDiscount(item.value),
 );
 
-const pixDiscount = computed(() =>
-  Math.max(0, parseFloat((creditCardTotal.value - pixTotal.value).toFixed(2))),
-);
-const pixTotal = computed(() =>
-  parseFloat((baseAmount.value + platformFee("pix")).toFixed(2)),
-);
-
 const platformFee = (paymentType) => {
   const feeRate = getSchedulePlatformFeeRate(
     item.value,
@@ -265,8 +234,20 @@ const platformFee = (paymentType) => {
   return parseFloat((baseAmount.value * (feeRate ?? 0)).toFixed(2));
 };
 
+const creditCardTotal = computed(() =>
+  parseFloat((baseAmount.value + platformFee("credit_card")).toFixed(2)),
+);
+
+const pixTotal = computed(() =>
+  parseFloat((baseAmount.value + platformFee("pix")).toFixed(2)),
+);
+
+const pixDiscount = computed(() =>
+  Math.max(0, parseFloat((creditCardTotal.value - pixTotal.value).toFixed(2))),
+);
+
 const onGoToPayment = () => {
-  const validPixPayment = paymentStore.getValidPixPaymentForServicePackage(item.value.id);
+  const validPixPayment = paymentStore.getPix(`service_package:${item.value.id}`);
   const hasValidPixPayment = !!validPixPayment;
 
   $q.dialog({
@@ -274,7 +255,6 @@ const onGoToPayment = () => {
       ? SchedulePaymentPixDialog
       : SchedulePaymentDialog,
     componentProps: {
-      servicePackage: item.value,
       ...(hasValidPixPayment ? { total: pixTotal.value } : {}),
     },
   }).onOk(() => {
@@ -285,6 +265,7 @@ const onGoToPayment = () => {
 onMounted(() => {
   loadPlatformFees().catch(() => {});
 });
+
 </script>
 
 <style scoped lang="scss">

+ 26 - 21
src/components/dashboard/ScheduleCancelDialog.vue → src/pages/dashboard/components/schedule/ScheduleCancelDialog.vue

@@ -1,9 +1,8 @@
 <template>
   <q-dialog ref="dialogRef" @hide="onDialogHide">
     <q-card class="cancel-dialog-card bg-surface shadow-card" :flat="false">
-
       <div class="row justify-end q-pt-sm q-pr-sm">
-        <q-btn flat round dense icon="close" color="grey-6" size="sm" @click="onDialogCancel" />
+        <q-btn color="grey-6" dense flat icon="close" round size="sm" @click="onDialogCancel" />
       </div>
 
       <q-card-section class="q-pt-none q-pb-sm q-px-lg text-center">
@@ -14,7 +13,8 @@
 
       <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 name="mdi-package-variant" color="negative" size="22px" class="q-mt-xs flex-shrink-0" />
+          <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>
@@ -25,30 +25,34 @@
         <div class="font14 fontbold text-grey-8 q-mb-xs text-center">
           {{ $t('provider.dashboard.cancel_schedule.reason_label') }}
         </div>
+
         <q-input
           v-model="cancelText"
-          type="textarea"
-          outlined
-          dense
-          :placeholder="$t('provider.dashboard.cancel_schedule.reason_placeholder')"
-          rows="4"
           color="secondary"
+          dense
+          hide-bottom-space
           input-class="text-black"
+          outlined
+          rows="4"
+          type="textarea"
+          :placeholder="$t('provider.dashboard.cancel_schedule.reason_placeholder')"
           :rules="[val => (val && val.trim().length >= 5) || ' ']"
-          hide-bottom-space
         />
       </q-card-section>
 
       <q-card-section class="q-pt-xs q-pb-md q-px-lg">
         <div class="warning-box row no-wrap q-gutter-x-sm q-pa-sm">
-          <q-icon name="mdi-alert-outline" color="secondary" size="22px" class="q-mt-xs flex-shrink-0" />
+          <q-icon class="q-mt-xs flex-shrink-0" color="secondary" name="mdi-alert-outline" size="22px" />
+
           <div class="text-primary">
             <span class="font14 fontbold">
               {{ $t('provider.dashboard.cancel_schedule.warning_title') }}
             </span>
+
             <span class="">
               {{ ' ' + $t('provider.dashboard.cancel_schedule.warning_free') }}
             </span>
+
             <div class="q-mt-sm">
               {{ $t('provider.dashboard.cancel_schedule.warning_fee') }}
             </div>
@@ -59,36 +63,36 @@
       <q-card-section class="q-pt-none q-pb-lg q-px-lg">
         <div class="row justify-center q-gutter-x-md">
           <q-btn
-            unelevated
-            rounded
-            no-caps
             class="btn-action bg-grey-6 text-grey-1"
-            :loading="loading"
+            no-caps
+            rounded
+            unelevated
             :disable="!cancelText || cancelText.trim().length < 5"
             :label="$t('provider.dashboard.cancel_schedule.btn_keep')"
+            :loading="loading"
             @click="confirmCancel"
           />
+
           <q-btn
-            unelevated
-            rounded
-            no-caps
-            color="secondary"
             class="btn-action"
+            color="secondary"
+            no-caps
+            rounded
+            unelevated
             :label="$t('provider.dashboard.cancel_schedule.btn_back')"
             @click="onDialogCancel"
           />
         </div>
       </q-card-section>
-
     </q-card>
   </q-dialog>
 </template>
 
 <script setup>
+import { cancelSchedule } from 'src/api/schedule'
 import { computed, ref } from 'vue'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
 import { useI18n } from 'vue-i18n'
-import { cancelSchedule } from 'src/api/schedule'
 
 const props = defineProps({
   schedule: {
@@ -97,9 +101,9 @@ const props = defineProps({
   }
 })
 
-const { t } = useI18n()
 const $q = useQuasar()
 const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent()
+const { t } = useI18n()
 
 const cancelText = ref('')
 const loading = ref(false)
@@ -130,6 +134,7 @@ const confirmCancel = async () => {
     loading.value = false
   }
 }
+
 </script>
 
 <style scoped lang="scss">

+ 59 - 65
src/components/dashboard/SchedulePaymentDialog.vue → src/pages/dashboard/components/schedule/SchedulePaymentDialog.vue

@@ -1,38 +1,44 @@
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down" @hide="onDialogHide">
+  <q-dialog ref="dialogRef" maximized persistent transition-hide="slide-down" transition-show="slide-up" @hide="onDialogHide">
     <div class="bg-page full-height column">
-
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
-        <q-btn class="header-back-btn" icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
+        <q-btn class="header-back-btn" color="primary" dense flat icon="mdi-chevron-left" round @click="onDialogCancel" />
+
         <q-space />
+
         <span class="font16 fontbold gradient-diarista">
           {{ $t('payment.title') }}
         </span>
+
         <q-space />
+
         <div style="width: 32px" />
       </div>
 
       <div class="col overflow-auto q-px-md q-pt-lg q-pb-xl">
-
         <div class="q-mb-sm text-text font14 fontbold">{{ $t('payment.schedule_address') }}</div>
+
         <div class="address-box row items-center no-wrap q-mb-lg">
           <div class="col">
             <div class="address-type-label fontbold">{{ addressTypeLabel }}</div>
+
             <div class="address-full-text text-grey-7">{{ addressFullText }}</div>
           </div>
-          <q-icon name="mdi-chevron-down" color="grey-5" size="22px" />
+
+          <q-icon color="grey-5" name="mdi-chevron-down" size="22px" />
         </div>
 
         <div class="text-text q-mb-sm font14 fontbold">{{ $t('payment.pay_with') }}</div>
-        <div class="row q-gutter-sm q-mb-sm">
 
+        <div class="row q-gutter-sm q-mb-sm">
           <div
             class="payment-option-card col column items-center justify-center q-pa-md cursor-pointer"
             :class="{ 'payment-option-selected': selectedMethod === 'pix' }"
             @click="selectedMethod = 'pix'"
           >
             <span class="payment-option-title">{{ $t('payment.pix') }}</span>
-            <q-icon name="mdi-cash-fast" size="32px" color="teal" class="q-mt-xs" />
+
+            <q-icon class="q-mt-xs" color="teal" name="mdi-cash-fast" size="32px" />
           </div>
 
           <div
@@ -41,10 +47,11 @@
             @click="openAddCard"
           >
             <span class="payment-option-title">{{ $t('payment.add_card') }}</span>
-            <q-icon name="mdi-plus-circle-outline" size="22px" color="grey-5" class="q-mt-xs" />
+
+            <q-icon class="q-mt-xs" color="grey-5" name="mdi-plus-circle-outline" size="22px" />
+
             <span class="payment-option-sub">{{ $t('payment.credit_debit') }}</span>
           </div>
-
         </div>
 
         <div v-if="loadingCards" class="flex flex-center q-py-md">
@@ -61,11 +68,15 @@
           >
             <div class="col column">
               <span class="card-titular-label">{{ $t('payment.card_holder') }}</span>
+
               <span class="card-holder-name text-text">{{ card.holder_name }}</span>
             </div>
+
             <div class="column items-end">
-              <span class="card-brand-text">{{ brandDisplay(card.brand) }}</span>
+              <span class="card-brand-text">{{ formatCardBrand(card.brand) }}</span>
+
               <span class="card-last-four">{{ '**** **** **** ' + card.last_four_digits }}</span>
+
               <span class="card-expiry-text">{{ card.expiration }}</span>
             </div>
           </div>
@@ -76,6 +87,7 @@
         <div class="payment-summary q-mb-lg">
           <div class="row items-center justify-between">
             <span class="summary-total-label">{{ $t('dashboard_client.pending_schedules.detail_total') }}</span>
+
             <span class="summary-total-value">{{ formatCurrency(selectedTotal) }}</span>
           </div>
         </div>
@@ -91,101 +103,80 @@
         </div>
 
         <q-btn
-          unelevated
-          rounded
+          class="full-width"
+          color="primary"
           no-caps
           padding="8px 12px"
-          color="primary"
-          class="full-width"
-          :label="$t('payment.confirm_btn')"
+          rounded
+          unelevated
           :disable="!canConfirm"
+          :label="$t('payment.confirm_btn')"
           @click="onConfirm"
         />
-
       </div>
     </div>
   </q-dialog>
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue'
-import { useDialogPluginComponent, useQuasar } from 'quasar'
-import { useI18n } from 'vue-i18n'
-import { userStore } from 'src/stores/user'
-import { usePaymentStore } from 'src/stores/payment'
+import { Browser } from "@capacitor/browser";
+import { computed, onMounted, ref } from 'vue'
+import { formatAddress, formatCardBrand, formatCurrency } from 'src/helpers/utils'
 import { getClientPaymentMethods } from 'src/api/clientPaymentMethod'
-import { formatCurrency } from 'src/helpers/utils'
 import { getScheduleTotalWithPlatformFee } from 'src/helpers/paymentPlatformFees'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
 import { usePaymentPlatformFees } from 'src/composables/usePaymentPlatformFees'
+import { usePaymentStore } from 'src/stores/payment'
+import { userStore } from 'src/stores/user'
+
 import ProfilePaymentAddDialog from 'src/components/profile/ProfilePaymentAddDialog.vue'
 import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
 import SchedulePaymentProcessingDialog from './SchedulePaymentProcessingDialog.vue'
-import { Browser } from "@capacitor/browser";
+
+defineEmits([...useDialogPluginComponent.emits])
 
 const props = defineProps({
-  servicePackage: {
-    type: Object,
-    required: true,
-  },
   targetType: {
     type: String,
     default: 'service_package',
   },
 })
 
-defineEmits([...useDialogPluginComponent.emits])
-
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
-const { t } = useI18n()
-const store = userStore()
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const paymentStore = usePaymentStore()
 const { platformFees, loadPlatformFees } = usePaymentPlatformFees()
+const store = userStore()
+const { t } = useI18n()
 
-const item = computed(() => props.servicePackage)
-
-const selectedMethod = ref(null)
-const paymentMethods = ref([])
 const loadingCards = ref(false)
+const paymentMethods = ref([])
+const selectedMethod = ref(null)
 
-const selectedPaymentType = computed(() => selectedMethod.value === 'pix' ? 'pix' : 'credit_card')
-const selectedTotal = computed(() => getScheduleTotalWithPlatformFee(item.value, selectedPaymentType.value, platformFees.value))
+const canConfirm = computed(() => selectedMethod.value !== null)
+const item = computed(() => paymentStore.currentPackage)
+const address = computed(() => item.value.schedules?.[0]?.address ?? item.value.address)
 
-const getAddress = () => {
-  return item.value.schedules?.[0]?.address ?? item.value.address ?? null;
-};
+const addressFullText = computed(() => formatAddress({
+  address: address.value?.address,
+  district: address.value?.district,
+  number: address.value?.number,
+}))
 
 const addressTypeLabel = computed(() => {
-  const addr = getAddress();
-  const type = addr?.address_type;
+  const type = address.value?.address_type;
   if (!type) return '';
   return t(`profile.address.type.${type}`, type);
 });
 
-const addressFullText = computed(() => {
-  const a = getAddress();
-  if (!a) return '';
-  const parts = [a.address, a.number, a.district].filter(Boolean);
-  return parts.join(', ');
-});
-
-const canConfirm = computed(() => selectedMethod.value !== null)
-
+const selectedPaymentType = computed(() => selectedMethod.value === 'pix' ? 'pix' : 'credit_card')
+const selectedTotal = computed(() => getScheduleTotalWithPlatformFee(item.value, selectedPaymentType.value, platformFees.value))
 
 //direcionamento para ttermos e serviços 
 const PRIVACY_POLICY_URL =
   "https://politicas.softpar.inf.br/politicas/politicaDiaristaCliente.html";
 
-const openTerms = async () => {
-  await Browser.open({ url: PRIVACY_POLICY_URL });
-};
-
-const brandDisplay = (brand) => {
-  if (!brand) return ''
-  const map = { visa: 'VISA', mastercard: 'Mastercard', elo: 'Elo', hipercard: 'Hipercard', diners: 'Diners', discover: 'Discover' }
-  return map[brand] ?? brand.toUpperCase()
-}
-
 const loadCards = async () => {
   loadingCards.value = true
   try {
@@ -216,7 +207,6 @@ const openPixPayment = () => {
   $q.dialog({
     component: SchedulePaymentPixDialog,
     componentProps: {
-      servicePackage: item.value,
       total: selectedTotal.value,
       targetType: props.targetType,
     },
@@ -235,7 +225,6 @@ const onConfirm = () => {
   $q.dialog({
     component: SchedulePaymentProcessingDialog,
     componentProps: {
-      servicePackage: item.value,
       clientPaymentMethodId,
       total: selectedTotal.value,
       targetType: props.targetType,
@@ -245,10 +234,14 @@ const onConfirm = () => {
   })
 }
 
+const openTerms = async () => {
+  await Browser.open({ url: PRIVACY_POLICY_URL });
+};
+
 onMounted(() => {
   loadPlatformFees().catch(() => {})
 
-  const existingPix = paymentStore.getValidPixPaymentForServicePackage(`${props.targetType}:${item.value.id}`)
+  const existingPix = paymentStore.getPix(`${props.targetType}:${item.value.id}`)
 
   if (existingPix) {
     openPixPayment()
@@ -257,6 +250,7 @@ onMounted(() => {
 
   loadCards()
 })
+
 </script>
 
 <style scoped lang="scss">

+ 112 - 64
src/components/dashboard/SchedulePaymentPixDialog.vue → src/pages/dashboard/components/schedule/SchedulePaymentPixDialog.vue

@@ -1,33 +1,43 @@
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down" @hide="onDialogHide">
+  <q-dialog ref="dialogRef" maximized persistent transition-hide="slide-down" transition-show="slide-up" @hide="onDialogHide">
     <div class="bg-page full-height column">
-
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
-        <q-btn class="header-back-btn" icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
+        <q-btn class="header-back-btn" color="primary" dense flat icon="mdi-chevron-left" round @click="onDialogCancel" />
+
         <q-space />
+
         <span class="font16 fontbold gradient-diarista">
           {{ $t('payment.pix_title') }}
         </span>
+
         <q-space />
+
         <div style="width: 32px" />
       </div>
 
       <div v-if="success" class="col column items-center justify-center q-px-xl">
         <q-btn
-          flat round icon="mdi-close" color="grey-5"
           class="self-end q-mb-md"
+          color="grey-5"
+          flat
+          icon="mdi-close"
+          round
           @click="onDialogOK"
         />
+
         <div class="success-icon-wrapper q-mb-lg">
-          <q-icon name="mdi-check-circle" size="100px" color="primary" />
+          <q-icon color="primary" name="mdi-check-circle" size="100px" />
         </div>
+
         <div class="success-title text-primary text-center q-mb-sm">
           {{ $t('payment.success_title') }}
         </div>
-        <i18n-t keypath="payment.success_message" tag="div" class="success-message text-grey-6 text-center">
+
+        <i18n-t class="success-message text-grey-6 text-center" keypath="payment.success_message" tag="div">
           <template #nextServices>
             <strong class="text-text">{{ $t('payment.success_next_services') }}</strong>
           </template>
+
           <template #agenda>
             <strong class="text-text">{{ $t('payment.success_agenda') }}</strong>
           </template>
@@ -35,71 +45,79 @@
       </div>
 
       <div v-else-if="processing" class="col column items-center justify-center q-px-xl">
-        <q-spinner-oval color="primary" size="72px" class="q-mb-lg" />
+        <q-spinner-oval class="q-mb-lg" color="primary" size="72px" />
+
         <div class="processing-title text-primary text-center q-mb-sm">
           {{ $t('payment.processing_title') }}
         </div>
+
         <div class="processing-message text-grey-6 text-center">
           {{ $t('payment.processing_message') }}
         </div>
       </div>
 
       <div v-else class="pix-payment-content col scroll q-px-lg q-pt-lg q-pb-xl column">
-
-        <div class="row items-center justify-between q-mb-sm">
+        <div class="pix-summary-row row items-center justify-between q-mb-sm">
           <span class="pix-label font14 fontbold">{{ $t('payment.pix_total') }}</span>
+
           <span class="text-primary font14 fontbold">{{ formatCurrency(total) }}</span>
         </div>
+
         <q-separator />
 
-        <div class="row items-center justify-between q-mt-sm q-mb-lg">
+        <div class="pix-summary-row row items-center justify-between q-mt-sm q-mb-lg">
           <span class="pix-label font14 fontbold">{{ $t('payment.pix_expires') }}</span>
+
           <span class="text-primary font14 fontbold">{{ countdown }}</span>
         </div>
 
         <div class="flex flex-center q-mb-md">
-          <q-icon name="mdi-cash-fast" size="48px" color="teal" />
+          <q-icon color="teal" name="mdi-cash-fast" size="48px" />
         </div>
 
         <div class="flex flex-center q-mb-md">
           <q-img
             v-if="pixQrCodeUrl"
-            :src="pixQrCodeUrl"
-            width="180px"
-            height="180px"
-            fit="contain"
             class="qrcode-wrapper"
+            fit="contain"
+            height="180px"
+            width="180px"
+            :src="pixQrCodeUrl"
           />
+
           <div v-else class="qrcode-wrapper column items-center justify-center">
-            <q-icon name="mdi-qrcode" size="80px" color="grey-6" />
+            <q-icon color="grey-6" name="mdi-qrcode" size="80px" />
           </div>
         </div>
 
         <div class="pix-code-text q-mb-md">{{ pixCode || 'Código Pix indisponível.' }}</div>
 
         <q-btn
-          unelevated
-          rounded
-          no-caps
+          class="full-width q-mb-lg"
           color="primary"
+          no-caps
           padding="4px 12px"
-          class="full-width q-mb-lg"
+          rounded
+          unelevated
           :label="$t('payment.pix_copy_btn')"
           :loading="processing"
           @click="copyCode"
         />
 
-        <i18n-t keypath="payment.pix_instructions" tag="p" class="pix-instructions-text font14 q-mb-sm">
+        <i18n-t class="pix-instructions-text font14 q-mb-sm" keypath="payment.pix_instructions" tag="p">
           <template #copyCode>
             <span class="fontbold">{{ $t('payment.pix_copy_code') }}</span>
           </template>
+
           <template #pasteCode>
             <span class="fontbold">{{ $t('payment.pix_paste_code') }}</span>
           </template>
+
           <template #finalize>
             <span class="fontbold">{{ $t('payment.pix_finalize') }}</span>
           </template>
         </i18n-t>
+
         <p class="pix-instructions-text font14">{{ $t('payment.pix_email_note') }}</p>
       </div>
     </div>
@@ -107,39 +125,44 @@
 </template>
 
 <script setup>
-import { computed, ref, onMounted, onUnmounted } from 'vue'
-import { useDialogPluginComponent, useQuasar, copyToClipboard } from 'quasar'
+import { computed, onMounted, onUnmounted, ref } from 'vue'
+import { copyToClipboard, useDialogPluginComponent, useQuasar } from 'quasar'
 import { formatCurrency } from 'src/helpers/utils'
-import { getServicePackagePix, payServicePackage, getScheduleProposalPix, payScheduleProposal } from 'src/api/payment'
+import { getScheduleProposalPix, getServicePackagePix, payScheduleProposal, payServicePackage } from 'src/api/payment'
+import { parseUtcDate } from 'src/helpers/scheduleDate'
 import { usePaymentStore } from 'src/stores/payment'
 
+defineEmits([...useDialogPluginComponent.emits])
+
 const props = defineProps({
-  servicePackage: { type: Object, required: true },
   total: { type: Number, required: true },
   targetType: { type: String, default: 'service_package' },
 })
 
-defineEmits([...useDialogPluginComponent.emits])
-
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const paymentStore = usePaymentStore()
 
-const pay = props.targetType === 'schedule_proposal' ? payScheduleProposal : payServicePackage
-const getPix = props.targetType === 'schedule_proposal' ? getScheduleProposalPix : getServicePackagePix
-
-const item = computed(() => props.servicePackage)
-const itemId = computed(() => item.value.id)
-const cacheKey = computed(() => `${props.targetType}:${itemId.value}`)
-
+const countdown = ref('')
 const payment = ref(null)
-const success = ref(false)
 const processing = ref(true)
+const success = ref(false)
+const totalSeconds = ref(20 * 60)
 
+const item = computed(() => paymentStore.currentPackage)
+const itemId = computed(() => item.value.id)
+const cacheKey = computed(() => `${props.targetType}:${itemId.value}`)
 const pixData = computed(() => payment.value?.pix ?? {})
 const pixCode = computed(() => pixData.value?.qr_code ?? '')
-const pixQrCodeUrl = computed(() => pixData.value?.qr_code_url ?? '')
 const pixExpiresAt = computed(() => pixData.value?.expires_at ?? payment.value?.expires_at ?? null)
+const pixQrCodeUrl = computed(() => pixData.value?.qr_code_url ?? '')
+
+const fetchPix = props.targetType === 'schedule_proposal' ? getScheduleProposalPix : getServicePackagePix
+const pay = props.targetType === 'schedule_proposal' ? payScheduleProposal : payServicePackage
+
+let countdownTimer = null
+let pollingInFlight = false
+let pollingTimer = null
 
 const copyCode = async () => {
   try {
@@ -157,12 +180,6 @@ const copyCode = async () => {
 
 }
 
-const totalSeconds = ref(20 * 60)
-const countdown = ref('')
-let countdownTimer = null
-let pollingTimer = null
-let pollingInFlight = false
-
 const stopPolling = () => {
   clearInterval(pollingTimer)
   pollingTimer = null
@@ -180,7 +197,7 @@ const applyPaymentStatus = (nextPayment) => {
       position: 'top',
     })
 
-    paymentStore.clearPixPaymentForServicePackage(cacheKey.value)
+    paymentStore.clearPix(cacheKey.value)
     stopPolling()
 
     onDialogOK()
@@ -191,14 +208,30 @@ const applyPaymentStatus = (nextPayment) => {
 
   if (['failed', 'cancelled'].includes(nextPayment.status)) {
     processing.value = false
-    paymentStore.clearPixPaymentForServicePackage(cacheKey.value)
+    paymentStore.clearPix(cacheKey.value)
     stopPolling()
     $q.notify({ type: 'negative', message: nextPayment.failure_message || 'Pagamento Pix não confirmado.' })
     onDialogCancel()
     return
   }
 
-  paymentStore.setPixPaymentForServicePackage(cacheKey.value, nextPayment)
+  paymentStore.setPix(cacheKey.value, nextPayment)
+}
+
+const updateCountdown = () => {
+  if (pixExpiresAt.value) {
+    totalSeconds.value = Math.max(0, Math.floor((parseUtcDate(pixExpiresAt.value).getTime() - Date.now()) / 1000))
+  }
+
+  const m = Math.floor(totalSeconds.value / 60)
+  const s = totalSeconds.value % 60
+  countdown.value = `${m} min, ${String(s).padStart(2, '0')} seg`
+  if (!pixExpiresAt.value && totalSeconds.value > 0) totalSeconds.value--
+
+  if (pixExpiresAt.value && totalSeconds.value <= 0) {
+    paymentStore.clearPix(cacheKey.value)
+    stopPolling()
+  }
 }
 
 const checkPaymentStatus = async () => {
@@ -206,7 +239,7 @@ const checkPaymentStatus = async () => {
 
   pollingInFlight = true
   try {
-    const result = await getPix(itemId.value)
+    const result = await fetchPix(itemId.value)
     applyPaymentStatus(result)
     updateCountdown()
   } catch (e) {
@@ -223,28 +256,12 @@ const startPolling = () => {
   pollingTimer = setInterval(checkPaymentStatus, 5000)
 }
 
-const updateCountdown = () => {
-  if (pixExpiresAt.value) {
-    totalSeconds.value = Math.max(0, Math.floor((new Date(pixExpiresAt.value).getTime() - Date.now()) / 1000))
-  }
-
-  const m = Math.floor(totalSeconds.value / 60)
-  const s = totalSeconds.value % 60
-  countdown.value = `${m} min, ${String(s).padStart(2, '0')} seg`
-  if (!pixExpiresAt.value && totalSeconds.value > 0) totalSeconds.value--
-
-  if (pixExpiresAt.value && totalSeconds.value <= 0) {
-    paymentStore.clearPixPaymentForServicePackage(cacheKey.value)
-    stopPolling()
-  }
-}
-
 onMounted(async () => {
   updateCountdown()
   countdownTimer = setInterval(updateCountdown, 1000)
 
   try {
-    const cachedPayment = paymentStore.getValidPixPaymentForServicePackage(cacheKey.value)
+    const cachedPayment = paymentStore.getPix(cacheKey.value)
 
     if (cachedPayment) {
       applyPaymentStatus(cachedPayment)
@@ -275,6 +292,7 @@ onUnmounted(() => {
   clearInterval(countdownTimer)
   stopPolling()
 })
+
 </script>
 
 <style scoped lang="scss">
@@ -286,6 +304,29 @@ onUnmounted(() => {
   color: #3a3a4a;
 }
 
+.pix-payment-content {
+  box-sizing: border-box;
+  min-width: 0;
+  width: 100%;
+  overflow-x: hidden;
+}
+
+.pix-summary-row {
+  min-width: 0;
+  width: 100%;
+  gap: 8px;
+}
+
+.pix-summary-row > * {
+  min-width: 0;
+  overflow-wrap: anywhere;
+}
+
+.pix-summary-row > :last-child {
+  margin-left: auto;
+  text-align: right;
+}
+
 .qrcode-wrapper {
   background: #fff;
   border: 1px solid #e0e0e0;
@@ -294,7 +335,11 @@ onUnmounted(() => {
 }
 
 .pix-code-text {
+  align-self: stretch;
+  box-sizing: border-box;
   color: #5a5a6a;
+  max-width: 100%;
+  min-width: 0;
   text-align: center;
   word-break: break-all;
   line-height: 1.5;
@@ -306,13 +351,16 @@ onUnmounted(() => {
 
 .pix-instructions-text {
   align-self: stretch;
+  box-sizing: border-box;
   line-height: 1.5;
   color: #3a3a4a;
   margin-left: 0;
   margin-right: 0;
   max-width: 100%;
+  min-width: 0;
   overflow-wrap: anywhere;
   text-align: left;
+  white-space: normal;
   width: 100%;
 }
 

+ 33 - 26
src/components/dashboard/SchedulePaymentProcessingDialog.vue → src/pages/dashboard/components/schedule/SchedulePaymentProcessingDialog.vue

@@ -1,12 +1,13 @@
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="fade" transition-hide="fade" @hide="onDialogHide">
+  <q-dialog ref="dialogRef" maximized persistent transition-hide="fade" transition-show="fade" @hide="onDialogHide">
     <div class="bg-surface full-height column items-center justify-center q-px-xl">
-
       <template v-if="!success">
-        <q-spinner-oval color="primary" size="72px" class="q-mb-lg" />
+        <q-spinner-oval class="q-mb-lg" color="primary" size="72px" />
+
         <div class="processing-title text-primary text-center q-mb-sm">
           {{ $t('payment.processing_title') }}
         </div>
+
         <div class="processing-message text-grey-6 text-center">
           {{ $t('payment.processing_message') }}
         </div>
@@ -14,58 +15,63 @@
 
       <template v-else>
         <q-btn
+          class="self-end q-mb-md"
+          color="grey-5"
           flat
-          round
           icon="mdi-close"
-          color="grey-5"
-          class="self-end q-mb-md"
+          round
           @click="onDialogOK"
         />
+
         <img
-          :src="LogoDiariaSucesso"
           alt="mascote"
           class="success-mascot q-mb-lg"
+          :src="LogoDiariaSucesso"
         />
+
         <div class="success-title font16 fontbold text-primary text-center q-mb-sm">
           {{ $t('payment.success_title') }}
         </div>
-        <i18n-t keypath="payment.success_message" tag="div" class="success-message text-grey-6 text-center font14">
+
+        <i18n-t class="success-message text-grey-6 text-center font14" keypath="payment.success_message" tag="div">
           <template #nextServices>
             <strong class="text-text">{{ $t('payment.success_next_services') }}</strong>
           </template>
+
           <template #agenda>
             <strong class="text-text">{{ $t('payment.success_agenda') }}</strong>
           </template>
         </i18n-t>
       </template>
-
     </div>
   </q-dialog>
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue'
+import { computed, onMounted, ref } from 'vue'
+import { payScheduleProposal, payServicePackage } from 'src/api/payment'
 import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { usePaymentStore } from 'src/stores/payment'
+
 import LogoDiariaSucesso from 'src/assets/diarinho-success-payment.svg';
-import { payServicePackage, payScheduleProposal } from 'src/api/payment'
+
+defineEmits([...useDialogPluginComponent.emits])
 
 const props = defineProps({
-  servicePackage: { type: Object, required: true },
   clientPaymentMethodId: { type: Number, required: true },
   total: { type: Number, default: null },
   targetType: { type: String, default: 'service_package' },
 })
 
-defineEmits([...useDialogPluginComponent.emits])
-
-const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
+const paymentStore = usePaymentStore()
 
-const pay = props.targetType === 'schedule_proposal' ? payScheduleProposal : payServicePackage
+const success = ref(false)
 
-const item = computed(() => props.servicePackage)
+const item = computed(() => paymentStore.currentPackage)
 
-const success = ref(false)
+const pay = props.targetType === 'schedule_proposal' ? payScheduleProposal : payServicePackage
 
 onMounted(async () => {
   try {
@@ -75,16 +81,16 @@ onMounted(async () => {
     })
 
     if (payment.status !== 'paid') {
-        throw new Error(payment.failure_message || 'Pagamento não confirmado.')
-      }
+      throw new Error(payment.failure_message || 'Pagamento não confirmado.')
+    }
 
-      $q.notify({
-        type: 'positive',
-        message: 'Pagamento confirmado!',
-        position: 'top',
-      })
+    $q.notify({
+      type: 'positive',
+      message: 'Pagamento confirmado!',
+      position: 'top',
+    })
 
-      onDialogOK()
+    onDialogOK()
       
   } catch (e) {
     console.error('Erro ao pagar:', e)
@@ -96,6 +102,7 @@ onMounted(async () => {
     onDialogCancel()
   }
 })
+
 </script>
 
 <style scoped lang="scss">

+ 19 - 19
src/components/dashboard/ScheduleRatingDialog.vue → src/pages/dashboard/components/schedule/ScheduleRatingDialog.vue

@@ -86,8 +86,8 @@
           <div
             v-for="tag in tags"
             :key="tag.id"
-            :class="{ 'tag-pill--selected': selectedTagIds.includes(tag.id) }"
             class="tag-pill"
+            :class="{ 'tag-pill--selected': selectedTagIds.includes(tag.id) }"
             @click="toggleTag(tag.id)"
           >
             {{ tag.description }}
@@ -213,6 +213,8 @@ import { userStore } from "src/stores/user";
 
 import ProfileHelpDialog from "src/components/profile/ProfileHelpDialog.vue";
 
+defineEmits([...useDialogPluginComponent.emits]);
+
 const props = defineProps({
   schedule: {
     type: Object,
@@ -220,25 +222,22 @@ const props = defineProps({
   },
 });
 
-defineEmits([...useDialogPluginComponent.emits]);
-
+const $q = useQuasar();
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
   useDialogPluginComponent();
-
-const { t } = useI18n();
-const $q = useQuasar();
 const store = userStore();
+const { t } = useI18n();
 
-const stars = ref(0);
-const selectedTagIds = ref([]);
-const comment = ref(null);
 const checkboxValue = ref(false);
-const tags = ref([]);
-const loadingTags = ref(false);
+const comment = ref(null);
 const loading = ref(false);
-const photos = ref([]);
-const photoPreviews = ref([]);
+const loadingTags = ref(false);
 const photoInputRef = ref(null);
+const photoPreviews = ref([]);
+const photos = ref([]);
+const selectedTagIds = ref([]);
+const stars = ref(0);
+const tags = ref([]);
 
 const avatarStyle = computed(() => {
   const c = avatarColors[props.schedule.provider_id % avatarColors.length];
@@ -283,12 +282,6 @@ const removePhoto = (idx) => {
   photoPreviews.value.splice(idx, 1);
 };
 
-const toggleTag = (id) => {
-  const idx = selectedTagIds.value.indexOf(id);
-  if (idx === -1) selectedTagIds.value.push(id);
-  else selectedTagIds.value.splice(idx, 1);
-};
-
 const submit = async () => {
   if (stars.value === 0) return;
 
@@ -326,6 +319,12 @@ const submit = async () => {
   }
 };
 
+const toggleTag = (id) => {
+  const idx = selectedTagIds.value.indexOf(id);
+  if (idx === -1) selectedTagIds.value.push(id);
+  else selectedTagIds.value.splice(idx, 1);
+};
+
 onMounted(async () => {
   loadingTags.value = true;
 
@@ -339,6 +338,7 @@ onMounted(async () => {
     loadingTags.value = false;
   }
 });
+
 </script>
 
 <style scoped lang="scss">

+ 7 - 5
src/pages/orders/components/CustomScheduleRequestCard.vue

@@ -1,12 +1,12 @@
 <template>
-  <q-card :flat="false" class="request-card bg-surface shadow-card q-mb-sm">
+  <q-card class="request-card bg-surface shadow-card q-mb-sm" :flat="false">
     <q-card-section class="q-pa-sm">
       <div class="row no-wrap items-start q-gutter-x-sm">
-        <q-avatar size="44px" class="request-avatar">
+        <q-avatar class="request-avatar" size="44px">
           <q-icon
-            :name="hasProposals ? 'mdi-format-list-bulleted' : 'mdi-clock-outline'"
-            size="20px"
             color="white"
+            size="20px"
+            :name="hasProposals ? 'mdi-format-list-bulleted' : 'mdi-clock-outline'"
           />
         </q-avatar>
 
@@ -75,7 +75,8 @@
 import { computed } from "vue";
 import { formatDayMonth, formatWeekday } from "src/helpers/scheduleDate";
 import { useQuasar } from "quasar";
-import NextSchedulesDetailsDialog from "src/components/dashboard/NextSchedulesDetailsDialog.vue";
+
+import NextSchedulesDetailsDialog from "src/pages/dashboard/components/schedule/NextSchedulesDetailsDialog.vue";
 
 const props = defineProps({
   schedule: {
@@ -98,6 +99,7 @@ const openDetailsDialog = () => {
     componentProps: { schedule: props.schedule },
   });
 };
+
 </script>
 
 <style scoped lang="scss">

+ 33 - 28
src/stores/payment.js

@@ -1,49 +1,54 @@
 import { defineStore } from 'pinia';
 import { ref } from 'vue';
+import { parseUtcDate } from 'src/helpers/scheduleDate';
 
-const getPixExpiresAt = (payment) => payment?.pix?.expires_at ?? payment?.expires_at ?? null;
+export const usePaymentStore = defineStore('payment', () => {
+  const currentPackage = ref(null);
+  const pixById = ref({});
 
-const isFutureDate = (date) => {
-  if (!date) return false;
-  const time = new Date(date).getTime();
-  return Number.isFinite(time) && time > Date.now();
-};
+  const clearPackage = () => {
+    currentPackage.value = null;
+  };
 
-export const usePaymentStore = defineStore('payment', () => {
-  const pixPaymentsByServicePackageId = ref({});
+  const clearPix = (id) => {
+    if (!id || !pixById.value[id]) return;
 
-  const getValidPixPaymentForServicePackage = (servicePackageId) => {
-    const payment = pixPaymentsByServicePackageId.value[servicePackageId] ?? null;
+    const pix = { ...pixById.value };
+    delete pix[id];
+    pixById.value = pix;
+  };
+
+  const getPix = (id) => {
+    const payment = pixById.value[id] ?? null;
     if (!payment) return null;
 
-    if (!isFutureDate(getPixExpiresAt(payment))) {
-      clearPixPaymentForServicePackage(servicePackageId);
+    const expiresAt = payment?.pix?.expires_at ?? payment?.expires_at;
+    const expiry = parseUtcDate(expiresAt)?.getTime();
+
+    if (!expiresAt || !Number.isFinite(expiry) || expiry <= Date.now()) {
+      clearPix(id);
       return null;
     }
 
     return payment;
   };
 
-  const setPixPaymentForServicePackage = (servicePackageId, payment) => {
-    if (!servicePackageId || !payment) return;
-    pixPaymentsByServicePackageId.value = {
-      ...pixPaymentsByServicePackageId.value,
-      [servicePackageId]: payment,
-    };
+  const setPackage = (item) => {
+    currentPackage.value = item;
   };
 
-  const clearPixPaymentForServicePackage = (servicePackageId) => {
-    if (!servicePackageId || !pixPaymentsByServicePackageId.value[servicePackageId]) return;
-
-    const payments = { ...pixPaymentsByServicePackageId.value };
-    delete payments[servicePackageId];
-    pixPaymentsByServicePackageId.value = payments;
+  const setPix = (id, payment) => {
+    if (!id || !payment) return;
+    pixById.value = { ...pixById.value, [id]: payment };
   };
 
   return {
-    pixPaymentsByServicePackageId,
-    getValidPixPaymentForServicePackage,
-    setPixPaymentForServicePackage,
-    clearPixPaymentForServicePackage,
+    clearPackage,
+    clearPix,
+    currentPackage,
+    getPix,
+    pixById,
+    setPackage,
+    setPix,
   };
 });