Преглед изворни кода

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

Gustavo Zanatta пре 4 дана
родитељ
комит
2f0ac8ebb8

+ 53 - 0
src/api/cart.js

@@ -0,0 +1,53 @@
+import api from "src/api";
+
+export const getCarts = async () => {
+  const { data } = await api.get("/cart");
+  return data.payload;
+};
+
+export const getMyCarts = async () => {
+  const { data } = await api.get("/cart/me");
+  return data.payload;
+};
+
+export const getCart = async (cartId) => {
+  const { data } = await api.get(`/cart/${cartId}`);
+  return data.payload;
+};
+
+export const createCart = async (payload) => {
+  const { data } = await api.post("/cart", payload);
+  return data.payload;
+};
+
+export const createMyCart = async (payload) => {
+  const { data } = await api.post("/cart/me", payload);
+  return data.payload;
+};
+
+export const createScheduleCart = createMyCart;
+
+export const updateCart = async (cartId, payload) => {
+  const { data } = await api.put(`/cart/${cartId}`, payload);
+  return data.payload;
+};
+
+export const deleteCart = async (cartId) => {
+  const { data } = await api.delete(`/cart/${cartId}`);
+  return data.payload;
+};
+
+export const createCartItem = async (payload) => {
+  const { data } = await api.post("/cart-item", payload);
+  return data.payload;
+};
+
+export const updateCartItem = async (cartItemId, payload) => {
+  const { data } = await api.put(`/cart-item/${cartItemId}`, payload);
+  return data.payload;
+};
+
+export const deleteCartItem = async (cartItemId) => {
+  const { data } = await api.delete(`/cart-item/${cartItemId}`);
+  return data.payload;
+};

+ 10 - 0
src/api/payment.js

@@ -4,3 +4,13 @@ export const paySchedule = async (scheduleId, payload) => {
   const { data } = await api.post(`/payment/schedule/${scheduleId}/pay`, payload);
   const { data } = await api.post(`/payment/schedule/${scheduleId}/pay`, payload);
   return data.payload;
   return data.payload;
 };
 };
+
+export const getSchedulePixPayment = async (scheduleId) => {
+  const { data } = await api.get(`/payment/schedule/${scheduleId}/pix`);
+  return data.payload;
+};
+
+export const getPaymentPlatformFees = async () => {
+  const { data } = await api.get('/payment/platform-fees');
+  return data.payload;
+};

+ 126 - 63
src/components/dashboard/NextSchedulesDetailsDialog.vue

@@ -5,20 +5,27 @@
       <q-card-section class="column items-center q-pt-lg q-pb-sm">
       <q-card-section class="column items-center q-pt-lg q-pb-sm">
         <q-avatar size="80px" :style="avatarStyle" class="fontbold text-h5 q-mb-sm">
         <q-avatar size="80px" :style="avatarStyle" class="fontbold text-h5 q-mb-sm">
           <img v-if="details?.provider_photo" :src="details.provider_photo" />
           <img v-if="details?.provider_photo" :src="details.provider_photo" />
-          <span v-else>{{ schedule.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}</span>
+
+          <span v-else>
+            {{ schedule.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}
+          </span>
         </q-avatar>
         </q-avatar>
 
 
         <div class="provider-name font16 fontbold">
         <div class="provider-name font16 fontbold">
           {{ schedule.provider_name }}
           {{ schedule.provider_name }}
+
           <span v-if="providerAge !== null" class=" text-grey-6">
           <span v-if="providerAge !== null" class=" text-grey-6">
             {{ '(' + providerAge + ' ' + $t('dashboard_client.next_schedules.provider_age_unit') + ')' }}
             {{ '(' + providerAge + ' ' + $t('dashboard_client.next_schedules.provider_age_unit') + ')' }}
           </span>
           </span>
         </div>
         </div>
+
         <div class="text-text font12 fontbold">
         <div class="text-text font12 fontbold">
           {{ schedule.address?.district }}
           {{ schedule.address?.district }}
         </div>
         </div>
+
         <div v-if="schedule.address" class="font9 fontmedium text-grey-7 q-mt-xs">
         <div v-if="schedule.address" class="font9 fontmedium text-grey-7 q-mt-xs">
           <q-icon name="mdi-map-marker" color="text" size="14px" class="q-mr-xs" />
           <q-icon name="mdi-map-marker" color="text" size="14px" class="q-mr-xs" />
+
           {{ formatAddress(schedule.address) }}
           {{ formatAddress(schedule.address) }}
         </div>
         </div>
       </q-card-section>
       </q-card-section>
@@ -31,6 +38,7 @@
             <q-spinner-dots color="primary" size="24px" />
             <q-spinner-dots color="primary" size="24px" />
           </div>
           </div>
         </template>
         </template>
+
         <template v-else>
         <template v-else>
           <div
           <div
             v-for="sp in details?.specialities"
             v-for="sp in details?.specialities"
@@ -43,12 +51,19 @@
                 color="primary"
                 color="primary"
                 size="12px"
                 size="12px"
               />
               />
-              <span class="text-grey-8 font10 fontmedium q-pl-sm">{{ sp.description }}</span>
+
+              <span class="text-grey-8 font10 fontmedium q-pl-sm">
+                {{ sp.description }}
+              </span>
             </div>
             </div>
           </div>
           </div>
+
           <div v-if="!details?.specialities?.length" class="row items-center q-gutter-x-sm">
           <div v-if="!details?.specialities?.length" class="row items-center q-gutter-x-sm">
-            <q-icon name="mdi-check" color="secondary" size="16px" />
-            <span class="text-grey-8 font10 fontmedium">{{ $t('dashboard_client.next_schedules.default_service') }}</span>
+            <q-icon color="secondary" name="mdi-check" size="16px" />
+
+            <span class="text-grey-8 font10 fontmedium">
+              {{ $t('dashboard_client.next_schedules.default_service') }}
+            </span>
           </div>
           </div>
         </template>
         </template>
       </q-card-section>
       </q-card-section>
@@ -57,27 +72,53 @@
 
 
       <q-card-section class="q-py-md q-px-lg font12 fontmedium">
       <q-card-section class="q-py-md q-px-lg font12 fontmedium">
         <div class="detail-row">
         <div class="detail-row">
-          <span class="detail-label text-primary q-pr-sm">{{ $t('dashboard_client.pending_schedules.detail_date') }}</span>
-          <span class="detail-value">{{ fullDateLabel }}</span>
+          <span class="detail-label text-primary q-pr-sm">
+            {{ $t('dashboard_client.pending_schedules.detail_date') }}
+          </span>
+
+          <span class="detail-value">
+            {{ fullDateLabel }}
+          </span>
         </div>
         </div>
+
         <div class="detail-row">
         <div class="detail-row">
-          <span class="detail-label text-primary q-pr-sm">{{ $t('dashboard_client.pending_schedules.detail_time') }}</span>
+          <span class="detail-label text-primary q-pr-sm">
+            {{ $t('dashboard_client.pending_schedules.detail_time') }}
+          </span>
+
           <span class="detail-value">
           <span class="detail-value">
             {{ schedule.start_time?.slice(0, 5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ schedule.end_time?.slice(0, 5) }}
             {{ schedule.start_time?.slice(0, 5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ schedule.end_time?.slice(0, 5) }}
           </span>
           </span>
         </div>
         </div>
+
         <div class="detail-row">
         <div class="detail-row">
-          <span class="detail-label text-primary q-pr-sm">{{ $t('dashboard_client.pending_schedules.detail_value') }}</span>
-          <span class="detail-value">{{ formatCurrency(schedule.total_amount) }}</span>
+          <span class="detail-label text-primary q-pr-sm">
+            {{ $t('dashboard_client.pending_schedules.detail_value') }}
+          </span>
+
+          <span class="detail-value">
+            {{ formatCurrency(schedule.total_amount) }}
+          </span>
         </div>
         </div>
+
         <div class="detail-row">
         <div class="detail-row">
-          <span class="detail-label text-primary q-pr-sm">{{ $t('dashboard_client.pending_schedules.detail_service_fee') }}</span>
-          <span class="detail-value">{{ formatCurrency(serviceFee) }}</span>
+          <span class="detail-label text-primary q-pr-sm">
+            {{ $t('dashboard_client.pending_schedules.detail_service_fee') }}
+          </span>
+
+          <span class="detail-value">
+            {{ formatCurrency(serviceFee) }}
+          </span>
         </div>
         </div>
 
 
         <div class="detail-row-total font16 fontbold">
         <div class="detail-row-total font16 fontbold">
-          <span class="detail-label text-primary q-pr-sm">{{ $t('dashboard_client.pending_schedules.detail_total') }}</span>
-          <span class="total-value">{{ formatCurrency(total) }}</span>
+          <span class="detail-label text-primary q-pr-sm">
+            {{ $t('dashboard_client.pending_schedules.detail_total') }}
+          </span>
+
+          <span class="total-value">
+            {{ formatCurrency(total) }}
+          </span>
         </div>
         </div>
         
         
         <q-separator class="q-my-sm divisoria-tracejada" />
         <q-separator class="q-my-sm divisoria-tracejada" />
@@ -85,11 +126,11 @@
 
 
       <q-card-section class="q-pt-none q-pb-sm q-px-lg">
       <q-card-section class="q-pt-none q-pb-sm q-px-lg">
         <q-btn
         <q-btn
-          unelevated
-          rounded
-          no-caps
-          color="primary"
           class="close-btn full-width"
           class="close-btn full-width"
+          color="primary"
+          no-caps
+          rounded
+          unelevated
           :label="$t('dashboard_client.next_schedules.btn_close')"
           :label="$t('dashboard_client.next_schedules.btn_close')"
           @click="onDialogCancel"
           @click="onDialogCancel"
         />
         />
@@ -98,101 +139,111 @@
       <q-card-section class="q-pt-xs q-pb-md text-center">
       <q-card-section class="q-pt-xs q-pb-md text-center">
         <div class="row justify-center q-gutter-x-lg">
         <div class="row justify-center q-gutter-x-lg">
           <q-btn
           <q-btn
+            color="grey-7"
             flat
             flat
             no-caps
             no-caps
-            color="grey-7"
             size="sm"
             size="sm"
             :label="$t('dashboard_client.pending_schedules.btn_cancel')"
             :label="$t('dashboard_client.pending_schedules.btn_cancel')"
             @click="openCancelDialog"
             @click="openCancelDialog"
           />
           />
+
           <q-btn
           <q-btn
+            color="grey-7"
             flat
             flat
             no-caps
             no-caps
-            color="grey-7"
             size="sm"
             size="sm"
             :label="$t('dashboard_client.next_schedules.btn_help')"
             :label="$t('dashboard_client.next_schedules.btn_help')"
             @click="openHelp"
             @click="openHelp"
           />
           />
         </div>
         </div>
       </q-card-section>
       </q-card-section>
-
     </q-card>
     </q-card>
   </q-dialog>
   </q-dialog>
 </template>
 </template>
 
 
 <script setup>
 <script setup>
 import { computed, onMounted, ref } from 'vue'
 import { computed, onMounted, ref } from 'vue'
-import { useDialogPluginComponent, useQuasar } from 'quasar'
-import { useI18n } from 'vue-i18n'
+import { formatAddress } from 'src/helpers/utils';
 import { formatCurrency } from 'src/helpers/utils'
 import { formatCurrency } from 'src/helpers/utils'
 import { getScheduleClienteDetails } from 'src/api/dashboard'
 import { getScheduleClienteDetails } from 'src/api/dashboard'
-import ScheduleCancelDialog from './ScheduleCancelDialog.vue'
+import { getSchedulePlatformFeeRate } from 'src/helpers/paymentPlatformFees'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+import { usePaymentPlatformFees } from 'src/composables/usePaymentPlatformFees'
+
 import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
 import ProfileHelpDialog from 'src/components/profile/ProfileHelpDialog.vue'
-import { formatAddress } from 'src/helpers/utils';
+import ScheduleCancelDialog from './ScheduleCancelDialog.vue'
 
 
 const props = defineProps({
 const props = defineProps({
-  schedule: {
-    type: Object,
-    required: true
-  }
+  schedule: { type: Object, required: true }
 })
 })
 
 
 defineEmits([...useDialogPluginComponent.emits])
 defineEmits([...useDialogPluginComponent.emits])
 
 
 const { t } = useI18n()
 const { t } = useI18n()
-const $q = useQuasar()
+const $q    = useQuasar()
+
 const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent()
 const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent()
 
 
-const details = ref(null)
+const { platformFees, loadPlatformFees } = usePaymentPlatformFees()
+
+const details        = ref(null)
 const loadingDetails = ref(true)
 const loadingDetails = ref(true)
 
 
-onMounted(async () => {
-  try {
-    details.value = await getScheduleClienteDetails(props.schedule.id)
-  } catch {
-    $q.notify({ message: t('http.errors.failed'), color: 'negative' })
-  } finally {
-    loadingDetails.value = false
-  }
-})
+const parseLocalDate = (dateStr) => {
+  if (!dateStr) return null
 
 
-const SERVICE_FEE_RATE = 0.11
+  const s = String(dateStr)
 
 
-const serviceFee = computed(() => {
-  const base = parseFloat(props.schedule.total_amount) || 0
-  return parseFloat((base * SERVICE_FEE_RATE).toFixed(2))
-})
+  const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
 
 
-const total = computed(() => {
-  const base = parseFloat(props.schedule.total_amount) || 0
-  return parseFloat((base + serviceFee.value).toFixed(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
+
+  const d = parseLocalDate(props.schedule.date)
+
+  if (!d) return props.schedule.date ?? ''
+
+  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' })
 })
 })
 
 
 const providerAge = computed(() => {
 const providerAge = computed(() => {
   if (!details.value?.provider_birth_date) return null
   if (!details.value?.provider_birth_date) return null
+
   const birth = new Date(details.value.provider_birth_date)
   const birth = new Date(details.value.provider_birth_date)
+
   const today = new Date()
   const today = new Date()
+
   let age = today.getFullYear() - birth.getFullYear()
   let age = today.getFullYear() - birth.getFullYear()
+
   const m = today.getMonth() - birth.getMonth()
   const m = today.getMonth() - birth.getMonth()
+
   if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--
   if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) age--
+
   return age
   return age
 })
 })
 
 
-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 serviceFee = computed(() => {
+  const base = parseFloat(props.schedule.total_amount) || 0
 
 
-const fullDateLabel = computed(() => {
-  if (props.schedule.formatted_date) return props.schedule.formatted_date
-  const d = parseLocalDate(props.schedule.date)
-  if (!d) return props.schedule.date ?? ''
-  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' })
+  const feeRate = getSchedulePlatformFeeRate(props.schedule, 'pix', platformFees.value)
+
+  return parseFloat((base * (feeRate ?? 0)).toFixed(2))
+})
+
+const total = computed(() => {
+  const base = parseFloat(props.schedule.total_amount) || 0
+
+  return parseFloat((base + serviceFee.value).toFixed(2))
 })
 })
 
 
 const avatarColors = [
 const avatarColors = [
@@ -205,14 +256,26 @@ const avatarStyle = computed(() => avatarColors[props.schedule.id % avatarColors
 
 
 const openCancelDialog = () => {
 const openCancelDialog = () => {
   $q.dialog({
   $q.dialog({
-    component: ScheduleCancelDialog,
-    componentProps: { schedule: props.schedule }
+    component: ScheduleCancelDialog, componentProps: { schedule: props.schedule }
   })
   })
 }
 }
 
 
 const openHelp = () => {
 const openHelp = () => {
   $q.dialog({ component: ProfileHelpDialog })
   $q.dialog({ component: ProfileHelpDialog })
 }
 }
+
+onMounted(async () => {
+  try {
+    details.value = await getScheduleClienteDetails(props.schedule.id)
+
+    loadPlatformFees().catch(() => {})
+  } catch {
+    $q.notify({ message: t('http.errors.failed'), color: 'negative' })
+  } finally {
+    loadingDetails.value = false
+  }
+})
+
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 105 - 37
src/components/dashboard/ScheduleAcceptedDialog.vue

@@ -7,8 +7,13 @@
           {{ schedule.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}
           {{ schedule.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}
         </q-avatar>
         </q-avatar>
 
 
-        <div class="font16 fontbold provider-name">{{ schedule.provider_name }}</div>
-        <div class="font14 fontmedium text-text">{{ schedule.address?.district || '' }}</div>
+        <div class="font16 fontbold provider-name">
+          {{ schedule.provider_name }}
+        </div>
+
+        <div class="font14 fontmedium text-text">
+          {{ schedule.address?.district || '' }}
+        </div>
       </q-card-section>
       </q-card-section>
 
 
       <q-card-section class="text-center q-pt-xs q-pb-md">
       <q-card-section class="text-center q-pt-xs q-pb-md">
@@ -21,46 +26,82 @@
 
 
       <q-card-section class="q-py-md q-px-lg">
       <q-card-section class="q-py-md q-px-lg">
         <div class="detail-row">
         <div class="detail-row">
-          <span class="text-primary font14 fontmedium">{{ $t('dashboard_client.pending_schedules.detail_date') }}</span>
-          <span class="detail-value">{{ formattedDate }}</span>
+          <span class="text-primary font14 fontmedium">
+            {{ $t('dashboard_client.pending_schedules.detail_date') }}
+          </span>
+
+          <span class="detail-value">
+            {{ formattedDate }}
+          </span>
         </div>
         </div>
         <div class="detail-row">
         <div class="detail-row">
-          <span class="text-primary font14 fontmedium">{{ $t('dashboard_client.pending_schedules.detail_time') }}</span>
+          <span class="text-primary font14 fontmedium">
+            {{ $t('dashboard_client.pending_schedules.detail_time') }}
+          </span>
+
           <span class="detail-valued text-text">
           <span class="detail-valued text-text">
             {{ schedule.start_time?.slice(0, 5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ schedule.end_time?.slice(0, 5) }}
             {{ schedule.start_time?.slice(0, 5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ schedule.end_time?.slice(0, 5) }}
           </span>
           </span>
         </div>
         </div>
+
         <div class="detail-row">
         <div class="detail-row">
-          <span class="text-primary font14 fontmedium">{{ $t('dashboard_client.pending_schedules.detail_value') }}</span>
-          <span class="detail-value">{{ formatCurrency(schedule.total_amount) }}</span>
+          <span class="text-primary font14 fontmedium">
+            {{ $t('dashboard_client.pending_schedules.detail_value') }}
+          </span>
+
+          <span class="detail-value">
+            {{ formatCurrency(baseAmount) }}
+          </span>
         </div>
         </div>
+
+        <q-separator class="q-my-sm" />
+
         <div class="detail-row">
         <div class="detail-row">
-          <span class="text-primary font14 fontmedium">{{ $t('dashboard_client.pending_schedules.detail_service_fee') }}</span>
-          <span class="detail-value">{{ formatCurrency(serviceFee) }}</span>
+          <span class="text-primary font14 fontmedium">
+            {{ $t('dashboard_client.pending_schedules.detail_pix_total') }}
+          </span>
+
+          <span class="total-value font14 fontbold">
+            {{ formatCurrency(pixTotal) }}
+          </span>
         </div>
         </div>
 
 
-        <q-separator class="q-my-sm" />
+        <div v-if="pixDiscount > 0" class="detail-row discount-row">
+          <span class="font13 fontmedium">
+            {{ $t('dashboard_client.pending_schedules.detail_pix_discount') }}
+          </span>
+
+          <span class="font13 fontbold">
+            {{ formatCurrency(pixDiscount) }}
+          </span>
+        </div>
 
 
-        <div class="text-center">
-          <span class="text-primary font14 fontmedium">{{ $t('dashboard_client.pending_schedules.detail_total') }}</span>
-          <span class="total-value font14 fontbold q-ml-sm">{{ formatCurrency(total) }}</span>
+        <div class="detail-row">
+          <span class="text-primary font14 fontmedium">
+            {{ $t('dashboard_client.pending_schedules.detail_credit_card_total') }}
+          </span>
+
+          <span class="total-value font14 fontbold">
+            {{ formatCurrency(creditCardTotal) }}
+          </span>
         </div>
         </div>
       </q-card-section>
       </q-card-section>
 
 
       <q-card-section class="q-pt-none q-pb-lg q-px-lg column q-gutter-y-sm">
       <q-card-section class="q-pt-none q-pb-lg q-px-lg column q-gutter-y-sm">
         <q-btn
         <q-btn
-          rounded
-          color="primary"
           class="payment-btn full-width"
           class="payment-btn full-width"
+          color="primary"
           padding="4px 12px"
           padding="4px 12px"
+          rounded
           :label="$t('dashboard_client.pending_schedules.btn_payment')"
           :label="$t('dashboard_client.pending_schedules.btn_payment')"
           @click="onGoToPayment"
           @click="onGoToPayment"
         />
         />
+
         <q-btn
         <q-btn
+          class="full-width"
+          color="grey-6"
           flat
           flat
           no-caps
           no-caps
-          color="grey-6"
-          class="full-width"
           padding="4px 12px"
           padding="4px 12px"
           :label="$t('dashboard_client.pending_schedules.btn_cancel')"
           :label="$t('dashboard_client.pending_schedules.btn_cancel')"
           @click="onDialogHide"
           @click="onDialogHide"
@@ -72,18 +113,17 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { computed } from 'vue'
-import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { computed, onMounted } from 'vue'
 import { formatCurrency } from 'src/helpers/utils'
 import { formatCurrency } from 'src/helpers/utils'
+import { getSchedulePlatformFeeRate } from 'src/helpers/paymentPlatformFees'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
 import { usePaymentStore } from 'src/stores/payment'
 import { usePaymentStore } from 'src/stores/payment'
+import { usePaymentPlatformFees } from 'src/composables/usePaymentPlatformFees'
 import SchedulePaymentDialog from './SchedulePaymentDialog.vue'
 import SchedulePaymentDialog from './SchedulePaymentDialog.vue'
 import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
 import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
 
 
 const props = defineProps({
 const props = defineProps({
-  schedule: {
-    type: Object,
-    required: true
-  }
+  schedule: { type: Object, required: true }
 })
 })
 
 
 defineEmits([...useDialogPluginComponent.emits])
 defineEmits([...useDialogPluginComponent.emits])
@@ -91,12 +131,37 @@ defineEmits([...useDialogPluginComponent.emits])
 const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent()
 const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent()
 
 
 const $q = useQuasar()
 const $q = useQuasar()
+
 const paymentStore = usePaymentStore()
 const paymentStore = usePaymentStore()
 
 
-const SERVICE_FEE_RATE = 0.11
+const { platformFees, loadPlatformFees } = usePaymentPlatformFees()
+
+const baseAmount = computed(() => Number(props.schedule.total_amount) || 0)
+
+const creditCardTotal = computed(() => parseFloat((baseAmount.value + platformFee('credit_card')).toFixed(2)))
+
+const formattedDate = computed(() => {
+  if (props.schedule.formatted_date) return props.schedule.formatted_date
+
+  const raw = String(props.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 pixDiscount = computed(() => Math.max(0, parseFloat((creditCardTotal.value - pixTotal.value).toFixed(2))))
+const pixTotal    = computed(() => parseFloat((baseAmount.value + platformFee('pix')).toFixed(2)))
 
 
-const serviceFee = computed(() => Number(props.schedule.total_amount) * SERVICE_FEE_RATE)
-const total = computed(() => Number(props.schedule.total_amount) + serviceFee.value)
+const platformFee = (paymentType) => {
+  const feeRate = getSchedulePlatformFeeRate(props.schedule, paymentType, platformFees.value)
+
+  return parseFloat((baseAmount.value * (feeRate ?? 0)).toFixed(2))
+}
 
 
 const onGoToPayment = () => {
 const onGoToPayment = () => {
   const hasValidPixPayment = !!paymentStore.getValidPixPayment(props.schedule.id)
   const hasValidPixPayment = !!paymentStore.getValidPixPayment(props.schedule.id)
@@ -104,30 +169,25 @@ const onGoToPayment = () => {
   $q.dialog({
   $q.dialog({
     component: hasValidPixPayment ? SchedulePaymentPixDialog : SchedulePaymentDialog,
     component: hasValidPixPayment ? SchedulePaymentPixDialog : SchedulePaymentDialog,
     componentProps: {
     componentProps: {
-      schedule: props.schedule,
-      total: total.value,
+      schedule: props.schedule, ...(hasValidPixPayment ? { total: pixTotal.value } : {}),
     },
     },
   }).onOk(() => {
   }).onOk(() => {
     onDialogOK()
     onDialogOK()
   })
   })
 }
 }
 
 
-const formattedDate = computed(() => {
-  if (props.schedule.formatted_date) return props.schedule.formatted_date
-  const raw = String(props.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 avatarColors = [
 const avatarColors = [
   { background: '#ffd5df', color: '#932e57' },
   { background: '#ffd5df', color: '#932e57' },
   { background: '#d7e8ff', color: '#2158a8' },
   { background: '#d7e8ff', color: '#2158a8' },
   { background: '#dfd',    color: '#2a7a3b' },
   { background: '#dfd',    color: '#2a7a3b' },
   { background: '#ffe5cc', color: '#8a4500' },
   { background: '#ffe5cc', color: '#8a4500' },
 ]
 ]
+
 const avatarStyle = computed(() => avatarColors[props.schedule.id % avatarColors.length])
 const avatarStyle = computed(() => avatarColors[props.schedule.id % avatarColors.length])
+
+onMounted(() => {
+  loadPlatformFees().catch(() => {})
+})
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">
@@ -166,6 +226,14 @@ const avatarStyle = computed(() => avatarColors[props.schedule.id % avatarColors
   color: #3a3a4a;
   color: #3a3a4a;
 }
 }
 
 
+.discount-row {
+  color: #16845d;
+  background: rgba(22, 132, 93, 0.08);
+  border-radius: 8px;
+  margin: 2px 0;
+  padding: 5px 8px;
+}
+
 .payment-btn {
 .payment-btn {
   height: 48px;
   height: 48px;
 }
 }

+ 54 - 6
src/components/dashboard/SchedulePaymentDialog.vue

@@ -73,6 +73,22 @@
 
 
         <q-separator class="q-my-lg" />
         <q-separator class="q-my-lg" />
 
 
+        <div class="payment-summary q-mb-lg">
+          <div class="row items-center justify-between q-mb-xs">
+            <span class="summary-label">{{ $t('dashboard_client.pending_schedules.detail_value') }}</span>
+            <span class="summary-value">{{ formatCurrency(baseAmount) }}</span>
+          </div>
+          <div class="row items-center justify-between q-mb-xs">
+            <span class="summary-label">{{ $t('dashboard_client.pending_schedules.detail_service_fee') }}</span>
+            <span class="summary-value">{{ formatCurrency(selectedPlatformFee) }}</span>
+          </div>
+          <q-separator class="q-my-sm" />
+          <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>
+
         <div class="row items-center q-mb-lg">
         <div class="row items-center q-mb-lg">
           <q-checkbox v-model="agreedToTerms" color="primary" keep-color />
           <q-checkbox v-model="agreedToTerms" color="primary" keep-color />
           <span class="terms-text">
           <span class="terms-text">
@@ -105,6 +121,9 @@ import { useI18n } from 'vue-i18n'
 import { userStore } from 'src/stores/user'
 import { userStore } from 'src/stores/user'
 import { usePaymentStore } from 'src/stores/payment'
 import { usePaymentStore } from 'src/stores/payment'
 import { getClientPaymentMethods } from 'src/api/clientPaymentMethod'
 import { getClientPaymentMethods } from 'src/api/clientPaymentMethod'
+import { formatCurrency } from 'src/helpers/utils'
+import { getSchedulePlatformFeeRate } from 'src/helpers/paymentPlatformFees'
+import { usePaymentPlatformFees } from 'src/composables/usePaymentPlatformFees'
 import ProfilePaymentAddDialog from 'src/components/profile/ProfilePaymentAddDialog.vue'
 import ProfilePaymentAddDialog from 'src/components/profile/ProfilePaymentAddDialog.vue'
 import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
 import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
 import SchedulePaymentProcessingDialog from './SchedulePaymentProcessingDialog.vue'
 import SchedulePaymentProcessingDialog from './SchedulePaymentProcessingDialog.vue'
@@ -114,10 +133,6 @@ const props = defineProps({
     type: Object,
     type: Object,
     required: true,
     required: true,
   },
   },
-  total: {
-    type: Number,
-    required: true,
-  },
 })
 })
 
 
 defineEmits([...useDialogPluginComponent.emits])
 defineEmits([...useDialogPluginComponent.emits])
@@ -127,12 +142,19 @@ const $q = useQuasar()
 const { t } = useI18n()
 const { t } = useI18n()
 const store = userStore()
 const store = userStore()
 const paymentStore = usePaymentStore()
 const paymentStore = usePaymentStore()
+const { platformFees, loadPlatformFees } = usePaymentPlatformFees()
 
 
 const selectedMethod = ref(null)
 const selectedMethod = ref(null)
 const agreedToTerms = ref(false)
 const agreedToTerms = ref(false)
 const paymentMethods = ref([])
 const paymentMethods = ref([])
 const loadingCards = ref(false)
 const loadingCards = ref(false)
 
 
+const baseAmount = computed(() => Number(props.schedule.total_amount) || 0)
+const selectedPaymentType = computed(() => selectedMethod.value === 'pix' ? 'pix' : 'credit_card')
+const selectedPlatformFeeRate = computed(() => getSchedulePlatformFeeRate(props.schedule, selectedPaymentType.value, platformFees.value) ?? platformFees.value.pix)
+const selectedPlatformFee = computed(() => parseFloat((baseAmount.value * selectedPlatformFeeRate.value).toFixed(2)))
+const selectedTotal = computed(() => parseFloat((baseAmount.value + selectedPlatformFee.value).toFixed(2)))
+
 const addressTypeLabel = computed(() => {
 const addressTypeLabel = computed(() => {
   const type = props.schedule.address?.address_type
   const type = props.schedule.address?.address_type
   if (!type) return ''
   if (!type) return ''
@@ -158,6 +180,12 @@ const loadCards = async () => {
   loadingCards.value = true
   loadingCards.value = true
   try {
   try {
     paymentMethods.value = await getClientPaymentMethods(store.user?.client_id)
     paymentMethods.value = await getClientPaymentMethods(store.user?.client_id)
+    const selectedCardId = String(selectedMethod.value || '').replace('card_', '')
+    const hasSelectedCard = paymentMethods.value.some((card) => String(card.id) === selectedCardId)
+
+    if (selectedMethod.value !== 'pix' && !hasSelectedCard) {
+      selectedMethod.value = paymentMethods.value.length > 0 ? `card_${paymentMethods.value[0].id}` : 'pix'
+    }
   } catch (e) {
   } catch (e) {
     console.error(e)
     console.error(e)
   } finally {
   } finally {
@@ -177,7 +205,7 @@ const openAddCard = () => {
 const openPixPayment = () => {
 const openPixPayment = () => {
   $q.dialog({
   $q.dialog({
     component: SchedulePaymentPixDialog,
     component: SchedulePaymentPixDialog,
-    componentProps: { schedule: props.schedule, total: props.total },
+    componentProps: { schedule: props.schedule, total: selectedTotal.value },
   }).onOk(() => {
   }).onOk(() => {
     onDialogOK()
     onDialogOK()
   })
   })
@@ -192,13 +220,15 @@ const onConfirm = () => {
 
 
   $q.dialog({
   $q.dialog({
     component: SchedulePaymentProcessingDialog,
     component: SchedulePaymentProcessingDialog,
-    componentProps: { schedule: props.schedule, clientPaymentMethodId },
+    componentProps: { schedule: props.schedule, clientPaymentMethodId, total: selectedTotal.value },
   }).onOk(() => {
   }).onOk(() => {
     onDialogOK()
     onDialogOK()
   })
   })
 }
 }
 
 
 onMounted(() => {
 onMounted(() => {
+  loadPlatformFees().catch(() => {})
+
   if (paymentStore.getValidPixPayment(props.schedule.id)) {
   if (paymentStore.getValidPixPayment(props.schedule.id)) {
     openPixPayment()
     openPixPayment()
     return
     return
@@ -253,6 +283,24 @@ onMounted(() => {
   margin-top: 2px;
   margin-top: 2px;
 }
 }
 
 
+.payment-summary {
+  border: 1px solid #e0e0e0;
+  border-radius: 10px;
+  background: #fff;
+  padding: 12px 16px;
+}
+
+.summary-label,
+.summary-value {
+  color: #5a5a6a;
+}
+
+.summary-total-label,
+.summary-total-value {
+  color: #3a3a4a;
+  font-weight: 700;
+}
+
 .saved-card-box {
 .saved-card-box {
   border: 1.5px solid #e0e0e0;
   border: 1.5px solid #e0e0e0;
   border-radius: 12px;
   border-radius: 12px;

+ 63 - 7
src/components/dashboard/SchedulePaymentPixDialog.vue

@@ -110,7 +110,7 @@
 import { computed, ref, onMounted, onUnmounted } from 'vue'
 import { computed, ref, onMounted, onUnmounted } from 'vue'
 import { useDialogPluginComponent, useQuasar, copyToClipboard } from 'quasar'
 import { useDialogPluginComponent, useQuasar, copyToClipboard } from 'quasar'
 import { formatCurrency } from 'src/helpers/utils'
 import { formatCurrency } from 'src/helpers/utils'
-import { paySchedule } from 'src/api/payment'
+import { getSchedulePixPayment, paySchedule } from 'src/api/payment'
 import { usePaymentStore } from 'src/stores/payment'
 import { usePaymentStore } from 'src/stores/payment'
 
 
 const props = defineProps({
 const props = defineProps({
@@ -151,7 +151,60 @@ const copyCode = async () => {
 
 
 const totalSeconds = ref(20 * 60)
 const totalSeconds = ref(20 * 60)
 const countdown = ref('')
 const countdown = ref('')
-let timer = null
+let countdownTimer = null
+let pollingTimer = null
+let pollingInFlight = false
+
+const stopPolling = () => {
+  clearInterval(pollingTimer)
+  pollingTimer = null
+}
+
+const applyPaymentStatus = (nextPayment) => {
+  if (!nextPayment) return
+
+  payment.value = nextPayment
+
+  if (nextPayment.status === 'paid') {
+    success.value = true
+    processing.value = false
+    paymentStore.clearPixPayment(props.schedule.id)
+    stopPolling()
+    return
+  }
+
+  if (['failed', 'cancelled'].includes(nextPayment.status)) {
+    processing.value = false
+    paymentStore.clearPixPayment(props.schedule.id)
+    stopPolling()
+    $q.notify({ type: 'negative', message: nextPayment.failure_message || 'Pagamento Pix não confirmado.' })
+    onDialogCancel()
+    return
+  }
+
+  paymentStore.setPixPayment(props.schedule.id, nextPayment)
+}
+
+const checkPaymentStatus = async () => {
+  if (pollingInFlight || success.value || totalSeconds.value <= 0) return
+
+  pollingInFlight = true
+  try {
+    applyPaymentStatus(await getSchedulePixPayment(props.schedule.id))
+    updateCountdown()
+  } catch (e) {
+    console.error('Erro ao verificar pagamento Pix:', e)
+  } finally {
+    pollingInFlight = false
+  }
+}
+
+const startPolling = () => {
+  stopPolling()
+  if (success.value || ['failed', 'cancelled'].includes(payment.value?.status) || totalSeconds.value <= 0) return
+  checkPaymentStatus()
+  pollingTimer = setInterval(checkPaymentStatus, 5000)
+}
 
 
 const updateCountdown = () => {
 const updateCountdown = () => {
   if (pixExpiresAt.value) {
   if (pixExpiresAt.value) {
@@ -165,25 +218,27 @@ const updateCountdown = () => {
 
 
   if (pixExpiresAt.value && totalSeconds.value <= 0) {
   if (pixExpiresAt.value && totalSeconds.value <= 0) {
     paymentStore.clearPixPayment(props.schedule.id)
     paymentStore.clearPixPayment(props.schedule.id)
+    stopPolling()
   }
   }
 }
 }
 
 
 onMounted(async () => {
 onMounted(async () => {
   updateCountdown()
   updateCountdown()
-  timer = setInterval(updateCountdown, 1000)
+  countdownTimer = setInterval(updateCountdown, 1000)
 
 
   try {
   try {
     const cachedPayment = paymentStore.getValidPixPayment(props.schedule.id)
     const cachedPayment = paymentStore.getValidPixPayment(props.schedule.id)
 
 
     if (cachedPayment) {
     if (cachedPayment) {
-      payment.value = cachedPayment
+      applyPaymentStatus(cachedPayment)
       updateCountdown()
       updateCountdown()
+      startPolling()
       return
       return
     }
     }
 
 
-    payment.value = await paySchedule(props.schedule.id, { payment_method: 'pix' })
-    paymentStore.setPixPayment(props.schedule.id, payment.value)
+    applyPaymentStatus(await paySchedule(props.schedule.id, { payment_method: 'pix' }))
     updateCountdown()
     updateCountdown()
+    startPolling()
   } catch (e) {
   } catch (e) {
     console.error('Erro ao criar pagamento Pix:', e)
     console.error('Erro ao criar pagamento Pix:', e)
     $q.notify({ type: 'negative', message: e?.response?.data?.message ?? 'Erro ao criar pagamento Pix.' })
     $q.notify({ type: 'negative', message: e?.response?.data?.message ?? 'Erro ao criar pagamento Pix.' })
@@ -194,7 +249,8 @@ onMounted(async () => {
 })
 })
 
 
 onUnmounted(() => {
 onUnmounted(() => {
-  clearInterval(timer)
+  clearInterval(countdownTimer)
+  stopPolling()
 })
 })
 </script>
 </script>
 
 

+ 1 - 0
src/components/dashboard/SchedulePaymentProcessingDialog.vue

@@ -52,6 +52,7 @@ import { paySchedule } from 'src/api/payment'
 const props = defineProps({
 const props = defineProps({
   schedule: { type: Object, required: true },
   schedule: { type: Object, required: true },
   clientPaymentMethodId: { type: Number, required: true },
   clientPaymentMethodId: { type: Number, required: true },
+  total: { type: Number, default: null },
 })
 })
 
 
 defineEmits([...useDialogPluginComponent.emits])
 defineEmits([...useDialogPluginComponent.emits])

+ 29 - 0
src/components/layout/ScheduleCartMenu.vue

@@ -0,0 +1,29 @@
+<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+<template>
+  <q-btn
+    class="schedule-cart-button"
+    color="primary"
+    icon="mdi-cart-outline"
+    round
+    unelevated
+    :to="{ name: 'ScheduleCartPage' }"
+  >
+    <q-badge v-if="cart.items.length" color="secondary" floating rounded>
+      {{ cart.items.length }}
+    </q-badge>
+
+    <q-tooltip>{{ $t('schedule_cart.title') }}</q-tooltip>
+  </q-btn>
+</template>
+
+<script setup>
+import { useScheduleCartStore } from "src/stores/scheduleCart";
+
+const cart = useScheduleCartStore();
+</script>
+
+<style scoped lang="scss">
+.schedule-cart-button {
+  box-shadow: 0 8px 24px rgba(166, 61, 247, 0.28);
+}
+</style>

+ 135 - 100
src/components/login/LoginStepThreePanel.vue

@@ -2,119 +2,127 @@
   <q-card-section class="no-padding">
   <q-card-section class="no-padding">
     <div>
     <div>
       <div class="text-text">
       <div class="text-text">
-        <span class="font14 fontbold">{{ $t('auth.full_name') }}</span>
+        <span class="font14 fontbold">{{ $t("auth.full_name") }}</span>
       </div>
       </div>
+
       <q-input
       <q-input
         v-model="form.name"
         v-model="form.name"
-        no-error-icon
-        outlined
-        rounded
         bg-color="surface"
         bg-color="surface"
         class="q-mt-sm q-mb-md"
         class="q-mt-sm q-mb-md"
+        hide-bottom-space
         input-class="text-text"
         input-class="text-text"
+        lazy-rules
+        no-error-icon
+        outlined
+        rounded
         :placeholder="$t('auth.full_name')"
         :placeholder="$t('auth.full_name')"
-        hide-bottom-space
         :rules="[inputRules.required]"
         :rules="[inputRules.required]"
-        lazy-rules
       />
       />
     </div>
     </div>
 
 
     <div>
     <div>
       <div class="text-text">
       <div class="text-text">
-        <span class="font14 fontbold">{{ $t('common.terms.cpf') }}</span>
+        <span class="font14 fontbold">{{ $t("common.terms.document") }}</span>
       </div>
       </div>
+
       <q-input
       <q-input
         v-model="form.document"
         v-model="form.document"
-        no-error-icon
-        outlined
-        rounded
         bg-color="surface"
         bg-color="surface"
         class="q-mt-sm q-mb-md"
         class="q-mt-sm q-mb-md"
-        input-class="text-text"
-        placeholder="000.000.000-00"
         hide-bottom-space
         hide-bottom-space
-        :rules="[inputRules.required, inputRules.cpf]"
+        input-class="text-text"
         lazy-rules
         lazy-rules
-        mask="###.###.###-##"
+        no-error-icon
+        outlined
+        rounded
+        :placeholder="$t('common.terms.document')"
+        :rules="[inputRules.cpfOrCnpj]"
       />
       />
     </div>
     </div>
 
 
     <div>
     <div>
       <div class="text-text">
       <div class="text-text">
-        <span class="font14 fontbold">{{ $t('common.terms.cep') }}</span>
+        <span class="font14 fontbold">{{ $t("common.terms.cep") }}</span>
       </div>
       </div>
+
       <q-input
       <q-input
         v-model="form.zip_code"
         v-model="form.zip_code"
-        no-error-icon
-        outlined
-        rounded
         bg-color="surface"
         bg-color="surface"
         class="q-mt-sm q-mb-md"
         class="q-mt-sm q-mb-md"
-        input-class="text-text"
-        placeholder="00000-000"
         hide-bottom-space
         hide-bottom-space
-        :rules="[inputRules.required, inputRules.cep]"
+        input-class="text-text"
         lazy-rules
         lazy-rules
         mask="#####-###"
         mask="#####-###"
+        no-error-icon
+        outlined
+        placeholder="00000-000"
+        rounded
         :loading="loadingCep"
         :loading="loadingCep"
+        :rules="[inputRules.required, inputRules.cep]"
         @update:model-value="onCepChange"
         @update:model-value="onCepChange"
       />
       />
     </div>
     </div>
 
 
     <div>
     <div>
       <div class="text-text">
       <div class="text-text">
-        <span class="font14 fontbold">{{ $t('common.terms.address') }}</span>
+        <span class="font14 fontbold">{{ $t("common.terms.address") }}</span>
       </div>
       </div>
+
       <q-input
       <q-input
         v-model="form.address"
         v-model="form.address"
-        no-error-icon
-        outlined
-        rounded
         bg-color="surface"
         bg-color="surface"
         class="q-mt-sm q-mb-md"
         class="q-mt-sm q-mb-md"
-        input-class="text-text"
-        :placeholder="`${$t('common.terms.address')}...`"
         hide-bottom-space
         hide-bottom-space
-        :rules="[inputRules.required]"
+        input-class="text-text"
         lazy-rules
         lazy-rules
+        no-error-icon
+        outlined
         readonly
         readonly
+        rounded
+        :placeholder="`${$t('common.terms.address')}...`"
+        :rules="[inputRules.required]"
       />
       />
     </div>
     </div>
 
 
     <div class="row q-col-gutter-sm">
     <div class="row q-col-gutter-sm">
       <div class="col-4">
       <div class="col-4">
         <div class="text-text">
         <div class="text-text">
-          <span class="font14 fontbold">{{ $t('common.terms.address_number') }}</span>
+          <span class="font14 fontbold">{{
+            $t("common.terms.address_number")
+          }}</span>
         </div>
         </div>
+
         <q-input
         <q-input
           v-model="form.number"
           v-model="form.number"
-          no-error-icon
-          outlined
-          rounded
           bg-color="surface"
           bg-color="surface"
-        class="q-mt-sm q-mb-md"
+          class="q-mt-sm q-mb-md"
+          hide-bottom-space
           input-class="text-text"
           input-class="text-text"
+          lazy-rules
+          no-error-icon
+          outlined
           placeholder="0000"
           placeholder="0000"
-          hide-bottom-space
+          rounded
           :rules="[inputRules.required]"
           :rules="[inputRules.required]"
-          lazy-rules
         />
         />
       </div>
       </div>
+
       <div class="col-8">
       <div class="col-8">
         <div class="text-text">
         <div class="text-text">
-          <span class="font14 fontbold">{{ $t('common.terms.district') }}</span>
+          <span class="font14 fontbold">{{ $t("common.terms.district") }}</span>
         </div>
         </div>
+
         <q-input
         <q-input
           v-model="form.district"
           v-model="form.district"
+          bg-color="surface"
+          class="q-mt-sm q-mb-md"
+          hide-bottom-space
+          input-class="text-text"
           no-error-icon
           no-error-icon
           outlined
           outlined
+          readonly
           rounded
           rounded
-          bg-color="surface"
-        class="q-mt-sm q-mb-md"
-          input-class="text-text"
           :placeholder="`${$t('common.terms.district')}...`"
           :placeholder="`${$t('common.terms.district')}...`"
-          hide-bottom-space
-          readonly
         />
         />
       </div>
       </div>
     </div>
     </div>
@@ -122,33 +130,36 @@
     <div class="row q-col-gutter-sm">
     <div class="row q-col-gutter-sm">
       <div class="col-8">
       <div class="col-8">
         <div class="text-text">
         <div class="text-text">
-          <span class="font14 fontbold">{{ $t('common.terms.city') }}</span>
+          <span class="font14 fontbold">{{ $t("common.terms.city") }}</span>
         </div>
         </div>
+
         <q-input
         <q-input
           v-model="form.city"
           v-model="form.city"
-          no-error-icon
-          outlined
-          rounded
           bg-color="surface"
           bg-color="surface"
-        class="q-mt-sm q-mb-md"
+          class="q-mt-sm q-mb-md"
           input-class="text-text"
           input-class="text-text"
+          no-error-icon
+          outlined
           readonly
           readonly
+          rounded
         />
         />
       </div>
       </div>
+
       <div class="col-4">
       <div class="col-4">
         <div class="text-text">
         <div class="text-text">
-          <span class="font14 fontbold">{{ $t('common.terms.state') }}</span>
+          <span class="font14 fontbold">{{ $t("common.terms.state") }}</span>
         </div>
         </div>
+
         <q-input
         <q-input
           v-model="form.state"
           v-model="form.state"
-          no-error-icon
-          outlined
-          rounded
           bg-color="surface"
           bg-color="surface"
-        class="q-mt-sm q-mb-md"
-          input-class="text-text"
+          class="q-mt-sm q-mb-md"
           hide-bottom-space
           hide-bottom-space
+          input-class="text-text"
+          no-error-icon
+          outlined
           readonly
           readonly
+          rounded
         />
         />
       </div>
       </div>
     </div>
     </div>
@@ -156,68 +167,76 @@
     <div>
     <div>
       <q-checkbox
       <q-checkbox
         v-model="form.no_complement"
         v-model="form.no_complement"
-        :label="$t('auth.no_complement')"
+        class="q-mb-md text-text font14 fontbold"
         color="primary"
         color="primary"
         keep-color
         keep-color
-        class="q-mb-md text-text font14 fontbold"
+        :label="$t('auth.no_complement')"
       />
       />
     </div>
     </div>
+
     <div>
     <div>
       <template v-if="!form.no_complement">
       <template v-if="!form.no_complement">
         <div class="text-text">
         <div class="text-text">
-          <span class="font14 fontbold">{{ $t('common.terms.complement') }}</span>
+          <span class="font14 fontbold">{{
+            $t("common.terms.complement")
+          }}</span>
         </div>
         </div>
+
         <q-input
         <q-input
           v-model="form.complement"
           v-model="form.complement"
+          bg-color="surface"
+          class="q-mt-sm q-mb-md"
+          hide-bottom-space
+          input-class="text-text"
+          lazy-rules
           no-error-icon
           no-error-icon
           outlined
           outlined
           rounded
           rounded
-          bg-color="surface"
-        class="q-mt-sm q-mb-md"
-          input-class="text-text"
           :placeholder="`${$t('common.ui.misc.example')}: Apartamento, Conjunto, Casa`"
           :placeholder="`${$t('common.ui.misc.example')}: Apartamento, Conjunto, Casa`"
-          hide-bottom-space
           :rules="!form.no_complement ? [inputRules.required] : []"
           :rules="!form.no_complement ? [inputRules.required] : []"
-          lazy-rules
         />
         />
       </template>
       </template>
     </div>
     </div>
 
 
     <div>
     <div>
       <div class="text-text">
       <div class="text-text">
-        <span class="font14 fontbold">{{ $t('auth.address_nickname') }}</span>
+        <span class="font14 fontbold">{{ $t("auth.address_nickname") }}</span>
       </div>
       </div>
+
       <q-input
       <q-input
         v-model="form.nickname"
         v-model="form.nickname"
-        no-error-icon
-        outlined
-        rounded
         bg-color="surface"
         bg-color="surface"
         class="q-mt-sm q-mb-md"
         class="q-mt-sm q-mb-md"
-        input-class="text-text"
-        :placeholder="`${$t('common.ui.misc.example')}: Casa`"
         hide-bottom-space
         hide-bottom-space
+        input-class="text-text"
         lazy-rules
         lazy-rules
+        no-error-icon
+        outlined
+        rounded
+        :placeholder="`${$t('common.ui.misc.example')}: Casa`"
       />
       />
     </div>
     </div>
 
 
     <div>
     <div>
       <div class="text-text">
       <div class="text-text">
-        <span class="font14 fontbold">{{ $t('auth.address_instructions') }}</span>
+        <span class="font14 fontbold">{{
+          $t("auth.address_instructions")
+        }}</span>
       </div>
       </div>
+
       <q-input
       <q-input
         v-model="form.instructions"
         v-model="form.instructions"
-        no-error-icon
-        outlined
-        rounded
+        autogrow
         bg-color="surface"
         bg-color="surface"
         class="q-mt-sm q-mb-md"
         class="q-mt-sm q-mb-md"
-        input-class="text-text"
-        type="textarea"
-        rows="3"
-        autogrow
         hide-bottom-space
         hide-bottom-space
+        input-class="text-text"
         lazy-rules
         lazy-rules
+        no-error-icon
+        outlined
+        rounded
+        rows="3"
+        type="textarea"
       />
       />
     </div>
     </div>
 
 
@@ -229,10 +248,10 @@
           :selected="form.address_type === type.value"
           :selected="form.address_type === type.value"
           clickable
           clickable
           color="primary"
           color="primary"
-          :outline="form.address_type !== type.value"
           text-color="surface"
           text-color="surface"
           :icon="type.icon"
           :icon="type.icon"
           :icon-selected="type.icon"
           :icon-selected="type.icon"
+          :outline="form.address_type !== type.value"
           @click="form.address_type = type.value"
           @click="form.address_type = type.value"
         >
         >
           {{ $t(type.label) }}
           {{ $t(type.label) }}
@@ -243,58 +262,74 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref } from 'vue';
-import { useInputRules } from 'src/composables/useInputRules';
-import axios from 'axios';
+import { ref } from "vue";
+import { useInputRules } from "src/composables/useInputRules";
+
+import axios from "axios";
 
 
 const form = defineModel({ type: Object, required: true });
 const form = defineModel({ type: Object, required: true });
 
 
 const { inputRules } = useInputRules();
 const { inputRules } = useInputRules();
+
 const loadingCep = ref(false);
 const loadingCep = ref(false);
 
 
 const addressTypes = [
 const addressTypes = [
-  { value: 'home', label: 'auth.address_type_home', icon: 'mdi-home-outline' },
-  { value: 'commercial', label: 'auth.address_type_commercial', icon: 'mdi-briefcase-variant-outline' },
-  { value: 'other', label: 'auth.address_type_other', icon: 'mdi-map-marker-outline' },
+  { value: "home", label: "auth.address_type_home", icon: "mdi-home-outline" },
+  {
+    value: "commercial",
+    label: "auth.address_type_commercial",
+    icon:  "mdi-briefcase-variant-outline",
+  },
+  {
+    value: "other",
+    label: "auth.address_type_other",
+    icon:  "mdi-map-marker-outline",
+  },
 ];
 ];
 
 
 const fetchCep = async (rawCep) => {
 const fetchCep = async (rawCep) => {
-  const cleaned = rawCep.replace(/\D/g, '');
+  const cleaned = rawCep.replace(/\D/g, "");
+
   if (cleaned.length !== 8) return;
   if (cleaned.length !== 8) return;
 
 
   loadingCep.value = true;
   loadingCep.value = true;
+
   try {
   try {
-    const { data } = await axios.get(`https://viacep.com.br/ws/${cleaned}/json/`);
+    const { data } = await axios.get(
+      `https://viacep.com.br/ws/${cleaned}/json/`,
+    );
+
     if (!data.erro) {
     if (!data.erro) {
-      form.value.address = data.logradouro ?? '';
-      form.value.district = data.bairro ?? '';
-      form.value.city = data.localidade ?? '';
-      form.value.state = data.uf ?? '';
+      form.value.address  = data.logradouro ?? "";
+      form.value.district = data.bairro     ?? "";
+      form.value.city     = data.localidade ?? "";
+      form.value.state    = data.uf         ?? "";
     } else {
     } else {
-      form.value.address = '';
-      form.value.district = '';
-      form.value.city = '';
-      form.value.state = '';
+      form.value.address  = "";
+      form.value.district = "";
+      form.value.city     = "";
+      form.value.state    = "";
     }
     }
   } catch {
   } catch {
-    form.value.address = '';
-    form.value.district = '';
-    form.value.city = '';
-    form.value.state = '';
+    form.value.address  = "";
+    form.value.district = "";
+    form.value.city     = "";
+    form.value.state    = "";
   } finally {
   } finally {
     loadingCep.value = false;
     loadingCep.value = false;
   }
   }
 };
 };
 
 
 const onCepChange = (val) => {
 const onCepChange = (val) => {
-  const cleaned = val?.replace(/\D/g, '') ?? '';
+  const cleaned = val ? val.replace(/\D/g, "") : "";
+
   if (cleaned.length === 8) {
   if (cleaned.length === 8) {
     fetchCep(val);
     fetchCep(val);
   } else {
   } else {
-    form.value.address = '';
-    form.value.district = '';
-    form.value.city = '';
-    form.value.state = '';
+    form.value.address  = "";
+    form.value.district = "";
+    form.value.city     = "";
+    form.value.state    = "";
   }
   }
 };
 };
 </script>
 </script>

+ 72 - 14
src/composables/useInputRules.js

@@ -5,54 +5,86 @@ export const useInputRules = () => {
 
 
   const emailPattern =
   const emailPattern =
     /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
     /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-  const passwordPattern = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
-  const cepPattern = /^[0-9]{5}-[0-9]{3}$/;
+
+ const passwordPattern = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
+
+ const cepPattern = /^[0-9]{5}-[0-9]{3}$/;
 
 
   const inputRules = {
   const inputRules = {
-    required: (value) => !!value || t("validation.rules.required"),
-    requiredNumber: (value) => !isNaN(value) || t("validation.rules.required"),
+    required:            (value) => !!value || t("validation.rules.required"),
+    requiredNumber:      (value) => !isNaN(value) || t("validation.rules.required"),
     requiredHideMessage: (value) => !!value,
     requiredHideMessage: (value) => !!value,
+
     min: (min) => (value) =>
     min: (min) => (value) =>
       value.length >= min ||
       value.length >= min ||
       `${t("validation.rules.min")} ${min} ${t("validation.rules.characters")}`,
       `${t("validation.rules.min")} ${min} ${t("validation.rules.characters")}`,
+
     max: (max) => (value) =>
     max: (max) => (value) =>
       value.length <= max ||
       value.length <= max ||
       `${t("validation.rules.max")} ${max} ${t("validation.rules.characters")}`,
       `${t("validation.rules.max")} ${max} ${t("validation.rules.characters")}`,
+
     minValue: (min) => (value) =>
     minValue: (min) => (value) =>
       value >= min || `${t("validation.rules.min")} ${min}`,
       value >= min || `${t("validation.rules.min")} ${min}`,
+
     maxValue: (max) => (value) =>
     maxValue: (max) => (value) =>
       value <= max || `${t("validation.rules.max")} ${max}`,
       value <= max || `${t("validation.rules.max")} ${max}`,
+
     email: (value) =>
     email: (value) =>
       !value || emailPattern.test(value) || t("validation.rules.email"),
       !value || emailPattern.test(value) || t("validation.rules.email"),
+
     emails: (value) => {
     emails: (value) => {
       if (!value) return true;
       if (!value) return true;
+
       const emails = value.split(";").map((email) => email.trim());
       const emails = value.split(";").map((email) => email.trim());
+
       return (
       return (
         emails.every((email) => inputRules.email(email) === true) ||
         emails.every((email) => inputRules.email(email) === true) ||
         t("validation.rules.email")
         t("validation.rules.email")
       );
       );
     },
     },
-    cpf: (value) => !value || isValidCPF(value) || t("validation.rules.cpf"),
+
+    cpf:  (value) => !value || isValidCPF(value) || t("validation.rules.cpf"),
     cnpj: (value) => !value || isValidCNPJ(value) || t("validation.rules.cnpj"),
     cnpj: (value) => !value || isValidCNPJ(value) || t("validation.rules.cnpj"),
+
+    cpfOrCnpj: (value) => {
+      const digits = String(value ?? "").replace(/\D/g, "");
+
+      if (!digits) return t("validation.rules.required");
+
+      if (digits.length <= 11) {
+        return isValidCPF(value) || t("validation.rules.cpf_or_cnpj");
+      }
+
+      return isValidCNPJ(value) || t("validation.rules.cpf_or_cnpj");
+    },
+
     samePassword: (otherValue) => (value) =>
     samePassword: (otherValue) => (value) =>
       value === otherValue || t("validation.rules.same_password"),
       value === otherValue || t("validation.rules.same_password"),
+
     password: (value) =>
     password: (value) =>
       !value || passwordPattern.test(value) || t("validation.rules.password"),
       !value || passwordPattern.test(value) || t("validation.rules.password"),
+
     cep: (value) => {
     cep: (value) => {
       if (!value) return true;
       if (!value) return true;
+
       return cepPattern.test(value) || t("validation.rules.cep");
       return cepPattern.test(value) || t("validation.rules.cep");
     },
     },
+
     notSameDocument: (allDocuments) => (value) => {
     notSameDocument: (allDocuments) => (value) => {
       if (!value) return true;
       if (!value) return true;
+
       let found = 0;
       let found = 0;
+
       for (const doc of allDocuments) {
       for (const doc of allDocuments) {
         if (doc == value) {
         if (doc == value) {
           found++;
           found++;
         }
         }
+
         if (found > 1) {
         if (found > 1) {
           return t("validation.rules.not_same_document");
           return t("validation.rules.not_same_document");
         }
         }
       }
       }
+
       return true;
       return true;
     },
     },
   };
   };
@@ -64,47 +96,73 @@ export const useInputRules = () => {
 
 
 function isValidCPF(cpf) {
 function isValidCPF(cpf) {
   if (!cpf) return false;
   if (!cpf) return false;
+
   cpf = cpf.replace(/[^\d]+/g, "");
   cpf = cpf.replace(/[^\d]+/g, "");
-  if (cpf.length !== 11) return false;
+
+  if (cpf.length !== 11)     return false;
   if (/^(\d)\1+$/.test(cpf)) return false;
   if (/^(\d)\1+$/.test(cpf)) return false;
+
   let sum = 0;
   let sum = 0;
+
   for (let i = 0; i < 9; i++) sum += parseInt(cpf.charAt(i)) * (10 - i);
   for (let i = 0; i < 9; i++) sum += parseInt(cpf.charAt(i)) * (10 - i);
+
   let rev = 11 - (sum % 11);
   let rev = 11 - (sum % 11);
+
   if (rev === 10 || rev === 11) rev = 0;
   if (rev === 10 || rev === 11) rev = 0;
+
   if (rev !== parseInt(cpf.charAt(9))) return false;
   if (rev !== parseInt(cpf.charAt(9))) return false;
+
   sum = 0;
   sum = 0;
+
   for (let i = 0; i < 10; i++) sum += parseInt(cpf.charAt(i)) * (11 - i);
   for (let i = 0; i < 10; i++) sum += parseInt(cpf.charAt(i)) * (11 - i);
+
   rev = 11 - (sum % 11);
   rev = 11 - (sum % 11);
+
   if (rev === 10 || rev === 11) rev = 0;
   if (rev === 10 || rev === 11) rev = 0;
+
   if (rev !== parseInt(cpf.charAt(10))) return false;
   if (rev !== parseInt(cpf.charAt(10))) return false;
+
   return true;
   return true;
 }
 }
 
 
 function isValidCNPJ(cnpj) {
 function isValidCNPJ(cnpj) {
   if (!cnpj) return false;
   if (!cnpj) return false;
+
   cnpj = cnpj.replace(/[^\d]+/g, "");
   cnpj = cnpj.replace(/[^\d]+/g, "");
-  if (cnpj.length !== 14) return false;
+
+  if (cnpj.length !== 14)     return false;
   if (/^(\d)\1+$/.test(cnpj)) return false;
   if (/^(\d)\1+$/.test(cnpj)) return false;
-  let length = cnpj.length - 2;
+
+  let length  = cnpj.length - 2;
   let numbers = cnpj.substring(0, length);
   let numbers = cnpj.substring(0, length);
-  let digits = cnpj.substring(length);
-  let sum = 0;
-  let pos = length - 7;
+  let digits  = cnpj.substring(length);
+  let sum     = 0;
+  let pos     = length - 7;
+
   for (let i = length; i >= 1; i--) {
   for (let i = length; i >= 1; i--) {
     sum += parseInt(numbers.charAt(length - i)) * pos--;
     sum += parseInt(numbers.charAt(length - i)) * pos--;
+
     if (pos < 2) pos = 9;
     if (pos < 2) pos = 9;
   }
   }
+
   let result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
   let result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
+
   if (result !== parseInt(digits.charAt(0))) return false;
   if (result !== parseInt(digits.charAt(0))) return false;
-  length = length + 1;
+
+  length  = length + 1;
   numbers = cnpj.substring(0, length);
   numbers = cnpj.substring(0, length);
-  sum = 0;
-  pos = length - 7;
+  sum     = 0;
+  pos     = length - 7;
+
   for (let i = length; i >= 1; i--) {
   for (let i = length; i >= 1; i--) {
     sum += parseInt(numbers.charAt(length - i)) * pos--;
     sum += parseInt(numbers.charAt(length - i)) * pos--;
+
     if (pos < 2) pos = 9;
     if (pos < 2) pos = 9;
   }
   }
+
   result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
   result = sum % 11 < 2 ? 0 : 11 - (sum % 11);
+
   if (result !== parseInt(digits.charAt(1))) return false;
   if (result !== parseInt(digits.charAt(1))) return false;
+
   return true;
   return true;
 }
 }

+ 75 - 0
src/composables/usePaymentPlatformFees.js

@@ -0,0 +1,75 @@
+import { getPaymentPlatformFees } from 'src/api/payment'
+import { ref } from 'vue'
+
+const platformFees = ref({
+  pix:                  null,
+  credit_card:          null,
+  cart_min_3_schedules: null,
+})
+
+let loadingPromise = null
+
+const normalizeFee = (value) => {
+  if (value === null || value === undefined || value === '') return null
+
+  const fee = Number(value)
+
+  if (!Number.isFinite(fee)) return null
+
+  return fee > 1 ? fee / 100 : fee
+}
+
+const firstDefined = (...values) => values.find((value) => value !== null && value !== undefined && value !== '')
+
+const normalizePlatformFees = (fees) => {
+  const normalized = {
+    pix: normalizeFee(firstDefined(
+      fees?.pix,
+      fees?.PIX,
+      fees?.pix_fee,
+      fees?.pix_fee_rate,
+      fees?.platform_pix_fee_rate,
+    )),
+    credit_card: normalizeFee(firstDefined(
+      fees?.credit_card,
+      fees?.creditCard,
+      fees?.credit_card_fee,
+      fees?.credit_card_fee_rate,
+      fees?.platform_credit_card_fee_rate,
+    )),
+    cart_min_3_schedules: normalizeFee(firstDefined(
+      fees?.cart_min_3_schedules,
+      fees?.cartMin3Schedules,
+      fees?.cart_min_3_schedules_fee,
+      fees?.cart_min_3_schedules_fee_rate,
+      fees?.platform_cart_min_3_schedules_fee_rate,
+    )),
+  }
+
+  if (normalized.pix === null || normalized.credit_card === null) {
+    console.warn('Invalid payment platform fees payload', fees)
+  }
+
+  return normalized
+}
+
+export function usePaymentPlatformFees() {
+  const loadPlatformFees = async () => {
+    if (!loadingPromise) {
+      loadingPromise = getPaymentPlatformFees()
+        .then((fees) => {
+          platformFees.value = normalizePlatformFees(fees)
+        })
+        .finally(() => {
+          loadingPromise = null
+        })
+    }
+
+    await loadingPromise
+  }
+
+  return {
+    platformFees,
+    loadPlatformFees,
+  }
+}

+ 12 - 0
src/helpers/paymentPlatformFees.js

@@ -0,0 +1,12 @@
+export const scheduleUsesCartDiscount = (schedule) =>
+  Number(schedule?.cart_items_count ?? schedule?.cartItemsCount ?? schedule?.cart?.items_count ?? 0) >= 3
+
+export const getSchedulePlatformFeeRate = (schedule, paymentType, platformFees) => {
+  const fees = platformFees?.value ?? platformFees ?? {}
+
+  if (scheduleUsesCartDiscount(schedule) && fees.cart_min_3_schedules !== null && fees.cart_min_3_schedules !== undefined) {
+    return fees.cart_min_3_schedules
+  }
+
+  return fees[paymentType]
+}

+ 70 - 3
src/i18n/locales/en.json

@@ -185,6 +185,7 @@
       "not_same_document": "The document must be unique for each participant",
       "not_same_document": "The document must be unique for each participant",
       "cpf": "This field must be a valid CPF",
       "cpf": "This field must be a valid CPF",
       "cnpj": "This field must be a valid CNPJ",
       "cnpj": "This field must be a valid CNPJ",
+      "cpf_or_cnpj": "This field must be a valid CPF or CNPJ",
       "cep": "This field must be a valid ZIP code",
       "cep": "This field must be a valid ZIP code",
       "value_smaller_than_zero": "Value cannot be less than zero"
       "value_smaller_than_zero": "Value cannot be less than zero"
     },
     },
@@ -510,6 +511,9 @@
       "detail_value": "Amount:",
       "detail_value": "Amount:",
       "detail_service_fee": "Service fee:",
       "detail_service_fee": "Service fee:",
       "detail_total": "Total:",
       "detail_total": "Total:",
+      "detail_pix_total": "Pix total:",
+      "detail_credit_card_total": "Credit card total:",
+      "detail_pix_discount": "Pix discount:",
       "btn_payment": "go to payment",
       "btn_payment": "go to payment",
       "btn_cancel": "Cancel request"
       "btn_cancel": "Cancel request"
     },
     },
@@ -542,8 +546,8 @@
     "placeholder_email": "Enter your email",
     "placeholder_email": "Enter your email",
     "phone": "Phone",
     "phone": "Phone",
     "placeholder_phone": "(11) 99999-9999",
     "placeholder_phone": "(11) 99999-9999",
-    "document": "CPF (Tax ID)",
-    "placeholder_document": "000.000.000-00",
+    "document": "Document",
+    "placeholder_document": "Enter your document",
     "update": "Update",
     "update": "Update",
     "language": "Language",
     "language": "Language",
     "lang_pt": "PT-br",
     "lang_pt": "PT-br",
@@ -776,6 +780,68 @@
       "no_primary_address": "Please add a primary address in your profile to schedule a service."
       "no_primary_address": "Please add a primary address in your profile to schedule a service."
     }
     }
   },
   },
+  "schedule_cart": {
+    "title": "Cart",
+    "empty_title": "Your cart is empty",
+    "empty_message": "Add time slots from favorite professionals to send a request.",
+    "add_to_cart": "Add to cart",
+    "add_success": "Time slot added to cart.",
+    "local_cart_title": "Cart",
+    "orders_title": "Orders",
+    "no_orders": "No orders created.",
+    "order_title": "Order #{id}",
+    "view_favorites": "view favorites",
+    "last_request_title": "Last request sent",
+    "created_count": "{count} booking(s) created",
+    "created_ids": "IDs: {ids}",
+    "time_range": "{start} to {end}",
+    "separator": "·",
+    "hours_short": "{hours}h",
+    "currency_value": "R$ {value}",
+    "with_meal": "With meal",
+    "without_meal": "Without meal",
+    "summary": "Summary",
+    "services_total": "Services total",
+    "pix_fee": "Pix service fee ({percent})",
+    "pix_total": "Pix total",
+    "credit_card_fee": "Card service fee ({percent})",
+    "credit_card_total": "Card total",
+    "pix_discount": "Pix discount: save R$ {value}",
+    "cart_fee_discount": "Cart fee discount: R$ {value}",
+    "cart_discount_hint": "Add {count} more time slot(s) to get the cart discount.",
+    "cart_discount_applied": "Discount applied: 3 or more time slots in the cart.",
+    "cart_discount_total": "With discount",
+    "provider": "Professional",
+    "address": "Address",
+    "quantity": "Quantity",
+    "schedule_count": "{count} time slot(s)",
+    "submit": "send request",
+    "close_cart": "close cart",
+    "cart_closed_success": "Cart closed. Wait for provider approval.",
+    "unknown": "No information",
+    "multiple_providers": "{count} professionals",
+    "loading": "Loading...",
+    "no_primary_address": "No primary address",
+    "provider_initial": "P",
+    "no_primary_address_notify": "Add a primary address to send the request.",
+    "submit_success": "Request sent successfully!",
+    "submit_error": "Could not send the request. Please try again.",
+    "load_error": "Could not load orders.",
+    "awaiting_approval": "Waiting for all providers to approve.",
+    "pay_order": "pay order",
+    "payment_method": "Payment method",
+    "credit_card": "Card",
+    "pix": "Pix",
+    "select_card": "Select card",
+    "payment_success": "Payment started successfully.",
+    "payment_error": "Could not pay order.",
+    "status_open": "Waiting",
+    "status_pending": "Pending",
+    "status_accepted": "Approved",
+    "status_paid": "Paid",
+    "status_rejected": "Rejected",
+    "status_cancelled": "Cancelled"
+  },
   "period_types": {
   "period_types": {
     "2": "Quick Clean (up to 2h)",
     "2": "Quick Clean (up to 2h)",
     "4": "Half day (up to 4h)",
     "4": "Half day (up to 4h)",
@@ -814,6 +880,7 @@
     "home": "Home",
     "home": "Home",
     "search": "Search",
     "search": "Search",
     "agenda": "Schedule",
     "agenda": "Schedule",
+    "cart": "Cart",
     "profile": "Profile"
     "profile": "Profile"
   },
   },
   "provider": {
   "provider": {
@@ -831,4 +898,4 @@
       }
       }
     }
     }
   }
   }
-}
+}

+ 70 - 3
src/i18n/locales/es.json

@@ -185,6 +185,7 @@
       "not_same_document": "El documento debe ser único para cada participante",
       "not_same_document": "El documento debe ser único para cada participante",
       "cpf": "Este campo debe ser un CPF válido",
       "cpf": "Este campo debe ser un CPF válido",
       "cnpj": "Este campo debe ser un CNPJ válido",
       "cnpj": "Este campo debe ser un CNPJ válido",
+      "cpf_or_cnpj": "Este campo debe ser un CPF o CNPJ válido",
       "cep": "Este campo debe ser un código postal válido",
       "cep": "Este campo debe ser un código postal válido",
       "value_smaller_than_zero": "El valor no puede ser menor que cero"
       "value_smaller_than_zero": "El valor no puede ser menor que cero"
     },
     },
@@ -506,6 +507,9 @@
       "detail_value": "Valor:",
       "detail_value": "Valor:",
       "detail_service_fee": "Tasa de servicio:",
       "detail_service_fee": "Tasa de servicio:",
       "detail_total": "Total:",
       "detail_total": "Total:",
+      "detail_pix_total": "Total en Pix:",
+      "detail_credit_card_total": "Total en tarjeta de crédito:",
+      "detail_pix_discount": "Descuento en Pix:",
       "btn_payment": "ir al pago",
       "btn_payment": "ir al pago",
       "btn_cancel": "Cancelar pedido"
       "btn_cancel": "Cancelar pedido"
     },
     },
@@ -538,8 +542,8 @@
     "placeholder_email": "Ingrese su correo electrónico",
     "placeholder_email": "Ingrese su correo electrónico",
     "phone": "Teléfono",
     "phone": "Teléfono",
     "placeholder_phone": "(11) 99999-9999",
     "placeholder_phone": "(11) 99999-9999",
-    "document": "CPF (RUT)",
-    "placeholder_document": "000.000.000-00",
+    "document": "Documento",
+    "placeholder_document": "Ingrese su documento",
     "update": "Actualizar",
     "update": "Actualizar",
     "language": "Idioma",
     "language": "Idioma",
     "lang_pt": "PT-br",
     "lang_pt": "PT-br",
@@ -772,6 +776,68 @@
       "no_primary_address": "Agregue una dirección principal en su perfil para agendar un servicio."
       "no_primary_address": "Agregue una dirección principal en su perfil para agendar un servicio."
     }
     }
   },
   },
+  "schedule_cart": {
+    "title": "Carrito",
+    "empty_title": "Su carrito está vacío",
+    "empty_message": "Agregue horarios de profesionales favoritos para enviar una solicitud.",
+    "add_to_cart": "Agregar al carrito",
+    "add_success": "Horario agregado al carrito.",
+    "local_cart_title": "Carrito",
+    "orders_title": "Pedidos",
+    "no_orders": "Ningún pedido creado.",
+    "order_title": "Pedido #{id}",
+    "view_favorites": "ver favoritos",
+    "last_request_title": "Última solicitud enviada",
+    "created_count": "{count} reserva(s) creada(s)",
+    "created_ids": "Códigos: {ids}",
+    "time_range": "{start} a {end}",
+    "separator": "·",
+    "hours_short": "{hours}h",
+    "currency_value": "R$ {value}",
+    "with_meal": "Con comida",
+    "without_meal": "Sin comida",
+    "summary": "Resumen",
+    "services_total": "Valor de los servicios",
+    "pix_fee": "Tarifa de servicio Pix ({percent})",
+    "pix_total": "Total con Pix",
+    "credit_card_fee": "Tarifa de servicio tarjeta ({percent})",
+    "credit_card_total": "Total con tarjeta",
+    "pix_discount": "Descuento con Pix: ahorre R$ {value}",
+    "cart_fee_discount": "Descuento de la tarifa del carrito: R$ {value}",
+    "cart_discount_hint": "Agregue {count} horario(s) más para recibir descuento en el carrito.",
+    "cart_discount_applied": "Descuento aplicado: 3 o más horarios en el carrito.",
+    "cart_discount_total": "Con descuento",
+    "provider": "Profesional",
+    "address": "Dirección",
+    "quantity": "Cantidad",
+    "schedule_count": "{count} horario(s)",
+    "submit": "enviar solicitud",
+    "close_cart": "cerrar carrito",
+    "cart_closed_success": "Carrito cerrado. Espere la aprobación de los profesionales.",
+    "unknown": "Sin información",
+    "multiple_providers": "{count} profesionales",
+    "loading": "Cargando...",
+    "no_primary_address": "Sin dirección principal",
+    "provider_initial": "P",
+    "no_primary_address_notify": "Agregue una dirección principal para enviar la solicitud.",
+    "submit_success": "¡Solicitud enviada con éxito!",
+    "submit_error": "No se pudo enviar la solicitud. Inténtelo de nuevo.",
+    "load_error": "No se pudieron cargar los pedidos.",
+    "awaiting_approval": "Esperando la aprobación de todos los profesionales.",
+    "pay_order": "pagar pedido",
+    "payment_method": "Forma de pago",
+    "credit_card": "Tarjeta",
+    "pix": "Pix",
+    "select_card": "Seleccione la tarjeta",
+    "payment_success": "Pago iniciado con éxito.",
+    "payment_error": "No se pudo pagar el pedido.",
+    "status_open": "Esperando",
+    "status_pending": "Pendiente",
+    "status_accepted": "Aprobado",
+    "status_paid": "Pagado",
+    "status_rejected": "Rechazado",
+    "status_cancelled": "Cancelado"
+  },
   "period_types": {
   "period_types": {
     "2": "Limpieza Rápida (hasta 2h)",
     "2": "Limpieza Rápida (hasta 2h)",
     "4": "Medio tiempo (hasta 4h)",
     "4": "Medio tiempo (hasta 4h)",
@@ -810,6 +876,7 @@
     "home": "Inicio",
     "home": "Inicio",
     "search": "Buscar",
     "search": "Buscar",
     "agenda": "Agenda",
     "agenda": "Agenda",
+    "cart": "Carrito",
     "profile": "Perfil"
     "profile": "Perfil"
   },
   },
   "provider": {
   "provider": {
@@ -827,4 +894,4 @@
       }
       }
     }
     }
   }
   }
-}
+}

+ 70 - 3
src/i18n/locales/pt.json

@@ -185,6 +185,7 @@
       "not_same_document": "O documento deve ser único para cada participante",
       "not_same_document": "O documento deve ser único para cada participante",
       "cpf": "Este campo deve ser um CPF válido",
       "cpf": "Este campo deve ser um CPF válido",
       "cnpj": "Este campo deve ser um CNPJ válido",
       "cnpj": "Este campo deve ser um CNPJ válido",
+      "cpf_or_cnpj": "Este campo deve ser um CPF ou CNPJ válido",
       "cep": "Este campo deve ser um CEP válido",
       "cep": "Este campo deve ser um CEP válido",
       "value_smaller_than_zero": "O valor não pode ser menor que zero"
       "value_smaller_than_zero": "O valor não pode ser menor que zero"
     },
     },
@@ -510,6 +511,9 @@
       "detail_value": "Valor:",
       "detail_value": "Valor:",
       "detail_service_fee": "Taxa de serviço:",
       "detail_service_fee": "Taxa de serviço:",
       "detail_total": "Total:",
       "detail_total": "Total:",
+      "detail_pix_total": "Total no Pix:",
+      "detail_credit_card_total": "Total no cartão de crédito:",
+      "detail_pix_discount": "Desconto no Pix:",
       "btn_payment": "ir para o pagamento",
       "btn_payment": "ir para o pagamento",
       "btn_cancel": "Cancelar pedido"
       "btn_cancel": "Cancelar pedido"
     },
     },
@@ -542,8 +546,8 @@
     "placeholder_email": "Digite seu e-mail",
     "placeholder_email": "Digite seu e-mail",
     "phone": "Telefone",
     "phone": "Telefone",
     "placeholder_phone": "(11) 99999-9999",
     "placeholder_phone": "(11) 99999-9999",
-    "document": "CPF",
-    "placeholder_document": "000.000.000-00",
+    "document": "Documento",
+    "placeholder_document": "Digite seu documento",
     "update": "Atualizar",
     "update": "Atualizar",
     "language": "Idioma",
     "language": "Idioma",
     "lang_pt": "PT-br",
     "lang_pt": "PT-br",
@@ -784,6 +788,68 @@
       "no_primary_address": "Cadastre um endereço principal no seu perfil para agendar."
       "no_primary_address": "Cadastre um endereço principal no seu perfil para agendar."
     }
     }
   },
   },
+  "schedule_cart": {
+    "title": "Carrinho",
+    "empty_title": "Seu carrinho está vazio",
+    "empty_message": "Adicione horários de profissionais favoritos para enviar uma solicitação.",
+    "add_to_cart": "Adicionar ao carrinho",
+    "add_success": "Horário adicionado ao carrinho.",
+    "local_cart_title": "Carrinho",
+    "orders_title": "Pedidos",
+    "no_orders": "Nenhum pedido criado.",
+    "order_title": "Pedido #{id}",
+    "view_favorites": "ver favoritos",
+    "last_request_title": "Última solicitação enviada",
+    "created_count": "{count} agendamento(s) criado(s)",
+    "created_ids": "Códigos: {ids}",
+    "time_range": "{start} às {end}",
+    "separator": "·",
+    "hours_short": "{hours}h",
+    "currency_value": "R$ {value}",
+    "with_meal": "Com refeição",
+    "without_meal": "Sem refeição",
+    "summary": "Resumo",
+    "services_total": "Valor dos serviços",
+    "pix_fee": "Taxa de serviço Pix ({percent})",
+    "pix_total": "Total com Pix",
+    "credit_card_fee": "Taxa de serviço cartão ({percent})",
+    "credit_card_total": "Total no cartão",
+    "pix_discount": "Desconto no Pix: economize R$ {value}",
+    "cart_fee_discount": "Desconto da taxa do carrinho: R$ {value}",
+    "cart_discount_hint": "Adicione mais {count} horário(s) para receber desconto no carrinho.",
+    "cart_discount_applied": "Desconto aplicado: 3 ou mais horários no carrinho.",
+    "cart_discount_total": "Com desconto",
+    "provider": "Profissional",
+    "address": "Endereço",
+    "quantity": "Quantidade",
+    "schedule_count": "{count} horário(s)",
+    "submit": "enviar solicitação",
+    "close_cart": "fechar carrinho",
+    "cart_closed_success": "Carrinho fechado. Aguarde a aprovação dos profissionais.",
+    "unknown": "Sem informação",
+    "multiple_providers": "{count} profissionais",
+    "loading": "Carregando...",
+    "no_primary_address": "Sem endereço principal",
+    "provider_initial": "D",
+    "no_primary_address_notify": "Cadastre um endereço principal para enviar a solicitação.",
+    "submit_success": "Solicitação enviada com sucesso!",
+    "submit_error": "Não foi possível enviar a solicitação. Tente novamente.",
+    "load_error": "Não foi possível carregar os pedidos.",
+    "awaiting_approval": "Aguardando aprovação de todos os profissionais.",
+    "pay_order": "pagar pedido",
+    "payment_method": "Forma de pagamento",
+    "credit_card": "Cartão",
+    "pix": "Pix",
+    "select_card": "Selecione o cartão",
+    "payment_success": "Pagamento iniciado com sucesso.",
+    "payment_error": "Não foi possível pagar o pedido.",
+    "status_open": "Aguardando",
+    "status_pending": "Pendente",
+    "status_accepted": "Aprovado",
+    "status_paid": "Pago",
+    "status_rejected": "Recusado",
+    "status_cancelled": "Cancelado"
+  },
   "period_types": {
   "period_types": {
     "2": "Diária Rápida (até 2h)",
     "2": "Diária Rápida (até 2h)",
     "4": "Meia diária (até 4h)",
     "4": "Meia diária (até 4h)",
@@ -822,6 +888,7 @@
     "home": "Início",
     "home": "Início",
     "search": "Busca",
     "search": "Busca",
     "agenda": "Agenda",
     "agenda": "Agenda",
+    "cart": "Carrinho",
     "profile": "Perfil"
     "profile": "Perfil"
   },
   },
   "provider": {
   "provider": {
@@ -844,4 +911,4 @@
     "unread": "Não lidas",
     "unread": "Não lidas",
     "mark_all_read": " Marcar todas como lidas"
     "mark_all_read": " Marcar todas como lidas"
   }
   }
-}
+}

+ 23 - 28
src/layouts/MainLayout.vue

@@ -7,39 +7,45 @@
         height: `calc(50px + env(safe-area-inset-top))`,
         height: `calc(50px + env(safe-area-inset-top))`,
       }"
       }"
     >
     >
+
     </q-header>
     </q-header>
+
     <q-page-container>
     <q-page-container>
       <q-page class="bg-surface main-layout-page" style="overflow: hidden;">
       <q-page class="bg-surface main-layout-page" style="overflow: hidden;">
         <q-scroll-area
         <q-scroll-area
           ref="scrollAreaRef"
           ref="scrollAreaRef"
           class="main-layout-scroll-area"
           class="main-layout-scroll-area"
-          :style="scrollAreaHeight"
-          :content-style="{ width: '100%', maxWidth: '100%', overflowX: 'hidden', boxSizing: 'border-box' }"
           :content-active-style="{ width: '100%', maxWidth: '100%', overflowX: 'hidden', boxSizing: 'border-box' }"
           :content-active-style="{ width: '100%', maxWidth: '100%', overflowX: 'hidden', boxSizing: 'border-box' }"
+          :content-style="{ width: '100%', maxWidth: '100%', overflowX: 'hidden', boxSizing: 'border-box' }"
+          :style="scrollAreaHeight"
         >
         >
           <router-view v-slot="{ Component }">
           <router-view v-slot="{ Component }">
             <Transition mode="out-in">
             <Transition mode="out-in">
               <component
               <component
                 :is="Component"
                 :is="Component"
-                class="main-layout-view"
                 :class="{ 'main-layout-view--mobile': $q.screen.lt.sm }"
                 :class="{ 'main-layout-view--mobile': $q.screen.lt.sm }"
+                class="main-layout-view"
               />
               />
             </Transition>
             </Transition>
           </router-view>
           </router-view>
         </q-scroll-area>
         </q-scroll-area>
       </q-page>
       </q-page>
     </q-page-container>
     </q-page-container>
+
     <q-footer v-if="$q.screen.lt.sm" class="provider-bottom-nav bg-white">
     <q-footer v-if="$q.screen.lt.sm" class="provider-bottom-nav bg-white">
       <nav class="provider-bottom-nav-inner">
       <nav class="provider-bottom-nav-inner">
         <router-link
         <router-link
           v-for="item in navItems"
           v-for="item in navItems"
           :key="item.name"
           :key="item.name"
           :to="{ name: item.name }"
           :to="{ name: item.name }"
-          class="provider-bottom-nav-item"
           :class="{ 'provider-bottom-nav-item--active': isNavItemActive(item) }"
           :class="{ 'provider-bottom-nav-item--active': isNavItemActive(item) }"
+          class="provider-bottom-nav-item"
         >
         >
           <q-icon :name="item.icon" class="provider-bottom-nav-icon" />
           <q-icon :name="item.icon" class="provider-bottom-nav-icon" />
-          <span class="provider-bottom-nav-label font12">{{ item.label }}</span>
+
+          <span class="provider-bottom-nav-label font12">
+            {{ item.label }}
+          </span>
         </router-link>
         </router-link>
       </nav>
       </nav>
     </q-footer>
     </q-footer>
@@ -48,42 +54,29 @@
 
 
 <script setup>
 <script setup>
 import { computed, useTemplateRef, watch } from "vue";
 import { computed, useTemplateRef, watch } from "vue";
+import { useI18n } from "vue-i18n";
 import { useQuasar } from "quasar";
 import { useQuasar } from "quasar";
 import { useRoute } from "vue-router";
 import { useRoute } from "vue-router";
-import { useI18n } from "vue-i18n";
 
 
 defineOptions({
 defineOptions({
   name: "MainLayout",
   name: "MainLayout",
 });
 });
 
 
-const $q = useQuasar();
+const $q    = useQuasar();
 const route = useRoute();
 const route = useRoute();
+
 const scrollAreaRef = useTemplateRef("scrollAreaRef");
 const scrollAreaRef = useTemplateRef("scrollAreaRef");
+
 const { t } = useI18n();
 const { t } = useI18n();
 
 
 let oldValue = route.path;
 let oldValue = route.path;
 
 
 const navItems = computed(() => [
 const navItems = computed(() => [
-  {
-    name: "DashboardPage",
-    label: t('nav.home'),
-    icon: "mdi-home-outline",
-  },
-  {
-    name: "SearchPage",
-    label: t('nav.search'),
-    icon: "mdi-magnify",
-  },
-  {
-    name: "CalendarPage",
-    label: t('nav.agenda'),
-    icon: "mdi-calendar-blank-outline",
-  },
-  {
-    name: "ProfilePage",
-    label: t('nav.profile'),
-    icon: "mdi-account-circle-outline",
-  },
+  { name: "DashboardPage",     label: t('nav.home'),    icon: "mdi-home-outline" },
+  { name: "SearchPage",        label: t('nav.search'),  icon: "mdi-magnify" },
+  { name: "CalendarPage",      label: t('nav.agenda'),  icon: "mdi-calendar-blank-outline" },
+  { name:  "ScheduleCartPage", label: t('nav.cart'),    icon: "mdi-cart-outline" },
+  { name:  "ProfilePage",      label: t('nav.profile'), icon: "mdi-account-circle-outline" },
 ]);
 ]);
 
 
 const isNavItemActive = (item) => route.name === item.name;
 const isNavItemActive = (item) => route.name === item.name;
@@ -92,6 +85,7 @@ const scrollAreaHeight = computed(() => {
   if ($q.screen.lt.sm) {
   if ($q.screen.lt.sm) {
     return 'height: calc(100dvh - 50px - env(safe-area-inset-top) - 80px - env(safe-area-inset-bottom)) !important;';
     return 'height: calc(100dvh - 50px - env(safe-area-inset-top) - 80px - env(safe-area-inset-bottom)) !important;';
   }
   }
+
   return 'height: 100dvh !important;';
   return 'height: 100dvh !important;';
 });
 });
 
 
@@ -102,6 +96,7 @@ watch(
       scrollAreaRef.value?.setScrollPosition("vertical", 0, 0);
       scrollAreaRef.value?.setScrollPosition("vertical", 0, 0);
       scrollAreaRef.value?.setScrollPosition("horizontal", 0, 0);
       scrollAreaRef.value?.setScrollPosition("horizontal", 0, 0);
     }
     }
+
     oldValue = value;
     oldValue = value;
   }
   }
 );
 );
@@ -165,7 +160,7 @@ watch(
 
 
 .provider-bottom-nav-inner {
 .provider-bottom-nav-inner {
   display: grid;
   display: grid;
-  grid-template-columns: repeat(4, minmax(0, 1fr));
+  grid-template-columns: repeat(5, minmax(0, 1fr));
   align-items: end;
   align-items: end;
   height: 60px;
   height: 60px;
   padding: 0px 0px calc(12px + env(safe-area-inset-bottom));
   padding: 0px 0px calc(12px + env(safe-area-inset-bottom));

+ 186 - 84
src/pages/profile/ProfileEditDialog.vue

@@ -1,11 +1,33 @@
 <template>
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-left" transition-hide="slide-right">
+  <q-dialog
+    ref="dialogRef"
+    maximized
+    persistent
+    transition-hide="slide-right"
+    transition-show="slide-left"
+  >
     <q-card class="bg-white full-width full-height">
     <q-card class="bg-white full-width full-height">
-      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile">
-        <q-btn flat round dense icon="mdi-chevron-left" color="primary" @click="onDialogCancel" />
+      <div
+        class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-profile"
+      >
+        <q-btn
+          color="primary"
+          dense
+          flat
+          icon="mdi-chevron-left"
+          round
+          @click="onDialogCancel"
+        />
+
         <q-space />
         <q-space />
-        <span class="text-subtitle1 text-primary font16 fontbold gradient-diarista">{{ $t('profile.edit_data') }}</span>
+
+        <span
+          class="text-subtitle1 text-primary font16 fontbold gradient-diarista"
+          >{{ $t("profile.edit_data") }}</span
+        >
+
         <q-space />
         <q-space />
+
         <div style="width: 32px"></div>
         <div style="width: 32px"></div>
       </div>
       </div>
 
 
@@ -16,91 +38,150 @@
       <template v-else>
       <template v-else>
         <q-scroll-area class="col" style="height: calc(100vh - 72px)">
         <q-scroll-area class="col" style="height: calc(100vh - 72px)">
           <div class="column items-center q-mt-xl q-mb-md">
           <div class="column items-center q-mt-xl q-mb-md">
-            <q-avatar size="140px" color="indigo-1" text-color="indigo-4" class="text-h2 shadow-1">
-              <img v-if="avatarPreview" :src="avatarPreview" style="object-fit: cover; width: 100%; height: 100%;" />
-              <span v-else>{{ form.name ? form.name.charAt(0).toUpperCase() : '' }}</span>
+            <q-avatar
+              class="text-h2 shadow-1"
+              color="indigo-1"
+              size="140px"
+              text-color="indigo-4"
+            >
+              <img
+                v-if="avatarPreview"
+                :src="avatarPreview"
+                style="object-fit: cover; width: 100%; height: 100%"
+              />
+
+              <span v-else>{{
+                form.name ? form.name.charAt(0).toUpperCase() : ""
+              }}</span>
             </q-avatar>
             </q-avatar>
+
             <input
             <input
               ref="fileInputRef"
               ref="fileInputRef"
-              type="file"
               accept="image/jpeg,image/png,image/webp"
               accept="image/jpeg,image/png,image/webp"
               class="hidden"
               class="hidden"
+              type="file"
               @change="onFileSelected"
               @change="onFileSelected"
             />
             />
-            <q-btn flat no-caps color="grey-6" class="q-mt-sm" :label="$t('profile.change_photo')" @click="fileInputRef.click()" />
+
+            <q-btn
+              class="q-mt-sm"
+              color="grey-6"
+              flat
+              no-caps
+              :label="$t('profile.change_photo')"
+              @click="fileInputRef.click()"
+            />
           </div>
           </div>
 
 
           <q-form ref="formRef" @submit.prevent="onSubmit">
           <q-form ref="formRef" @submit.prevent="onSubmit">
             <div class="q-px-xl q-gutter-y-lg">
             <div class="q-px-xl q-gutter-y-lg">
               <div>
               <div>
-                <div class="text-grey-8 q-mb-sm font12 fontbold">{{ $t('profile.full_name') }}</div>
+                <div class="text-grey-8 q-mb-sm font12 fontbold">
+                  {{ $t("profile.full_name") }}
+                </div>
+
                 <q-input
                 <q-input
                   v-model="form.name"
                   v-model="form.name"
-                  outlined
                   dense
                   dense
                   input-class="text-text"
                   input-class="text-text"
-                  :placeholder="$t('profile.placeholder_name')"
-                  :rules="[inputRules.required]"
+                  outlined
                   :error="!!serverErrors.name"
                   :error="!!serverErrors.name"
                   :error-message="serverErrors.name"
                   :error-message="serverErrors.name"
+                  :placeholder="$t('profile.placeholder_name')"
+                  :rules="[inputRules.required]"
                   @update:model-value="serverErrors.name = null"
                   @update:model-value="serverErrors.name = null"
                 />
                 />
               </div>
               </div>
 
 
               <div>
               <div>
-                <div class="text-grey-8 q-mb-sm font12 fontbold">{{ $t('profile.email') }}</div>
+                <div class="text-grey-8 q-mb-sm font12 fontbold">
+                  {{ $t("profile.email") }}
+                </div>
+
                 <q-input
                 <q-input
                   v-model="form.email"
                   v-model="form.email"
-                  outlined
                   dense
                   dense
                   input-class="text-text"
                   input-class="text-text"
-                  :placeholder="$t('profile.placeholder_email')"
-                  :rules="[inputRules.email]"
+                  outlined
                   :error="!!serverErrors.email"
                   :error="!!serverErrors.email"
                   :error-message="serverErrors.email"
                   :error-message="serverErrors.email"
+                  :placeholder="$t('profile.placeholder_email')"
+                  :rules="[inputRules.email]"
                   @update:model-value="serverErrors.email = null"
                   @update:model-value="serverErrors.email = null"
                 />
                 />
               </div>
               </div>
 
 
               <div>
               <div>
-                <div class="text-grey-8 q-mb-sm font12 fontbold">{{ $t('profile.phone') }}</div>
+                <div class="text-grey-8 q-mb-sm font12 fontbold">
+                  {{ $t("profile.phone") }}
+                </div>
+
                 <q-input
                 <q-input
                   v-model="form.phone"
                   v-model="form.phone"
-                  outlined
                   dense
                   dense
                   input-class="text-text"
                   input-class="text-text"
                   mask="(##) #####-####"
                   mask="(##) #####-####"
+                  outlined
                   unmasked-value
                   unmasked-value
-                  :placeholder="$t('profile.placeholder_phone')"
                   :error="!!serverErrors.phone"
                   :error="!!serverErrors.phone"
                   :error-message="serverErrors.phone"
                   :error-message="serverErrors.phone"
+                  :placeholder="$t('profile.placeholder_phone')"
                   @update:model-value="serverErrors.phone = null"
                   @update:model-value="serverErrors.phone = null"
                 />
                 />
               </div>
               </div>
 
 
               <div>
               <div>
-                <div class="text-grey-8 q-mb-sm font12 fontbold">{{ $t('profile.document') }}</div>
+                <div class="text-grey-8 q-mb-sm font12 fontbold">
+                  {{ $t("profile.document") }}
+                </div>
+
                 <q-input
                 <q-input
                   v-model="form.document"
                   v-model="form.document"
-                  outlined
                   dense
                   dense
                   input-class="text-text"
                   input-class="text-text"
-                  mask="###.###.###-##"
-                  unmasked-value
-                  :placeholder="$t('profile.placeholder_document')"
-                  :rules="[inputRules.cpf]"
+                  outlined
                   :error="!!serverErrors.document"
                   :error="!!serverErrors.document"
                   :error-message="serverErrors.document"
                   :error-message="serverErrors.document"
+                  :placeholder="$t('profile.placeholder_document')"
                   @update:model-value="serverErrors.document = null"
                   @update:model-value="serverErrors.document = null"
                 />
                 />
               </div>
               </div>
 
 
               <div>
               <div>
-                <div class="text-grey-8 q-mb-sm font12 fontbold">{{ $t('profile.language') }}</div>
+                <div class="text-grey-8 q-mb-sm font12 fontbold">
+                  {{ $t("profile.language") }}
+                </div>
+
                 <div class="row">
                 <div class="row">
-                  <q-radio v-model="selectedLocale" val="pt" :label="$t('profile.lang_pt')" color="primary" class="text-text col-4 font10 fontmedium" keep-color @update:model-value="onLocaleChange" />
-                  <q-radio v-model="selectedLocale" val="en" :label="$t('profile.lang_en')" color="primary" class="text-text col-4 font10 fontmedium" keep-color @update:model-value="onLocaleChange" />
-                  <q-radio v-model="selectedLocale" val="es" :label="$t('profile.lang_es')" color="primary" class="text-text col-4 font10 fontmedium" keep-color @update:model-value="onLocaleChange" />
+                  <q-radio
+                    v-model="selectedLocale"
+                    class="text-text col-4 font10 fontmedium"
+                    color="primary"
+                    keep-color
+                    val="pt"
+                    :label="$t('profile.lang_pt')"
+                    @update:model-value="onLocaleChange"
+                  />
+
+                  <q-radio
+                    v-model="selectedLocale"
+                    class="text-text col-4 font10 fontmedium"
+                    color="primary"
+                    keep-color
+                    val="en"
+                    :label="$t('profile.lang_en')"
+                    @update:model-value="onLocaleChange"
+                  />
+
+                  <q-radio
+                    v-model="selectedLocale"
+                    class="text-text col-4 font10 fontmedium"
+                    color="primary"
+                    keep-color
+                    val="es"
+                    :label="$t('profile.lang_es')"
+                    @update:model-value="onLocaleChange"
+                  />
                 </div>
                 </div>
               </div>
               </div>
             </div>
             </div>
@@ -109,15 +190,15 @@
 
 
             <div class="q-pa-xl q-mt-md">
             <div class="q-pa-xl q-mt-md">
               <q-btn
               <q-btn
-                type="submit"
-                unelevated
-                rounded
+                class="full-width q-py-md"
                 no-caps
                 no-caps
                 padding="8px 16px"
                 padding="8px 16px"
-                class="full-width q-py-md"
-                :label="$t('profile.update')"
+                rounded
+                type="submit"
+                unelevated
                 :color="hasUpdatedFields || avatarFile ? 'primary' : 'grey-4'"
                 :color="hasUpdatedFields || avatarFile ? 'primary' : 'grey-4'"
                 :disable="!hasUpdatedFields && !avatarFile"
                 :disable="!hasUpdatedFields && !avatarFile"
+                :label="$t('profile.update')"
                 :loading="submitting"
                 :loading="submitting"
               />
               />
             </div>
             </div>
@@ -129,82 +210,97 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, onMounted } from 'vue';
-import { useDialogPluginComponent, Cookies } from 'quasar';
-import { updateMe } from 'src/api/user';
-import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker';
-import { useSubmitHandler } from 'src/composables/useSubmitHandler';
-import { useInputRules } from 'src/composables/useInputRules';
-import { i18n } from 'src/boot/i18n';
+import { Cookies, useDialogPluginComponent } from "quasar";
+import { i18n } from "src/boot/i18n";
+import { onMounted, ref } from "vue";
+import { updateMe } from "src/api/user";
+import { useFormUpdateTracker } from "src/composables/useFormUpdateTracker";
+import { useInputRules } from "src/composables/useInputRules";
+import { useSubmitHandler } from "src/composables/useSubmitHandler";
 
 
 const props = defineProps({
 const props = defineProps({
-  userData: {
-    type: Object,
-    default: null
-  }
+  userData: { type: Object, default: null },
 });
 });
 
 
 defineEmits([...useDialogPluginComponent.emits]);
 defineEmits([...useDialogPluginComponent.emits]);
 
 
-const normalizeLocale = (loc) => {
-  if (!loc) return 'pt';
-  const l = String(loc).toLowerCase();
-  if (l.startsWith('pt')) return 'pt';
-  if (l.startsWith('en')) return 'en';
-  if (l.startsWith('es')) return 'es';
-  return 'pt';
-};
-
 const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
 const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();
-const { form, hasUpdatedFields, setUpdateFormAsOriginal } = useFormUpdateTracker({
-  name: '',
-  email: '',
-  phone: '',
-  document: '',
-});
-const { loading: submitting, serverErrors, execute: submitForm } = useSubmitHandler((data) => {
+
+const { form, hasUpdatedFields, setUpdateFormAsOriginal } =
+  useFormUpdateTracker({ name: "", email: "", phone: "", document: "" });
+
+const {
+  loading: submitting,
+  serverErrors,
+  execute: submitForm,
+} = useSubmitHandler((data) => {
   setUpdateFormAsOriginal(data);
   setUpdateFormAsOriginal(data);
   onDialogOK(data);
   onDialogOK(data);
 });
 });
+
 const { inputRules } = useInputRules();
 const { inputRules } = useInputRules();
 
 
-const formRef = ref(null);
-const fileInputRef = ref(null);
-const loading = ref(false);
-const avatarFile = ref(null);
+const formRef       = ref(null);
+const fileInputRef  = ref(null);
+const loading       = ref(false);
+const avatarFile    = ref(null);
 const avatarPreview = ref(null);
 const avatarPreview = ref(null);
-const selectedLocale = ref(normalizeLocale(i18n.global.locale.value ?? i18n.global.locale));
 
 
-const onLocaleChange = (val) => {
-  i18n.global.locale.value = val;
-  Cookies.set('locale', val, { expires: 365, path: '/' });
+const normalizeLocale = (loc) => {
+  if (!loc) return "pt";
+
+  const l = String(loc).toLowerCase();
+
+  if (l.startsWith("pt")) return "pt";
+  if (l.startsWith("en")) return "en";
+  if (l.startsWith("es")) return "es";
+
+  return "pt";
 };
 };
 
 
+const selectedLocale = ref(
+  normalizeLocale(i18n.global.locale.value ?? i18n.global.locale),
+);
+
 const onFileSelected = (event) => {
 const onFileSelected = (event) => {
   const file = event.target.files[0];
   const file = event.target.files[0];
+
   if (!file) return;
   if (!file) return;
+
   avatarFile.value = file;
   avatarFile.value = file;
+
   avatarPreview.value = URL.createObjectURL(file);
   avatarPreview.value = URL.createObjectURL(file);
 };
 };
 
 
+const onLocaleChange = (val) => {
+  i18n.global.locale.value = val;
+
+  Cookies.set("locale", val, { expires: 365, path: "/" });
+};
+
 const onSubmit = async () => {
 const onSubmit = async () => {
   const valid = await formRef.value.validate();
   const valid = await formRef.value.validate();
+
   if (!valid) return;
   if (!valid) return;
 
 
   let payload;
   let payload;
+
   if (avatarFile.value) {
   if (avatarFile.value) {
     payload = new FormData();
     payload = new FormData();
-    payload.append('name', form.name);
-    payload.append('email', form.email);
-    payload.append('phone', form.phone);
-    if (form.document) payload.append('document', form.document);
-    payload.append('avatar', avatarFile.value);
-    payload.append('_method', 'PUT');
+
+    payload.append("name", form.name);
+    payload.append("email", form.email);
+    payload.append("phone", form.phone);
+
+    if (form.document) payload.append("document", form.document);
+
+    payload.append("avatar", avatarFile.value);
+    payload.append("_method", "PUT");
   } else {
   } else {
     payload = {
     payload = {
-      name: form.name,
-      email: form.email,
-      phone: form.phone,
+      name:     form.name,
+      email:    form.email,
+      phone:    form.phone,
       document: form.document || null,
       document: form.document || null,
     };
     };
   }
   }
@@ -215,12 +311,16 @@ const onSubmit = async () => {
 onMounted(async () => {
 onMounted(async () => {
   if (props.userData) {
   if (props.userData) {
     const data = props.userData;
     const data = props.userData;
-    form.name = data.name || '';
-    form.email = data.email || '';
-    form.phone = data.phone || '';
-    form.document = data.client_document || '';
+
+    form.name     = data.name            || "";
+    form.email    = data.email           || "";
+    form.phone    = data.phone           || "";
+    form.document = data.client_document || "";
+
     avatarPreview.value = data.client?.profile_media?.url ?? null;
     avatarPreview.value = data.client?.profile_media?.url ?? null;
+
     setUpdateFormAsOriginal(data);
     setUpdateFormAsOriginal(data);
+
     return;
     return;
   }
   }
 
 
@@ -231,7 +331,9 @@ onMounted(async () => {
 <style scoped lang="scss">
 <style scoped lang="scss">
 :deep(.q-field--outlined .q-field__control) {
 :deep(.q-field--outlined .q-field__control) {
   border-radius: 8px;
   border-radius: 8px;
-  &::before { border: 1px solid #e0e0e0; }
+  &::before {
+    border: 1px solid #e0e0e0;
+  }
 }
 }
 
 
 .hidden {
 .hidden {

+ 529 - 0
src/pages/schedule-cart/ScheduleCartPage.vue

@@ -0,0 +1,529 @@
+<template>
+  <q-page class="schedule-cart-page bg-page q-pb-xl">
+    <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-cart">
+      <q-btn
+        color="primary"
+        dense
+        flat
+        icon="mdi-chevron-left"
+        round @click="router.back()"
+      />
+
+      <div class="col text-center font16 fontbold gradient-diarista text-primary">
+        {{ $t('schedule_cart.title') }}
+      </div>
+
+      <div style="width: 36px"></div>
+    </div>
+
+    <div v-if="cart.items.length === 0" class="q-px-md q-pt-md">
+      <q-card :flat="false" class="empty-card bg-surface shadow-card card-border">
+        <q-card-section class="column items-center text-center q-pa-lg">
+          <q-icon
+            color="grey-5"
+            name="mdi-cart-off"
+            size="42px"
+          />
+
+          <span class="font14 fontbold text-text q-mt-sm">
+            {{ $t('schedule_cart.empty_title') }}
+          </span>
+
+          <span class="font12 text-grey-7 q-mt-xs">
+            {{ $t('schedule_cart.empty_message') }}
+          </span>
+
+          <q-btn
+            class="q-mt-md"
+            color="primary"
+            no-caps
+            padding="8px 18px"
+            rounded
+            unelevated
+            :label="$t('schedule_cart.view_favorites')"
+            @click="router.push({ name: 'ProfilePage' })"
+          />
+        </q-card-section>
+      </q-card>
+
+      <q-card v-if="cart.lastCreatedGroup" :flat="false" class="created-card bg-surface shadow-card card-border q-mt-md">
+        <q-card-section class="q-pa-md">
+          <div class="row items-center no-wrap">
+            <q-icon
+              color="positive"
+              name="mdi-check-circle-outline"
+              size="24px"
+            />
+
+            <div class="column q-ml-sm">
+              <span class="font14 fontbold text-text">
+                {{ $t('schedule_cart.last_request_title') }}
+              </span>
+
+              <span class="font12 text-grey-7">
+                {{ $t('schedule_cart.created_count', { count: cart.lastCreatedGroup.schedule_ids.length }) }}
+              </span>
+            </div>
+          </div>
+        </q-card-section>
+      </q-card>
+    </div>
+
+    <template v-else>
+      <div class="q-px-md q-pt-md column q-gutter-y-sm">
+        <div class="cart-discount-banner row items-center no-wrap q-pa-sm">
+          <q-icon
+            color="positive"
+            name="mdi-sale-outline"
+            size="20px"
+          />
+
+          <span class="font12 fontbold text-positive q-ml-sm">
+            {{ cartDiscountMessage }}
+          </span>
+        </div>
+
+        <q-card
+          v-for="(item, index) in cart.items"
+          :key="item.local_id"
+          :flat="false"
+          class="cart-item-card bg-surface shadow-card card-border"
+        >
+          <q-card-section class="row no-wrap q-pa-md">
+            <q-avatar color="indigo-1" size="44px" text-color="indigo-4">
+              <span>
+                {{ getProviderInitial(item) }}
+              </span>
+            </q-avatar>
+
+            <div class="column col q-ml-sm">
+              <span class="font14 fontbold text-text">
+                {{ item.provider_name }}
+              </span>
+
+              <span class="font12 text-grey-7">
+                {{ formatCartDate(item.date) }}
+              </span>
+
+              <span class="font12 text-grey-7">
+                {{ $t('schedule_cart.time_range', { start: item.start_time, end: item.end_time }) }}
+              </span>
+
+              <div class="row items-center q-gutter-x-sm q-mt-xs">
+                <q-chip
+                  class="font10"
+                  color="purple-1"
+                  dense
+                  square
+                  text-color="primary"
+                >
+                  {{ $t('schedule_cart.hours_short', { hours: item.period_type }) }}
+                </q-chip>
+
+                <q-chip class="font10" color="grey-2" dense square text-color="grey-8">
+                  {{ item.offers_meal ? $t('schedule_cart.with_meal') : $t('schedule_cart.without_meal') }}
+                </q-chip>
+              </div>
+
+              <div class="item-prices q-mt-sm">
+                <div class="row items-center no-wrap">
+                  <span class="font11 fontbold text-primary">{{ $t('schedule_cart.pix_total') }}</span>
+
+                  <q-space />
+
+                  <span class="font12 fontbold text-primary">
+                    {{ formatCurrencyValue(getStandardPixItemTotal(item)) }}
+                  </span>
+                </div>
+
+                <div class="row items-center no-wrap q-mt-xs">
+                  <span class="font11 text-grey-7">
+                    {{ $t('schedule_cart.credit_card_total') }}
+                  </span>
+
+                  <q-space />
+
+                  <span class="font12 fontbold text-text">
+                    {{ formatCurrencyValue(getCreditCardItemTotal(item)) }}
+                  </span>
+                </div>
+
+                <div v-if="hasCartDiscount" class="row items-center no-wrap q-mt-xs">
+                  <span class="font11 fontbold text-positive">
+                    {{ $t('schedule_cart.cart_discount_total') }}
+                  </span>
+
+                  <q-space />
+
+                  <span class="font12 fontbold text-positive">
+                    {{ formatCurrencyValue(getCartDiscountItemTotal(item)) }}
+                  </span>
+                </div>
+              </div>
+            </div>
+
+            <q-btn
+              color="negative"
+              dense
+              flat
+              icon="mdi-delete-outline"
+              round
+              @click="cart.removeItem(index)"
+            />
+          </q-card-section>
+        </q-card>
+      </div>
+
+      <div class="q-px-md q-pt-md">
+        <q-card :flat="false" class="checkout-card bg-surface shadow-card card-border">
+          <q-card-section class="q-pa-md">
+            <div class="row items-center no-wrap q-mb-sm">
+              <span class="font14 fontbold text-primary">
+                {{ $t('schedule_cart.summary') }}
+              </span>
+
+              <q-space />
+            </div>
+
+            <div class="summary-row row items-center no-wrap q-py-xs">
+              <span class="font12 text-grey-7">
+                {{ $t('schedule_cart.provider') }}
+              </span>
+
+              <q-space />
+
+              <span class="font12 fontbold text-text text-right">
+                {{ providerName }}
+              </span>
+            </div>
+
+            <div class="summary-row row items-center no-wrap q-py-xs">
+              <span class="font12 text-grey-7">
+                {{ $t('schedule_cart.address') }}
+              </span>
+
+              <q-space />
+
+              <span class="font12 fontbold text-text text-right">
+                {{ primaryAddressLabel }}
+              </span>
+            </div>
+
+            <div class="summary-row row items-center no-wrap q-py-xs">
+              <span class="font12 text-grey-7">
+                {{ $t('schedule_cart.quantity') }}
+              </span>
+
+              <q-space />
+
+              <span class="font12 fontbold text-text">
+                {{ $t('schedule_cart.schedule_count', { count: cart.items.length }) }}
+              </span>
+            </div>
+
+            <div class="summary-row row items-center no-wrap q-py-xs">
+              <span class="font12 fontbold text-primary">
+                {{ $t('schedule_cart.pix_total') }}
+              </span>
+
+              <q-space />
+
+              <span class="font14 fontbold text-primary">
+                {{ formatCurrencyValue(pixTotalAmount) }}
+              </span>
+            </div>
+
+            <div v-if="hasCartDiscount" class="summary-row row items-center no-wrap q-py-xs">
+              <span class="font12 fontbold text-positive">
+                {{ $t('schedule_cart.cart_discount_total') }}
+              </span>
+
+              <q-space />
+
+              <span class="font14 fontbold text-positive">
+                {{ formatCurrencyValue(cartDiscountTotalAmount) }}
+              </span>
+            </div>
+
+            <div v-if="pixDiscountAmount > 0" class="summary-discount row items-center no-wrap q-py-xs q-px-sm q-mt-sm">
+              <q-icon color="positive" name="mdi-tag-outline" size="18px" />
+
+              <span class="font12 fontbold text-positive q-ml-xs">
+                {{ $t('schedule_cart.cart_fee_discount', { value: formatPrice(pixDiscountAmount) }) }}
+              </span>
+            </div>
+          </q-card-section>
+
+          <q-card-actions class="q-px-md q-pb-md">
+            <q-btn
+              class="full-width"
+              color="primary"
+              no-caps
+              padding="10px 16px"
+              rounded
+              unelevated
+              :disable="addressLoading || !platformFeesReady"
+              :label="$t('schedule_cart.close_cart')"
+              :loading="submitting"
+              @click="submitCart"
+            />
+          </q-card-actions>
+        </q-card>
+      </div>
+    </template>
+  </q-page>
+</template>
+
+<script setup>
+import { computed, onMounted, ref } from "vue";
+import { useRouter } from "vue-router";
+import { useI18n } from "vue-i18n";
+import { useQuasar } from "quasar";
+import { getAddresses } from "src/api/address";
+import { createScheduleCart } from "src/api/cart";
+import { usePaymentPlatformFees } from "src/composables/usePaymentPlatformFees";
+import { useScheduleCartStore } from "src/stores/scheduleCart";
+import { userStore } from "src/stores/user";
+
+const router = useRouter();
+const $q     = useQuasar();
+const { t }  = useI18n();
+
+const cart  = useScheduleCartStore();
+const store = userStore();
+
+const { platformFees, loadPlatformFees } = usePaymentPlatformFees();
+
+const primaryAddress = ref(null);
+const addressLoading = ref(false);
+const submitting     = ref(false);
+
+const cartDiscountMessage = computed(() => {
+  if (hasCartDiscount.value) {
+    return t("schedule_cart.cart_discount_applied");
+  }
+
+  return t("schedule_cart.cart_discount_hint", { count: missingSchedulesForDiscount.value });
+});
+
+const cartDiscountTotalAmount = computed(() => roundNullableMoney(sumCartItems(getCartDiscountItemTotal)));
+
+const hasCartDiscount = computed(() => cart.items.length >= 3);
+
+const missingSchedulesForDiscount = computed(() => Math.max(3 - cart.items.length, 0));
+
+const platformFeesReady = computed(() =>
+  isValidFee(platformFees.value.pix)         &&
+  isValidFee(platformFees.value.credit_card) &&
+  (!hasCartDiscount.value || isValidFee(platformFees.value.cart_min_3_schedules))
+);
+
+const pixDiscountAmount = computed(() => {
+  if (!hasCartDiscount.value) return 0;
+
+  return roundNullableMoney(sumCartItems((item) => {
+    const standardPixItemTotal   = getStandardPixItemTotal(item);
+    const discountedPixItemTotal = getCartDiscountItemTotal(item);
+
+    if (standardPixItemTotal === null || discountedPixItemTotal === null) return null;
+
+    return standardPixItemTotal - discountedPixItemTotal;
+  })) ?? 0;
+});
+
+const pixTotalAmount = computed(() => roundNullableMoney(sumCartItems(getStandardPixItemTotal)));
+
+const primaryAddressLabel = computed(() => {
+  if (addressLoading.value)  return t("schedule_cart.loading");
+  if (!primaryAddress.value) return t("schedule_cart.no_primary_address");
+
+  return [
+    primaryAddress.value.address,
+    primaryAddress.value.number,
+    primaryAddress.value.district,
+  ].filter(Boolean).join(", ");
+});
+
+const providerName = computed(() => {
+  const providerNames = [...new Set(cart.items.map((item) => item.provider_name).filter(Boolean))];
+
+  if (providerNames.length === 0) return t("schedule_cart.unknown");
+  if (providerNames.length === 1) return providerNames[0];
+
+  return t("schedule_cart.multiple_providers", { count: providerNames.length });
+});
+
+const formatCartDate = (value) => {
+  const parsed = new Date(`${normalizeDate(value)}T12:00:00`);
+
+  return new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "2-digit", year: "numeric" }).format(parsed);
+};
+
+const formatCurrencyValue = (value) => {
+  if (value === null || value === undefined || !Number.isFinite(Number(value))) {
+    return t("schedule_cart.loading");
+  }
+
+  return t("schedule_cart.currency_value", { value: formatPrice(value) });
+};
+
+const formatPrice = (value) => {
+  if (value === null || value === undefined || !Number.isFinite(Number(value))) {
+    return t("schedule_cart.loading");
+  }
+
+  return Number(value ?? 0).toLocaleString("pt-BR", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+};
+
+const getCartDiscountItemTotal = (item) => getTotalWithFee(item, platformFees.value.cart_min_3_schedules);
+
+const getCreditCardItemTotal = (item) => getTotalWithFee(item, platformFees.value.credit_card);
+
+const getProviderInitial = (item) => item.provider_name?.slice(0, 1).toUpperCase() ?? t("schedule_cart.provider_initial");
+
+const getStandardPixItemTotal = (item) => getTotalWithFee(item, platformFees.value.pix);
+
+const getTotalWithFee = (item, fee) => {
+  if (!isValidFee(fee)) return null;
+
+  return roundMoney(Number(item.total_amount ?? 0) * (1 + Number(fee)));
+};
+
+const isValidFee = (fee) => fee !== null && fee !== undefined && Number.isFinite(Number(fee));
+
+const loadPrimaryAddress = async () => {
+  const clientId = store.user?.client_id ?? store.user?.client?.id;
+
+  if (!clientId) return;
+
+  addressLoading.value = true;
+
+  try {
+    const addresses = await getAddresses("client", clientId);
+
+    primaryAddress.value = (addresses ?? []).find((address) => address.is_primary) ?? null;
+  } catch {
+    primaryAddress.value = null;
+  } finally {
+    addressLoading.value = false;
+  }
+};
+
+const normalizeDate = (value) => String(value ?? "").replace(/\//g, "-").slice(0, 10);
+
+const roundMoney = (value) => Number(Number(value ?? 0).toFixed(2));
+
+const roundNullableMoney = (value) => {
+  if (value === null || value === undefined || !Number.isFinite(Number(value))) return null;
+
+  return roundMoney(value);
+};
+
+const submitCart = async () => {
+  if (!cart.items.length || submitting.value) return;
+
+  if (!primaryAddress.value) {
+    $q.notify({ type: "warning", message: t("schedule_cart.no_primary_address_notify") });
+
+    return;
+  }
+
+  const payload = {
+    client_id:     store.user.client_id ?? store.user?.client?.id,
+    address_id:    primaryAddress.value.id,
+    schedule_type: "default",
+
+    schedules: cart.items.map((item) => ({
+      provider_id:  item.provider_id,
+      date:         item.date,
+      period_type:  item.period_type,
+      start_time:   item.start_time,
+      end_time:     item.end_time,
+      total_amount: item.total_amount,
+      offers_meal:  item.offers_meal,
+    })),
+  };
+
+  submitting.value = true;
+
+  try {
+    const createdCart = await createScheduleCart(payload);
+
+    const createdSchedules = createdCart?.schedules ?? [];
+
+    const scheduleIds = createdCart?.schedule_ids ?? createdSchedules.map((schedule) => schedule.id);
+
+    cart.setLastCreatedGroup({
+      local_group_id: createdCart?.id ?? `cart-${Date.now()}`,
+      cart_id:        createdCart?.id ?? null,
+      schedule_ids:   scheduleIds,
+      schedules:      createdSchedules,
+    });
+
+    cart.clear();
+
+    $q.notify({ type: "positive", message: t("schedule_cart.cart_closed_success") });
+  } catch (error) {
+    const message = error?.response?.data?.message ?? error?.message ?? t("schedule_cart.submit_error");
+
+    $q.notify({ type: "negative", message });
+  } finally {
+    submitting.value = false;
+  }
+};
+
+const sumCartItems = (callback) => {
+  let total = 0;
+
+  for (const item of cart.items) {
+    const itemTotal = callback(item);
+
+    if (itemTotal === null) return null;
+
+    total += itemTotal;
+  }
+
+  return total;
+};
+
+onMounted(() => {
+  loadPrimaryAddress();
+  loadPlatformFees().catch(() => {});
+});
+</script>
+
+<style scoped lang="scss">
+.schedule-cart-page {
+  min-height: 100%;
+}
+
+.shadow-cart {
+  box-shadow: 0 1px 8px rgba(0, 0, 0, 0.1);
+}
+
+.cart-item-card,
+.checkout-card,
+.empty-card,
+.created-card {
+  border-radius: 8px;
+}
+
+.summary-row {
+  border-top: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+.item-prices {
+  max-width: 220px;
+}
+
+.cart-discount-banner {
+  background: rgba(34, 197, 94, 0.1);
+  border: 1px solid rgba(34, 197, 94, 0.22);
+  border-radius: 8px;
+}
+
+.summary-discount {
+  background: rgba(34, 197, 94, 0.12);
+  border-radius: 8px;
+}
+</style>

+ 177 - 102
src/pages/search/components/OrderSummaryDialog.vue

@@ -3,7 +3,8 @@
     <div class="dialog-root">
     <div class="dialog-root">
 
 
       <div class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white">
       <div class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white">
-        <q-btn v-close-popup flat round dense icon="mdi-chevron-left" color="primary" />
+        <q-btn v-close-popup color="primary" dense flat icon="mdi-chevron-left" round />
+
         <div class="col text-center font16 fontbold gradient-diarista">
         <div class="col text-center font16 fontbold gradient-diarista">
           {{ $t('scheduling_page.title') }}
           {{ $t('scheduling_page.title') }}
         </div>
         </div>
@@ -17,6 +18,7 @@
             <div class="font14 fontbold text-primary">
             <div class="font14 fontbold text-primary">
               {{ $t('scheduling_page.order_summary.info_text') }}
               {{ $t('scheduling_page.order_summary.info_text') }}
             </div>
             </div>
+
             <div class="font12 fontmedium text-primary q-mt-xs">
             <div class="font12 fontmedium text-primary q-mt-xs">
               {{ $t('scheduling_page.order_summary.info_note') }}
               {{ $t('scheduling_page.order_summary.info_note') }}
             </div>
             </div>
@@ -37,39 +39,50 @@
             <q-card-section class="q-pa-md row items-center no-wrap">
             <q-card-section class="q-pa-md row items-center no-wrap">
               <div class="col text-center">
               <div class="col text-center">
                 <div class="text-text">
                 <div class="text-text">
-                  <span class="font14 fontregular">{{ $t('scheduling_page.order_summary.service_label') }}</span>
-                  <span class="font14 fontbold">{{ ` ${booking.serviceType.label} (${booking.serviceType.hours})` }}</span>
+                  <span class="font14 fontregular">
+                    {{ $t('scheduling_page.order_summary.service_label') }}
+                  </span>
+
+                  <span class="font14 fontbold">
+                    {{ ` ${booking.serviceType.label} (${booking.serviceType.hours})` }}
+                  </span>
                 </div>
                 </div>
-                <div class="font14 fontbold text-text">{{ formatDate(booking.date) }}</div>
+
+                <div class="font14 fontbold text-text">
+                  {{ formatDate(booking.date) }}
+                </div>
+
                 <div class="font14 fontbold text-text">
                 <div class="font14 fontbold text-text">
                   {{ $t('scheduling_page.order_summary.time_range', { start: booking.slot.startHour, end: booking.slot.endHour }) }}
                   {{ $t('scheduling_page.order_summary.time_range', { start: booking.slot.startHour, end: booking.slot.endHour }) }}
                 </div>
                 </div>
               </div>
               </div>
+
               <q-btn
               <q-btn
+                color="grey-5"
                 flat round dense
                 flat round dense
                 icon="mdi-minus-circle-outline"
                 icon="mdi-minus-circle-outline"
-                color="grey-5"
                 padding="4px 8px"
                 padding="4px 8px"
                 @click="confirmRemove(idx)"
                 @click="confirmRemove(idx)"
               />
               />
             </q-card-section>
             </q-card-section>
+
             <q-btn
             <q-btn
+              class="full-width q-mt-sm"
+              color="secondary"
+              padding="4px 8px"
               unelevated rounded no-caps
               unelevated rounded no-caps
               :label="$t('scheduling_page.order_summary.send_btn')"
               :label="$t('scheduling_page.order_summary.send_btn')"
-              color="secondary"
-                padding="4px 8px"
-              class="full-width q-mt-sm"
               @click="submitOrder"
               @click="submitOrder"
             />
             />
           </q-card>
           </q-card>
 
 
           <div class="full-width text-center">
           <div class="full-width text-center">
             <q-btn
             <q-btn
-              outline rounded no-caps
-              :label="$t('scheduling_page.order_summary.add_date_btn')"
-              color="primary"
               class="q-mt-xs"
               class="q-mt-xs"
+              color="primary"
+              outline rounded no-caps
               :disable="showCalendar"
               :disable="showCalendar"
+              :label="$t('scheduling_page.order_summary.add_date_btn')"
               @click="showCalendar = true"
               @click="showCalendar = true"
             />
             />
           </div>
           </div>
@@ -79,12 +92,13 @@
           <div v-if="loadingAvailability" class="row items-center justify-center q-py-lg">
           <div v-if="loadingAvailability" class="row items-center justify-center q-py-lg">
             <q-spinner-dots color="primary" size="36px" />
             <q-spinner-dots color="primary" size="36px" />
           </div>
           </div>
+
           <div v-else class="calendar-wrapper shadow-card">
           <div v-else class="calendar-wrapper shadow-card">
             <q-date
             <q-date
               v-model="addDateValue"
               v-model="addDateValue"
-              minimal
-              color="purple"
               class=""
               class=""
+              color="purple"
+              minimal
               :first-day-of-week="0"
               :first-day-of-week="0"
               :options="dateOptions"
               :options="dateOptions"
               @update:model-value="onAddDateSelected"
               @update:model-value="onAddDateSelected"
@@ -97,17 +111,19 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, computed, onMounted } from 'vue';
-import { useDialogPluginComponent, useQuasar } from 'quasar';
+import { computed, onMounted, ref } from 'vue';
+import { createSchedule, getClientProviderBlocks } from 'src/api/schedule';
 import { date } from 'quasar';
 import { date } from 'quasar';
-import { useI18n } from 'vue-i18n';
-import { getProviderWorkingDays, getProviderBlockedDays } from 'src/api/providerAvailability';
 import { getAddresses } from 'src/api/address';
 import { getAddresses } from 'src/api/address';
-import { createSchedule, getClientProviderBlocks } from 'src/api/schedule';
+import { getProviderBlockedDays, getProviderWorkingDays } from 'src/api/providerAvailability';
+import { useDialogPluginComponent, useQuasar } from 'quasar';
+import { useI18n } from 'vue-i18n';
 import { userStore } from 'src/stores/user';
 import { userStore } from 'src/stores/user';
+import { useRouter } from 'vue-router';
+import { useScheduleCartStore } from 'src/stores/scheduleCart';
+
 import ServiceSelectionSheet from './ServiceSelectionSheet.vue';
 import ServiceSelectionSheet from './ServiceSelectionSheet.vue';
 import ServiceTimeSelectionDialog from './ServiceTimeSelectionDialog.vue';
 import ServiceTimeSelectionDialog from './ServiceTimeSelectionDialog.vue';
-import { useRouter } from 'vue-router';
 
 
 const props = defineProps({
 const props = defineProps({
   provider:       { type: Object, required: true },
   provider:       { type: Object, required: true },
@@ -116,79 +132,120 @@ const props = defineProps({
 
 
 defineEmits([...useDialogPluginComponent.emits]);
 defineEmits([...useDialogPluginComponent.emits]);
 
 
+const $q     = useQuasar();
+const router = useRouter();
+
 const { dialogRef, onDialogOK } = useDialogPluginComponent();
 const { dialogRef, onDialogOK } = useDialogPluginComponent();
-const $q = useQuasar();
+
 const { t, locale } = useI18n();
 const { t, locale } = useI18n();
-const store = userStore();
 
 
-const bookings = ref([props.initialBooking]);
-const submitting = ref(false);
-const primaryAddress = ref(null);
-const router = useRouter();
-const showCalendar = ref(false);
-const addDateValue = ref(null);
+const store        = userStore();
+const scheduleCart = useScheduleCartStore();
+
+const bookings            = ref([props.initialBooking]);
+const submitting          = ref(false);
+const primaryAddress      = ref(null);
+const showCalendar        = ref(false);
+const addDateValue        = ref(null);
 const loadingAvailability = ref(false);
 const loadingAvailability = ref(false);
-const workingDays = ref([]);
-const blockedDays = ref([]);
-const providerClientBlocks = ref({
-  existing_schedules: [],
-  fully_blocked_weeks: [],
-});
+const workingDays         = ref([]);
+const blockedDays         = ref([]);
 
 
-const normalizeDate = (d) => d.replace(/\//g, '-');
+const providerClientBlocks = ref({ existing_schedules: [], fully_blocked_weeks: [] });
 
 
-const getWeekStart = (dateStr) => {
-  const d = new Date(normalizeDate(dateStr) + 'T12:00:00');
-  d.setDate(d.getDate() - d.getDay());
-  return d.toISOString().slice(0, 10);
-};
+const availableWeekDays = computed(() =>
+  [...new Set(workingDays.value.map(wd => wd.day))]
+);
+
+const blockedDateSet = computed(() =>
+  new Set(blockedDays.value.filter(bd => bd.period === 'all').map(bd => bd.date))
+);
 
 
 const blockedWeekStartSet = computed(() =>
 const blockedWeekStartSet = computed(() =>
   new Set(providerClientBlocks.value.fully_blocked_weeks ?? [])
   new Set(providerClientBlocks.value.fully_blocked_weeks ?? [])
 );
 );
 
 
-const getServerWeekCount = (newDateStr) => {
+const confirmRemove = (idx) => {
+  $q.dialog({
+    title:      t('scheduling_page.order_summary.remove_confirm_title'),
+    cancel:     { label: t('scheduling_page.order_summary.remove_confirm_cancel'), flat: true, color: 'grey-6' },
+    ok:         { label: t('scheduling_page.order_summary.remove_confirm_ok'), unelevated: true, color: 'primary', rounded: true, noCaps: true },
+    persistent: true,
+  }).onOk(() => {
+    bookings.value.splice(idx, 1);
+  });
+};
+
+const dateOptions = (d) => {
+  const today = date.formatDate(new Date(), 'YYYY/MM/DD');
+
+  if (d < today)               return false;
+  if (wouldExceedWeekLimit(d)) return false;
+
+  const raw = normalizeDate(d);
+
+  const parsed = new Date(`${raw}T12:00:00`);
+
+  const dayOfWeek     = parsed.getDay();
+  const isWorking     = availableWeekDays.value.includes(dayOfWeek);
+  const isBlocked     = blockedDateSet.value.has(raw);
+  const isWeekBlocked = blockedWeekStartSet.value.has(getWeekStart(raw));
+
+  return isWorking && !isBlocked && !isWeekBlocked;
+};
+
+const formatDate = (dateStr) => {
+  const d = new Date(normalizeDate(dateStr) + 'T12:00:00');
+
+  const localeMap = { pt: 'pt-BR', en: 'en-US', es: 'es-ES' };
+
+  const loc = localeMap[locale.value] ?? 'pt-BR';
+
+  const weekday       = new Intl.DateTimeFormat(loc, { weekday: 'long' }).format(d);
+  const dateFormatted = new Intl.DateTimeFormat(loc, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(d);
+
+  return `${weekday.charAt(0).toUpperCase() + weekday.slice(1)}, ${dateFormatted}`;
+};
+
+const formatHour = (h) => `${String(h).padStart(2, '0')}:00`;
+
+const getCartWeekCount = (newDateStr) => {
   const newWeek = getWeekStart(newDateStr);
   const newWeek = getWeekStart(newDateStr);
-  return (providerClientBlocks.value.existing_schedules ?? []).filter(
-    (schedule) => getWeekStart(schedule.date) === newWeek
+
+  return scheduleCart.items.filter((item) =>
+    Number(item.provider_id) === Number(props.provider.provider_id) &&
+    getWeekStart(item.date) === newWeek
   ).length;
   ).length;
 };
 };
 
 
 const getLocalWeekCount = (newDateStr) => {
 const getLocalWeekCount = (newDateStr) => {
   const newWeek = getWeekStart(newDateStr);
   const newWeek = getWeekStart(newDateStr);
+
   return bookings.value.filter((booking) => getWeekStart(booking.date) === newWeek).length;
   return bookings.value.filter((booking) => getWeekStart(booking.date) === newWeek).length;
 };
 };
 
 
-const wouldExceedWeekLimit = (newDateStr) => {
-  const count = getServerWeekCount(newDateStr) + getLocalWeekCount(newDateStr);
-  return count >= 2;
+const getServerWeekCount = (newDateStr) => {
+  const newWeek = getWeekStart(newDateStr);
+
+  return (providerClientBlocks.value.existing_schedules ?? []).filter(
+    (schedule) => getWeekStart(schedule.date) === newWeek
+  ).length;
 };
 };
 
 
-const availableWeekDays = computed(() =>
-  [...new Set(workingDays.value.map(wd => wd.day))]
-);
+const getWeekStart = (dateStr) => {
+  const d = new Date(normalizeDate(dateStr) + 'T12:00:00');
 
 
-const blockedDateSet = computed(() =>
-  new Set(blockedDays.value.filter(bd => bd.period === 'all').map(bd => bd.date))
-);
+  d.setDate(d.getDate() - d.getDay());
 
 
-const dateOptions = (d) => {
-  const today = date.formatDate(new Date(), 'YYYY/MM/DD');
-  if (d < today) return false;
-  if (wouldExceedWeekLimit(d)) return false;
-  const raw = normalizeDate(d);
-  const parsed = new Date(`${raw}T12:00:00`);
-  const dayOfWeek = parsed.getDay();
-  const isWorking = availableWeekDays.value.includes(dayOfWeek);
-  const isBlocked = blockedDateSet.value.has(raw);
-  const isWeekBlocked = blockedWeekStartSet.value.has(getWeekStart(raw));
-  return isWorking && !isBlocked && !isWeekBlocked;
+  return d.toISOString().slice(0, 10);
 };
 };
 
 
 const loadAvailability = async () => {
 const loadAvailability = async () => {
   loadingAvailability.value = true;
   loadingAvailability.value = true;
+
   try {
   try {
     const clientId = store.user?.client_id;
     const clientId = store.user?.client_id;
+
     const defaultClientBlocks = { existing_schedules: [], fully_blocked_weeks: [] };
     const defaultClientBlocks = { existing_schedules: [], fully_blocked_weeks: [] };
 
 
     const [wd, bd, clientBlocks] = await Promise.all([
     const [wd, bd, clientBlocks] = await Promise.all([
@@ -198,12 +255,15 @@ const loadAvailability = async () => {
         ? getClientProviderBlocks(clientId, props.provider.provider_id)
         ? getClientProviderBlocks(clientId, props.provider.provider_id)
         : Promise.resolve(defaultClientBlocks),
         : Promise.resolve(defaultClientBlocks),
     ]);
     ]);
+
     workingDays.value = wd ?? [];
     workingDays.value = wd ?? [];
     blockedDays.value = bd ?? [];
     blockedDays.value = bd ?? [];
+
     providerClientBlocks.value = clientBlocks ?? defaultClientBlocks;
     providerClientBlocks.value = clientBlocks ?? defaultClientBlocks;
   } catch {
   } catch {
     workingDays.value = [];
     workingDays.value = [];
     blockedDays.value = [];
     blockedDays.value = [];
+
     providerClientBlocks.value = { existing_schedules: [], fully_blocked_weeks: [] };
     providerClientBlocks.value = { existing_schedules: [], fully_blocked_weeks: [] };
   } finally {
   } finally {
     loadingAvailability.value = false;
     loadingAvailability.value = false;
@@ -213,21 +273,24 @@ const loadAvailability = async () => {
 const loadPrimaryAddress = async () => {
 const loadPrimaryAddress = async () => {
   try {
   try {
     const clientId = store.user?.client_id;
     const clientId = store.user?.client_id;
+
     if (!clientId) return;
     if (!clientId) return;
+
     const addresses = await getAddresses('client', clientId);
     const addresses = await getAddresses('client', clientId);
+
     primaryAddress.value = (addresses ?? []).find(a => a.is_primary) ?? null;
     primaryAddress.value = (addresses ?? []).find(a => a.is_primary) ?? null;
   } catch {
   } catch {
     primaryAddress.value = null;
     primaryAddress.value = null;
   }
   }
 };
 };
 
 
-onMounted(() => Promise.all([loadAvailability(), loadPrimaryAddress()]));
-
-const formatHour = (h) => `${String(h).padStart(2, '0')}:00`;
+const normalizeDate = (d) => d.replace(/\//g, '-');
 
 
 const onAddDateSelected = (val) => {
 const onAddDateSelected = (val) => {
   if (!val) return;
   if (!val) return;
+
   addDateValue.value = null;
   addDateValue.value = null;
+
   const valFormatted = normalizeDate(val);
   const valFormatted = normalizeDate(val);
 
 
   const blocksOfDate = blockedDays.value.filter(
   const blocksOfDate = blockedDays.value.filter(
@@ -235,14 +298,17 @@ const onAddDateSelected = (val) => {
   );
   );
 
 
   const dayOfWeek = new Date(`${valFormatted}T12:00:00`).getDay();
   const dayOfWeek = new Date(`${valFormatted}T12:00:00`).getDay();
+
   const dayPeriods = workingDays.value
   const dayPeriods = workingDays.value
     .filter(wd => wd.day === dayOfWeek)
     .filter(wd => wd.day === dayOfWeek)
     .map(wd => wd.period);
     .map(wd => wd.period);
 
 
   const workingDayBlocks = [];
   const workingDayBlocks = [];
+
   if (!dayPeriods.includes('afternoon')) {
   if (!dayPeriods.includes('afternoon')) {
     workingDayBlocks.push({ init_hour: '14:00:00', end_hour: '20:00:00' });
     workingDayBlocks.push({ init_hour: '14:00:00', end_hour: '20:00:00' });
   }
   }
+
   if (!dayPeriods.includes('morning')) {
   if (!dayPeriods.includes('morning')) {
     workingDayBlocks.push({ init_hour: '7:00:00', end_hour: '13:00:00' });
     workingDayBlocks.push({ init_hour: '7:00:00', end_hour: '13:00:00' });
   }
   }
@@ -258,85 +324,94 @@ const onAddDateSelected = (val) => {
     .filter((schedule) => schedule.date === valFormatted)
     .filter((schedule) => schedule.date === valFormatted)
     .map((schedule) => ({
     .map((schedule) => ({
       init_hour: schedule.start_time,
       init_hour: schedule.start_time,
-      end_hour: schedule.end_time,
+      end_hour:  schedule.end_time,
+    }));
+
+  const cartBookingBlocks = scheduleCart.items
+    .filter((item) =>
+      Number(item.provider_id) === Number(props.provider.provider_id) &&
+      normalizeDate(item.date) === valFormatted
+    )
+    .map((item) => ({
+      init_hour: item.start_time,
+      end_hour:  item.end_time,
     }));
     }));
 
 
-  const partialBlocks = [...blocksOfDate, ...workingDayBlocks, ...existingBookingBlocks, ...serverBookingBlocks];
+  const partialBlocks = [
+    ...blocksOfDate,
+    ...workingDayBlocks,
+    ...existingBookingBlocks,
+    ...serverBookingBlocks,
+    ...cartBookingBlocks,
+  ];
 
 
   $q.dialog({
   $q.dialog({
-    component: ServiceSelectionSheet,
-    componentProps: { provider: props.provider, selectedDate: val, partialBlocks },
+    component: ServiceSelectionSheet, componentProps: { provider: props.provider, selectedDate: val, partialBlocks },
   }).onOk(({ serviceType, date: date_, provider: prov }) => {
   }).onOk(({ serviceType, date: date_, provider: prov }) => {
     $q.dialog({
     $q.dialog({
-      component: ServiceTimeSelectionDialog,
-      componentProps: { serviceType, selectedDate: date_, provider: prov, partialBlocks },
+      component: ServiceTimeSelectionDialog, componentProps: { serviceType, selectedDate: date_, provider: prov, partialBlocks },
     }).onOk((booking) => {
     }).onOk((booking) => {
       if (wouldExceedWeekLimit(booking.date)) {
       if (wouldExceedWeekLimit(booking.date)) {
         $q.notify({ type: 'negative', message: t('scheduling_page.order_summary.week_limit_error') });
         $q.notify({ type: 'negative', message: t('scheduling_page.order_summary.week_limit_error') });
+
         return;
         return;
       }
       }
+
       bookings.value.push(booking);
       bookings.value.push(booking);
+
       showCalendar.value = false;
       showCalendar.value = false;
     });
     });
   });
   });
 };
 };
 
 
-const confirmRemove = (idx) => {
-  $q.dialog({
-    title: t('scheduling_page.order_summary.remove_confirm_title'),
-    cancel: { label: t('scheduling_page.order_summary.remove_confirm_cancel'), flat: true, color: 'grey-6' },
-    ok:     { label: t('scheduling_page.order_summary.remove_confirm_ok'), unelevated: true, color: 'primary', rounded: true, noCaps: true },
-    persistent: true,
-  }).onOk(() => {
-    bookings.value.splice(idx, 1);
-  });
-};
-
-const formatDate = (dateStr) => {
-  const d = new Date(normalizeDate(dateStr) + 'T12:00:00');
-  const localeMap = { pt: 'pt-BR', en: 'en-US', es: 'es-ES' };
-  const loc = localeMap[locale.value] ?? 'pt-BR';
-  const weekday = new Intl.DateTimeFormat(loc, { weekday: 'long' }).format(d);
-  const dateFormatted = new Intl.DateTimeFormat(loc, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(d);
-  return `${weekday.charAt(0).toUpperCase() + weekday.slice(1)}, ${dateFormatted}`;
-};
-
 const submitOrder = async () => {
 const submitOrder = async () => {
   if (!primaryAddress.value) {
   if (!primaryAddress.value) {
     $q.notify({ type: 'warning', message: t('scheduling_page.order_summary.no_primary_address') });
     $q.notify({ type: 'warning', message: t('scheduling_page.order_summary.no_primary_address') });
+
     return;
     return;
   }
   }
 
 
-  const payload = {
+  const payloads = bookings.value.map(b => ({
     client_id:     store.user.client_id,
     client_id:     store.user.client_id,
     provider_id:   props.provider.provider_id,
     provider_id:   props.provider.provider_id,
     address_id:    primaryAddress.value.id,
     address_id:    primaryAddress.value.id,
     schedule_type: 'default',
     schedule_type: 'default',
-    schedules: bookings.value.map(b => ({
-      date:         normalizeDate(b.date),
-      period_type:  b.serviceType.hoursCount,
-      start_time:   formatHour(b.slot.startHour),
-      end_time:     formatHour(b.slot.endHour),
-      total_amount: b.serviceType.price,
-      offers_meal:  b.meal === 'offer' ? true : b.meal === 'no_offer' ? false : null,
-    })),
-  };
+    date:          normalizeDate(b.date),
+    period_type:   b.serviceType.hoursCount,
+    start_time:    formatHour(b.slot.startHour),
+    end_time:      formatHour(b.slot.endHour),
+    total_amount:  b.serviceType.price,
+    offers_meal:   b.meal === 'offer' ? true : b.meal === 'no_offer' ? false : null,
+  }));
 
 
   submitting.value = true;
   submitting.value = true;
+
   try {
   try {
-    await createSchedule(payload);
+    await Promise.all(payloads.map((payload) => createSchedule(payload)));
+
     $q.notify({ type: 'positive', message: t('scheduling_page.order_summary.submit_success') });
     $q.notify({ type: 'positive', message: t('scheduling_page.order_summary.submit_success') });
+
     onDialogOK();
     onDialogOK();
   } catch (err) {
   } catch (err) {
     const msg = err?.response?.data?.message
     const msg = err?.response?.data?.message
       ?? err?.message
       ?? err?.message
       ?? t('scheduling_page.order_summary.submit_error');
       ?? t('scheduling_page.order_summary.submit_error');
+
     $q.notify({ type: 'negative', message: msg });
     $q.notify({ type: 'negative', message: msg });
   } finally {
   } finally {
     submitting.value = false;
     submitting.value = false;
+
     router.push({ name: 'DashboardPage' });
     router.push({ name: 'DashboardPage' });
   }
   }
 };
 };
+
+const wouldExceedWeekLimit = (newDateStr) => {
+  const count = getServerWeekCount(newDateStr) + getLocalWeekCount(newDateStr) + getCartWeekCount(newDateStr);
+
+  return count >= 2;
+};
+
+onMounted(() => Promise.all([loadAvailability(), loadPrimaryAddress()]));
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 239 - 144
src/pages/search/components/SchedulingDialog.vue

@@ -1,51 +1,99 @@
 <template>
 <template>
-  <q-dialog ref="dialogRef" maximized transition-show="slide-up" transition-hide="slide-down">
+  <q-dialog
+    ref="dialogRef"
+    maximized
+    transition-hide="slide-down"
+    transition-show="slide-up"
+  >
     <div class="dialog-root">
     <div class="dialog-root">
-
       <div class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white">
       <div class="dialog-header row items-center q-px-md q-pt-md q-pb-sm bg-white">
-        <q-btn v-close-popup flat round dense icon="mdi-chevron-left" color="primary" />
+        <q-btn
+          v-close-popup
+          color="primary"
+          dense
+          flat
+          icon="mdi-chevron-left"
+          round
+        />
+
         <div class="col text-center font16 fontbold text-primary gradient-diarista q-mb-xs">
         <div class="col text-center font16 fontbold text-primary gradient-diarista q-mb-xs">
           {{ $t('scheduling_page.title') }}
           {{ $t('scheduling_page.title') }}
         </div>
         </div>
-        <div style="width: 36px" />
+
+        <div style="width: 36px"></div>
       </div>
       </div>
 
 
       <div class="dialog-body">
       <div class="dialog-body">
-
         <div class="q-px-md q-pt-md">
         <div class="q-px-md q-pt-md">
           <div class="font16 fontbold gradient-diarista q-mb-xs">
           <div class="font16 fontbold gradient-diarista q-mb-xs">
             {{ $t('scheduling_page.about_provider') }}
             {{ $t('scheduling_page.about_provider') }}
           </div>
           </div>
-          <q-card class="card-border shadow-card bg-surface text-text" :flat="false">
+
+          <q-card :flat="false" class="card-border shadow-card bg-surface text-text">
             <q-card-section class="q-pa-md">
             <q-card-section class="q-pa-md">
               <div class="row items-center no-wrap q-gutter-x-md">
               <div class="row items-center no-wrap q-gutter-x-md">
                 <div class="col-2">
                 <div class="col-2">
                   <q-avatar :style="avatarColors[provider.provider_id % avatarColors.length]">
                   <q-avatar :style="avatarColors[provider.provider_id % avatarColors.length]">
-                    <img v-if="provider.provider_photo" :src="provider.provider_photo" style="object-fit:cover;border-radius:50%;" />
-                    <span v-else>{{ provider.provider_name?.slice(0,1) ?? '—' }}</span>
+                    <img
+                      v-if="provider.provider_photo"
+                      :src="provider.provider_photo"
+                      style="object-fit:cover;border-radius:50%;"
+                    />
+
+                    <span v-else>
+                      {{ provider.provider_name?.slice(0,1) ?? '—' }}
+                    </span>
                   </q-avatar>
                   </q-avatar>
                 </div>
                 </div>
+
                 <div class="col-3 column">
                 <div class="col-3 column">
-                  <div class="font12 fontbold text-text">{{ provider?.provider_name ?? '—' }}</div>
-                  <div class="font10 fontmedium text-grey-7">{{ provider?.district ?? '—' }}</div>
+                  <div class="font12 fontbold text-text">
+                    {{ provider?.provider_name ?? '—' }}
+                  </div>
+
+                  <div class="font10 fontmedium text-grey-7">
+                    {{ provider?.district ?? '—' }}
+                  </div>
                 </div>
                 </div>
+
                 <div class="col-3">
                 <div class="col-3">
                   <div class="row items-center q-gutter-x-md q-mt-xs">
                   <div class="row items-center q-gutter-x-md q-mt-xs">
                     <div class="row items-center">
                     <div class="row items-center">
-                      <q-icon name="mdi-star" color="warning" size="12px" />
+                      <q-icon color="warning" name="mdi-star" size="12px" />
+
                       <span class="font9 fontmedium q-ml-xs">
                       <span class="font9 fontmedium q-ml-xs">
                         {{ (provider?.average_rating != null ? Number(provider.average_rating).toFixed(1) : '') + ' (' + (provider?.total_reviews ?? 0) + ')' }}
                         {{ (provider?.average_rating != null ? Number(provider.average_rating).toFixed(1) : '') + ' (' + (provider?.total_reviews ?? 0) + ')' }}
                       </span>
                       </span>
                     </div>
                     </div>
+
                     <div class="row items-center">
                     <div class="row items-center">
-                      <q-icon name="mdi-broom" color="secondary" size="14px" />
-                      <span class="font9 fontmedium q-ml-xs">{{ provider?.total_services ?? 0 }}</span>
+                      <q-icon color="secondary" name="mdi-broom" size="14px" />
+
+                      <span class="font9 fontmedium q-ml-xs">
+                        {{ provider?.total_services ?? 0 }}
+                      </span>
                     </div>
                     </div>
                   </div>
                   </div>
                 </div>
                 </div>
+
                 <div class="col-2 column items-center q-gutter-y-xs">
                 <div class="col-2 column items-center q-gutter-y-xs">
-                  <q-btn flat round dense icon="mdi-heart-outline" color="pink-4" size="sm" />
-                  <q-btn flat round dense icon="mdi-information-outline" color="grey-5" size="sm" />
+                  <q-btn
+                    color="pink-4"
+                    dense
+                    flat
+                    icon="mdi-heart-outline"
+                    round
+                    size="sm"
+                  />
+
+                  <q-btn
+                    color="grey-5"
+                    dense
+                    flat
+                    icon="mdi-information-outline"
+                    round
+                    size="sm"
+                  />
                 </div>
                 </div>
               </div>
               </div>
             </q-card-section>
             </q-card-section>
@@ -64,11 +112,11 @@
           <div v-else class="calendar-wrapper shadow-card q-mb-md">
           <div v-else class="calendar-wrapper shadow-card q-mb-md">
             <q-date
             <q-date
               v-model="selectedDate"
               v-model="selectedDate"
-              square
               class="full-width"
               class="full-width"
+              minimal
+              square
               :first-day-of-week="0"
               :first-day-of-week="0"
               :options="dateOptions"
               :options="dateOptions"
-              minimal
               @update:model-value="onDateSelected"
               @update:model-value="onDateSelected"
             />
             />
           </div>
           </div>
@@ -79,14 +127,22 @@
             <div class="font16 fontbold gradient-diarista">
             <div class="font16 fontbold gradient-diarista">
               {{ $t('scheduling_page.reviews_title') }}
               {{ $t('scheduling_page.reviews_title') }}
             </div>
             </div>
+
             <span class=" text-text cursor-pointer">
             <span class=" text-text cursor-pointer">
               {{ $t('scheduling_page.see_all') }}
               {{ $t('scheduling_page.see_all') }}
-              <q-icon name="mdi-chevron-right" class="text-text" /> 
+
+              <q-icon
+                class="text-text"
+                name="mdi-chevron-right"
+              />
             </span>
             </span>
           </div>
           </div>
 
 
           <div v-if="loadingReviews" class="row items-center justify-center q-py-md">
           <div v-if="loadingReviews" class="row items-center justify-center q-py-md">
-            <q-spinner-dots color="primary" size="36px" />
+            <q-spinner-dots
+              color="primary"
+              size="36px"
+            />
           </div>
           </div>
 
 
           <div v-else-if="reviews.length === 0" class="text-center text-grey-6 text-body2 q-py-md">
           <div v-else-if="reviews.length === 0" class="text-center text-grey-6 text-body2 q-py-md">
@@ -97,18 +153,27 @@
             <q-card
             <q-card
               v-for="review in reviews"
               v-for="review in reviews"
               :key="review.id"
               :key="review.id"
-              class="review-card card-border bg-white q-mr-sm shadow-card"
               :flat="false"
               :flat="false"
+              class="review-card card-border bg-white q-mr-sm shadow-card"
             >
             >
               <q-card-section class="q-pa-sm">
               <q-card-section class="q-pa-sm">
                 <div class="row items-center no-wrap q-gutter-x-sm q-mb-xs">
                 <div class="row items-center no-wrap q-gutter-x-sm q-mb-xs">
                   <q-avatar :style="avatarColors[review.schedule?.client?.id % avatarColors.length]">
                   <q-avatar :style="avatarColors[review.schedule?.client?.id % avatarColors.length]">
-                    <img v-if="review.schedule?.client?.profile_photo" :src="review.schedule?.client?.profile_photo" style="object-fit:cover;border-radius:50%;" />
-                    <span v-else>{{review.schedule?.client?.name?.slice(0,1) ?? '—' }}</span>
+                    <img
+                      v-if="review.schedule?.client?.profile_photo"
+                      :src="review.schedule?.client?.profile_photo"
+                      style="object-fit:cover;border-radius:50%;"
+                    />
+
+                    <span v-else>
+                      {{review.schedule?.client?.name?.slice(0,1) ?? '—' }}
+                    </span>
                   </q-avatar>
                   </q-avatar>
+
                   <div class="col-3 text-text font10 fontbold">
                   <div class="col-3 text-text font10 fontbold">
                     {{ review.schedule?.client?.name ?? $t('scheduling_page.unknown_client') }}
                     {{ review.schedule?.client?.name ?? $t('scheduling_page.unknown_client') }}
                   </div>
                   </div>
+
                   <div class="row items-center q-mb-xs q-my-auto">
                   <div class="row items-center q-mb-xs q-my-auto">
                     <q-icon
                     <q-icon
                       v-for="s in 5"
                       v-for="s in 5"
@@ -119,6 +184,7 @@
                     />
                     />
                   </div>
                   </div>
                 </div>
                 </div>
+
                 <div class="font9 fontregular text-text review-comment">
                 <div class="font9 fontregular text-text review-comment">
                   {{ review.comment ?? '--------' }}
                   {{ review.comment ?? '--------' }}
                 </div>
                 </div>
@@ -132,36 +198,40 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import { ref, computed, onMounted } from 'vue';
-import { useDialogPluginComponent, useQuasar } from 'quasar';
+import { computed, onMounted, ref } from 'vue';
 import { date } from 'quasar';
 import { date } from 'quasar';
-import { getProviderWorkingDays, getProviderBlockedDays } from 'src/api/providerAvailability';
 import { getClientProviderBlocks } from 'src/api/schedule';
 import { getClientProviderBlocks } from 'src/api/schedule';
+import { getProviderBlockedDays, getProviderWorkingDays } from 'src/api/providerAvailability';
 import { getProviderReceivedReviews } from 'src/api/review';
 import { getProviderReceivedReviews } from 'src/api/review';
+import { useDialogPluginComponent, useQuasar } from 'quasar';
+import { useI18n } from 'vue-i18n';
+import { useScheduleCartStore } from 'src/stores/scheduleCart';
 import { userStore } from 'src/stores/user';
 import { userStore } from 'src/stores/user';
+import OrderSummaryDialog from './OrderSummaryDialog.vue';
 import ServiceSelectionSheet from './ServiceSelectionSheet.vue';
 import ServiceSelectionSheet from './ServiceSelectionSheet.vue';
 import ServiceTimeSelectionDialog from './ServiceTimeSelectionDialog.vue';
 import ServiceTimeSelectionDialog from './ServiceTimeSelectionDialog.vue';
-import OrderSummaryDialog from './OrderSummaryDialog.vue'
 
 
 const props = defineProps({
 const props = defineProps({
-  provider: {
-    type: Object,
-    required: true,
-  },
+  provider: { required: true, type: Object },
 });
 });
 
 
 defineEmits([...useDialogPluginComponent.emits]);
 defineEmits([...useDialogPluginComponent.emits]);
 
 
 const { dialogRef } = useDialogPluginComponent();
 const { dialogRef } = useDialogPluginComponent();
-const $q = useQuasar();
+
+const $q    = useQuasar();
+const { t } = useI18n();
+
 const store = userStore();
 const store = userStore();
 
 
-const selectedDate = ref(null);
-const workingDays = ref([]);
-const blockedDays = ref([]);
+const scheduleCart = useScheduleCartStore();
+
+const selectedDate        = ref(null);
+const workingDays         = ref([]);
+const blockedDays         = ref([]);
 const loadingAvailability = ref(true);
 const loadingAvailability = ref(true);
-const reviews = ref([]);
-const loadingReviews = ref(true);
+const reviews             = ref([]);
+const loadingReviews      = ref(true);
 
 
 const avatarColors = [
 const avatarColors = [
   { background: '#ffd5df', color: '#932e57' },
   { background: '#ffd5df', color: '#932e57' },
@@ -171,17 +241,13 @@ const avatarColors = [
 ];
 ];
 
 
 const bookings = ref([]);
 const bookings = ref([]);
-const providerClientBlocks = ref({
-  existing_schedules: [],
-  fully_blocked_weeks: [],
-});
 
 
+const providerClientBlocks = ref({ existing_schedules: [], fully_blocked_weeks: [] });
 
 
 const availableWeekDays = computed(() =>
 const availableWeekDays = computed(() =>
   [...new Set(workingDays.value.map((wd) => wd.day))]
   [...new Set(workingDays.value.map((wd) => wd.day))]
 );
 );
 
 
-
 const blockedDateSet = computed(() =>
 const blockedDateSet = computed(() =>
   new Set(
   new Set(
     blockedDays.value
     blockedDays.value
@@ -190,71 +256,124 @@ const blockedDateSet = computed(() =>
   )
   )
 );
 );
 
 
+const blockedWeekStartSet = computed(() =>
+  new Set(providerClientBlocks.value.fully_blocked_weeks ?? [])
+);
 
 
-const normalizeDate = (dateStr) => (dateStr ?? '').replace(/\//g, '-');
+const dateOptions = (d) => {
+  const today = date.formatDate(new Date(), 'YYYY/MM/DD');
 
 
-const getWeekStart = (dateStr) => {
-  const normalizedDate = normalizeDate(dateStr);
-  const d = new Date(`${normalizedDate}T12:00:00`);
-  d.setDate(d.getDate() - d.getDay());
-  return d.toISOString().split('T')[0];
+  if (d < today) return false;
+
+  const raw       = normalizeDate(d);
+  const parsed    = new Date(`${raw}T12:00:00`);
+  const dayOfWeek = parsed.getDay();
+
+  const isWorkingDay  = availableWeekDays.value.includes(dayOfWeek);
+  const isBlocked     = blockedDateSet.value.has(raw);
+  const isWeekBlocked = blockedWeekStartSet.value.has(getWeekStart(raw));
+
+  if (!isWorkingDay || isBlocked || isWeekBlocked) return false;
+
+  if (wouldExceedWeekLimit(raw)) return false;
+
+  return true;
 };
 };
 
 
-const blockedWeekStartSet = computed(() =>
-  new Set(providerClientBlocks.value.fully_blocked_weeks ?? [])
-);
+const formatHour = (h) => `${String(h).padStart(2, '0')}:00`;
 
 
-const getServerWeekCount = (selectedDate) => {
+const getCartWeekCount = (selectedDate) => {
   const weekStart = getWeekStart(selectedDate);
   const weekStart = getWeekStart(selectedDate);
-  return (providerClientBlocks.value.existing_schedules ?? []).filter(
-    (schedule) => getWeekStart(schedule.date) === weekStart
+
+  return scheduleCart.items.filter((item) =>
+    Number(item.provider_id) === Number(props.provider.provider_id) &&
+    getWeekStart(item.date) === weekStart
   ).length;
   ).length;
 };
 };
 
 
 const getLocalWeekCount = (selectedDate) => {
 const getLocalWeekCount = (selectedDate) => {
   const weekStart = getWeekStart(selectedDate);
   const weekStart = getWeekStart(selectedDate);
+
   return bookings.value.filter((booking) => getWeekStart(booking.date) === weekStart).length;
   return bookings.value.filter((booking) => getWeekStart(booking.date) === weekStart).length;
 };
 };
 
 
-const wouldExceedWeekLimit = (selectedDate) => {
-  const serverCount = getServerWeekCount(selectedDate);
-  const localCount = getLocalWeekCount(selectedDate);
-  return (serverCount + localCount) >= 2;
+const getServerWeekCount = (selectedDate) => {
+  const weekStart = getWeekStart(selectedDate);
+
+  return (providerClientBlocks.value.existing_schedules ?? []).filter(
+    (schedule) => getWeekStart(schedule.date) === weekStart
+  ).length;
 };
 };
 
 
+const getWeekStart = (dateStr) => {
+  const normalizedDate = normalizeDate(dateStr);
 
 
-const dateOptions = (d) => {
-  const today = date.formatDate(new Date(), 'YYYY/MM/DD');
-  if (d < today) return false;
+  const d = new Date(`${normalizedDate}T12:00:00`);
 
 
-  const raw = normalizeDate(d);
-  const parsed = new Date(`${raw}T12:00:00`);
-  const dayOfWeek = parsed.getDay();
+  d.setDate(d.getDate() - d.getDay());
 
 
-  const isWorkingDay = availableWeekDays.value.includes(dayOfWeek);
-  const isBlocked = blockedDateSet.value.has(raw);
-  const isWeekBlocked = blockedWeekStartSet.value.has(getWeekStart(raw));
+  return d.toISOString().split('T')[0];
+};
 
 
-  if (!isWorkingDay || isBlocked || isWeekBlocked) return false;
+const loadAvailability = async () => {
+  loadingAvailability.value = true;
 
 
-  if (wouldExceedWeekLimit(raw)) return false;
+  try {
+    const clientId = store.user?.client?.id;
 
 
-  return true;
+    const defaultClientBlocks = { existing_schedules: [], fully_blocked_weeks: [] };
+
+    const [wd, bd, clientBlocks] = await Promise.all([
+      getProviderWorkingDays(props.provider.provider_id),
+      getProviderBlockedDays(props.provider.provider_id),
+      clientId
+        ? getClientProviderBlocks(clientId, props.provider.provider_id)
+        : Promise.resolve(defaultClientBlocks),
+    ]);
+
+    blockedDays.value = bd ?? [];
+
+    providerClientBlocks.value = clientBlocks ?? defaultClientBlocks;
+
+    workingDays.value = wd ?? [];
+  } catch {
+    blockedDays.value = [];
+
+    providerClientBlocks.value = { existing_schedules: [], fully_blocked_weeks: [] };
+
+    workingDays.value = [];
+  } finally {
+    loadingAvailability.value = false;
+  }
 };
 };
 
 
+const loadReviews = async () => {
+  loadingReviews.value = true;
+
+  try {
+    const all = await getProviderReceivedReviews(props.provider.provider_id);
+
+    reviews.value = (all ?? []).slice(0, 10);
+  } catch {
+    reviews.value = [];
+  } finally {
+    loadingReviews.value = false;
+  }
+};
+
+const normalizeDate = (dateStr) => (dateStr ?? '').replace(/\//g, '-');
 
 
 const onDateSelected = (val) => {
 const onDateSelected = (val) => {
   if (!val) return;
   if (!val) return;
 
 
   selectedDate.value = null;
   selectedDate.value = null;
-  const valFormatted = normalizeDate(val);
 
 
+  const valFormatted = normalizeDate(val);
 
 
   const blocksOfDate = blockedDays.value.filter(
   const blocksOfDate = blockedDays.value.filter(
     (bd) => bd.date === valFormatted && bd.period !== 'all'
     (bd) => bd.date === valFormatted && bd.period !== 'all'
   );
   );
 
 
-  
   const dayOfWeek = new Date(`${valFormatted}T12:00:00`).getDay();
   const dayOfWeek = new Date(`${valFormatted}T12:00:00`).getDay();
 
 
   const dayPeriods = workingDays.value
   const dayPeriods = workingDays.value
@@ -271,103 +390,79 @@ const onDateSelected = (val) => {
     workingDayBlocks.push({ init_hour: '07:00:00', end_hour: '13:00:00' });
     workingDayBlocks.push({ init_hour: '07:00:00', end_hour: '13:00:00' });
   }
   }
 
 
-  
   const localBookingBlocks = bookings.value
   const localBookingBlocks = bookings.value
-    .filter(b => b.date.replace(/\//g, '-') === valFormatted)
-    .map(b => ({
-      init_hour: `${b.slot.startHour}:00:00`,
-      end_hour: `${b.slot.endHour}:00:00`
+    .filter((booking) => booking.date.replace(/\//g, '-') === valFormatted)
+    .map((booking) => ({
+      end_hour:  `${booking.slot.endHour}:00:00`,
+      init_hour: `${booking.slot.startHour}:00:00`,
+    }));
+
+  const cartBookingBlocks = scheduleCart.items
+    .filter((item) =>
+      Number(item.provider_id) === Number(props.provider.provider_id) &&
+      normalizeDate(item.date) === valFormatted
+    )
+    .map((item) => ({
+      end_hour:  item.end_time,
+      init_hour: item.start_time,
     }));
     }));
 
 
   const serverBookingBlocks = (providerClientBlocks.value.existing_schedules ?? [])
   const serverBookingBlocks = (providerClientBlocks.value.existing_schedules ?? [])
     .filter((schedule) => normalizeDate(schedule.date) === valFormatted)
     .filter((schedule) => normalizeDate(schedule.date) === valFormatted)
     .map((schedule) => ({
     .map((schedule) => ({
+      end_hour:  schedule.end_time,
       init_hour: schedule.start_time,
       init_hour: schedule.start_time,
-      end_hour: schedule.end_time,
     }));
     }));
 
 
-  
   const partialBlocks = [
   const partialBlocks = [
     ...blocksOfDate,
     ...blocksOfDate,
     ...workingDayBlocks,
     ...workingDayBlocks,
     ...localBookingBlocks,
     ...localBookingBlocks,
     ...serverBookingBlocks,
     ...serverBookingBlocks,
+    ...cartBookingBlocks,
   ];
   ];
 
 
-  // fluxo
-  $q.dialog({
-  component: ServiceSelectionSheet,
-  componentProps: {
-    provider: props.provider,
-    selectedDate: val,
-    partialBlocks
-  },
-}).onOk(({ serviceType, date: date_, provider: prov }) => {
-
   $q.dialog({
   $q.dialog({
-    component: ServiceTimeSelectionDialog,
-    componentProps: {
-      serviceType,
-      selectedDate: date_,
-      provider: prov,
-      partialBlocks
-    },
-  }).onOk((booking) => {
-
-    bookings.value.push(booking);
+    component: ServiceSelectionSheet, componentProps: { partialBlocks, provider: props.provider, selectedDate: val },
+  }).onOk(({ action, serviceType, date: date_, provider: prov }) => {
     $q.dialog({
     $q.dialog({
-      component: OrderSummaryDialog,
-      componentProps: {
-        provider: props.provider,
-        initialBooking: booking
+      component: ServiceTimeSelectionDialog, componentProps: {partialBlocks, provider: prov, selectedDate: date_, serviceType },
+    }).onOk((booking) => {
+      if (action === 'cart') {
+        scheduleCart.addItem({
+          date:          normalizeDate(booking.date),
+          end_time:      formatHour(booking.slot.endHour),
+          local_id:      `${prov.provider_id}-${booking.date}-${booking.slot.value}-${Date.now()}`,
+          offers_meal:   booking.meal === 'offer' ? true : booking.meal === 'no_offer' ? false : null,
+          period_type:   booking.serviceType.hoursCount,
+          provider_id:   prov.provider_id,
+          provider_name: prov.provider_name,
+          start_time:    formatHour(booking.slot.startHour),
+          total_amount:  booking.serviceType.price,
+        });
+
+        $q.notify({ message: t('schedule_cart.add_success'), type: 'positive' });
+
+        return;
       }
       }
-    }).onOk(() => {
-      dialogRef.value.hide();
-    })
-  });
-});
-};
 
 
-const loadAvailability = async () => {
-  loadingAvailability.value = true;
-
-  try {
-    const clientId = store.user?.client?.id;
-    const defaultClientBlocks = { existing_schedules: [], fully_blocked_weeks: [] };
+      bookings.value.push(booking);
 
 
-    const [wd, bd, clientBlocks] = await Promise.all([
-      getProviderWorkingDays(props.provider.provider_id),
-      getProviderBlockedDays(props.provider.provider_id),
-      clientId
-        ? getClientProviderBlocks(clientId, props.provider.provider_id)
-        : Promise.resolve(defaultClientBlocks),
-    ]);
-
-    workingDays.value = wd ?? [];
-    blockedDays.value = bd ?? [];
-    providerClientBlocks.value = clientBlocks ?? defaultClientBlocks;
-
-  } catch {
-    workingDays.value = [];
-    blockedDays.value = [];
-    providerClientBlocks.value = { existing_schedules: [], fully_blocked_weeks: [] };
-
-  } finally {
-    loadingAvailability.value = false;
-  }
+      $q.dialog({
+        component: OrderSummaryDialog, componentProps: { initialBooking: booking, provider: props.provider },
+      }).onOk(() => {
+        dialogRef.value.hide();
+      });
+    });
+  });
 };
 };
 
 
+const wouldExceedWeekLimit = (selectedDate) => {
+  const cartCount   = getCartWeekCount(selectedDate);
+  const localCount  = getLocalWeekCount(selectedDate);
+  const serverCount = getServerWeekCount(selectedDate);
 
 
-const loadReviews = async () => {
-  loadingReviews.value = true;
-  try {
-    const all = await getProviderReceivedReviews(props.provider.provider_id);
-    reviews.value = (all ?? []).slice(0, 10);
-  } catch {
-    reviews.value = [];
-  } finally {
-    loadingReviews.value = false;
-  }
+  return (serverCount + localCount + cartCount) >= 2;
 };
 };
 
 
 onMounted(() => {
 onMounted(() => {

+ 93 - 61
src/pages/search/components/ServiceSelectionSheet.vue

@@ -1,10 +1,21 @@
 <template>
 <template>
-  <q-dialog ref="dialogRef" position="bottom" @hide="onDialogHide">
+  <q-dialog
+    ref="dialogRef"
+    position="bottom"
+    @hide="onDialogHide"
+  >
     <q-card class="bg-surface text-text full-width sheet-card">
     <q-card class="bg-surface text-text full-width sheet-card">
-
       <q-card-section class="row items-center q-pb-none">
       <q-card-section class="row items-center q-pb-none">
         <q-space />
         <q-space />
-        <q-btn flat round dense icon="mdi-close-circle-outline" color="grey-6" @click="onDialogCancel" />
+
+        <q-btn
+          color="grey-6"
+          dense
+          flat
+          icon="mdi-close-circle-outline"
+          round
+          @click="onDialogCancel"
+        />
       </q-card-section>
       </q-card-section>
 
 
       <q-separator class="q-mt-sm" />
       <q-separator class="q-mt-sm" />
@@ -21,36 +32,58 @@
         >
         >
           <div class="col">
           <div class="col">
             <div class="row items-center no-wrap q-gutter-x-xs">
             <div class="row items-center no-wrap q-gutter-x-xs">
-              <span class="font14 fontbold text-text">{{ type.label }}</span>
+              <span class="font14 fontbold text-text">
+                {{ type.label }}
+              </span>
+
               <q-btn
               <q-btn
-                flat round dense
-                icon="mdi-information-outline"
                 color="primary"
                 color="primary"
+                dense
+                flat
+                icon="mdi-information-outline"
                 size="xs"
                 size="xs"
                 @click="openInfo(type)"
                 @click="openInfo(type)"
               />
               />
             </div>
             </div>
-            <div class="font12 fontmedium text-grey-6">{{ type.hours }}</div>
+
+            <div class="font12 fontmedium text-grey-6">
+              {{ type.hours }}
+            </div>
           </div>
           </div>
 
 
           <div class="font16 fontbold text-text q-mx-md" style="white-space: nowrap;">
           <div class="font16 fontbold text-text q-mx-md" style="white-space: nowrap;">
             {{ type.price != null ? formatPrice(type.price) : $t('scheduling_page.no_price') }}
             {{ type.price != null ? formatPrice(type.price) : $t('scheduling_page.no_price') }}
           </div>
           </div>
 
 
-          <q-btn
-            unelevated
-            rounded
-            no-caps
-            :label="$t('scheduling_page.book')"
-            :disable="type.price == null"
-            color="secondary"
-            size="md"
-            padding="2px 12px"
-            @click="onDialogOK({ serviceType: type, date: selectedDate, provider })"
-          />
+          <div class="row no-wrap q-gutter-x-xs">
+            <q-btn
+              color="secondary"
+              no-caps
+              padding="2px 12px"
+              rounded
+              size="md"
+              unelevated
+              :disable="type.price == null"
+              :label="$t('scheduling_page.book')"
+              @click="onDialogOK({ action: 'book', serviceType: type, date: selectedDate, provider })"
+            />
+
+            <q-btn
+              color="primary"
+              icon="mdi-cart-plus"
+              outline
+              round
+              size="md"
+              :disable="type.price == null"
+              @click="onDialogOK({ action: 'cart', serviceType: type, date: selectedDate, provider })"
+            >
+              <q-tooltip>
+                {{ $t('schedule_cart.add_to_cart') }}
+              </q-tooltip>
+            </q-btn>
+          </div>
         </div>
         </div>
       </q-card-section>
       </q-card-section>
-
     </q-card>
     </q-card>
   </q-dialog>
   </q-dialog>
 </template>
 </template>
@@ -62,64 +95,51 @@ import { useI18n } from 'vue-i18n';
 import ServiceTypeInfoDialog from './ServiceTypeInfoDialog.vue';
 import ServiceTypeInfoDialog from './ServiceTypeInfoDialog.vue';
 
 
 const props = defineProps({
 const props = defineProps({
-  provider: { type: Object, required: true },
-  selectedDate: { type: String, required: true },
-  partialBlocks: { type: Array, required: false, default: () => [] },
+  partialBlocks: { type: Array,  required: false, default: () => [] },
+  provider:      { type: Object, required: true },
+  selectedDate:  { type: String, required: true },
 });
 });
 
 
 defineEmits([...useDialogPluginComponent.emits]);
 defineEmits([...useDialogPluginComponent.emits]);
 
 
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
 const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
-const $q = useQuasar();
-const { t } = useI18n();
 
 
-const slotConflicts = (slotStart, slotEnd, blocks) =>
-  blocks.some(b => {
-    const blockStart = parseInt(b.init_hour);
-    const blockEnd   = parseInt(b.end_hour);
-    return slotEnd >= blockStart && slotStart <= blockEnd;
-  });
-
-const hasValidSlots = (hoursCount) => {
-  for (let s = 7; s + hoursCount <= 20; s++) {
-    if (!slotConflicts(s, s + hoursCount, props.partialBlocks)) return true;
-  }
-  return false;
-};
+const $q    = useQuasar();
+const { t } = useI18n();
 
 
 const availableServiceTypes = computed(() =>
 const availableServiceTypes = computed(() =>
   [
   [
     {
     {
-      key: 'integral',
-      hoursCount: 8,
-      label: t('scheduling_page.service_types.integral.label'),
-      hours: t('scheduling_page.service_types.integral.hours'),
       description: t('scheduling_page.service_types.integral.description'),
       description: t('scheduling_page.service_types.integral.description'),
-      price: props.provider?.daily_price_8h ?? null,
+      hours:       t('scheduling_page.service_types.integral.hours'),
+      hoursCount:  8,
+      key:         'integral',
+      label:       t('scheduling_page.service_types.integral.label'),
+      price:       props.provider?.daily_price_8h ?? null,
     },
     },
     {
     {
-      key: 'padrao',
-      hoursCount: 6,
-      label: t('scheduling_page.service_types.padrao.label'),
-      hours: t('scheduling_page.service_types.padrao.hours'),
       description: t('scheduling_page.service_types.padrao.description'),
       description: t('scheduling_page.service_types.padrao.description'),
-      price: props.provider?.daily_price_6h ?? null,
+      hours:       t('scheduling_page.service_types.padrao.hours'),
+      hoursCount:  6,
+      key:         'padrao',
+      label:       t('scheduling_page.service_types.padrao.label'),
+      price:       props.provider?.daily_price_6h ?? null,
     },
     },
     {
     {
-      key: 'meio_periodo',
-      hoursCount: 4,
-      label: t('scheduling_page.service_types.meio_periodo.label'),
-      hours: t('scheduling_page.service_types.meio_periodo.hours'),
       description: t('scheduling_page.service_types.meio_periodo.description'),
       description: t('scheduling_page.service_types.meio_periodo.description'),
-      price: props.provider?.daily_price_4h ?? null,
+      hours:       t('scheduling_page.service_types.meio_periodo.hours'),
+      hoursCount:  4,
+      key:         'meio_periodo',
+      label:       t('scheduling_page.service_types.meio_periodo.label'),
+      price:       props.provider?.daily_price_4h ?? null,
     },
     },
     {
     {
-      key: 'diaria_rapida',
-      hoursCount: 2,
-      label: t('scheduling_page.service_types.diaria_rapida.label'),
-      hours: t('scheduling_page.service_types.diaria_rapida.hours'),
       description: t('scheduling_page.service_types.diaria_rapida.description'),
       description: t('scheduling_page.service_types.diaria_rapida.description'),
-      price: props.provider?.daily_price_2h ?? null,
+      hours:       t('scheduling_page.service_types.diaria_rapida.hours'),
+      hoursCount:  2,
+      key:         'diaria_rapida',
+      label:       t('scheduling_page.service_types.diaria_rapida.label'),
+      price:       props.provider?.daily_price_2h ?? null,
     },
     },
   ].filter(type => hasValidSlots(type.hoursCount))
   ].filter(type => hasValidSlots(type.hoursCount))
 );
 );
@@ -127,12 +147,24 @@ const availableServiceTypes = computed(() =>
 const formatPrice = (value) =>
 const formatPrice = (value) =>
   Number(value).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
   Number(value).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
 
 
+const hasValidSlots = (hoursCount) => {
+  for (let s = 7; s + hoursCount <= 20; s++) {
+    if (!slotConflicts(s, s + hoursCount, props.partialBlocks)) return true;
+  }
+  return false;
+};
+
 const openInfo = (type) => {
 const openInfo = (type) => {
-  $q.dialog({
-    component: ServiceTypeInfoDialog,
-    componentProps: { serviceType: type },
-  });
+  $q.dialog({ component: ServiceTypeInfoDialog, componentProps: { serviceType: type } });
 };
 };
+
+const slotConflicts = (slotStart, slotEnd, blocks) =>
+  blocks.some(b => {
+    const blockEnd   = parseInt(b.end_hour);
+    const blockStart = parseInt(b.init_hour);
+
+    return slotEnd >= blockStart && slotStart <= blockEnd;
+  });
 </script>
 </script>
 
 
 <style scoped lang="scss">
 <style scoped lang="scss">

+ 21 - 0
src/router/routes/schedule-cart.route.js

@@ -0,0 +1,21 @@
+export default [
+  {
+    path: "carrinho",
+    name: "ScheduleCartPage",
+    component: () => import("src/pages/schedule-cart/ScheduleCartPage.vue"),
+    meta: {
+      title: "schedule_cart.title",
+      requireAuth: true,
+      breadcrumbs: [
+        {
+          name: "DashboardPage",
+          title: "ui.navigation.dashboard",
+        },
+        {
+          name: "ScheduleCartPage",
+          title: "schedule_cart.title",
+        },
+      ],
+    },
+  },
+];

+ 33 - 0
src/stores/scheduleCart.js

@@ -0,0 +1,33 @@
+import { defineStore } from "pinia";
+import { ref } from "vue";
+
+export const useScheduleCartStore = defineStore("scheduleCart", () => {
+  const items = ref([]);
+
+  const lastCreatedGroup = ref(null);
+
+  const addItem = (item) => {
+    items.value.push(item);
+  };
+
+  const removeItem = (index) => {
+    items.value.splice(index, 1);
+  };
+
+  const clear = () => {
+    items.value = [];
+  };
+
+  const setLastCreatedGroup = (group) => {
+    lastCreatedGroup.value = group;
+  };
+
+  return {
+    items,
+    lastCreatedGroup,
+    addItem,
+    removeItem,
+    clear,
+    setLastCreatedGroup,
+  };
+});