| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403 |
- <template>
- <q-dialog ref="dialogRef" 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, getClientProviderBlocks } from 'src/api/schedule';
- import { userStore } from 'src/stores/user';
- import ServiceSelectionSheet from './ServiceSelectionSheet.vue';
- import ServiceTimeSelectionDialog from './ServiceTimeSelectionDialog.vue';
- import { useRouter } from 'vue-router';
- const props = defineProps({
- provider: { type: Object, required: true },
- initialBooking: { type: Object, required: true },
- });
- defineEmits([...useDialogPluginComponent.emits]);
- const { dialogRef, onDialogOK } = 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 router = useRouter();
- const showCalendar = ref(false);
- const addDateValue = ref(null);
- const loadingAvailability = ref(false);
- const workingDays = ref([]);
- const blockedDays = ref([]);
- const providerClientBlocks = ref({
- existing_schedules: [],
- fully_blocked_weeks: [],
- });
- const normalizeDate = (d) => d.replace(/\//g, '-');
- const getWeekStart = (dateStr) => {
- const d = new Date(normalizeDate(dateStr) + 'T12:00:00');
- d.setDate(d.getDate() - d.getDay());
- return d.toISOString().slice(0, 10);
- };
- const blockedWeekStartSet = computed(() =>
- new Set(providerClientBlocks.value.fully_blocked_weeks ?? [])
- );
- const getServerWeekCount = (newDateStr) => {
- const newWeek = getWeekStart(newDateStr);
- return (providerClientBlocks.value.existing_schedules ?? []).filter(
- (schedule) => getWeekStart(schedule.date) === newWeek
- ).length;
- };
- const getLocalWeekCount = (newDateStr) => {
- const newWeek = getWeekStart(newDateStr);
- return bookings.value.filter((booking) => getWeekStart(booking.date) === newWeek).length;
- };
- const wouldExceedWeekLimit = (newDateStr) => {
- const count = getServerWeekCount(newDateStr) + getLocalWeekCount(newDateStr);
- return count >= 2;
- };
- const availableWeekDays = computed(() =>
- [...new Set(workingDays.value.map(wd => wd.day))]
- );
- const blockedDateSet = computed(() =>
- new Set(blockedDays.value.filter(bd => bd.period === 'all').map(bd => bd.date))
- );
- const dateOptions = (d) => {
- const today = date.formatDate(new Date(), 'YYYY/MM/DD');
- if (d < today) return false;
- if (wouldExceedWeekLimit(d)) return false;
- const raw = normalizeDate(d);
- const parsed = new Date(`${raw}T12:00:00`);
- const dayOfWeek = parsed.getDay();
- const isWorking = availableWeekDays.value.includes(dayOfWeek);
- const isBlocked = blockedDateSet.value.has(raw);
- const isWeekBlocked = blockedWeekStartSet.value.has(getWeekStart(raw));
- return isWorking && !isBlocked && !isWeekBlocked;
- };
- const loadAvailability = async () => {
- loadingAvailability.value = true;
- try {
- const clientId = store.user?.client_id;
- const defaultClientBlocks = { existing_schedules: [], fully_blocked_weeks: [] };
- const [wd, bd, clientBlocks] = await Promise.all([
- getProviderWorkingDays(props.provider.provider_id),
- getProviderBlockedDays(props.provider.provider_id),
- clientId
- ? getClientProviderBlocks(clientId, props.provider.provider_id)
- : Promise.resolve(defaultClientBlocks),
- ]);
- workingDays.value = wd ?? [];
- blockedDays.value = bd ?? [];
- providerClientBlocks.value = clientBlocks ?? defaultClientBlocks;
- } catch {
- workingDays.value = [];
- blockedDays.value = [];
- providerClientBlocks.value = { existing_schedules: [], fully_blocked_weeks: [] };
- } finally {
- loadingAvailability.value = false;
- }
- };
- 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 onAddDateSelected = (val) => {
- if (!val) return;
- addDateValue.value = null;
- const valFormatted = normalizeDate(val);
- 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 serverBookingBlocks = (providerClientBlocks.value.existing_schedules ?? [])
- .filter((schedule) => schedule.date === valFormatted)
- .map((schedule) => ({
- init_hour: schedule.start_time,
- end_hour: schedule.end_time,
- }));
- const partialBlocks = [...blocksOfDate, ...workingDayBlocks, ...existingBookingBlocks, ...serverBookingBlocks];
- $q.dialog({
- component: ServiceSelectionSheet,
- componentProps: { provider: props.provider, selectedDate: val, partialBlocks },
- }).onOk(({ serviceType, date: date_, provider: prov }) => {
- $q.dialog({
- component: ServiceTimeSelectionDialog,
- componentProps: { serviceType, selectedDate: date_, provider: prov, partialBlocks },
- }).onOk((booking) => {
- if (wouldExceedWeekLimit(booking.date)) {
- $q.notify({ type: 'negative', message: t('scheduling_page.order_summary.week_limit_error') });
- return;
- }
- bookings.value.push(booking);
- showCalendar.value = false;
- });
- });
- };
- const confirmRemove = (idx) => {
- $q.dialog({
- title: t('scheduling_page.order_summary.remove_confirm_title'),
- cancel: { label: t('scheduling_page.order_summary.remove_confirm_cancel'), flat: true, color: 'grey-6' },
- ok: { label: t('scheduling_page.order_summary.remove_confirm_ok'), unelevated: true, color: 'primary', rounded: true, noCaps: true },
- persistent: true,
- }).onOk(() => {
- bookings.value.splice(idx, 1);
- });
- };
- const formatDate = (dateStr) => {
- const d = new Date(normalizeDate(dateStr) + 'T12:00:00');
- const localeMap = { pt: 'pt-BR', en: 'en-US', es: 'es-ES' };
- const loc = localeMap[locale.value] ?? 'pt-BR';
- const weekday = new Intl.DateTimeFormat(loc, { weekday: 'long' }).format(d);
- const dateFormatted = new Intl.DateTimeFormat(loc, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(d);
- return `${weekday.charAt(0).toUpperCase() + weekday.slice(1)}, ${dateFormatted}`;
- };
- const submitOrder = async () => {
- if (!primaryAddress.value) {
- $q.notify({ type: 'warning', message: t('scheduling_page.order_summary.no_primary_address') });
- return;
- }
- const payload = {
- client_id: store.user.client_id,
- provider_id: props.provider.provider_id,
- address_id: primaryAddress.value.id,
- schedule_type: 'default',
- schedules: bookings.value.map(b => ({
- date: normalizeDate(b.date),
- period_type: b.serviceType.hoursCount,
- start_time: formatHour(b.slot.startHour),
- end_time: formatHour(b.slot.endHour),
- total_amount: b.serviceType.price,
- offers_meal: b.meal === 'offer' ? true : b.meal === 'no_offer' ? false : null,
- })),
- };
- submitting.value = true;
- try {
- await createSchedule(payload);
- $q.notify({ type: 'positive', message: t('scheduling_page.order_summary.submit_success') });
- onDialogOK();
- } 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;
- router.push({ name: 'DashboardPage' });
- }
- };
- </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>
|