6
0

15 Коммитууд 6508b9f431 ... cd2f074131

Эзэн SHA1 Мессеж Огноо
  kayo henrique cd2f074131 feat: :sparkles: feat: ✨ feat(agendamento-sob-medida): fazendo ajuste nos bloqueio de dias e semanas para agendmaento 1 долоо хоног өмнө
  Gustavo Zanatta 689d574a35 fix merges 2 долоо хоног өмнө
  Gustavo Zanatta 988b3fda8b Merge remote-tracking branch 'origin/feature/diariaapp-kay-agendamentos-sob-medida-apps' into development 2 долоо хоног өмнө
  Gustavo Zanatta d49900faf9 ajustes merge 2 долоо хоног өмнө
  zntt 178779adfe Merge branch 'feature/diariaapp-gus-agendamentos-apps' of Softpar/sfp_front_vue_diarista_cliente into development 2 долоо хоног өмнө
  Gustavo Zanatta d864c8dd3f feat: :sparkles: feat (agendamentos) criando regras de negocio para contratacao 2 долоо хоног өмнө
  Gustavo Zanatta 8358b6b44b Merge branch 'feature/diariaapp-gus-agendamentos-apps' of gogs.softpar.inf.br:Softpar/sfp_front_vue_diarista_cliente into feature/diariaapp-gus-agendamentos-apps 2 долоо хоног өмнө
  Gustavo Zanatta 903e4bd6f5 feat: :sparkles: feat (traducoes) adiciona opção de troca de linguagem no perfil 2 долоо хоног өмнө
  Gustavo Zanatta f642cf5346 fix: :bug: fix(próximos-servicos) corrigindo traducao 2 долоо хоног өмнө
  Gustavo Zanatta 2bb9cb956f fix: :bug: fix(próximos-servicos) corrigindo traducao 2 долоо хоног өмнө
  Gustavo Zanatta 7005fae3eb feat: :sparkles: fix (agendamento) corrigir exibição de informações e filtro de status 2 долоо хоног өмнө
  Gustavo Zanatta 0dda2ca76a feat: :sparkles: feat (agendamentos default) fluxo de gerar agendamentos default 3 долоо хоног өмнө
  zntt 0dee890b5c Merge branch 'fix/diariaapp-gus-ajustes-layout' of Softpar/sfp_front_vue_diarista_cliente into development 3 долоо хоног өмнө
  Gustavo Zanatta c5f5c86ab4 Merge branch 'development' into fix/diariaapp-gus-ajustes-layout 3 долоо хоног өмнө
  Gustavo Zanatta a8dfff87ea fix: :bug: ajustes dashboard cliente 3 долоо хоног өмнө
28 өөрчлөгдсөн 3024 нэмэгдсэн , 155 устгасан
  1. 16 0
      src/api/providerAvailability.js
  2. 6 0
      src/api/review.js
  3. 11 0
      src/api/schedule.js
  4. 33 12
      src/components/dashboard/DashboardFavoriteProviders.vue
  5. 8 2
      src/components/dashboard/DashboardLastDoneSchedules.vue
  6. 1 1
      src/components/dashboard/DashboardNextSchedules.vue
  7. 140 0
      src/components/dashboard/DashboardPendingSchedules.vue
  8. 175 0
      src/components/dashboard/ScheduleAcceptedDialog.vue
  9. 304 0
      src/components/dashboard/SchedulePaymentDialog.vue
  10. 273 0
      src/components/dashboard/SchedulePaymentPixDialog.vue
  11. 105 0
      src/components/dashboard/SchedulePaymentProcessingDialog.vue
  12. 85 1
      src/components/defaults/DefaultInputDatePicker.vue
  13. 82 0
      src/css/app.scss
  14. 89 14
      src/i18n/locales/en.json
  15. 69 9
      src/i18n/locales/es.json
  16. 69 9
      src/i18n/locales/pt.json
  17. 8 6
      src/layouts/MainLayout.vue
  18. 32 8
      src/pages/dashboard/DashboardPage.vue
  19. 26 2
      src/pages/profile/ProfileEditDialog.vue
  20. 1 1
      src/pages/schedules/SobMedidaPage.vue
  21. 332 0
      src/pages/scheduling/SchedulingPage.vue
  22. 26 17
      src/pages/search/SearchPage.vue
  23. 366 0
      src/pages/search/components/OrderSummaryDialog.vue
  24. 466 0
      src/pages/search/components/SchedulingDialog.vue
  25. 208 0
      src/pages/search/components/SearchFilterDialog.vue
  26. 64 40
      src/pages/search/components/ServiceSelectionSheet.vue
  27. 28 32
      src/pages/search/components/ServiceTimeSelectionDialog.vue
  28. 1 1
      src/pages/search/components/ServiceTypeInfoDialog.vue

+ 16 - 0
src/api/providerAvailability.js

@@ -0,0 +1,16 @@
+import api from 'src/api'
+
+export const getProviderWorkingDays = async (providerId) => {
+  const { data } = await api.get(`/provider/working-days/${providerId}`)
+  return data.payload
+}
+
+export const getProviderBlockedDays = async (providerId) => {
+  const { data } = await api.get(`/provider/blocked-days/${providerId}`)
+  return data.payload
+}
+
+export const getProviderSchedules = async (providerId) => {
+  const response = await api.get(`/provider/${providerId}/schedules`)
+  return response.data.payload
+}

+ 6 - 0
src/api/review.js

@@ -0,0 +1,6 @@
+import api from 'src/api'
+
+export const getProviderReceivedReviews = async (providerId) => {
+  const { data } = await api.get(`/reviews/provider/${providerId}/received`)
+  return data.payload
+}

+ 11 - 0
src/api/schedule.js

@@ -0,0 +1,11 @@
+import api from 'src/api';
+
+export const createSchedule = async (scheduleData) => {
+  const { data } = await api.post('/schedule', scheduleData);
+  return data.payload;
+};
+
+export const updateScheduleStatus = async (id, status) => {
+  const { data } = await api.patch(`/schedule/${id}/status`, { status });
+  return data.payload;
+};

+ 33 - 12
src/components/dashboard/DashboardFavoriteProviders.vue

@@ -10,23 +10,26 @@
         :flat="false"
       >
         <q-card-section class="q-pa-sm column text-text">
-          <div class="row items-start no-wrap q-gutter-x-sm">
-            <div class="col-3">
-              <q-avatar :style="avatarColors[item.provider_id % avatarColors.length]" size="46px" class="text-weight-bold">
+          <div class="row items-start no-wrap">
+            <div class="col-3 q-my-auto">
+              <q-avatar :style="avatarColors[item.provider_provider_id % avatarColors.length]" size="46px" class="text-weight-bold">
                 {{ item.provider_name?.slice(0,1).toUpperCase() ?? '—' }}
               </q-avatar>
             </div>
-            <div class="col-9 column q-gutter-y-xs">
+            <div class="col-5 column q-gutter-y-xs q-my-auto">
               <span class="text-fav-name">{{ item.provider_name ?? 'Prestador' }}</span>
+              <span class="text-fav-region">{{ item.provider_district ?? 'N/A' }}</span>
               <div v-if="item.average_rating != null" class="row items-center q-gutter-x-xs">
                 <q-icon name="mdi-star" color="warning" size="sm" />
                 <span class="text-fav-name">{{ Number(item.average_rating).toFixed(1) }}</span>
               </div>
-              <span class="text-fav-price">{{ bestPrice(item) }}</span>
+              <!-- <span class="text-fav-price">{{ bestPrice(item) }}</span> -->
+            </div>
+            <div class="col-4 column q-mt-auto">
               <q-btn
                 rounded color="primary"
-                padding="1px 5px" size="sm"
-                class="q-mt-xs"
+                padding="1px 5px"
+                size="sm"
                 :label="$t('dashboard_client.favorites.view_schedule')"
               />
             </div>
@@ -38,12 +41,17 @@
 </template>
 
 <script setup>
-import { formatCurrency } from 'src/helpers/utils';
-import { useI18n } from 'vue-i18n';
+// import { formatCurrency } from 'src/helpers/utils';
+// import { useI18n } from 'vue-i18n';
+
+// import { formatCurrency } from 'src/helpers/utils';
+// import { useI18n } from 'vue-i18n';
 
 defineProps({ data: { type: Array, default: () => [] } });
 
-const { t } = useI18n();
+// const { t } = useI18n();
+
+// const { t } = useI18n();
 
 const avatarColors = [
   { background: '#ffd5df', color: '#932e57' },
@@ -52,7 +60,20 @@ const avatarColors = [
   { background: '#ffe5cc', color: '#8a4500' },
 ];
 
-const bestPrice = (item) => {
+// const bestPrice = (item) => {
+//   const prices = [
+//     item.daily_price_2h,
+//     item.daily_price_4h,
+//     item.daily_price_6h,
+//     item.daily_price_8h,
+//   ].filter(p => p != null && Number(p) > 0);
+
+//   if (!prices.length) return t('dashboard_client.favorites.no_price');
+//   const min = Math.min(...prices.map(Number));
+//   return t('dashboard_client.favorites.from') + ' ' + formatCurrency(min);
+// };
+
+/*const bestPrice = (item) => {
   const prices = [
     item.daily_price_2h,
     item.daily_price_4h,
@@ -63,7 +84,7 @@ const bestPrice = (item) => {
   if (!prices.length) return t('dashboard_client.favorites.no_price');
   const min = Math.min(...prices.map(Number));
   return t('dashboard_client.favorites.from') + ' ' + formatCurrency(min);
-};
+};*/
 </script>
 
 <style scoped lang="scss">

+ 8 - 2
src/components/dashboard/DashboardLastDoneSchedules.vue

@@ -9,11 +9,12 @@
         class="mini-card shadow-card bg-surface card-border"
         :flat="false"
       >
-        <q-card-section class="column items-center q-pa-md q-gutter-y-xs text-text">
-          <q-avatar :style="avatarColors[item.provider_id % avatarColors.length]" size="56px" class="text-weight-bold">
+        <q-card-section class="column q-pa-md q-gutter-y-xs text-text">
+          <q-avatar :style="avatarColors[item.provider_provider_id % avatarColors.length]" size="56px" class="text-weight-bold q-mx-auto">
             {{ item.provider_name?.slice(0,1).toUpperCase() ?? '—' }}
           </q-avatar>
           <span class="text-done-name">{{ item.provider_name ?? 'Prestador' }}</span>
+          <span v-if="item.provider_district" class="text-done-district">{{ item.provider_district != null ? item.provider_district : $t('dashboard_client.last_schedules.no_address') }}</span>
           <q-btn
             rounded color="secondary"
             padding="1px 5px" size="sm"
@@ -53,4 +54,9 @@ const avatarColors = [
   flex: 0 0 130px;
   scroll-snap-align: start;
 }
+.text-done-district {
+  font-size: 11px;
+  color: var(--q-text-light, #9ca3af);
+  text-align: center;
+}
 </style>

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

@@ -108,7 +108,7 @@ const addressIcon = (type) => type === 'home' ? 'mdi-home-outline' : 'mdi-office
 const addressLabel = (type) => {
   if (type === 'home') return t('address.types.commercial.home');
   if (type === 'apartment') return t('dashboard_client.next_schedules.place_apartment');
-  if (type === 'commercial') return t('address.types.commercial');
+  if (type === 'commercial') return t('address.types.commercial.commercial');
   return t('dashboard_client.next_schedules.place_unknown');
 };
 

+ 140 - 0
src/components/dashboard/DashboardPendingSchedules.vue

@@ -0,0 +1,140 @@
+<template>
+  <div class="q-mx-md q-mb-md">
+    <div class="scroll-wrapper">
+      <div class="scroll-track">
+        <q-card
+          v-for="item in data"
+          :key="item.id"
+          class="pending-card card-border shadow-card bg-surface"
+          :flat="false"
+          :class="{ 'cursor-pointer': item.status === 'accepted' }"
+          @click="item.status === 'accepted' && emit('view-details', item)"
+        >
+          <q-card-section class="q-pa-md">
+
+            <div class="row no-wrap items-start q-mb-sm">
+              <q-avatar size="40px" :style="avatarColors[item.id % avatarColors.length]" class="text-weight-bold q-mr-sm flex-shrink-0">
+                {{ item.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}
+              </q-avatar>
+
+              <div class="col column no-wrap overflow-hidden">
+                <span class="text-body2 text-text">
+                  {{ $t('dashboard_client.pending_schedules.requesting_with') }}
+                  <span class="text-weight-bold">{{ item.provider_name ?? '—' }}</span>
+                </span>
+                <div class="row items-center q-mt-xs">
+                  <q-icon name="mdi-clock-outline" size="13px" color="grey-5" class="q-mr-xs" />
+                  <span class="text-caption text-grey-5">{{ item.time_since_request }}</span>
+                </div>
+              </div>
+
+              <div class="clock-badge-col q-ml-sm flex-shrink-0 column items-center">
+                <div class="clock-badge">
+                  <q-icon name="mdi-clock-outline" size="18px" color="white" />
+                </div>
+                <span class="text-caption text-primary text-weight-bold q-mt-xs badge-status-text">
+                  {{ $t(`dashboard_client.pending_schedules.status.${item.status ?? 'pending'}`) }}
+                </span>
+              </div>
+            </div>
+
+            <div class="progress-track q-mb-md">
+              <div class="progress-fill" :style="{ width: progressPercent(item.status) + '%' }" />
+            </div>
+
+            <div class="row items-center no-wrap">
+              <q-btn
+                flat no-caps dense
+                :label="$t('dashboard_client.pending_schedules.cancel_btn')"
+                color="primary"
+                size="sm"
+                class="q-mr-sm flex-shrink-0"
+              />
+              <q-space />
+              <q-icon name="mdi-map-marker-outline" size="13px" color="grey-6" class="q-mr-xs flex-shrink-0" />
+              <span class="text-caption text-grey-6 col ellipsis text-right">
+                {{ [item.address?.address, item.address?.number, item.address?.district].filter(Boolean).join(', ') || '—' }}
+              </span>
+            </div>
+          </q-card-section>
+        </q-card>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+const avatarColors = [
+  { background: '#ffd5df', color: '#932e57' },
+  { background: '#d7e8ff', color: '#2158a8' },
+  { background: '#dfd',    color: '#2a7a3b' },
+  { background: '#ffe5cc', color: '#8a4500' },
+];
+
+const statusProgressMap = {
+  pending:  20,
+  accepted: 40,
+  paid:     60,
+  started:  80,
+  finished: 100,
+};
+
+const progressPercent = (status) => statusProgressMap[status] ?? 20;
+
+defineProps({ data: { type: Array, default: () => [] } });
+const emit = defineEmits(['view-details']);
+</script>
+
+<style scoped lang="scss">
+.scroll-wrapper { overflow: hidden; }
+.scroll-track {
+  display: flex;
+  flex-direction: row;
+  gap: 12px;
+  overflow-x: auto;
+  overscroll-behavior-x: contain;
+  scroll-snap-type: x proximity;
+  padding-bottom: 8px;
+  &::-webkit-scrollbar { display: none; }
+  &::after { content: ''; flex: 0 0 1px; }
+}
+.pending-card {
+  min-width: 80%;
+  scroll-snap-align: start;
+}
+
+.clock-badge-col {
+  min-width: 52px;
+  align-items: center;
+}
+
+.clock-badge {
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #8B5CF6, #EC4899);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.badge-status-text {
+  font-size: 10px;
+  white-space: nowrap;
+}
+
+.progress-track {
+  width: 100%;
+  height: 5px;
+  background: #E2E8F0;
+  border-radius: 3px;
+  overflow: hidden;
+}
+
+.progress-fill {
+  height: 100%;
+  border-radius: 3px;
+  background: linear-gradient(90deg, #8B5CF6, #EC4899);
+  transition: width 0.4s ease;
+}
+</style>

+ 175 - 0
src/components/dashboard/ScheduleAcceptedDialog.vue

@@ -0,0 +1,175 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="accepted-dialog-card bg-surface" :flat="false">
+
+      <q-card-section class="column items-center q-pt-lg q-pb-sm">
+        <q-avatar size="80px" :style="avatarStyle" class="text-weight-bold text-h5 q-mb-sm">
+          {{ schedule.provider_name?.slice(0, 2).toUpperCase() ?? '??' }}
+        </q-avatar>
+
+        <div class="text-h6 text-weight-bold provider-name">{{ schedule.provider_name }}</div>
+        <div class="text-caption text-text">{{ schedule.provider_district || '' }}</div>
+      </q-card-section>
+
+      <q-card-section class="text-center q-pt-xs q-pb-md">
+        <div class="accepted-title text-weight-bold">
+          {{ $t('dashboard_client.pending_schedules.accepted_title') }}
+        </div>
+      </q-card-section>
+
+      <q-separator />
+
+      <q-card-section class="q-py-md q-px-lg">
+        <div class="detail-row">
+          <span class="detail-label">{{ $t('dashboard_client.pending_schedules.detail_date') }}</span>
+          <span class="detail-value">{{ formattedDate }}</span>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">{{ $t('dashboard_client.pending_schedules.detail_time') }}</span>
+          <span class="detail-value text-weight-bold">
+            {{ schedule.start_time?.slice(0, 5) }} {{ $t('dashboard_client.next_schedules.to') }} {{ schedule.end_time?.slice(0, 5) }}
+          </span>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">{{ $t('dashboard_client.pending_schedules.detail_value') }}</span>
+          <span class="detail-value">{{ formatCurrency(schedule.total_amount) }}</span>
+        </div>
+        <div class="detail-row">
+          <span class="detail-label">{{ $t('dashboard_client.pending_schedules.detail_service_fee') }}</span>
+          <span class="detail-value">{{ formatCurrency(serviceFee) }}</span>
+        </div>
+
+        <q-separator class="q-my-sm" />
+
+        <div class="detail-row">
+          <span class="detail-label text-weight-bold text-text">{{ $t('dashboard_client.pending_schedules.detail_total') }}</span>
+          <span class="total-value">{{ formatCurrency(total) }}</span>
+        </div>
+      </q-card-section>
+
+      <q-card-section class="q-pt-none q-pb-lg q-px-lg column q-gutter-y-sm">
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          class="payment-btn full-width"
+          :label="$t('dashboard_client.pending_schedules.btn_payment')"
+          @click="onGoToPayment"
+        />
+        <q-btn
+          flat
+          no-caps
+          color="grey-6"
+          class="full-width"
+          :label="$t('dashboard_client.pending_schedules.btn_cancel')"
+          @click="onDialogHide"
+        />
+      </q-card-section>
+
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { formatCurrency } from 'src/helpers/utils'
+import SchedulePaymentDialog from './SchedulePaymentDialog.vue'
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    required: true
+  }
+})
+
+defineEmits([...useDialogPluginComponent.emits])
+
+const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent()
+
+const $q = useQuasar()
+
+const SERVICE_FEE_RATE = 0.10
+
+const serviceFee = computed(() => Number(props.schedule.total_amount) * SERVICE_FEE_RATE)
+const total = computed(() => Number(props.schedule.total_amount) + serviceFee.value)
+
+const onGoToPayment = () => {
+  $q.dialog({
+    component: SchedulePaymentDialog,
+    componentProps: {
+      schedule: props.schedule,
+      total: total.value,
+    },
+  }).onOk(() => {
+    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 = [
+  { background: '#ffd5df', color: '#932e57' },
+  { background: '#d7e8ff', color: '#2158a8' },
+  { background: '#dfd',    color: '#2a7a3b' },
+  { background: '#ffe5cc', color: '#8a4500' },
+]
+const avatarStyle = computed(() => avatarColors[props.schedule.id % avatarColors.length])
+</script>
+
+<style scoped lang="scss">
+.accepted-dialog-card {
+  width: 320px;
+  max-width: 92vw;
+  border-radius: 20px !important;
+  overflow: hidden;
+}
+
+.accepted-title {
+  font-size: 22px;
+  line-height: 1.3;
+  color: #8B5CF6;
+}
+
+.provider-name {
+  color: #8B5CF6;
+}
+
+.detail-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 4px 0;
+}
+
+.detail-label {
+  font-size: 13px;
+  color: #8a8a9a;
+}
+
+.detail-value {
+  font-size: 13px;
+  color: #3a3a4a;
+}
+
+.total-value {
+  font-size: 18px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+
+.payment-btn {
+  background: linear-gradient(90deg, #8B5CF6, #EC4899);
+  color: white;
+  font-weight: 700;
+  font-size: 15px;
+  height: 48px;
+}
+</style>

+ 304 - 0
src/components/dashboard/SchedulePaymentDialog.vue

@@ -0,0 +1,304 @@
+<template>
+  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down" @hide="onDialogHide">
+    <div class="bg-page full-height column">
+
+      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
+        <q-btn icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
+        <q-space />
+        <span class="text-subtitle1 text-weight-bold text-primary">
+          {{ $t('payment.title') }}
+        </span>
+        <q-space />
+        <div style="width: 32px" />
+      </div>
+
+      <div class="col overflow-auto q-px-md q-pt-lg q-pb-xl">
+
+        <div class="section-label q-mb-sm">{{ $t('payment.schedule_address') }}</div>
+        <div class="address-box row items-center no-wrap q-mb-lg">
+          <div class="col">
+            <div class="address-type-label">{{ addressTypeLabel }}</div>
+            <div class="address-full-text text-grey-7">{{ addressFullText }}</div>
+          </div>
+          <q-icon name="mdi-chevron-down" color="grey-5" size="22px" />
+        </div>
+
+        <div class="section-label q-mb-sm">{{ $t('payment.pay_with') }}</div>
+        <div class="row q-gutter-sm q-mb-sm">
+
+          <div
+            class="payment-option-card col column items-center justify-center q-pa-md cursor-pointer"
+            :class="{ 'payment-option-selected': selectedMethod === 'pix' }"
+            @click="selectedMethod = 'pix'"
+          >
+            <span class="payment-option-title">{{ $t('payment.pix') }}</span>
+            <q-icon name="mdi-cash-fast" size="32px" color="teal" class="q-mt-xs" />
+          </div>
+
+          <div
+            class="payment-option-card col column items-center justify-center q-pa-md cursor-pointer"
+            :class="{ 'payment-option-selected': selectedMethod === 'new_card' }"
+            @click="openAddCard"
+          >
+            <span class="payment-option-title">{{ $t('payment.add_card') }}</span>
+            <q-icon name="mdi-plus-circle-outline" size="22px" color="grey-5" class="q-mt-xs" />
+            <span class="payment-option-sub">{{ $t('payment.credit_debit') }}</span>
+          </div>
+
+        </div>
+
+        <div v-if="loadingCards" class="flex flex-center q-py-md">
+          <q-spinner color="primary" size="2em" />
+        </div>
+
+        <div v-else-if="paymentMethods.length > 0" class="column q-gutter-y-sm q-mb-lg">
+          <div
+            v-for="card in paymentMethods"
+            :key="card.id"
+            class="saved-card-box row items-center no-wrap q-pa-md cursor-pointer"
+            :class="{ 'payment-option-selected': selectedMethod === `card_${card.id}` }"
+            @click="selectedMethod = `card_${card.id}`"
+          >
+            <div class="col column">
+              <span class="card-titular-label">{{ $t('payment.card_holder') }}</span>
+              <span class="card-holder-name">{{ card.holder_name }}</span>
+            </div>
+            <div class="column items-end">
+              <span class="card-brand-text">{{ brandDisplay(card.brand) }}</span>
+              <span class="card-last-four">{{ '**** **** **** ' + card.last_four_digits }}</span>
+              <span class="card-expiry-text">{{ card.expiration }}</span>
+            </div>
+          </div>
+        </div>
+
+        <q-separator class="q-my-lg" />
+
+        <div class="row items-center q-mb-lg">
+          <q-checkbox v-model="agreedToTerms" color="primary" keep-color />
+          <span class="terms-text">
+            {{ $t('payment.agree_prefix') }}
+            <span class="text-primary text-weight-bold cursor-pointer text-underline">{{ $t('payment.terms_link') }}</span>
+          </span>
+        </div>
+
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          color="primary"
+          class="full-width confirm-btn"
+          :label="$t('payment.confirm_btn')"
+          :disable="!canConfirm"
+          @click="onConfirm"
+        />
+
+      </div>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+import { userStore } from 'src/stores/user'
+import { getClientPaymentMethods } from 'src/api/clientPaymentMethod'
+import ProfilePaymentAddDialog from 'src/components/profile/ProfilePaymentAddDialog.vue'
+import SchedulePaymentPixDialog from './SchedulePaymentPixDialog.vue'
+import SchedulePaymentProcessingDialog from './SchedulePaymentProcessingDialog.vue'
+
+const props = defineProps({
+  schedule: {
+    type: Object,
+    required: true,
+  },
+  total: {
+    type: Number,
+    required: true,
+  },
+})
+
+defineEmits([...useDialogPluginComponent.emits])
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
+const $q = useQuasar()
+const { t } = useI18n()
+const store = userStore()
+
+const selectedMethod = ref(null)
+const agreedToTerms = ref(false)
+const paymentMethods = ref([])
+const loadingCards = ref(false)
+
+const addressTypeLabel = computed(() => {
+  const type = props.schedule.address?.address_type
+  if (!type) return ''
+  return t(`profile.address.type.${type}`, type)
+})
+
+const addressFullText = computed(() => {
+  const a = props.schedule.address
+  if (!a) return ''
+  const parts = [a.address, a.number, a.district].filter(Boolean)
+  return parts.join(', ')
+})
+
+const canConfirm = computed(() => selectedMethod.value !== null && agreedToTerms.value)
+
+const brandDisplay = (brand) => {
+  if (!brand) return ''
+  const map = { visa: 'VISA', mastercard: 'Mastercard', elo: 'Elo', hipercard: 'Hipercard', diners: 'Diners', discover: 'Discover' }
+  return map[brand] ?? brand.toUpperCase()
+}
+
+const loadCards = async () => {
+  loadingCards.value = true
+  try {
+    paymentMethods.value = await getClientPaymentMethods(store.user?.client_id)
+  } catch (e) {
+    console.error(e)
+  } finally {
+    loadingCards.value = false
+  }
+}
+
+const openAddCard = () => {
+  $q.dialog({
+    component: ProfilePaymentAddDialog,
+    componentProps: { clientId: store.user?.client_id },
+  }).onOk(() => {
+    loadCards()
+  })
+}
+
+const onConfirm = () => {
+  if (selectedMethod.value === 'pix') {
+    $q.dialog({
+      component: SchedulePaymentPixDialog,
+      componentProps: { schedule: props.schedule, total: props.total },
+    }).onOk(() => {
+      onDialogOK()
+    })
+  } else {
+    $q.dialog({
+      component: SchedulePaymentProcessingDialog,
+      componentProps: { schedule: props.schedule },
+    }).onOk(() => {
+      onDialogOK()
+    })
+  }
+}
+
+onMounted(() => {
+  loadCards()
+})
+</script>
+
+<style scoped lang="scss">
+.shadow-header {
+  box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.1);
+}
+
+.section-label {
+  font-size: 15px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+
+.address-box {
+  border: 1px solid #e0e0e0;
+  border-radius: 10px;
+  padding: 12px 16px;
+  background: #fff;
+}
+
+.address-type-label {
+  font-size: 14px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+
+.address-full-text {
+  font-size: 12px;
+  line-height: 1.4;
+  margin-top: 2px;
+}
+
+.payment-option-card {
+  border: 1.5px solid #e0e0e0;
+  border-radius: 12px;
+  background: #fff;
+  min-height: 90px;
+  text-align: center;
+  transition: border-color 0.2s;
+}
+
+.payment-option-selected {
+  border-color: #22c55e !important;
+  box-shadow: 0 0 0 1px #22c55e;
+}
+
+.payment-option-title {
+  font-size: 15px;
+  font-weight: 700;
+  color: #3a3a4a;
+}
+
+.payment-option-sub {
+  font-size: 11px;
+  color: #9a9aaa;
+  margin-top: 2px;
+}
+
+.saved-card-box {
+  border: 1.5px solid #e0e0e0;
+  border-radius: 12px;
+  background: #fff;
+  transition: border-color 0.2s;
+}
+
+.card-titular-label {
+  font-size: 11px;
+  color: #9a9aaa;
+}
+
+.card-holder-name {
+  font-size: 14px;
+  font-weight: 600;
+  color: #3a3a4a;
+}
+
+.card-brand-text {
+  font-size: 14px;
+  font-weight: 700;
+  color: #3a3a4a;
+  text-align: right;
+}
+
+.card-last-four {
+  font-size: 13px;
+  color: #6a6a7a;
+  letter-spacing: 1px;
+}
+
+.card-expiry-text {
+  font-size: 12px;
+  color: #9a9aaa;
+}
+
+.terms-text {
+  font-size: 13px;
+  color: #5a5a6a;
+  line-height: 1.4;
+}
+
+.text-underline {
+  text-decoration: underline;
+}
+
+.confirm-btn {
+  font-size: 16px;
+  font-weight: 700;
+  height: 52px;
+}
+</style>

+ 273 - 0
src/components/dashboard/SchedulePaymentPixDialog.vue

@@ -0,0 +1,273 @@
+<template>
+  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down" @hide="onDialogHide">
+    <div class="bg-page full-height column">
+
+      <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
+        <q-btn icon="mdi-chevron-left" flat round dense color="primary" @click="onDialogCancel" />
+        <q-space />
+        <span class="text-subtitle1 text-weight-bold text-primary">
+          {{ $t('payment.pix_title') }}
+        </span>
+        <q-space />
+        <div style="width: 32px" />
+      </div>
+
+      <div v-if="success" class="col column items-center justify-center q-px-xl">
+        <q-btn
+          flat round icon="mdi-close" color="grey-5"
+          class="self-end q-mb-md"
+          @click="onDialogOK"
+        />
+        <div class="success-icon-wrapper q-mb-lg">
+          <q-icon name="mdi-check-circle" size="100px" color="primary" />
+        </div>
+        <div class="success-title text-primary text-weight-bold text-center q-mb-sm">
+          {{ $t('payment.success_title') }}
+        </div>
+        <i18n-t keypath="payment.success_message" tag="div" class="success-message text-grey-6 text-center">
+          <template #nextServices>
+            <strong class="text-text">{{ $t('payment.success_next_services') }}</strong>
+          </template>
+          <template #agenda>
+            <strong class="text-text">{{ $t('payment.success_agenda') }}</strong>
+          </template>
+        </i18n-t>
+      </div>
+
+      <div v-else class="col scroll q-px-lg q-pt-lg q-pb-xl column">
+
+        <div class="row items-center justify-between q-mb-sm">
+          <span class="pix-label">{{ $t('payment.pix_total') }}</span>
+          <span class="pix-value text-primary">{{ formatCurrency(total) }}</span>
+        </div>
+        <q-separator />
+
+        <div class="row items-center justify-between q-mt-sm q-mb-lg">
+          <span class="pix-label">{{ $t('payment.pix_expires') }}</span>
+          <span class="pix-value text-primary">{{ countdown }}</span>
+        </div>
+
+        <div class="flex flex-center q-mb-md">
+          <q-icon name="mdi-cash-fast" size="48px" color="teal" />
+        </div>
+
+        <div class="flex flex-center q-mb-md">
+          <div class="qrcode-wrapper">
+            <svg viewBox="0 0 200 200" width="180" height="180" xmlns="http://www.w3.org/2000/svg">
+              <rect x="10" y="10" width="60" height="60" rx="4" fill="none" stroke="#000" stroke-width="6"/>
+              <rect x="22" y="22" width="36" height="36" rx="2" fill="#000"/>
+              <rect x="130" y="10" width="60" height="60" rx="4" fill="none" stroke="#000" stroke-width="6"/>
+              <rect x="142" y="22" width="36" height="36" rx="2" fill="#000"/>
+              <rect x="10" y="130" width="60" height="60" rx="4" fill="none" stroke="#000" stroke-width="6"/>
+              <rect x="22" y="142" width="36" height="36" rx="2" fill="#000"/>
+              <g fill="#000">
+                <rect x="85" y="10" width="8" height="8"/>
+                <rect x="100" y="10" width="8" height="8"/>
+                <rect x="85" y="24" width="8" height="8"/>
+                <rect x="108" y="24" width="8" height="8"/>
+                <rect x="85" y="38" width="16" height="8"/>
+                <rect x="85" y="52" width="8" height="8"/>
+                <rect x="100" y="52" width="16" height="8"/>
+                <rect x="10" y="85" width="8" height="8"/>
+                <rect x="24" y="85" width="16" height="8"/>
+                <rect x="48" y="85" width="8" height="8"/>
+                <rect x="62" y="85" width="8" height="8"/>
+                <rect x="10" y="99" width="16" height="8"/>
+                <rect x="34" y="99" width="8" height="8"/>
+                <rect x="54" y="99" width="16" height="8"/>
+                <rect x="10" y="113" width="8" height="8"/>
+                <rect x="26" y="113" width="16" height="8"/>
+                <rect x="56" y="113" width="8" height="8"/>
+                <rect x="85" y="85" width="8" height="8"/>
+                <rect x="100" y="85" width="16" height="8"/>
+                <rect x="124" y="85" width="8" height="8"/>
+                <rect x="140" y="85" width="16" height="8"/>
+                <rect x="166" y="85" width="8" height="8"/>
+                <rect x="85" y="100" width="16" height="8"/>
+                <rect x="108" y="100" width="8" height="8"/>
+                <rect x="130" y="100" width="16" height="8"/>
+                <rect x="154" y="100" width="8" height="8"/>
+                <rect x="170" y="100" width="8" height="8"/>
+                <rect x="85" y="115" width="8" height="8"/>
+                <rect x="100" y="115" width="8" height="8"/>
+                <rect x="116" y="115" width="16" height="8"/>
+                <rect x="140" y="115" width="8" height="8"/>
+                <rect x="156" y="115" width="16" height="8"/>
+                <rect x="85" y="130" width="16" height="8"/>
+                <rect x="108" y="130" width="8" height="8"/>
+                <rect x="124" y="130" width="16" height="8"/>
+                <rect x="148" y="130" width="8" height="8"/>
+                <rect x="164" y="130" width="8" height="8"/>
+                <rect x="85" y="144" width="8" height="8"/>
+                <rect x="100" y="144" width="16" height="8"/>
+                <rect x="124" y="144" width="8" height="8"/>
+                <rect x="140" y="144" width="16" height="8"/>
+                <rect x="166" y="144" width="8" height="8"/>
+                <rect x="85" y="158" width="16" height="8"/>
+                <rect x="110" y="158" width="8" height="8"/>
+                <rect x="126" y="158" width="8" height="8"/>
+                <rect x="144" y="158" width="8" height="8"/>
+                <rect x="160" y="158" width="16" height="8"/>
+                <rect x="85" y="172" width="8" height="8"/>
+                <rect x="100" y="172" width="8" height="8"/>
+                <rect x="116" y="172" width="16" height="8"/>
+                <rect x="142" y="172" width="8" height="8"/>
+                <rect x="158" y="172" width="8" height="8"/>
+                <rect x="174" y="172" width="8" height="8"/>
+              </g>
+            </svg>
+          </div>
+        </div>
+
+        <div class="pix-code-text q-mb-md">{{ pixCode }}</div>
+
+        <q-btn
+          unelevated
+          rounded
+          no-caps
+          color="primary"
+          class="full-width copy-btn q-mb-lg"
+          :label="$t('payment.pix_copy_btn')"
+          :loading="processing"
+          @click="copyCode"
+        />
+
+        <p class="pix-instructions-text q-mb-sm">
+          <strong>{{ $t('payment.pix_instructions') }}</strong>
+        </p>
+        <p class="pix-instructions-text text-grey-6">
+          {{ $t('payment.pix_email_note') }}
+        </p>
+
+      </div>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted } from 'vue'
+import { useDialogPluginComponent, useQuasar, copyToClipboard } from 'quasar'
+import { formatCurrency } from 'src/helpers/utils'
+import { updateScheduleStatus } from 'src/api/schedule'
+
+const props = defineProps({
+  schedule: { type: Object, required: true },
+  total: { type: Number, required: true },
+})
+
+defineEmits([...useDialogPluginComponent.emits])
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
+const $q = useQuasar()
+
+const pixCode = `00020126580014br.gov.bcb.pix0136sfp-diaria-${props.schedule.id}-${props.schedule.provider_id}5204000053039865406${(props.total * 100).toFixed(0).padStart(8, '0')}5802BR5913Diaria App6009SAO PAULO62070503***6304ABCD`
+
+const success = ref(false)
+const processing = ref(false)
+
+const copyCode = async () => {
+  try {
+    await copyToClipboard(pixCode)
+    $q.notify({ type: 'positive', message: 'Código copiado!' })
+  } catch {
+    $q.notify({ type: 'negative', message: 'Erro ao copiar.' })
+    return
+  }
+
+  processing.value = true
+  try {
+    await updateScheduleStatus(props.schedule.id, 'paid')
+  } catch (e) {
+    console.error('Erro ao atualizar status:', e)
+  } finally {
+    processing.value = false
+  }
+  success.value = true
+  setTimeout(() => onDialogOK(), 3000)
+}
+
+const totalSeconds = ref(20 * 60)
+const countdown = ref('')
+let timer = null
+
+const updateCountdown = () => {
+  const m = Math.floor(totalSeconds.value / 60)
+  const s = totalSeconds.value % 60
+  countdown.value = `${m} min, ${String(s).padStart(2, '0')} seg`
+  if (totalSeconds.value > 0) totalSeconds.value--
+}
+
+onMounted(() => {
+  updateCountdown()
+  timer = setInterval(updateCountdown, 1000)
+})
+
+onUnmounted(() => {
+  clearInterval(timer)
+})
+</script>
+
+<style scoped lang="scss">
+.shadow-header {
+  box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.1);
+}
+
+.pix-label {
+  font-size: 15px;
+  color: #3a3a4a;
+  font-weight: 500;
+}
+
+.pix-value {
+  font-size: 15px;
+  font-weight: 700;
+}
+
+.qrcode-wrapper {
+  background: #fff;
+  border: 1px solid #e0e0e0;
+  border-radius: 12px;
+  padding: 12px;
+}
+
+.pix-code-text {
+  font-size: 11px;
+  color: #5a5a6a;
+  text-align: center;
+  word-break: break-all;
+  line-height: 1.5;
+  background: #f5f5f8;
+  border-radius: 8px;
+  padding: 10px;
+}
+
+.copy-btn {
+  font-size: 16px;
+  font-weight: 700;
+  height: 52px;
+}
+
+.pix-instructions-text {
+  font-size: 13px;
+  line-height: 1.5;
+  color: #3a3a4a;
+}
+
+.success-icon-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.success-title {
+  font-size: 24px;
+  line-height: 1.3;
+  max-width: 280px;
+}
+
+.success-message {
+  font-size: 14px;
+  line-height: 1.6;
+  max-width: 280px;
+}
+</style>

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

@@ -0,0 +1,105 @@
+<template>
+  <q-dialog ref="dialogRef" persistent maximized transition-show="fade" transition-hide="fade" @hide="onDialogHide">
+    <div class="bg-surface full-height column items-center justify-center q-px-xl">
+
+      <template v-if="!success">
+        <q-spinner-oval color="primary" size="72px" class="q-mb-lg" />
+        <div class="processing-title text-primary text-weight-bold text-center q-mb-sm">
+          {{ $t('payment.processing_title') }}
+        </div>
+        <div class="processing-message text-grey-6 text-center">
+          {{ $t('payment.processing_message') }}
+        </div>
+      </template>
+
+      <template v-else>
+        <q-btn
+          flat
+          round
+          icon="mdi-close"
+          color="grey-5"
+          class="self-end q-mb-md"
+          @click="onDialogOK"
+        />
+        <img
+          src="/logo_diaria_branco.svg"
+          alt="mascote"
+          class="success-mascot q-mb-lg"
+          style="display:none"
+        />
+        <div class="success-icon-wrapper q-mb-lg">
+          <q-icon name="mdi-check-circle" size="100px" color="primary" />
+        </div>
+        <div class="success-title text-primary text-weight-bold text-center q-mb-sm">
+          {{ $t('payment.success_title') }}
+        </div>
+        <i18n-t keypath="payment.success_message" tag="div" class="success-message text-grey-6 text-center">
+          <template #nextServices>
+            <strong class="text-text">{{ $t('payment.success_next_services') }}</strong>
+          </template>
+          <template #agenda>
+            <strong class="text-text">{{ $t('payment.success_agenda') }}</strong>
+          </template>
+        </i18n-t>
+      </template>
+
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useDialogPluginComponent } from 'quasar'
+import { updateScheduleStatus } from 'src/api/schedule'
+
+const props = defineProps({
+  schedule: { type: Object, required: true },
+})
+
+defineEmits([...useDialogPluginComponent.emits])
+
+const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent()
+
+const success = ref(false)
+
+onMounted(async () => {
+  await new Promise(resolve => setTimeout(resolve, 4000))
+  try {
+    await updateScheduleStatus(props.schedule.id, 'paid')
+  } catch (e) {
+    console.error('Erro ao atualizar status:', e)
+  }
+  success.value = true
+})
+</script>
+
+<style scoped lang="scss">
+.processing-title {
+  font-size: 22px;
+  line-height: 1.3;
+}
+
+.processing-message {
+  font-size: 15px;
+  line-height: 1.5;
+  max-width: 280px;
+}
+
+.success-icon-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.success-title {
+  font-size: 24px;
+  line-height: 1.3;
+  max-width: 280px;
+}
+
+.success-message {
+  font-size: 14px;
+  line-height: 1.6;
+  max-width: 280px;
+}
+</style>

+ 85 - 1
src/components/defaults/DefaultInputDatePicker.vue

@@ -18,7 +18,7 @@
         >
           <q-popup-proxy cover transition-show="scale" transition-hide="scale">
             <template v-if="!time">
-              <q-date v-model="date" mask="YYYY-MM-DD">
+              <q-date v-model="date" mask="YYYY-MM-DD" color="primary" class="bg-surface text-text calendar-custom">
                 <div class="row items-center justify-end">
                   <q-btn v-close-popup label="Close" color="primary" flat />
                 </div>
@@ -160,3 +160,87 @@ watch(
   { immediate: true },
 );
 </script>
+
+<style scoped lang="scss">
+
+.calendar-custom {
+  border-radius: 20px;
+  background-color: white !important;
+
+  :deep(.q-date__main) {
+    background-color: white !important;
+  }
+
+  :deep(.q-date__content) {
+    background-color: white !important;
+  }
+
+  :deep(.q-date__calendar) {
+    background-color: white !important;
+  }
+
+  :deep(.q-date__calendar-item--out) {
+    .q-btn__content {
+      color: #CBD5E1 !important;
+    }
+  }
+
+  :deep(.q-date__calendar-days .q-btn__content) {
+    font-family: 'Inter', sans-serif;
+    font-weight: 500;
+    color: #1E293B;
+  }
+
+  :deep(.q-date__calendar-weekdays > div) {
+    color: #6366F1;
+    font-weight: 700;
+    opacity: 0.8;
+  }
+
+  :deep(.q-date__navigation) {
+    .q-btn {
+      color: #1E293B !important;
+    }
+    .q-btn__content {
+      color: #1E293B !important;
+    }
+  }
+
+  :deep(.q-date__nav-btn-month),
+  :deep(.q-date__nav-btn-year) {
+    color: #6366F1 !important;
+    font-weight: 700;
+  }
+
+  :deep(.q-date__event) {
+    bottom: 4px;
+    height: 6px;
+    width: 6px;
+    border-radius: 50%;
+  }
+
+  :deep(.q-date__today) {
+    .q-btn__content {
+      color: #7c4dff !important;
+      background: #7c4dff15;
+      border-radius: 50%;
+    }
+  }
+
+  :deep(.q-date__selected) {
+    .q-btn__content {
+      background: #6366F1 !important;
+      color: white !important;
+      border-radius: 50%;
+      box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
+    }
+  }
+
+  :deep(.q-date__view--months),
+  :deep(.q-date__view--years) {
+    .q-btn {
+      color: #1E293B !important;
+    }
+  }
+}
+</style>

+ 82 - 0
src/css/app.scss

@@ -324,4 +324,86 @@ box-shadow: 1px 4px 4px 0px rgba(0,0,0,0.2);
   font-size: 20px;
   font-weight: 600;
   line-height: 1.05;
+}
+
+// customizando calendario das agendas para padrao do figma
+.calendar-custom {
+  border-radius: 20px;
+  background-color: white !important;
+
+  :deep(.q-date__main) {
+    background-color: white !important;
+  }
+
+  :deep(.q-date__content) {
+    background-color: white !important;
+  }
+
+  :deep(.q-date__calendar) {
+    background-color: white !important;
+  }
+
+  :deep(.q-date__calendar-item--out) {
+    .q-btn__content {
+      color: #CBD5E1 !important;
+    }
+  }
+
+  :deep(.q-date__calendar-days .q-btn__content) {
+    font-family: 'Inter', sans-serif;
+    font-weight: 500;
+    color: #1E293B;
+  }
+
+  :deep(.q-date__calendar-weekdays > div) {
+    color: #6366F1;
+    font-weight: 700;
+    opacity: 0.8;
+  }
+
+  :deep(.q-date__navigation) {
+    .q-btn {
+      color: #1E293B !important;
+    }
+    .q-btn__content {
+      color: #1E293B !important;
+    }
+  }
+
+  :deep(.q-date__nav-btn-month),
+  :deep(.q-date__nav-btn-year) {
+    color: #6366F1 !important;
+    font-weight: 700;
+  }
+
+  :deep(.q-date__event) {
+    bottom: 4px;
+    height: 6px;
+    width: 6px;
+    border-radius: 50%;
+  }
+
+  :deep(.q-date__today) {
+    .q-btn__content {
+      color: #7c4dff !important;
+      background: #7c4dff15;
+      border-radius: 50%;
+    }
+  }
+
+  :deep(.q-date__selected) {
+    .q-btn__content {
+      background: #6366F1 !important;
+      color: white !important;
+      border-radius: 50%;
+      box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
+    }
+  }
+
+  :deep(.q-date__view--months),
+  :deep(.q-date__view--years) {
+    .q-btn {
+      color: #1E293B !important;
+    }
+  }
 }

+ 89 - 14
src/i18n/locales/en.json

@@ -414,7 +414,8 @@
     },
     "last_schedules": {
       "title": "Last services",
-      "reschedule": "reschedule"
+      "reschedule": "reschedule",
+      "no_address": "No address"
     },
     "favorites": {
       "title": "Favorites",
@@ -431,6 +432,27 @@
       "until_2h": "Up to 2h",
       "place_home": "Home",
       "no_price": "to arrange"
+    },
+    "pending_schedules": {
+      "title": "Awaiting",
+      "requesting_with": "Requesting booking with",
+      "no_provider": "Provider not defined",
+      "cancel_btn": "cancel",
+      "status": {
+        "pending": "Awaiting",
+        "accepted": "Accepted",
+        "paid": "Paid",
+        "started": "In progress",
+        "finished": "Completed"
+      },
+      "accepted_title": "Accepted to do your daily!",
+      "detail_date": "Date:",
+      "detail_time": "Time:",
+      "detail_value": "Amount:",
+      "detail_service_fee": "Service fee:",
+      "detail_total": "Total:",
+      "btn_payment": "go to payment",
+      "btn_cancel": "Cancel request"
     }
   },
   "profile": {
@@ -445,6 +467,10 @@
     "phone": "Phone",
     "placeholder_phone": "(11) 99999-9999",
     "update": "Update",
+    "language": "Language",
+    "lang_pt": "PT-br",
+    "lang_en": "EN-us",
+    "lang_es": "ES-es",
     "payments": {
       "title": "Payments",
       "description": "My cards",
@@ -598,14 +624,6 @@
       }
     }
   },
-  "period_types": {
-    "2": "Quick (up to 2h)",
-    "4": "Medium (up to 4h)",
-    "6": "Standard (up to 6h)",
-    "8": "Full day (up to 8h)",
-    "unknown": "No information"
-  },
-
   "scheduling_page": {
     "title": "Scheduling",
     "about_provider": "About the professional",
@@ -615,6 +633,7 @@
     "no_reviews": "No reviews found.",
     "unknown_client": "Client",
     "select_service": "Select service",
+    "no_slots_available": "No time slots available for this day.",
     "book": "schedule",
     "no_price": "to be agreed",
     "time_selection": {
@@ -629,10 +648,26 @@
       "slot_required": "Select a time slot to continue."
     },
     "service_types": {
-      "integral":      { "label": "Full day",     "hours": "up to 8h of service", "description": "Ideal for cleaning with higher demands and larger spaces." },
-      "padrao":        { "label": "Standard",     "hours": "up to 6h of service", "description": "Ideal for residential and commercial cleaning seeking a traditional cleaning routine." },
-      "meio_periodo":  { "label": "Half day",     "hours": "up to 4h of service", "description": "Ideal for smaller spaces, studios or offices." },
-      "diaria_rapida": { "label": "Quick Clean",  "hours": "up to 2h of service", "description": "Ideal for hotel rooms, small spaces or specific services." }
+      "integral": {
+        "label": "Full day",
+        "hours": "up to 8h of service",
+        "description": "Ideal for cleaning with higher demands and larger spaces."
+      },
+      "padrao": {
+        "label": "Standard",
+        "hours": "up to 6h of service",
+        "description": "Ideal for residential and commercial cleaning seeking a traditional cleaning routine."
+      },
+      "meio_periodo": {
+        "label": "Half day",
+        "hours": "up to 4h of service",
+        "description": "Ideal for smaller spaces, studios or offices."
+      },
+      "diaria_rapida": {
+        "label": "Quick Clean",
+        "hours": "up to 2h of service",
+        "description": "Ideal for hotel rooms, small spaces or specific services."
+      }
     },
     "order_summary": {
       "title": "Order summary",
@@ -645,7 +680,47 @@
       "remove_confirm_title": "Are you sure you want to remove this time slot from the order?",
       "remove_confirm_ok": "remove time slot",
       "remove_confirm_cancel": "cancel",
-      "week_limit_error": "Limit of 2 bookings per week with the same professional reached."
+      "week_limit_error": "Limit of 2 bookings per week with the same professional reached.",
+      "submit_success": "Request sent successfully!",
+      "submit_error": "Could not send the request. Please try again.",
+      "no_primary_address": "Please add a primary address in your profile to schedule a service."
     }
+  },
+  "period_types": {
+    "2": "Quick (up to 2h)",
+    "4": "Medium (up to 4h)",
+    "6": "Standard (up to 6h)",
+    "8": "Full day (up to 8h)",
+    "unknown": "No information"
+  },
+  "payment": {
+    "title": "Payment",
+    "schedule_address": "Schedule to address",
+    "pay_with": "Pay with",
+    "add_card": "Add card",
+    "credit_debit": "Credit or debit",
+    "card_holder": "Holder",
+    "agree_prefix": "I agree with the",
+    "terms_link": "terms and conditions",
+    "confirm_btn": "confirm payment",
+    "pix_title": "PIX Payment",
+    "pix_total": "Total payment",
+    "pix_expires": "Pay within",
+    "pix_copy_btn": "copy pix code",
+    "pix_instructions": "Copy the Pix code above, open your bank app, choose to pay with Pix, paste the code and complete the payment. Your payment will be approved in seconds.",
+    "pix_email_note": "You will also receive instructions by email. Then just wait for confirmation from the chosen housekeeper.",
+    "processing_title": "Awaiting payment",
+    "processing_message": "We are processing your payment, please wait a moment...",
+    "success_title": "Service scheduled successfully!",
+    "success_message": "You can view your bookings in {nextServices} and in your {agenda}.",
+    "success_next_services": "upcoming services",
+    "success_agenda": "calendar",
+    "pix": "PIX"
+  },
+  "nav": {
+    "home": "Home",
+    "search": "Search",
+    "agenda": "Schedule",
+    "profile": "Profile"
   }
 }

+ 69 - 9
src/i18n/locales/es.json

@@ -414,7 +414,8 @@
     },
     "last_schedules": {
       "title": "Últimos servicios",
-      "reschedule": "reagendar"
+      "reschedule": "reagendar",
+      "no_address": "Sin dirección"
     },
     "favorites": {
       "title": "Favoritos",
@@ -431,6 +432,27 @@
       "until_2h": "Hasta 2h",
       "place_home": "Casa",
       "no_price": "a convenir"
+    },
+    "pending_schedules": {
+      "title": "En espera de confirmación",
+      "requesting_with": "Solicitando reserva con",
+      "no_provider": "Proveedor no definido",
+      "cancel_btn": "cancelar",
+      "status": {
+        "pending": "En espera de confirmación",
+        "accepted": "Aceptado",
+        "paid": "Pagado",
+        "started": "En curso",
+        "finished": "Completado"
+      },
+      "accepted_title": "¡Aceptó realizar su jornada!",
+      "detail_date": "Fecha:",
+      "detail_time": "Horario:",
+      "detail_value": "Valor:",
+      "detail_service_fee": "Tasa de servicio:",
+      "detail_total": "Total:",
+      "btn_payment": "ir al pago",
+      "btn_cancel": "Cancelar pedido"
     }
   },
   "profile": {
@@ -445,6 +467,10 @@
     "phone": "Teléfono",
     "placeholder_phone": "(11) 99999-9999",
     "update": "Actualizar",
+    "language": "Idioma",
+    "lang_pt": "PT-br",
+    "lang_en": "EN-us",
+    "lang_es": "ES-es",
     "payments": {
       "title": "Pagos",
       "description": "Mis tarjetas",
@@ -598,13 +624,6 @@
       }
     }
   },
-  "period_types": {
-    "2": "Rápido (hasta 2h)",
-    "4": "Medio (hasta 4h)",
-    "6": "Estándar (hasta 6h)",
-    "8": "Día completo (hasta 8h)",
-    "unknown": "Sin información"
-  },
   "scheduling_page": {
     "title": "Agendamiento",
     "about_provider": "Sobre el profesional",
@@ -614,6 +633,7 @@
     "no_reviews": "No se encontraron reseñas.",
     "unknown_client": "Cliente",
     "select_service": "Seleccionar servicio",
+    "no_slots_available": "No hay horarios disponibles para este día.",
     "book": "agendar",
     "no_price": "a convenir",
     "time_selection": {
@@ -660,7 +680,47 @@
       "remove_confirm_title": "¿Está seguro de que desea retirar este horario del pedido?",
       "remove_confirm_ok": "retirar horario",
       "remove_confirm_cancel": "cancelar",
-      "week_limit_error": "Límite de 2 citas por semana con el mismo profesional alcanzado."
+      "week_limit_error": "Límite de 2 citas por semana con el mismo profesional alcanzado.",
+      "submit_success": "¡Solicitud enviada con éxito!",
+      "submit_error": "No se pudo enviar la solicitud. Inténtelo de nuevo.",
+      "no_primary_address": "Agregue una dirección principal en su perfil para agendar un servicio."
     }
+  },
+  "period_types": {
+    "2": "Rápido (hasta 2h)",
+    "4": "Medio (hasta 4h)",
+    "6": "Estándar (hasta 6h)",
+    "8": "Día completo (hasta 8h)",
+    "unknown": "Sin información"
+  },
+  "payment": {
+    "title": "Pago",
+    "schedule_address": "Agendar para la dirección",
+    "pay_with": "Pagar con",
+    "add_card": "Agregar tarjeta",
+    "credit_debit": "Crédito o débito",
+    "card_holder": "Titular",
+    "agree_prefix": "Acepto los",
+    "terms_link": "términos y condiciones",
+    "confirm_btn": "confirmar pago",
+    "pix_title": "Pago con PIX",
+    "pix_total": "Pago total",
+    "pix_expires": "Pagar en hasta",
+    "pix_copy_btn": "copiar código pix",
+    "pix_instructions": "Copie el código Pix de arriba, abra la aplicación de su banco, elija pagar con Pix, pegue el código y finalice el pago. Su pago será aprobado en segundos.",
+    "pix_email_note": "También recibirá instrucciones por correo electrónico. Luego solo espere la confirmación de la empleada doméstica elegida.",
+    "processing_title": "Esperando pago",
+    "processing_message": "Estamos procesando su pago, por favor espere un momento...",
+    "success_title": "¡Servicio programado con éxito!",
+    "success_message": "Puede ver sus reservas en {nextServices} y en su {agenda}.",
+    "success_next_services": "próximos servicios",
+    "success_agenda": "agenda",
+    "pix": "PIX"
+  },
+  "nav": {
+    "home": "Inicio",
+    "search": "Buscar",
+    "agenda": "Agenda",
+    "profile": "Perfil"
   }
 }

+ 69 - 9
src/i18n/locales/pt.json

@@ -419,7 +419,8 @@
     },
     "last_schedules": {
       "title": "Últimos serviços",
-      "reschedule": "reagendar"
+      "reschedule": "reagendar",
+      "no_address": "Sem endereço"
     },
     "favorites": {
       "title": "Favoritos",
@@ -436,6 +437,27 @@
       "until_2h": "Até 2h",
       "place_home": "Casa",
       "no_price": "a combinar"
+    },
+    "pending_schedules": {
+      "title": "Aguardando confirmação",
+      "requesting_with": "Solicitando agendamento com",
+      "no_provider": "Prestador não definido",
+      "cancel_btn": "cancelar",
+      "status": {
+        "pending": "Aguardando",
+        "accepted": "Aceito",
+        "paid": "Pago",
+        "started": "Em andamento",
+        "finished": "Concluído"
+      },
+      "accepted_title": "Aceitou realizar sua diária!",
+      "detail_date": "Data:",
+      "detail_time": "Horário:",
+      "detail_value": "Valor:",
+      "detail_service_fee": "Taxa de serviço:",
+      "detail_total": "Total:",
+      "btn_payment": "ir para o pagamento",
+      "btn_cancel": "Cancelar pedido"
     }
   },
   "profile": {
@@ -450,6 +472,10 @@
     "phone": "Telefone",
     "placeholder_phone": "(11) 99999-9999",
     "update": "Atualizar",
+    "language": "Idioma",
+    "lang_pt": "PT-br",
+    "lang_en": "EN-us",
+    "lang_es": "ES-es",
     "payments": {
       "title": "Pagamentos",
       "description": "Meus cartões",
@@ -603,13 +629,6 @@
       }
     }
   },
-  "period_types": {
-    "2": "Rápida (até 2h)",
-    "4": "Média (até 4h)",
-    "6": "Padrão (até 6h)",
-    "8": "Dia completo (até 8h)",
-    "unknown": "Sem informação"
-  },
   "scheduling_page": {
     "title": "Agendamento",
     "about_provider": "Sobre o profissional",
@@ -619,6 +638,7 @@
     "no_reviews": "Nenhuma avaliação encontrada.",
     "unknown_client": "Cliente",
     "select_service": "Selecionar serviço",
+    "no_slots_available": "Não há horários disponíveis para este dia.",
     "book": "agendar",
     "no_price": "a combinar",
     "time_selection": {
@@ -665,7 +685,47 @@
       "remove_confirm_title": "Tem certeza que deseja retirar esse horário do pedido?",
       "remove_confirm_ok": "retirar horário",
       "remove_confirm_cancel": "cancelar",
-      "week_limit_error": "Limite de 2 agendamentos por semana com o mesmo profissional atingido."
+      "week_limit_error": "Limite de 2 agendamentos por semana com o mesmo profissional atingido.",
+      "submit_success": "Solicitação enviada com sucesso!",
+      "submit_error": "Não foi possível enviar a solicitação. Tente novamente.",
+      "no_primary_address": "Cadastre um endereço principal no seu perfil para agendar."
     }
+  },
+  "period_types": {
+    "2": "Rápida (até 2h)",
+    "4": "Média (até 4h)",
+    "6": "Padrão (até 6h)",
+    "8": "Dia completo (até 8h)",
+    "unknown": "Sem informação"
+  },
+  "payment": {
+    "title": "Pagamento",
+    "schedule_address": "Agendar para o endereço",
+    "pay_with": "Pagar com",
+    "add_card": "Adicionar cartão",
+    "credit_debit": "Crédito ou débito",
+    "card_holder": "Titular",
+    "agree_prefix": "Concordo com os",
+    "terms_link": "termos e condições",
+    "confirm_btn": "confirmar pagamento",
+    "pix_title": "Pagamento com PIX",
+    "pix_total": "Pagamento total",
+    "pix_expires": "Pagar em até",
+    "pix_copy_btn": "copiar código pix",
+    "pix_instructions": "Copie o código Pix acima, acesse o app do seu banco, escolha pagar com Pix, cole o código e finalize o pagamento. Seu pagamento será aprovado em alguns segundos.",
+    "pix_email_note": "Você também receberá as instruções no seu email. Depois, é só aguardar a confirmação do diarista escolhido.",
+    "processing_title": "Aguardando pagamento",
+    "processing_message": "Estamos processando seu pagamento, aguarde um momento...",
+    "success_title": "Diária agendada com sucesso!",
+    "success_message": "Você pode visualizar seus agendamentos em {nextServices} e em sua {agenda}.",
+    "success_next_services": "próximos serviços",
+    "success_agenda": "agenda",
+    "pix": "PIX"
+  },
+  "nav": {
+    "home": "Início",
+    "search": "Busca",
+    "agenda": "Agenda",
+    "profile": "Perfil"
   }
 }

+ 8 - 6
src/layouts/MainLayout.vue

@@ -50,6 +50,7 @@
 import { computed, useTemplateRef, watch } from "vue";
 import { useQuasar } from "quasar";
 import { useRoute } from "vue-router";
+import { useI18n } from "vue-i18n";
 
 defineOptions({
   name: "MainLayout",
@@ -58,31 +59,32 @@ defineOptions({
 const $q = useQuasar();
 const route = useRoute();
 const scrollAreaRef = useTemplateRef("scrollAreaRef");
+const { t } = useI18n();
 
 let oldValue = route.path;
 
-const navItems = [
+const navItems = computed(() => [
   {
     name: "DashboardPage",
-    label: "Início",
+    label: t('nav.home'),
     icon: "mdi-home-outline",
   },
   {
     name: "SearchPage",
-    label: "Busca",
+    label: t('nav.search'),
     icon: "mdi-magnify",
   },
   {
     name: "AgendaPage",
-    label: "Agenda",
+    label: t('nav.agenda'),
     icon: "mdi-calendar-blank-outline",
   },
   {
     name: "ProfilePage",
-    label: "Perfil",
+    label: t('nav.profile'),
     icon: "mdi-account-circle-outline",
   },
-];
+]);
 
 const isNavItemActive = (item) => route.name === item.name;
 

+ 32 - 8
src/pages/dashboard/DashboardPage.vue

@@ -8,6 +8,11 @@
     <template v-else>
       <DashboardHeaderBar :data="headerBar" />
       <DashboardSummaryInfos :data="summaryInfos" />
+      <DashboardPendingSchedules
+        v-if="pendingSchedules.length > 0"
+        :data="pendingSchedules"
+        @view-details="openAcceptedDialog"
+      />
       <DashboardScrollAreaSchedules />
       <DashboardPendingCustomSchedules />
       <DashboardNextSchedules v-if="nextSchedules.length > 0" :data="nextSchedules" />
@@ -21,6 +26,8 @@
 <script setup>
 import DashboardHeaderBar from 'src/components/dashboard/DashboardHeaderBar.vue';
 import DashboardSummaryInfos from 'src/components/dashboard/DashboardSummaryInfos.vue';
+import DashboardPendingSchedules from 'src/components/dashboard/DashboardPendingSchedules.vue';
+import ScheduleAcceptedDialog from 'src/components/dashboard/ScheduleAcceptedDialog.vue';
 import DashboardScrollAreaSchedules from 'src/components/dashboard/DashboardScrollAreaSchedules.vue';
 import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedules.vue';
 import DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
@@ -30,29 +37,42 @@ import FinalSuccesModal from '../schedules/components/FinalSuccesModal.vue';
 import DashboardPendingCustomSchedules from 'src/pages/dashboard/components/DashboardPendingCustomSchedules.vue';
 import { useRouter } from 'vue-router'
 import { onMounted, ref } from 'vue';
-import { dadosDashboard } from 'src/api/dashboard';
 import { useQuasar } from 'quasar';
+import { dadosDashboard } from 'src/api/dashboard';
 
 const router = useRouter()
 const headerBar = ref({});
 const summaryInfos = ref({});
+const pendingSchedules = ref([]);
 const nextSchedules = ref([]);
 const lastDoneSchedules = ref([]);
 const favoriteProviders = ref([]);
 const providersClose = ref([]);
-const loading = ref(true);
 const $q = useQuasar();
+const loading = ref(true);
 
 const showSuccessModal = ref(router.currentRoute.value.fullPath.includes('showSuccessModal') || false);
-onMounted( async () => {
+
+const openAcceptedDialog = (schedule) => {
+  $q.dialog({
+    component: ScheduleAcceptedDialog,
+    componentProps: { schedule }
+  }).onOk(() => {
+    reloadDashboard();
+  });
+};
+
+const reloadDashboard = async () => {
+  loading.value = true;
   const response = await dadosDashboard();
-  if(response) {
+  if (response) {
     headerBar.value = response.headerBar;
     summaryInfos.value = response.summaryInfos;
-    nextSchedules.value = response.nextSchedules;
-    lastDoneSchedules.value = response.lastDoneSchedules;
-    favoriteProviders.value = response.favoriteProviders;
-    providersClose.value = response.providersClose;
+    pendingSchedules.value = response.pendingSchedules ?? [];
+    nextSchedules.value = response.nextSchedules ?? [];
+    lastDoneSchedules.value = response.lastDoneSchedules ?? [];
+    favoriteProviders.value = response.favoriteProviders ?? [];
+    providersClose.value = response.providersClose ?? [];
   }
   if( showSuccessModal.value ) {
     $q.dialog({
@@ -64,6 +84,10 @@ onMounted( async () => {
 
 
   loading.value = false;
+};
+
+onMounted(async () => {
+  await reloadDashboard();
 });
 </script>
 

+ 26 - 2
src/pages/profile/ProfileEditDialog.vue

@@ -57,6 +57,15 @@
                 :placeholder="$t('profile.placeholder_phone')"
               />
             </div>
+
+            <div>
+              <div class="text-weight-bold text-grey-8 q-mb-sm">{{ $t('profile.language') }}</div>
+              <div class="row">
+                <q-radio v-model="selectedLocale" val="pt" :label="$t('profile.lang_pt')" color="primary" class="text-text col-4" 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" 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" keep-color @update:model-value="onLocaleChange" />
+              </div>
+            </div>
           </div>
 
           <q-space/>
@@ -83,9 +92,10 @@
 
 <script setup>
 import { ref, onMounted } from 'vue';
-import { useDialogPluginComponent } from 'quasar';
+import { useDialogPluginComponent, Cookies } from 'quasar';
 import { updateUser } from 'src/api/user';
 import { useFormUpdateTracker } from 'src/composables/useFormUpdateTracker';
+import { i18n } from 'src/boot/i18n';
 
 const props = defineProps({
   userData: {
@@ -101,6 +111,21 @@ const loading = ref(false);
 const submitting = ref(false);
 const userId = ref(null);
 
+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 onLocaleChange = (val) => {
+  i18n.global.locale.value = val   // troca em tempo real (composition API mode)
+  Cookies.set('locale', val, { expires: 365, path: '/' })
+}
+
 const { form, hasUpdatedFields, setUpdateFormAsOriginal } = useFormUpdateTracker({
   name: '',
   email: '',
@@ -117,7 +142,6 @@ const submitUpdate = async () => {
       email: form.email,
       phone: form.phone
     }, userId.value);
-    console.log(data)
     setUpdateFormAsOriginal(data);
     onDialogOK(data);
   } catch (error) {

+ 1 - 1
src/pages/schedules/SobMedidaPage.vue

@@ -262,7 +262,7 @@ const openServiceTimeSelection = (serviceType) => {
 }
 
 const saveFinalOrder = async (payloadFinal) => {
-  let [startHour, endHour] = payloadFinal.slot.split('-')
+  let [startHour, endHour] = payloadFinal.slot.value.split('-')
 
   startHour = String(startHour).padStart(2, '0')
   endHour = String(endHour).padStart(2, '0')

+ 332 - 0
src/pages/scheduling/SchedulingPage.vue

@@ -0,0 +1,332 @@
+<template>
+  <q-page class="bg-page">
+
+    <div class="row items-center q-px-md q-pt-md q-pb-sm bg-white shadow-header">
+      <q-btn flat round dense icon="mdi-chevron-left" color="primary" @click="router.back()" />
+      <div class="col text-center text-subtitle1 text-weight-bold text-primary">
+        {{ $t('scheduling_page.title') }}
+      </div>
+      <div style="width: 36px" />
+    </div>
+
+    <div class="col overflow-auto q-pb-xl">
+
+      <div class="q-px-md q-pt-md">
+        <q-card class="card-border bg-white" flat>
+          <q-card-section class="q-pa-md">
+            <div class="text-subtitle2 text-weight-bold text-primary q-mb-sm">
+              {{ $t('scheduling_page.about_provider') }}
+            </div>
+            <div class="row items-center no-wrap q-gutter-x-md">
+              <q-avatar :style="avatarStyle" size="52px" class="text-weight-bold text-body1">
+                {{ provider?.provider_name?.slice(0, 1).toUpperCase() ?? '—' }}
+              </q-avatar>
+              <div class="col">
+                <div class="text-weight-bold text-text">{{ provider?.provider_name ?? '—' }}</div>
+                <div class="text-caption text-grey-6">{{ provider?.city ?? provider?.district ?? '—' }}</div>
+                <div class="row items-center q-gutter-x-sm q-mt-xs">
+                  <div class="row items-center">
+                    <q-icon name="mdi-star" color="warning" size="14px" />
+                    <span class="text-caption text-weight-medium q-ml-xs">
+                      {{ provider?.average_rating != null ? Number(provider.average_rating).toFixed(1) : '' }}
+                      <span class="text-grey-5">{{ '(' + (provider?.total_reviews ?? 0) + ')' }}</span>
+                    </span>
+                  </div>
+                  <div class="row items-center">
+                    <q-icon name="mdi-broom" color="secondary" size="14px" />
+                    <span class="text-caption q-ml-xs">{{ provider?.total_services ?? 0 }}</span>
+                  </div>
+                </div>
+              </div>
+              <div class="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" />
+              </div>
+            </div>
+          </q-card-section>
+        </q-card>
+      </div>
+
+      <div class="q-px-md q-pt-md">
+        <div class="text-h6 text-weight-bold gradient-diarista q-mb-xs">
+          {{ $t('scheduling_page.schedule_service') }}
+        </div>
+
+        <div v-if="loadingAvailability" class="row items-center justify-center q-py-lg">
+          <q-spinner-dots color="primary" size="36px" />
+        </div>
+
+        <div v-else class="shadow-card q-mb-md" style="border-radius: 20px; overflow: hidden;">
+          <q-date
+            v-model="selectedDate"
+            square
+            class="full-width calendar-custom text-text"
+            :first-day-of-week="0"
+            :events="availableDatesForCalendar"
+            event-color="positive"
+            :options="dateOptions"
+            minimal
+          />
+        </div>
+      </div>
+
+      <div class="q-px-md q-pt-sm">
+        <div class="row items-center justify-between q-mb-sm">
+          <div class="text-h6 text-weight-bold gradient-diarista">
+            {{ $t('scheduling_page.reviews_title') }}
+          </div>
+          <span class="text-caption text-primary cursor-pointer">
+            {{ $t('scheduling_page.see_all') }}
+          </span>
+        </div>
+
+        <div v-if="loadingReviews" class="row items-center justify-center q-py-lg">
+          <q-spinner-dots color="primary" size="36px" />
+        </div>
+
+        <div v-else-if="reviews.length === 0" class="text-center text-grey-6 text-body2 q-py-md">
+          {{ $t('scheduling_page.no_reviews') }}
+        </div>
+
+        <div v-else class="row no-wrap scroll-reviews q-pb-sm" style="overflow-x: auto;">
+          <q-card
+            v-for="review in reviews"
+            :key="review.id"
+            class="review-card card-border bg-white q-mr-sm flex-shrink-0"
+            flat
+            style="min-width: 220px; max-width: 240px;"
+          >
+            <q-card-section class="q-pa-sm">
+              <div class="row items-center no-wrap q-gutter-x-sm q-mb-xs">
+                <q-avatar size="32px" :style="clientAvatarStyle(review)" class="text-weight-bold text-caption">
+                  {{ review.schedule?.client?.name?.slice(0, 1).toUpperCase() ?? '?' }}
+                </q-avatar>
+                <div class="col text-weight-medium text-text text-caption ellipsis">
+                  {{ review.schedule?.client?.name ?? $t('scheduling_page.unknown_client') }}
+                </div>
+              </div>
+              <div class="row items-center q-mb-xs">
+                <q-icon
+                  v-for="s in 5"
+                  :key="s"
+                  :name="s <= review.stars ? 'mdi-star' : 'mdi-star-outline'"
+                  color="warning"
+                  size="14px"
+                />
+              </div>
+              <div class="text-caption text-text review-comment ellipsis-2-lines">
+                {{ review.comment ?? '' }}
+              </div>
+            </q-card-section>
+          </q-card>
+        </div>
+      </div>
+
+    </div>
+  </q-page>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+import { useRouter } from 'vue-router';
+import { date } from 'quasar';
+import { getProviderWorkingDays, getProviderBlockedDays } from 'src/api/providerAvailability';
+import { getProviderReceivedReviews } from 'src/api/review';
+
+const router = useRouter();
+
+const provider = ref(history.state?.provider ?? null);
+
+const selectedDate = ref(null);
+const workingDays = ref([]);
+const blockedDays = ref([]);
+const loadingAvailability = ref(true);
+
+const reviews = ref([]);
+const loadingReviews = ref(true);
+
+const avatarColors = [
+  { background: '#ffd5df', color: '#932e57' },
+  { background: '#d7e8ff', color: '#2158a8' },
+  { background: '#dfd',    color: '#2a7a3b' },
+  { background: '#ffe5cc', color: '#8a4500' },
+];
+
+const avatarStyle = computed(() => {
+  const idx = (provider.value?.provider_id ?? 0) % avatarColors.length;
+  return avatarColors[idx];
+});
+
+const clientAvatarStyle = (review) => {
+  const idx = (review.id ?? 0) % avatarColors.length;
+  return avatarColors[idx];
+};
+
+const availableWeekDays = computed(() =>
+  [...new Set(workingDays.value.map((wd) => wd.day))]
+);
+
+const blockedDateSet = computed(() =>
+  new Set(blockedDays.value.map((bd) => bd.date))
+);
+
+const availableDatesForCalendar = computed(() => {
+  const result = [];
+  const today = new Date();
+  const end = date.addToDate(today, { months: 3 });
+  let current = new Date(today);
+  while (current <= end) {
+    const dayOfWeek = current.getDay();
+    const dateStr = date.formatDate(current, 'YYYY-MM-DD');
+    const isWorkingDay = availableWeekDays.value.includes(dayOfWeek);
+    const isBlocked = blockedDateSet.value.has(dateStr);
+    if (isWorkingDay && !isBlocked) {
+      result.push(date.formatDate(current, 'YYYY/MM/DD'));
+    }
+    current = date.addToDate(current, { days: 1 });
+  }
+  return result;
+});
+
+const dateOptions = (d) => {
+  const today = date.formatDate(new Date(), 'YYYY/MM/DD');
+  if (d < today) return false;
+  const raw = d.replace(/\//g, '-');
+  const dayOfWeek = new Date(d.replace(/\//g, '-')).getDay();
+  const isWorkingDay = availableWeekDays.value.includes(dayOfWeek);
+  const isBlocked = blockedDateSet.value.has(raw);
+  return isWorkingDay && !isBlocked;
+};
+
+const loadAvailability = async () => {
+  if (!provider.value?.provider_id) return;
+  loadingAvailability.value = true;
+  try {
+    const [wd, bd] = await Promise.all([
+      getProviderWorkingDays(provider.value.provider_id),
+      getProviderBlockedDays(provider.value.provider_id),
+    ]);
+    workingDays.value = wd ?? [];
+    blockedDays.value = bd ?? [];
+  } catch {
+    workingDays.value = [];
+    blockedDays.value = [];
+  } finally {
+    loadingAvailability.value = false;
+  }
+};
+
+const loadReviews = async () => {
+  if (!provider.value?.provider_id) return;
+  loadingReviews.value = true;
+  try {
+    const all = await getProviderReceivedReviews(provider.value.provider_id);
+    reviews.value = (all ?? []).slice(0, 10);
+  } catch {
+    reviews.value = [];
+  } finally {
+    loadingReviews.value = false;
+  }
+};
+
+onMounted(async () => {
+  await Promise.all([loadAvailability(), loadReviews()]);
+});
+</script>
+
+<style scoped lang="scss">
+.shadow-header {
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+}
+
+.review-card {
+  border-radius: 12px;
+}
+
+.review-comment {
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
+
+.calendar-custom {
+  border-radius: 20px;
+  background-color: white !important;
+
+  :deep(.q-date__main) {
+    background-color: white !important;
+  }
+
+  :deep(.q-date__content) {
+    background-color: white !important;
+  }
+
+  :deep(.q-date__calendar) {
+    background-color: white !important;
+  }
+
+  :deep(.q-date__calendar-item--out) {
+    .q-btn__content {
+      color: #CBD5E1 !important;
+    }
+  }
+
+  :deep(.q-date__calendar-days .q-btn__content) {
+    font-family: 'Inter', sans-serif;
+    font-weight: 500;
+    color: #1E293B;
+  }
+
+  :deep(.q-date__calendar-weekdays > div) {
+    color: #6366F1;
+    font-weight: 700;
+    opacity: 0.8;
+  }
+
+  :deep(.q-date__navigation) {
+    .q-btn {
+      color: #1E293B !important;
+    }
+    .q-btn__content {
+      color: #1E293B !important;
+    }
+  }
+
+  :deep(.q-date__nav-btn-month),
+  :deep(.q-date__nav-btn-year) {
+    color: #6366F1 !important;
+    font-weight: 700;
+  }
+
+  :deep(.q-date__event) {
+    bottom: 4px;
+    height: 6px;
+    width: 6px;
+    border-radius: 50%;
+  }
+
+  :deep(.q-date__today) {
+    .q-btn__content {
+      color: #7c4dff !important;
+      background: #7c4dff15;
+      border-radius: 50%;
+    }
+  }
+
+  :deep(.q-date__selected) {
+    .q-btn__content {
+      background: #6366F1 !important;
+      color: white !important;
+      border-radius: 50%;
+      box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
+    }
+  }
+
+  :deep(.q-date__view--months),
+  :deep(.q-date__view--years) {
+    .q-btn {
+      color: #1E293B !important;
+    }
+  }
+}
+</style>

+ 26 - 17
src/pages/search/SearchPage.vue

@@ -126,6 +126,7 @@
                       size="sm"
                       padding="3px 12px"
                       :label="$t('search_page.schedule_btn')"
+                      @click="goToScheduling(p)"
                     />
                   </div>
                 </div>
@@ -143,14 +144,15 @@
 import { ref, computed, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
 import { useI18n } from 'vue-i18n';
-// import { useQuasar } from 'quasar';
+import { useQuasar } from 'quasar';
 import { buscaPrestadores } from 'src/api/dashboard';
 import { formatCurrency } from 'src/helpers/utils';
-// import SearchFilterDialog from 'src/pages/search/components/SearchFilterDialog.vue';
+import SearchFilterDialog from 'src/pages/search/components/SearchFilterDialog.vue';
+import SchedulingDialog from 'src/pages/search/components/SchedulingDialog.vue';
 
 const { t } = useI18n();
 const router = useRouter();
-// const $q = useQuasar();
+const $q = useQuasar();
 
 const allProviders = ref([]);
 const loading      = ref(true);
@@ -227,20 +229,27 @@ const loadProviders = async () => {
 
 const onNameChange = () => loadProviders();
 
-// const openFilterDialog = () => {
-//   $q.dialog({
-//     component: SearchFilterDialog,
-//     componentProps: {
-//       initialSort: activeSort.value,
-//       initialDate: activeDate.value,
-//     },
-//   }).onOk(({ sort, date }) => {
-//     const dateChanged = date !== activeDate.value;
-//     activeSort.value = sort;
-//     activeDate.value = date;
-//     if (dateChanged) loadProviders();
-//   });
-// };
+const openFilterDialog = () => {
+  $q.dialog({
+    component: SearchFilterDialog,
+    componentProps: {
+      initialSort: activeSort.value,
+      initialDate: activeDate.value,
+    },
+  }).onOk(({ sort, date }) => {
+    const dateChanged = date !== activeDate.value;
+    activeSort.value = sort;
+    activeDate.value = date;
+    if (dateChanged) loadProviders();
+  });
+};
+
+const goToScheduling = (provider) => {
+  $q.dialog({
+    component: SchedulingDialog,
+    componentProps: { provider },
+  });
+};
 
 const avatarColors = [
   { background: '#ffd5df', color: '#932e57' },

+ 366 - 0
src/pages/search/components/OrderSummaryDialog.vue

@@ -0,0 +1,366 @@
+<template>
+  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down">
+    <div class="dialog-root">
+
+      <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" />
+        <div class="col text-center text-subtitle1 text-weight-bold text-primary">
+          {{ $t('scheduling_page.title') }}
+        </div>
+        <div style="width: 36px" />
+      </div>
+
+      <div class="dialog-body">
+
+        <div class="q-px-md q-pt-md">
+          <div class="info-banner card-border q-pa-md">
+            <div class="text-body2 text-weight-medium text-primary">
+              {{ $t('scheduling_page.order_summary.info_text') }}
+            </div>
+            <div class="text-caption text-primary q-mt-xs" style="opacity: 0.75;">
+              {{ $t('scheduling_page.order_summary.info_note') }}
+            </div>
+          </div>
+        </div>
+
+        <div class="q-px-md q-pt-md">
+          <div class="text-h6 text-weight-bold gradient-diarista q-mb-sm">
+            {{ $t('scheduling_page.order_summary.title') }}
+          </div>
+
+          <q-card
+            v-for="(booking, idx) in bookings"
+            :key="idx"
+            :flat="false"
+            class="card-border bg-surface q-mb-sm shadow-card"
+          >
+            <q-card-section class="q-pa-md row items-center no-wrap">
+              <div class="col">
+                <div class="text-body2 text-text">
+                  <span class="text-weight-bold">{{ $t('scheduling_page.order_summary.service_label') }}</span>
+                  <span class="text-weight-bold">{{ ` ${booking.serviceType.label} (${booking.serviceType.hours})` }}</span>
+                </div>
+                <div class="text-body2 text-weight-bold text-text">{{ formatDate(booking.date) }}</div>
+                <div class="text-body2 text-text">
+                  {{ $t('scheduling_page.order_summary.time_range', { start: booking.slot.startHour, end: booking.slot.endHour }) }}
+                </div>
+              </div>
+              <q-btn
+                flat round dense
+                icon="mdi-minus-circle-outline"
+                color="grey-5"
+                @click="confirmRemove(idx)"
+              />
+            </q-card-section>
+          </q-card>
+
+          <q-btn
+            unelevated rounded no-caps
+            :label="$t('scheduling_page.order_summary.send_btn')"
+            color="secondary"
+            class="full-width q-mt-sm"
+            @click="submitOrder"
+          />
+
+          <q-btn
+            outline rounded no-caps
+            :label="$t('scheduling_page.order_summary.add_date_btn')"
+            color="primary"
+            class="full-width q-mt-xs"
+            :disable="showCalendar"
+            @click="showCalendar = true"
+          />
+        </div>
+
+        <div v-if="showCalendar" class="q-px-md q-pt-lg q-pb-xl">
+          <div v-if="loadingAvailability" class="row items-center justify-center q-py-lg">
+            <q-spinner-dots color="primary" size="36px" />
+          </div>
+          <div v-else class="calendar-wrapper shadow-card">
+            <q-date
+              v-model="addDateValue"
+              square
+              class="full-width"
+              :first-day-of-week="0"
+              :options="dateOptions"
+              minimal
+              @update:model-value="onAddDateSelected"
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+import { useDialogPluginComponent, useQuasar } 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 { createSchedule } from 'src/api/schedule';
+import { userStore } from 'src/stores/user';
+import ServiceSelectionSheet from './ServiceSelectionSheet.vue';
+import ServiceTimeSelectionDialog from './ServiceTimeSelectionDialog.vue';
+
+const props = defineProps({
+  provider:       { type: Object, required: true },
+  initialBooking: { type: Object, required: true },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef } = useDialogPluginComponent();
+const $q = useQuasar();
+const { t, locale } = useI18n();
+const store = userStore();
+
+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 workingDays = ref([]);
+const blockedDays = ref([]);
+
+const getWeekStart = (dateStr) => {
+  const d = new Date(dateStr.replace(/\//g, '-') + 'T12:00:00');
+  d.setDate(d.getDate() - d.getDay());
+  return d.toISOString().slice(0, 10);
+};
+
+const wouldExceedWeekLimit = (newDateStr) => {
+  const newWeek = getWeekStart(newDateStr);
+  const count = bookings.value.filter(b => getWeekStart(b.date) === newWeek).length;
+  return count >= 2;
+};
+
+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 dateOptions = (d) => {
+  const today = date.formatDate(new Date(), 'YYYY/MM/DD');
+  if (d < today) return false;
+  if (wouldExceedWeekLimit(d)) return false;
+  const raw = d.replace(/\//g, '-');
+  const parsed = new Date(`${raw}T12:00:00`);
+  const dayOfWeek = parsed.getDay();
+  const isWorking = availableWeekDays.value.includes(dayOfWeek);
+  const isBlocked = blockedDateSet.value.has(raw);
+  return isWorking && !isBlocked;
+};
+
+const loadAvailability = async () => {
+  loadingAvailability.value = true;
+  try {
+    const [wd, bd] = await Promise.all([
+      getProviderWorkingDays(props.provider.provider_id),
+      getProviderBlockedDays(props.provider.provider_id),
+    ]);
+    workingDays.value = wd ?? [];
+    blockedDays.value = bd ?? [];
+  } catch {
+    workingDays.value = [];
+    blockedDays.value = [];
+  } finally {
+    loadingAvailability.value = false;
+  }
+};
+
+const loadPrimaryAddress = async () => {
+  try {
+    const clientId = store.user?.client_id;
+    if (!clientId) return;
+    const addresses = await getAddresses('client', clientId);
+    primaryAddress.value = (addresses ?? []).find(a => a.is_primary) ?? null;
+  } catch {
+    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) => {
+  if (!val) return;
+  addDateValue.value = null;
+  const valFormatted = val.replace(/\//g, '-');
+
+  const blocksOfDate = blockedDays.value.filter(
+    bd => bd.date === valFormatted && bd.period !== 'all'
+  );
+
+  const dayOfWeek = new Date(`${valFormatted}T12:00:00`).getDay();
+  const dayPeriods = workingDays.value
+    .filter(wd => wd.day === dayOfWeek)
+    .map(wd => wd.period);
+
+  const workingDayBlocks = [];
+  if (!dayPeriods.includes('afternoon')) {
+    workingDayBlocks.push({ init_hour: '14:00:00', end_hour: '20:00:00' });
+  }
+  if (!dayPeriods.includes('morning')) {
+    workingDayBlocks.push({ init_hour: '7:00:00', end_hour: '13:00:00' });
+  }
+
+  const existingBookingBlocks = 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`,
+    }));
+
+  const partialBlocks = [...blocksOfDate, ...workingDayBlocks, ...existingBookingBlocks];
+
+  $q.dialog({
+    component: ServiceSelectionSheet,
+    componentProps: { provider: props.provider, selectedDate: val, partialBlocks },
+  }).onOk(({ serviceType, date: date_, provider: prov }) => {
+    $q.dialog({
+      component: ServiceTimeSelectionDialog,
+      componentProps: { serviceType, selectedDate: date_, provider: prov, partialBlocks },
+    }).onOk((booking) => {
+      if (wouldExceedWeekLimit(booking.date)) {
+        $q.notify({ type: 'negative', message: t('scheduling_page.order_summary.week_limit_error') });
+        return;
+      }
+      bookings.value.push(booking);
+      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 () => {
+  if (!primaryAddress.value) {
+    $q.notify({ type: 'warning', message: t('scheduling_page.order_summary.no_primary_address') });
+    return;
+  }
+
+  const payload = {
+    client_id:     store.user.client_id,
+    provider_id:   props.provider.provider_id,
+    address_id:    primaryAddress.value.id,
+    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,
+    })),
+  };
+
+  submitting.value = true;
+  try {
+    await createSchedule(payload);
+    $q.notify({ type: 'positive', message: t('scheduling_page.order_summary.submit_success') });
+    dialogRef.value.hide();
+  } catch (err) {
+    const msg = err?.response?.data?.message
+      ?? err?.message
+      ?? t('scheduling_page.order_summary.submit_error');
+    $q.notify({ type: 'negative', message: msg });
+  } finally {
+    submitting.value = false;
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.dialog-root {
+  width: 100vw;
+  max-width: 100vw;
+  height: 100vh;
+  max-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  background: #F9FAFB;
+}
+
+.dialog-header {
+  flex-shrink: 0;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+}
+
+.dialog-body {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+  overflow-x: clip;
+}
+
+.info-banner {
+  background: rgba(139, 92, 246, 0.08);
+  border: 1px solid rgba(139, 92, 246, 0.2);
+}
+
+.calendar-wrapper {
+  border-radius: 20px;
+  overflow: hidden;
+  background: white;
+
+  :deep(.q-date) { background: white; width: 100%; }
+  :deep(.q-date__main),
+  :deep(.q-date__content),
+  :deep(.q-date__calendar) { background: white !important; }
+
+  :deep(.q-date__calendar-item .q-btn) {
+    font-size: 13px !important;
+    min-width: 0 !important;
+    padding: 6px 2px !important;
+  }
+  :deep(.q-date__calendar-item .q-btn.disabled),
+  :deep(.q-date__calendar-item .q-btn[disabled]) { opacity: 1 !important; }
+  :deep(.q-date__calendar-item .q-btn.disabled .q-btn__content),
+  :deep(.q-date__calendar-item .q-btn[disabled] .q-btn__content) { color: #CBD5E1 !important; }
+
+  :deep(.q-date__calendar-days .q-btn__content) { font-weight: 500; color: #1E293B; }
+  :deep(.q-date__calendar-weekdays > div) { color: #6366F1; font-weight: 700; opacity: 0.8; }
+  :deep(.q-date__navigation) {
+    .q-btn { color: #6366F1 !important; }
+    .q-btn__content { color: #6366F1 !important; }
+  }
+  :deep(.q-date__event) { bottom: 4px; height: 6px; width: 6px; border-radius: 50%; }
+  :deep(.q-date__today .q-btn__content) { color: #7c4dff !important; background: #7c4dff15; border-radius: 50%; }
+  :deep(.q-date__selected .q-btn__content) {
+    background: #6366F1 !important;
+    color: white !important;
+    border-radius: 50%;
+    box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
+  }
+}
+</style>

+ 466 - 0
src/pages/search/components/SchedulingDialog.vue

@@ -0,0 +1,466 @@
+<template>
+  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down">
+    <div class="dialog-root">
+
+      <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" />
+        <div class="col text-center text-subtitle1 text-weight-bold text-primary gradient-diarista q-mb-xs">
+          {{ $t('scheduling_page.title') }}
+        </div>
+        <div style="width: 36px" />
+      </div>
+
+      <div class="dialog-body">
+
+        <div class="q-px-md q-pt-md">
+          <div class="text-h6 text-weight-bold gradient-diarista q-mb-xs">
+            {{ $t('scheduling_page.about_provider') }}
+          </div>
+          <q-card class="card-border shadow-card bg-surface text-text" :flat="false">
+            <q-card-section class="q-pa-md">
+              <div class="row items-center no-wrap q-gutter-x-md">
+                <q-avatar :style="avatarStyle" size="52px" class="text-weight-bold text-body1">
+                  {{ provider?.provider_name?.slice(0, 1).toUpperCase() ?? '—' }}
+                </q-avatar>
+                <div class="col min-width-0">
+                  <div class="text-weight-bold text-text">{{ provider?.provider_name ?? '—' }}</div>
+                  <div class="text-caption text-grey-6">{{ provider?.district ?? '—' }}</div>
+                  <div class="row items-center q-gutter-x-md q-mt-xs">
+                    <div class="row items-center">
+                      <q-icon name="mdi-star" color="warning" size="14px" />
+                      <span class="text-caption text-weight-medium q-ml-xs">
+                        {{ (provider?.average_rating != null ? Number(provider.average_rating).toFixed(1) : '') + ' (' + (provider?.total_reviews ?? 0) + ')' }}
+                      </span>
+                    </div>
+                    <div class="row items-center">
+                      <q-icon name="mdi-broom" color="secondary" size="14px" />
+                      <span class="text-caption q-ml-xs">{{ provider?.total_services ?? 0 }}</span>
+                    </div>
+                  </div>
+                </div>
+                <div class="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" />
+                </div>
+              </div>
+            </q-card-section>
+          </q-card>
+        </div>
+
+        <div class="q-px-md q-pt-lg">
+          <div class="text-h6 text-weight-bold gradient-diarista q-mb-xs">
+            {{ $t('scheduling_page.schedule_service') }}
+          </div>
+
+          <div v-if="loadingAvailability" class="row items-center justify-center q-py-lg">
+            <q-spinner-dots color="primary" size="36px" />
+          </div>
+
+          <div v-else class="calendar-wrapper shadow-card q-mb-md">
+            <q-date
+              v-model="selectedDate"
+              square
+              class="full-width"
+              :first-day-of-week="0"
+              :options="dateOptions"
+              minimal
+              @update:model-value="onDateSelected"
+            />
+          </div>
+        </div>
+
+        <div class="q-px-md q-pt-sm q-pb-xl">
+          <div class="row items-center justify-between q-mb-sm">
+            <div class="text-h6 text-weight-bold gradient-diarista">
+              {{ $t('scheduling_page.reviews_title') }}
+            </div>
+            <span class="text-caption text-primary cursor-pointer">
+              {{ $t('scheduling_page.see_all') }}
+            </span>
+          </div>
+
+          <div v-if="loadingReviews" class="row items-center justify-center q-py-md">
+            <q-spinner-dots color="primary" size="36px" />
+          </div>
+
+          <div v-else-if="reviews.length === 0" class="text-center text-grey-6 text-body2 q-py-md">
+            {{ $t('scheduling_page.no_reviews') }}
+          </div>
+
+          <div v-else class="reviews-scroll">
+            <q-card
+              v-for="review in reviews"
+              :key="review.id"
+              class="review-card card-border bg-white q-mr-sm shadow-card"
+              :flat="false"
+            >
+              <q-card-section class="q-pa-sm">
+                <div class="row items-center no-wrap q-gutter-x-sm q-mb-xs">
+                  <q-avatar size="32px" :style="clientAvatarStyle(review)" class="text-weight-bold text-caption">
+                    {{ review.schedule?.client?.name?.slice(0, 1).toUpperCase() ?? '?' }}
+                  </q-avatar>
+                  <div class="col text-weight-medium text-text text-caption ellipsis">
+                    {{ review.schedule?.client?.name ?? $t('scheduling_page.unknown_client') }}
+                  </div>
+                </div>
+                <div class="row items-center q-mb-xs">
+                  <q-icon
+                    v-for="s in 5"
+                    :key="s"
+                    :name="s <= review.stars ? 'mdi-star' : 'mdi-star-outline'"
+                    color="warning"
+                    size="14px"
+                  />
+                </div>
+                <div class="text-caption text-text review-comment">
+                  {{ review.comment ?? '' }}
+                </div>
+              </q-card-section>
+            </q-card>
+          </div>
+        </div>
+      </div>
+    </div>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+import { useDialogPluginComponent, useQuasar } from 'quasar';
+import { date } from 'quasar';
+import { getProviderWorkingDays, getProviderBlockedDays } from 'src/api/providerAvailability';
+import { getProviderReceivedReviews } from 'src/api/review';
+import ServiceSelectionSheet from './ServiceSelectionSheet.vue';
+import ServiceTimeSelectionDialog from './ServiceTimeSelectionDialog.vue';
+import OrderSummaryDialog from './OrderSummaryDialog.vue'
+
+const props = defineProps({
+  provider: {
+    type: Object,
+    required: true,
+  },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef } = useDialogPluginComponent();
+const $q = useQuasar();
+
+const selectedDate = ref(null);
+const workingDays = ref([]);
+const blockedDays = ref([]);
+const loadingAvailability = ref(true);
+
+const reviews = ref([]);
+const loadingReviews = ref(true);
+
+
+const bookings = ref([]);
+
+
+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 getWeekRange = (dateStr) => {
+  const d = new Date(`${dateStr}T12:00:00`)
+  const day = d.getDay()
+
+  const start = new Date(d)
+  start.setDate(d.getDate() - day)
+
+  const end = new Date(start)
+  end.setDate(start.getDate() + 6)
+
+  return {
+    start: start.toISOString().split('T')[0],
+    end: end.toISOString().split('T')[0]
+  }
+}
+
+const wouldExceedWeekLimit = (selectedDate) => {
+  const { start, end } = getWeekRange(selectedDate)
+
+  const count = bookings.value.filter(b => {
+    if (!b.date) return false
+
+    const normalizedDate = b.date.replace(/\//g, '-')
+
+    return normalizedDate >= start && normalizedDate <= end
+  }).length
+
+  return count >= 2
+}
+
+
+const dateOptions = (d) => {
+  const today = date.formatDate(new Date(), 'YYYY/MM/DD');
+  if (d < today) return false;
+
+  const raw = d.replace(/\//g, '-');
+  const parsed = new Date(`${raw}T12:00:00`);
+  const dayOfWeek = parsed.getDay();
+
+  const isWorkingDay = availableWeekDays.value.includes(dayOfWeek);
+  const isBlocked = blockedDateSet.value.has(raw);
+
+  if (!isWorkingDay || isBlocked) return false;
+
+if (wouldExceedWeekLimit(raw)) return false;
+
+return true;
+};
+
+
+const onDateSelected = (val) => {
+  if (!val) return;
+
+  selectedDate.value = null;
+  const valFormatted = val.replace(/\//g, '-');
+
+
+  const blocksOfDate = blockedDays.value.filter(
+    (bd) => bd.date === valFormatted && bd.period !== 'all'
+  );
+
+  
+  const dayOfWeek = new Date(`${valFormatted}T12:00:00`).getDay();
+
+  const dayPeriods = workingDays.value
+    .filter((wd) => wd.day === dayOfWeek)
+    .map((wd) => wd.period);
+
+  const workingDayBlocks = [];
+
+  if (!dayPeriods.includes('afternoon')) {
+    workingDayBlocks.push({ init_hour: '14:00:00', end_hour: '20:00:00' });
+  }
+
+  if (!dayPeriods.includes('morning')) {
+    workingDayBlocks.push({ init_hour: '07:00:00', end_hour: '13:00:00' });
+  }
+
+  
+  const existingBookingBlocks = 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`
+    }));
+
+  
+  const partialBlocks = [
+    ...blocksOfDate,
+    ...workingDayBlocks,
+    ...existingBookingBlocks
+  ];
+
+  // fluxo
+  $q.dialog({
+  component: ServiceSelectionSheet,
+  componentProps: {
+    provider: props.provider,
+    selectedDate: val,
+    partialBlocks
+  },
+}).onOk(({ serviceType, date: date_, provider: prov }) => {
+
+  $q.dialog({
+    component: ServiceTimeSelectionDialog,
+    componentProps: {
+      serviceType,
+      selectedDate: date_,
+      provider: prov,
+      partialBlocks
+    },
+  }).onOk((booking) => {
+
+    bookings.value.push(booking);
+
+    $q.dialog({
+      component: OrderSummaryDialog,
+      componentProps: {
+        provider: props.provider,
+        initialBooking: booking
+      }
+    });
+
+  });
+
+});
+};
+
+
+const loadAvailability = async () => {
+  loadingAvailability.value = true;
+  try {
+    const [wd, bd] = await Promise.all([
+      getProviderWorkingDays(props.provider.provider_id),
+      getProviderBlockedDays(props.provider.provider_id),
+      // getProviderWeekDatesBlocks(props.provider_id, provider.client_id)
+    ]);
+    workingDays.value = wd ?? [];
+    blockedDays.value = bd ?? [];
+  } catch {
+    workingDays.value = [];
+    blockedDays.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;
+  }
+};
+
+onMounted(() => {
+  loadAvailability();
+  loadReviews();
+});
+</script>
+
+<style scoped lang="scss">
+.dialog-root {
+  width: 100vw;
+  max-width: 100vw;
+  height: 100vh;
+  max-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  background: #F9FAFB;
+}
+
+.dialog-header {
+  flex-shrink: 0;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+}
+
+.dialog-body {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+  overflow-x: clip;
+}
+
+.calendar-wrapper {
+  border-radius: 20px;
+  overflow: hidden;
+  background: white;
+
+  :deep(.q-date) {
+    background: white;
+    width: 100%;
+  }
+
+  :deep(.q-date__calendar-days .q-btn__content) {
+    color: #1E293B !important;
+  }
+
+  // dias desabilitados: visíveis mas opacos
+  :deep(.q-date__calendar-days .q-btn.disabled .q-btn__content),
+  :deep(.q-date__calendar-days .q-btn[disabled] .q-btn__content) {
+    color: #000000 !important;
+    opacity: 1 !important;
+  }
+
+  // o Quasar aplica opacity no elemento .q-btn quando disabled — reseta
+  :deep(.q-date__calendar-days .q-btn.disabled),
+  :deep(.q-date__calendar-days .q-btn[disabled]) {
+    opacity: 1 !important;
+  }
+
+  // cabeçalho dos dias (dom, seg, ter, qua...)
+  :deep(.q-date__calendar-weekdays > div) {
+    color: #6366F1;
+    font-weight: 700;
+    opacity: 0.8;
+  }
+
+  :deep(.q-date__navigation) {
+    .q-btn { color: #6366F1 !important; }
+    .q-btn__content { color: #6366F1 !important; }
+  }
+
+  :deep(.q-date__event) {
+    bottom: 4px;
+    height: 6px;
+    width: 6px;
+    border-radius: 50%;
+  }
+
+  :deep(.q-date__today .q-btn__content) {
+    color: #7c4dff !important;
+    background: #7c4dff15;
+    border-radius: 50%;
+  }
+
+  :deep(.q-date__selected .q-btn__content),
+  :deep(.q-date__calendar-item .q-btn.q-date__selected .q-btn__content) {
+    background: #6366F1 !important;
+    color: #ffffff !important;
+    border-radius: 50%;
+    box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
+  }
+
+  :deep(.q-date__view--months .q-btn),
+  :deep(.q-date__view--years .q-btn) {
+    color: #6366F1 !important;
+  }
+
+  :deep(.q-date__calendar-item--out) {
+    color: #b9b9b9 !important;
+    opacity: 0.8 !important;
+  }
+}
+
+// Reviews scroll horizontal
+.reviews-scroll {
+  display: flex;
+  flex-direction: row;
+  overflow-x: auto;
+  -webkit-overflow-scrolling: touch;
+  scrollbar-width: none;
+  padding-bottom: 8px;
+  &::-webkit-scrollbar { display: none; }
+}
+
+.review-card {
+  flex-shrink: 0;
+  min-width: 220px;
+  max-width: 240px;
+  border-radius: 12px;
+  margin-right: 8px;
+}
+
+.review-comment {
+  display: -webkit-box;
+  -webkit-line-clamp: 3;
+  line-clamp: 3;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+}
+
+.min-width-0 {
+  min-width: 0;
+}
+</style>

+ 208 - 0
src/pages/search/components/SearchFilterDialog.vue

@@ -0,0 +1,208 @@
+<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
+<template>
+  <q-dialog ref="dialogRef" position="standard" @hide="onDialogHide">
+    <q-card class="filter-card bg-surface text-text">
+
+      <q-card-section class="row items-center q-pb-none">
+        <span class="text-subtitle1 text-weight-bold text-text">{{ $t('search_filter.title') }}</span>
+        <q-space />
+        <q-btn flat round dense icon="mdi-close" color="grey-6" @click="onDialogCancel" />
+      </q-card-section>
+
+      <q-separator class="q-mt-sm" />
+
+      <q-card-section class="q-pt-sm q-pb-none scroll filter-body">
+
+        <div class="text-caption text-weight-bold text-grey-7 q-mb-sm">{{ $t('search_filter.sort_by') }}</div>
+
+        <div v-for="group in sortGroups" :key="group.key" class="q-mb-md">
+          <div class="text-body2 text-weight-medium text-text q-mb-xs">{{ group.label }}</div>
+          <div class="row q-gutter-x-lg">
+            <q-radio
+              v-for="opt in group.options"
+              :key="opt.value"
+              v-model="localSort"
+              :val="opt.value"
+              :label="opt.label"
+              :disable="opt.disable ?? false"
+              color="primary"
+              keep-color
+              dense
+            />
+          </div>
+        </div>
+
+        <q-separator class="q-my-sm" />
+
+        <div class="text-caption text-weight-bold text-grey-7 q-mb-xs">{{ $t('search_filter.filter_by') }}</div>
+        <div class="text-body2 text-weight-medium text-text q-mb-xs">{{ $t('search_filter.availability') }}</div>
+        <DefaultInputDatePicker
+          v-model:untreated-date="localDate"
+          :label="$t('search_filter.availability_placeholder')"
+          dense
+          class="date-picker-primary"
+          input-class="text-text"
+        />
+      </q-card-section>
+
+      <q-separator />
+      <q-card-actions class="q-px-md q-py-md row q-gutter-x-sm">
+        <q-btn
+          outline
+          color="grey-6"
+          :label="$t('search_filter.clear')"
+          rounded
+          no-caps
+          class="col"
+          @click="clearFilters"
+        />
+        <q-btn
+          color="primary-button"
+          :label="$t('search_filter.apply')"
+          rounded
+          no-caps
+          unelevated
+          class="col"
+          @click="applyFilters"
+        />
+      </q-card-actions>
+
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue';
+import { useDialogPluginComponent } from 'quasar';
+import { useI18n } from 'vue-i18n';
+import DefaultInputDatePicker from 'src/components/defaults/DefaultInputDatePicker.vue';
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { initialSort, initialDate } = defineProps({
+  initialSort: { type: String, default: null },
+  initialDate: { type: String, default: null },
+});
+
+const { t } = useI18n();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+
+const localSort = ref(initialSort ?? null);
+const localDate = ref(initialDate ?? null);
+
+const sortGroups = computed(() => [
+  {
+    key: 'price',
+    label: t('search_filter.groups.price'),
+    options: [
+      { value: 'price_desc', label: t('search_filter.sort.higher') },
+      { value: 'price_asc',  label: t('search_filter.sort.lower') },
+    ],
+  },
+  {
+    key: 'distance',
+    label: t('search_filter.groups.distance'),
+    options: [
+      { value: 'distance_desc', label: t('search_filter.sort.higher'), disable: true },
+      { value: 'distance_asc',  label: t('search_filter.sort.lower'),  disable: true },
+    ],
+  },
+  {
+    key: 'reviews',
+    label: t('search_filter.groups.reviews'),
+    options: [
+      { value: 'reviews_desc', label: t('search_filter.sort.higher') },
+      { value: 'reviews_asc',  label: t('search_filter.sort.lower') },
+    ],
+  },
+  {
+    key: 'rating',
+    label: t('search_filter.groups.rating'),
+    options: [
+      { value: 'rating_desc', label: t('search_filter.sort.higher') },
+      { value: 'rating_asc',  label: t('search_filter.sort.lower') },
+    ],
+  },
+  {
+    key: 'services',
+    label: t('search_filter.groups.services'),
+    options: [
+      { value: 'services_desc', label: t('search_filter.sort.higher') },
+      { value: 'services_asc',  label: t('search_filter.sort.lower') },
+    ],
+  },
+  {
+    key: 'oldest',
+    label: t('search_filter.groups.oldest'),
+    options: [
+      { value: 'oldest',  label: t('search_filter.sort.oldest_asc') },
+      { value: 'newest',  label: t('search_filter.sort.oldest_desc') },
+    ],
+  },
+]);
+
+const clearFilters = () => {
+  localSort.value = null;
+  localDate.value = null;
+  onDialogOK({ sort: null, date: null });
+};
+
+const applyFilters = () => {
+  onDialogOK({ sort: localSort.value, date: localDate.value });
+};
+</script>
+
+<style scoped lang="scss">
+.filter-card {
+  border-radius: 16px;
+  width: min(92vw, 420px);
+}
+.filter-body {
+  max-height: 60dvh;
+  overflow-y: auto;
+}
+.date-picker-primary {
+  :deep(.q-field__label) {
+    color: var(--q-primary);
+  }
+  :deep(.q-field__native),
+  :deep(.q-field__input) {
+    color: var(--q-text, #555555) !important;
+  }
+  :deep(.q-icon) {
+    color: var(--q-primary);
+  }
+}
+</style>
+
+<style lang="scss">
+.q-popup-proxy {
+  .q-date {
+    background: var(--q-surface, #ffffff) !important;
+    color: var(--q-text, #555555) !important;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
+
+    .q-date__header {
+      background: var(--q-primary);
+      color: #fff;
+    }
+
+    .q-date__content {
+      color: var(--q-text, #555555) !important;
+    }
+
+    .q-date__calendar-item .q-btn {
+      color: var(--q-text, #555555) !important;
+    }
+
+    .q-date__navigation .q-btn {
+      color: var(--q-text, #555555) !important;
+    }
+
+    .q-date__calendar-weekdays > div {
+      color: var(--q-text, #555555) !important;
+      opacity: 0.6;
+    }
+  }
+}
+</style>

+ 64 - 40
src/pages/search/components/ServiceSelectionSheet.vue

@@ -11,12 +11,15 @@
       <q-separator class="q-mt-sm" />
 
       <q-card-section class="q-pt-sm q-pb-md">
+        <div v-if="availableServiceTypes.length === 0" class="text-center text-grey-6 text-body2 q-py-md">
+          {{ $t('scheduling_page.no_slots_available') }}
+        </div>
+
         <div
-          v-for="type in serviceTypes"
+          v-for="type in availableServiceTypes"
           :key="type.key"
           class="row items-center no-wrap q-py-sm"
         >
-          <!-- Nome e horas -->
           <div class="col">
             <div class="row items-center no-wrap q-gutter-x-xs">
               <span class="text-body2 text-weight-bold text-text">{{ type.label }}</span>
@@ -31,19 +34,20 @@
             <div class="text-caption text-grey-6">{{ type.hours }}</div>
           </div>
 
-          <!-- Preço -->
-          
+          <div class="text-body2 text-weight-bold text-text q-mx-md" style="white-space: nowrap;">
+            {{ type.price != null ? formatPrice(type.price) : $t('scheduling_page.no_price') }}
+          </div>
 
-          <!-- Botão agendar -->
           <q-btn
             unelevated
             rounded
             no-caps
             :label="$t('scheduling_page.book')"
+            :disable="type.price == null"
             color="secondary"
             size="sm"
             style="min-width: 80px;"
-            @click="onDialogOK({ serviceType: type, date: selectedDate, provider: props.provider })"
+            @click="onDialogOK({ serviceType: type, date: selectedDate, provider })"
           />
         </div>
       </q-card-section>
@@ -61,6 +65,7 @@ import ServiceTypeInfoDialog from './ServiceTypeInfoDialog.vue';
 const props = defineProps({
   provider: { type: Object, required: true },
   selectedDate: { type: String, required: true },
+  partialBlocks: { type: Array, required: false, default: () => [] },
 });
 
 defineEmits([...useDialogPluginComponent.emits]);
@@ -69,40 +74,59 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginC
 const $q = useQuasar();
 const { t } = useI18n();
 
-const serviceTypes = 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'),
-   
-  },
-  {
-    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'),
-    
-  },
-  {
-    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'),
-    
-  },
-  {
-    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'),
-    
-  },
-]);
+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 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'),
+      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'),
+      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'),
+      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'),
+      price: props.provider?.daily_price_2h ?? null,
+    },
+  ].filter(type => hasValidSlots(type.hoursCount))
+);
+
+const formatPrice = (value) =>
+  Number(value).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
 
 const openInfo = (type) => {
   $q.dialog({

+ 28 - 32
src/pages/search/components/ServiceTimeSelectionDialog.vue

@@ -2,7 +2,6 @@
   <q-dialog ref="dialogRef" @hide="onDialogHide">
     <q-card class="card-border bg-surface text-text time-card">
 
-      <!-- Fechar -->
       <q-btn
         flat round dense
         icon="mdi-close-circle-outline"
@@ -11,7 +10,6 @@
         @click="onDialogCancel"
       />
 
-      <!-- Título -->
       <q-card-section class="text-center q-pt-lg q-pb-xs">
         <div class="text-subtitle1 text-weight-bold text-text">
           {{ serviceType.label }}
@@ -22,7 +20,6 @@
         </div>
       </q-card-section>
 
-      <!-- Slots de horário -->
       <q-card-section class="q-pt-xs">
         <div class="row q-col-gutter-xs">
           <div
@@ -42,7 +39,6 @@
         </div>
       </q-card-section>
 
-      <!-- Refeição (apenas 6h e 8h) -->
       <template v-if="hasMealSection">
         <q-separator class="q-mx-md" />
         <q-card-section class="q-py-sm">
@@ -70,20 +66,19 @@
         </q-card-section>
       </template>
 
-      <!-- Botão continuar -->
       <q-card-actions class="q-px-md q-pb-xs q-pt-sm">
         <q-btn
           unelevated
           rounded
           no-caps
           :label="$t('scheduling_page.time_selection.continue')"
+          :disable="!selectedSlot"
           color="secondary"
           class="full-width"
           @click="handleContinue"
         />
       </q-card-actions>
 
-      <!-- Nota de pausa -->
       <div v-if="pauseNote" class="text-center text-caption text-primary q-pb-md">
         {{ pauseNote }}
       </div>
@@ -94,32 +89,52 @@
 
 <script setup>
 import { ref, computed } from 'vue';
-import {  useDialogPluginComponent } from 'quasar';
+import { useDialogPluginComponent } from 'quasar';
 import { useI18n } from 'vue-i18n';
 
 const props = defineProps({
-  serviceType: { type: Object, required: true },
-  provider:    { type: Object, required: true },
-  selectedDate:{ type: String, required: true },
+  serviceType:   { type: Object, required: true },
+  provider:      { type: Object, required: true },
+  selectedDate:  { type: String, required: true },
+  partialBlocks: { type: Array,  required: false, default: () => [] },
 });
 
 defineEmits([...useDialogPluginComponent.emits]);
 
-const { dialogRef, onDialogHide, onDialogCancel, onDialogOK } = useDialogPluginComponent();
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
 const { t } = useI18n();
 
 const selectedSlot = ref(null);
 const selectedMeal = ref(null);
 
-// Gera todos os slots possíveis de 7h até 20h para a duração dada
+const handleContinue = () => {
+  const slotObj = timeSlots.value.find(s => s.value === selectedSlot.value);
+  onDialogOK({
+    serviceType: props.serviceType,
+    date: props.selectedDate,
+    slot: { value: selectedSlot.value, startHour: slotObj.startHour, endHour: slotObj.endHour },
+    meal: selectedMeal.value,
+  });
+};
+
+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 timeSlots = computed(() => {
   const h = props.serviceType.hoursCount;
   const slots = [];
   for (let start = 7; start + h <= 20; start++) {
     const end = start + h;
+    if (slotConflicts(start, end, props.partialBlocks)) continue;
     slots.push({
       value: `${start}-${end}`,
-      label: `${start >= 10 ? start : '0' + start}h às ${end >= 10 ? end : '0' + end}h`,
+      startHour: start,
+      endHour: end,
+      label: `${start}h às ${end}h`,
     });
   }
   return slots;
@@ -133,29 +148,10 @@ const pauseNote = computed(() => {
   const map = { 8: t('scheduling_page.time_selection.pause_note_8h'), 6: t('scheduling_page.time_selection.pause_note_6h'), 4: t('scheduling_page.time_selection.pause_note_4h') };
   return map[props.serviceType.hoursCount] ?? null;
 });
-
-
-// Função que ira pegar os valores para o banco de dados
-function handleContinue(){
-  if(!selectedSlot.value) {
-    return;
-  }
-  if(hasMealSection.value && !selectedMeal.value) {
-    return;
-  }
-  onDialogOK({
-    slot: selectedSlot.value,
-    meal: selectedMeal.value,
-    date: props.selectedDate,
-    serviceType: props.serviceType,
-  })
-};
 </script>
 
 <style scoped lang="scss">
 .time-card {
   width: min(88vw, 360px);
 }
-
-
 </style>

+ 1 - 1
src/pages/search/components/ServiceTypeInfoDialog.vue

@@ -35,4 +35,4 @@ defineProps({
 defineEmits([...useDialogPluginComponent.emits]);
 
 const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
-</script>
+</script>