|
|
@@ -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>
|