Explorar el Código

feat: :sparkles: feat (agendamentos) criando regras de negocio para contratacao

foram aplicadas as regras de negocio de contratacao condicional, obedecendo as regras de negocio de disponibilidade, alem de melhoria no fluxo de simulacao de pagamento para redirecionar à dashboard após confirmacao

fase:dev | origin:escopo
Gustavo Zanatta hace 2 semanas
padre
commit
d864c8dd3f

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

@@ -95,7 +95,6 @@ const serviceFee = computed(() => Number(props.schedule.total_amount) * SERVICE_
 const total = computed(() => Number(props.schedule.total_amount) + serviceFee.value)
 const total = computed(() => Number(props.schedule.total_amount) + serviceFee.value)
 
 
 const onGoToPayment = () => {
 const onGoToPayment = () => {
-  onDialogHide()
   $q.dialog({
   $q.dialog({
     component: SchedulePaymentDialog,
     component: SchedulePaymentDialog,
     componentProps: {
     componentProps: {

+ 2 - 2
src/components/dashboard/SchedulePaymentDialog.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down">
+  <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="bg-page full-height column">
 
 
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
@@ -120,7 +120,7 @@ const props = defineProps({
 
 
 defineEmits([...useDialogPluginComponent.emits])
 defineEmits([...useDialogPluginComponent.emits])
 
 
-const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent()
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
 const $q = useQuasar()
 const { t } = useI18n()
 const { t } = useI18n()
 const store = userStore()
 const store = userStore()

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

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="slide-up" transition-hide="slide-down">
+  <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="bg-page full-height column">
 
 
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
       <div class="row items-center q-px-md q-pt-md q-pb-sm bg-surface shadow-header">
@@ -12,7 +12,29 @@
         <div style="width: 32px" />
         <div style="width: 32px" />
       </div>
       </div>
 
 
-      <div class="col scroll q-px-lg q-pt-lg q-pb-xl column">
+      <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">
         <div class="row items-center justify-between q-mb-sm">
           <span class="pix-label">{{ $t('payment.pix_total') }}</span>
           <span class="pix-label">{{ $t('payment.pix_total') }}</span>
@@ -106,6 +128,7 @@
           color="primary"
           color="primary"
           class="full-width copy-btn q-mb-lg"
           class="full-width copy-btn q-mb-lg"
           :label="$t('payment.pix_copy_btn')"
           :label="$t('payment.pix_copy_btn')"
+          :loading="processing"
           @click="copyCode"
           @click="copyCode"
         />
         />
 
 
@@ -125,6 +148,7 @@
 import { ref, onMounted, onUnmounted } from 'vue'
 import { ref, onMounted, onUnmounted } from 'vue'
 import { useDialogPluginComponent, useQuasar, copyToClipboard } from 'quasar'
 import { useDialogPluginComponent, useQuasar, copyToClipboard } from 'quasar'
 import { formatCurrency } from 'src/helpers/utils'
 import { formatCurrency } from 'src/helpers/utils'
+import { updateScheduleStatus } from 'src/api/schedule'
 
 
 const props = defineProps({
 const props = defineProps({
   schedule: { type: Object, required: true },
   schedule: { type: Object, required: true },
@@ -133,15 +157,33 @@ const props = defineProps({
 
 
 defineEmits([...useDialogPluginComponent.emits])
 defineEmits([...useDialogPluginComponent.emits])
 
 
-const { dialogRef, onDialogCancel } = useDialogPluginComponent()
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
 const $q = useQuasar()
 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 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 copyCode = () => {
-  copyToClipboard(pixCode)
-    .then(() => $q.notify({ type: 'positive', message: 'Código copiado!' }))
-    .catch(() => $q.notify({ type: 'negative', message: 'Erro ao copiar.' }))
+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 totalSeconds = ref(20 * 60)
@@ -210,4 +252,22 @@ onUnmounted(() => {
   line-height: 1.5;
   line-height: 1.5;
   color: #3a3a4a;
   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>
 </style>

+ 3 - 3
src/components/dashboard/SchedulePaymentProcessingDialog.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <q-dialog ref="dialogRef" persistent maximized transition-show="fade" transition-hide="fade">
+  <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">
     <div class="bg-surface full-height column items-center justify-center q-px-xl">
 
 
       <template v-if="!success">
       <template v-if="!success">
@@ -19,7 +19,7 @@
           icon="mdi-close"
           icon="mdi-close"
           color="grey-5"
           color="grey-5"
           class="self-end q-mb-md"
           class="self-end q-mb-md"
-          @click="onDialogHide"
+          @click="onDialogOK"
         />
         />
         <img
         <img
           src="/logo_diaria_branco.svg"
           src="/logo_diaria_branco.svg"
@@ -58,7 +58,7 @@ const props = defineProps({
 
 
 defineEmits([...useDialogPluginComponent.emits])
 defineEmits([...useDialogPluginComponent.emits])
 
 
-const { dialogRef, onDialogHide } = useDialogPluginComponent()
+const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent()
 
 
 const success = ref(false)
 const success = ref(false)
 
 

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

@@ -18,7 +18,7 @@
         >
         >
           <q-popup-proxy cover transition-show="scale" transition-hide="scale">
           <q-popup-proxy cover transition-show="scale" transition-hide="scale">
             <template v-if="!time">
             <template v-if="!time">
-              <q-date v-model="date" mask="YYYY-MM-DD" color="primary" class="bg-surface text-text">
+              <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">
                 <div class="row items-center justify-end">
                   <q-btn v-close-popup label="Close" color="primary" flat />
                   <q-btn v-close-popup label="Close" color="primary" flat />
                 </div>
                 </div>
@@ -160,3 +160,87 @@ watch(
   { immediate: true },
   { immediate: true },
 );
 );
 </script>
 </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>

+ 1 - 0
src/i18n/locales/en.json

@@ -606,6 +606,7 @@
     "no_reviews": "No reviews found.",
     "no_reviews": "No reviews found.",
     "unknown_client": "Client",
     "unknown_client": "Client",
     "select_service": "Select service",
     "select_service": "Select service",
+    "no_slots_available": "No time slots available for this day.",
     "book": "schedule",
     "book": "schedule",
     "no_price": "to be agreed",
     "no_price": "to be agreed",
     "time_selection": {
     "time_selection": {

+ 1 - 0
src/i18n/locales/es.json

@@ -606,6 +606,7 @@
     "no_reviews": "No se encontraron reseñas.",
     "no_reviews": "No se encontraron reseñas.",
     "unknown_client": "Cliente",
     "unknown_client": "Cliente",
     "select_service": "Seleccionar servicio",
     "select_service": "Seleccionar servicio",
+    "no_slots_available": "No hay horarios disponibles para este día.",
     "book": "agendar",
     "book": "agendar",
     "no_price": "a convenir",
     "no_price": "a convenir",
     "time_selection": {
     "time_selection": {

+ 1 - 0
src/i18n/locales/pt.json

@@ -606,6 +606,7 @@
     "no_reviews": "Nenhuma avaliação encontrada.",
     "no_reviews": "Nenhuma avaliação encontrada.",
     "unknown_client": "Cliente",
     "unknown_client": "Cliente",
     "select_service": "Selecionar serviço",
     "select_service": "Selecionar serviço",
+    "no_slots_available": "Não há horários disponíveis para este dia.",
     "book": "agendar",
     "book": "agendar",
     "no_price": "a combinar",
     "no_price": "a combinar",
     "time_selection": {
     "time_selection": {

+ 31 - 3
src/pages/search/components/OrderSummaryDialog.vue

@@ -144,7 +144,7 @@ const availableWeekDays = computed(() =>
 );
 );
 
 
 const blockedDateSet = computed(() =>
 const blockedDateSet = computed(() =>
-  new Set(blockedDays.value.map(bd => bd.date))
+  new Set(blockedDays.value.filter(bd => bd.period === 'all').map(bd => bd.date))
 );
 );
 
 
 const dateOptions = (d) => {
 const dateOptions = (d) => {
@@ -196,13 +196,41 @@ const normalizeDate = (d) => d.replace(/\//g, '-');
 const onAddDateSelected = (val) => {
 const onAddDateSelected = (val) => {
   if (!val) return;
   if (!val) return;
   addDateValue.value = null;
   addDateValue.value = null;
+  const valFormatted = 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({
   $q.dialog({
     component: ServiceSelectionSheet,
     component: ServiceSelectionSheet,
-    componentProps: { provider: props.provider, selectedDate: val },
+    componentProps: { provider: props.provider, selectedDate: val, partialBlocks },
   }).onOk(({ serviceType, date: date_, provider: prov }) => {
   }).onOk(({ serviceType, date: date_, provider: prov }) => {
     $q.dialog({
     $q.dialog({
       component: ServiceTimeSelectionDialog,
       component: ServiceTimeSelectionDialog,
-      componentProps: { serviceType, selectedDate: date_, provider: prov },
+      componentProps: { serviceType, selectedDate: date_, provider: prov, partialBlocks },
     }).onOk((booking) => {
     }).onOk((booking) => {
       if (wouldExceedWeekLimit(booking.date)) {
       if (wouldExceedWeekLimit(booking.date)) {
         $q.notify({ type: 'negative', message: t('scheduling_page.order_summary.week_limit_error') });
         $q.notify({ type: 'negative', message: t('scheduling_page.order_summary.week_limit_error') });

+ 24 - 3
src/pages/search/components/SchedulingDialog.vue

@@ -149,13 +149,34 @@ const $q = useQuasar();
 const onDateSelected = (val) => {
 const onDateSelected = (val) => {
   if (!val) return;
   if (!val) return;
   selectedDate.value = null;
   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: '7:00:00', end_hour: '13:00:00' });
+  }
+
+  const partialBlocks = [...blocksOfDate, ...workingDayBlocks];
+
   $q.dialog({
   $q.dialog({
     component: ServiceSelectionSheet,
     component: ServiceSelectionSheet,
-    componentProps: { provider: props.provider, selectedDate: val },
+    componentProps: { provider: props.provider, selectedDate: val, partialBlocks },
   }).onOk(({ serviceType, date: date_, provider: prov }) => {
   }).onOk(({ serviceType, date: date_, provider: prov }) => {
     $q.dialog({
     $q.dialog({
       component: ServiceTimeSelectionDialog,
       component: ServiceTimeSelectionDialog,
-      componentProps: { serviceType, selectedDate: date_, provider: prov },
+      componentProps: { serviceType, selectedDate: date_, provider: prov, partialBlocks },
     }).onOk((booking) => {
     }).onOk((booking) => {
       $q.dialog({
       $q.dialog({
         component: OrderSummaryDialog,
         component: OrderSummaryDialog,
@@ -195,7 +216,7 @@ const availableWeekDays = computed(() =>
 );
 );
 
 
 const blockedDateSet = computed(() =>
 const blockedDateSet = computed(() =>
-  new Set(blockedDays.value.map((bd) => bd.date))
+  new Set(blockedDays.value.map((bd) => bd.date && bd.period == 'all'))
 );
 );
 
 
 const dateOptions = (d) => {
 const dateOptions = (d) => {

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

@@ -43,7 +43,6 @@
           class="date-picker-primary"
           class="date-picker-primary"
           input-class="text-text"
           input-class="text-text"
         />
         />
-
       </q-card-section>
       </q-card-section>
 
 
       <q-separator />
       <q-separator />

+ 56 - 35
src/pages/search/components/ServiceSelectionSheet.vue

@@ -11,8 +11,12 @@
       <q-separator class="q-mt-sm" />
       <q-separator class="q-mt-sm" />
 
 
       <q-card-section class="q-pt-sm q-pb-md">
       <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
         <div
-          v-for="type in serviceTypes"
+          v-for="type in availableServiceTypes"
           :key="type.key"
           :key="type.key"
           class="row items-center no-wrap q-py-sm"
           class="row items-center no-wrap q-py-sm"
         >
         >
@@ -61,6 +65,7 @@ import ServiceTypeInfoDialog from './ServiceTypeInfoDialog.vue';
 const props = defineProps({
 const props = defineProps({
   provider: { type: Object, required: true },
   provider: { type: Object, required: true },
   selectedDate: { type: String, required: true },
   selectedDate: { type: String, required: true },
+  partialBlocks: { type: Array, required: false, default: () => [] },
 });
 });
 
 
 defineEmits([...useDialogPluginComponent.emits]);
 defineEmits([...useDialogPluginComponent.emits]);
@@ -69,40 +74,56 @@ const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginC
 const $q = useQuasar();
 const $q = useQuasar();
 const { t } = useI18n();
 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'),
-    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,
-  },
-]);
+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) =>
 const formatPrice = (value) =>
   Number(value).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
   Number(value).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });

+ 12 - 3
src/pages/search/components/ServiceTimeSelectionDialog.vue

@@ -93,9 +93,10 @@ import { useDialogPluginComponent } from 'quasar';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 
 
 const props = defineProps({
 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]);
 defineEmits([...useDialogPluginComponent.emits]);
@@ -116,11 +117,19 @@ const handleContinue = () => {
   });
   });
 };
 };
 
 
+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 timeSlots = computed(() => {
   const h = props.serviceType.hoursCount;
   const h = props.serviceType.hoursCount;
   const slots = [];
   const slots = [];
   for (let start = 7; start + h <= 20; start++) {
   for (let start = 7; start + h <= 20; start++) {
     const end = start + h;
     const end = start + h;
+    if (slotConflicts(start, end, props.partialBlocks)) continue;
     slots.push({
     slots.push({
       value: `${start}-${end}`,
       value: `${start}-${end}`,
       startHour: start,
       startHour: start,