Quellcode durchsuchen

feat: :sparkles: feat (agendamentos default) fluxo de gerar agendamentos default

foi criado o fluxo onde o cliente consegue solicitar um agendamento para o prestador

fase:dev | origin: escopo
Gustavo Zanatta vor 3 Wochen
Ursprung
Commit
0dda2ca76a

+ 11 - 0
src/api/providerAvailability.js

@@ -0,0 +1,11 @@
+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
+}

+ 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
+}

+ 6 - 0
src/api/schedule.js

@@ -0,0 +1,6 @@
+import api from 'src/api';
+
+export const createSchedule = async (scheduleData) => {
+  const { data } = await api.post('/schedule', scheduleData);
+  return data.payload;
+};

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

@@ -0,0 +1,141 @@
+<template>
+  <div class="q-mx-md q-mb-md">
+    <div class="dashboard-section-title gradient-diarista q-mb-sm">
+      {{ $t('dashboard_client.pending_schedules.title') }}
+    </div>
+
+    <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"
+        >
+          <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: () => [] } });
+</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>

+ 1 - 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">
                 <div class="row items-center justify-end">
                   <q-btn v-close-popup label="Close" color="primary" flat />
                 </div>

+ 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;
+    }
+  }
 }

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

@@ -405,6 +405,19 @@
       "until_2h": "Up to 2h",
       "place_home": "Home",
       "no_price": "to arrange"
+    },
+    "pending_schedules": {
+      "title": "Awaiting confirmation",
+      "requesting_with": "Requesting booking with",
+      "no_provider": "Provider not defined",
+      "cancel_btn": "cancel",
+      "status": {
+        "pending": "Awaiting confirmation",
+        "accepted": "Accepted",
+        "paid": "Paid",
+        "started": "In progress",
+        "finished": "Completed"
+      }
     }
   },
   "profile": {
@@ -572,6 +585,51 @@
       }
     }
   },
+  "scheduling_page": {
+    "title": "Scheduling",
+    "about_provider": "About the professional",
+    "schedule_service": "Schedule a service",
+    "reviews_title": "Reviews",
+    "see_all": "see all",
+    "no_reviews": "No reviews found.",
+    "unknown_client": "Client",
+    "select_service": "Select service",
+    "book": "schedule",
+    "no_price": "to be agreed",
+    "time_selection": {
+      "subtitle": "Select the desired start and end time for the service.",
+      "meal_section": "Professional's meal",
+      "meal_offer": "I offer a meal",
+      "meal_no_offer": "I don't offer a meal",
+      "continue": "continue",
+      "pause_note_8h": "Includes a break of up to 1 hour.",
+      "pause_note_6h": "Includes a break of up to 30 minutes.",
+      "pause_note_4h": "Includes a break of up to 10 minutes.",
+      "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." }
+    },
+    "order_summary": {
+      "title": "Order summary",
+      "info_text": "Send the service request or add more dates to your order.",
+      "info_note": "*You can schedule the same cleaner up to twice a week.",
+      "service_label": "Service:",
+      "time_range": "from {start}h to {end}h",
+      "send_btn": "send request",
+      "add_date_btn": "+ Add date",
+      "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.",
+      "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)",

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

@@ -405,6 +405,19 @@
       "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"
+      }
     }
   },
   "profile": {
@@ -572,6 +585,51 @@
       }
     }
   },
+  "scheduling_page": {
+    "title": "Agendamiento",
+    "about_provider": "Sobre el profesional",
+    "schedule_service": "Agendar un servicio",
+    "reviews_title": "Reseñas",
+    "see_all": "ver todas",
+    "no_reviews": "No se encontraron reseñas.",
+    "unknown_client": "Cliente",
+    "select_service": "Seleccionar servicio",
+    "book": "agendar",
+    "no_price": "a convenir",
+    "time_selection": {
+      "subtitle": "Seleccione el horario deseado para inicio y fin del servicio.",
+      "meal_section": "Comida del profesional",
+      "meal_offer": "Ofrezco comida",
+      "meal_no_offer": "No ofrezco comida",
+      "continue": "continuar",
+      "pause_note_8h": "Incluye pausa de hasta 1 hora.",
+      "pause_note_6h": "Incluye pausa de hasta 30 minutos.",
+      "pause_note_4h": "Incluye pausa de hasta 10 minutos.",
+      "slot_required": "Seleccione un horario para continuar."
+    },
+    "service_types": {
+      "integral":      { "label": "Integral",        "hours": "hasta 8h de servicio", "description": "Ideal para limpieza con mayores demandas y espacios más amplios." },
+      "padrao":        { "label": "Estándar",        "hours": "hasta 6h de servicio", "description": "Ideal para limpiezas residenciales y comerciales con rutina de limpieza tradicional." },
+      "meio_periodo":  { "label": "Medio tiempo",    "hours": "hasta 4h de servicio", "description": "Ideal para espacios más pequeños, estudios u oficinas." },
+      "diaria_rapida": { "label": "Limpieza Rápida", "hours": "hasta 2h de servicio", "description": "Ideal para habitaciones de hotel, pequeños ambientes o servicios específicos." }
+    },
+    "order_summary": {
+      "title": "Resumen del pedido",
+      "info_text": "Envíe la solicitud del servicio o agregue más fechas a su pedido.",
+      "info_note": "*Puede agendar el mismo profesional hasta dos veces por semana.",
+      "service_label": "Servicio:",
+      "time_range": "de {start}h a {end}h",
+      "send_btn": "enviar solicitud",
+      "add_date_btn": "+ Agregar fecha",
+      "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.",
+      "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)",

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

@@ -405,6 +405,19 @@
       "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"
+      }
     }
   },
   "profile": {
@@ -572,6 +585,51 @@
       }
     }
   },
+  "scheduling_page": {
+    "title": "Agendamento",
+    "about_provider": "Sobre o profissional",
+    "schedule_service": "Agende um serviço",
+    "reviews_title": "Avaliações",
+    "see_all": "ver todas",
+    "no_reviews": "Nenhuma avaliação encontrada.",
+    "unknown_client": "Cliente",
+    "select_service": "Selecionar serviço",
+    "book": "agendar",
+    "no_price": "a combinar",
+    "time_selection": {
+      "subtitle": "Selecione o horário desejado para início e término do serviço.",
+      "meal_section": "Refeição do profissional",
+      "meal_offer": "Ofereço refeição",
+      "meal_no_offer": "Não ofereço refeição",
+      "continue": "continuar",
+      "pause_note_8h": "Incluso pausa de até 1 hora.",
+      "pause_note_6h": "Incluso pausa de até 30 minutos.",
+      "pause_note_4h": "Incluso pausa de até 10 minutos.",
+      "slot_required": "Selecione um horário para continuar."
+    },
+    "service_types": {
+      "integral":      { "label": "Integral",      "hours": "até 8h de serviço", "description": "Ideal para limpeza com demandas maiores e espaços mais amplos." },
+      "padrao":        { "label": "Padrão",         "hours": "até 6h de serviço", "description": "Ideal para limpezas residenciais e comerciais que buscam uma rotina de limpeza tradicional." },
+      "meio_periodo":  { "label": "Meio período",   "hours": "até 4h de serviço", "description": "Ideal para limpezas de espaços menores, estúdios ou escritórios." },
+      "diaria_rapida": { "label": "Diária Rápida",  "hours": "até 2h de serviço", "description": "Ideal para limpezas de quartos de hotéis, pequenos ambientes ou serviços específicos." }
+    },
+    "order_summary": {
+      "title": "Resumo do pedido",
+      "info_text": "Envie a solicitação do serviço ou adicione mais datas ao seu pedido.",
+      "info_note": "*Você pode agendar o mesmo diarista até duas vezes na semana.",
+      "service_label": "Serviço:",
+      "time_range": "das {start}h às {end}h",
+      "send_btn": "enviar solicitação",
+      "add_date_btn": "+ Adicionar data",
+      "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.",
+      "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)",

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

@@ -8,6 +8,7 @@
     <template v-else>
       <DashboardHeaderBar :data="headerBar" />
       <DashboardSummaryInfos :data="summaryInfos" />
+      <DashboardPendingSchedules v-if="pendingSchedules.length > 0" :data="pendingSchedules" />
       <DashboardScrollAreaSchedules />
       <DashboardNextSchedules v-if="nextSchedules.length > 0" :data="nextSchedules" />
       <DashboardLastDoneSchedules v-if="lastDoneSchedules.length > 0" :data="lastDoneSchedules" />
@@ -20,6 +21,7 @@
 <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 DashboardScrollAreaSchedules from 'src/components/dashboard/DashboardScrollAreaSchedules.vue';
 import DashboardNextSchedules from 'src/components/dashboard/DashboardNextSchedules.vue';
 import DashboardLastDoneSchedules from 'src/components/dashboard/DashboardLastDoneSchedules.vue';
@@ -30,6 +32,7 @@ import { dadosDashboard } from 'src/api/dashboard';
 
 const headerBar = ref({});
 const summaryInfos = ref({});
+const pendingSchedules = ref([]);
 const nextSchedules = ref([]);
 const lastDoneSchedules = ref([]);
 const favoriteProviders = ref([]);
@@ -40,10 +43,11 @@ onMounted( async () => {
   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 ?? [];
   }
   loading.value = false;
 });

+ 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

@@ -125,6 +125,7 @@
                       size="sm"
                       padding="3px 12px"
                       :label="$t('search_page.schedule_btn')"
+                      @click="goToScheduling(p)"
                     />
                   </div>
                 </div>
@@ -142,14 +143,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);
@@ -226,20 +228,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' },

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

@@ -0,0 +1,338 @@
+<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.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;
+  $q.dialog({
+    component: ServiceSelectionSheet,
+    componentProps: { provider: props.provider, selectedDate: val },
+  }).onOk(({ serviceType, date: date_, provider: prov }) => {
+    $q.dialog({
+      component: ServiceTimeSelectionDialog,
+      componentProps: { serviceType, selectedDate: date_, provider: prov },
+    }).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>

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

@@ -0,0 +1,371 @@
+<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 onDateSelected = (val) => {
+  if (!val) return;
+  selectedDate.value = null;
+  $q.dialog({
+    component: ServiceSelectionSheet,
+    componentProps: { provider: props.provider, selectedDate: val },
+  }).onOk(({ serviceType, date: date_, provider: prov }) => {
+    $q.dialog({
+      component: ServiceTimeSelectionDialog,
+      componentProps: { serviceType, selectedDate: date_, provider: prov },
+    }).onOk((booking) => {
+      $q.dialog({
+        component: OrderSummaryDialog,
+        componentProps: { provider: props.provider, initialBooking: booking },
+      });
+    });
+  });
+};
+
+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 = (props.provider?.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 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);
+  return isWorkingDay && !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 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(() => {
+  Promise.all([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>

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

@@ -0,0 +1,209 @@
+<!-- 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>

+ 122 - 0
src/pages/search/components/ServiceSelectionSheet.vue

@@ -0,0 +1,122 @@
+<template>
+  <q-dialog ref="dialogRef" position="bottom" @hide="onDialogHide">
+    <q-card class="bg-surface text-text full-width sheet-card">
+
+      <q-card-section class="row items-center q-pb-none">
+        <div class="text-subtitle1 text-weight-bold text-text">{{ $t('scheduling_page.select_service') }}</div>
+        <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-md">
+        <div
+          v-for="type in serviceTypes"
+          :key="type.key"
+          class="row items-center no-wrap q-py-sm"
+        >
+          <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>
+              <q-btn
+                flat round dense
+                icon="mdi-information-outline"
+                color="primary"
+                size="xs"
+                @click="openInfo(type)"
+              />
+            </div>
+            <div class="text-caption text-grey-6">{{ type.hours }}</div>
+          </div>
+
+          <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>
+
+          <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 })"
+          />
+        </div>
+      </q-card-section>
+
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import { useDialogPluginComponent, useQuasar } from 'quasar';
+import { useI18n } from 'vue-i18n';
+import ServiceTypeInfoDialog from './ServiceTypeInfoDialog.vue';
+
+const props = defineProps({
+  provider: { type: Object, required: true },
+  selectedDate: { type: String, required: true },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+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'),
+    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 formatPrice = (value) =>
+  Number(value).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
+
+const openInfo = (type) => {
+  $q.dialog({
+    component: ServiceTypeInfoDialog,
+    componentProps: { serviceType: type },
+  });
+};
+</script>
+
+<style scoped lang="scss">
+.sheet-card {
+  border-radius: 20px 20px 0 0;
+}
+</style>

+ 148 - 0
src/pages/search/components/ServiceTimeSelectionDialog.vue

@@ -0,0 +1,148 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="card-border bg-surface text-text time-card">
+
+      <q-btn
+        flat round dense
+        icon="mdi-close-circle-outline"
+        color="grey-5"
+        class="absolute-top-right q-mt-sm q-mr-sm"
+        @click="onDialogCancel"
+      />
+
+      <q-card-section class="text-center q-pt-lg q-pb-xs">
+        <div class="text-subtitle1 text-weight-bold text-text">
+          {{ serviceType.label }}
+          <span class="text-weight-regular text-grey-6">{{ '(' + serviceType.hoursCount + 'h)' }}</span>
+        </div>
+        <div class="text-caption text-grey-6 q-mt-xs">
+          {{ $t('scheduling_page.time_selection.subtitle') }}
+        </div>
+      </q-card-section>
+
+      <q-card-section class="q-pt-xs">
+        <div class="row q-col-gutter-xs">
+          <div
+            v-for="slot in timeSlots"
+            :key="slot.value"
+            class="col-6"
+          >
+            <q-radio
+              v-model="selectedSlot"
+              :val="slot.value"
+              :label="slot.label"
+              color="primary"
+              keep-color
+              dense
+            />
+          </div>
+        </div>
+      </q-card-section>
+
+      <template v-if="hasMealSection">
+        <q-separator class="q-mx-md" />
+        <q-card-section class="q-py-sm">
+          <div class="text-body2 text-weight-bold text-text q-mb-sm text-center">
+            {{ $t('scheduling_page.time_selection.meal_section') }}
+          </div>
+          <div class="row justify-center q-gutter-x-xl">
+            <q-radio
+              v-model="selectedMeal"
+              val="offer"
+              :label="$t('scheduling_page.time_selection.meal_offer')"
+              color="primary"
+              keep-color
+              dense
+            />
+            <q-radio
+              v-model="selectedMeal"
+              val="no_offer"
+              :label="$t('scheduling_page.time_selection.meal_no_offer')"
+              color="primary"
+              keep-color
+              dense
+            />
+          </div>
+        </q-card-section>
+      </template>
+
+      <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>
+
+      <div v-if="pauseNote" class="text-center text-caption text-primary q-pb-md">
+        {{ pauseNote }}
+      </div>
+
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue';
+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 },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent();
+const { t } = useI18n();
+
+const selectedSlot = ref(null);
+const selectedMeal = ref(null);
+
+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 timeSlots = computed(() => {
+  const h = props.serviceType.hoursCount;
+  const slots = [];
+  for (let start = 7; start + h <= 20; start++) {
+    const end = start + h;
+    slots.push({
+      value: `${start}-${end}`,
+      startHour: start,
+      endHour: end,
+      label: `${start}h às ${end}h`,
+    });
+  }
+  return slots;
+});
+
+const hasMealSection = computed(() =>
+  props.serviceType.hoursCount >= 6
+);
+
+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;
+});
+</script>
+
+<style scoped lang="scss">
+.time-card {
+  width: min(88vw, 360px);
+}
+</style>

+ 38 - 0
src/pages/search/components/ServiceTypeInfoDialog.vue

@@ -0,0 +1,38 @@
+<template>
+  <q-dialog ref="dialogRef" @hide="onDialogHide">
+    <q-card class="card-border bg-surface text-text" style="width: min(88vw, 360px);">
+
+      <q-card-section class="row items-center q-pb-none">
+        <div class="text-subtitle1 text-weight-bold text-text">{{ serviceType.label }}</div>
+        <q-space />
+        <q-btn flat round dense icon="mdi-close" color="grey-6" @click="onDialogCancel" />
+      </q-card-section>
+
+      <q-card-section class="text-caption text-grey-7 q-pt-xs">
+        {{ serviceType.hours }}
+      </q-card-section>
+
+      <q-separator />
+
+      <q-card-section class="text-body2 text-text">
+        {{ serviceType.description }}
+      </q-card-section>
+
+    </q-card>
+  </q-dialog>
+</template>
+
+<script setup>
+import { useDialogPluginComponent } from 'quasar';
+
+defineProps({
+  serviceType: {
+    type: Object,
+    required: true,
+  },
+});
+
+defineEmits([...useDialogPluginComponent.emits]);
+
+const { dialogRef, onDialogHide, onDialogCancel } = useDialogPluginComponent();
+</script>