987 lines
37 KiB
Vue
987 lines
37 KiB
Vue
<template>
|
|
<div
|
|
class="date-picker text-center"
|
|
:class="[calendarViewDirection, { 'has-error': errors.length > 0 }]">
|
|
<fieldset id="date-picker-fieldset">
|
|
<legend class="sr-only">
|
|
When would you like service?
|
|
</legend>
|
|
<div class="appointment-date-picker-header">
|
|
When would you like service?
|
|
</div>
|
|
<div class="appointment-estimate-header">
|
|
Your service will take approximately <span>{{ appointmentEstimate }}</span>
|
|
</div>
|
|
<div
|
|
class="appointment-date-picker-container"
|
|
:class="isMobileView ? 'mobile' : 'desktop'">
|
|
<div class="header-cell header-start">
|
|
<button
|
|
v-if="activePageIndex !== 0"
|
|
type="button"
|
|
class="span-block-flex"
|
|
@click="gotoPreviousPage()">
|
|
<img
|
|
class="arrow-icon"
|
|
src="~@/assets/img/icons/icon-ablue-previous.png"
|
|
alt="Left arrow icon" />
|
|
Previous
|
|
</button>
|
|
</div>
|
|
<div class="header-cell header-middle">
|
|
{{ headerDateString }}
|
|
</div>
|
|
<div class="header-cell header-end">
|
|
<button
|
|
type="button"
|
|
class="span-block-flex"
|
|
@click="gotoNextPage()">
|
|
More
|
|
<img
|
|
class="arrow-icon"
|
|
src="~@/assets/img/icons/icon-ablue-next.png"
|
|
alt="Right arrow icon" />
|
|
</button>
|
|
</div>
|
|
<div class="week-row-container date-picker-row">
|
|
<template
|
|
v-for="n in daysToAdd"
|
|
:key="n">
|
|
<div class="day-of-week-header">
|
|
<span class="span-block day-of-week-caption">{{ showDayOfWeekForIndex(n - 1) }}</span>
|
|
<span class="span-block month-day-caption">{{ showMonthAndDayForIndex(n - 1) }}</span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div class="morning-row-container date-picker-row">
|
|
<template
|
|
v-for="n in daysToAdd"
|
|
:key="n">
|
|
<div
|
|
tabindex="0"
|
|
class="morning-row-cell"
|
|
:class="[showMorningAvailabilityForIndex(n - 1) > 0 ? 'has-appointments' : 'no-appointments'
|
|
, checkIsSelectedDay(n - 1, 'morning') ? 'selected-date' : '']"
|
|
@click="showMorningTimes(n - 1)"
|
|
@keydown.enter="showMorningTimes(n - 1)">
|
|
<span class="span-block top-caption">Morning</span>
|
|
<span class="span-block bottom-caption">
|
|
{{ showMorningAvailabilityForIndex(n - 1) }} available
|
|
</span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div class="afternoon-row-container date-picker-row">
|
|
<template
|
|
v-for="n in daysToAdd"
|
|
:key="n">
|
|
<div
|
|
tabindex="0"
|
|
class="afternoon-row-cell"
|
|
:class="[showAfternoonAvailabilityForIndex(n - 1) > 0 ? 'has-appointments' : 'no-appointments'
|
|
, checkIsSelectedDay(n - 1, 'afternoon') ? 'selected-date' : '']"
|
|
@click="showAfternoonTimes(n - 1)"
|
|
@keydown.enter="showAfternoonTimes(n - 1)">
|
|
<span class="span-block top-caption">Afternoon</span>
|
|
<span class="span-block bottom-caption">
|
|
{{ showAfternoonAvailabilityForIndex(n - 1) }} available
|
|
</span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="selectedDate"
|
|
class="appointment-time-picker-container"
|
|
:class="isMobileView ? 'mobile' : 'desktop'">
|
|
<div class="appointment-time-picker-header">
|
|
Available times:
|
|
</div>
|
|
<template
|
|
v-for="timeSlot in selectableTimeSlotsData"
|
|
:key="`${timeSlot.startTime}-${timeSlot.isDropoff}`">
|
|
<input
|
|
:id="`timeslot${timeSlot.startTime}-${timeSlot.isDropoff}`"
|
|
v-model="selectedTime"
|
|
type="radio"
|
|
name="time-slot"
|
|
class="sr-only"
|
|
:value="timeSlot.startTime"
|
|
@focus="timeSlotFocused(timeSlot)"
|
|
@change="timeSlotInputChanged(timeSlot)" />
|
|
<label
|
|
:for="`timeslot${timeSlot.startTime}`"
|
|
class="time-slot-button"
|
|
:class="[checkIsSelectedTimeSlot(timeSlot) ? 'selected-time-slot' : ''
|
|
, {'has-date-error': showTimeSlotError}]"
|
|
@click="selectTimeSlotForDay(timeSlot)"
|
|
v-html="displayTimeSlotTime(timeSlot)">
|
|
</label>
|
|
<div
|
|
v-if="timeSlot.isDropoff"
|
|
class="time-slot-drop-off-information"
|
|
v-html="displayDropOffInformation(timeSlot)">
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div class="row form-test-error">
|
|
<div v-if="showTimeSlotError">
|
|
{{ displayErrorMessage }}
|
|
</div>
|
|
</div>
|
|
</fieldset>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
// Supporting files
|
|
import { useMainStore } from '@/store';
|
|
import { useField } from 'vee-validate';
|
|
import { deepClone } from '@/helpers/object-helper';
|
|
import {
|
|
convertDateToDateString,
|
|
convertDateToShortMonth,
|
|
convertDateToTwoDigitDay,
|
|
convertDateToTwoDigitMonth,
|
|
getDisplayTextForDurationLength,
|
|
isAfternoon,
|
|
militaryToTwelveHourTime
|
|
} from '@/helpers/date-helper';
|
|
import {
|
|
AppointmentTypeStrings,
|
|
PREMIUM_TIME_SLOT_ID_FLAG
|
|
} from '@/constants/schedule-constants';
|
|
import showIssLoadingModal from '@/helpers/loading-modal-helper.js';
|
|
import errorMessages from '@/constants/error-messages.js';
|
|
import { selectableDaysOptions } from './mixins/helpers';
|
|
|
|
export default {
|
|
name: 'date-picker',
|
|
props: {
|
|
activeAppointmentType: {
|
|
type: String,
|
|
default: null
|
|
},
|
|
customComponentId: String,
|
|
selectableDatesSetting: {
|
|
type: String,
|
|
validator(value) {
|
|
return Object.values(selectableDaysOptions).includes(value);
|
|
},
|
|
default: selectableDaysOptions.PAST
|
|
},
|
|
isMobileView: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
modelValue: {
|
|
type: Object
|
|
},
|
|
todayOverrideDateString: {
|
|
// keep for use in unit tests to override today's date
|
|
type: String,
|
|
default: null
|
|
},
|
|
customSelectableDatesCallback: {
|
|
type: Function,
|
|
default() {
|
|
return [];
|
|
}
|
|
},
|
|
showTimeSlotError: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
validationRules: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
},
|
|
emits: ['dateSelected', 'timeSlotSelected'],
|
|
setup(props) {
|
|
const uuid = crypto.randomUUID();
|
|
const componentId = !props.customComponentId
|
|
? `component-${uuid}`
|
|
: props.customComponentId;
|
|
|
|
const mainStore = useMainStore();
|
|
|
|
const { modelValue } = deepClone(props);
|
|
const initialValue = modelValue;
|
|
|
|
const fieldOptions = {
|
|
value: modelValue,
|
|
initialValue
|
|
};
|
|
|
|
const {
|
|
errorMessage,
|
|
handleBlur,
|
|
handleChange,
|
|
meta,
|
|
validate,
|
|
errors,
|
|
resetField
|
|
} = useField(componentId, props.validationRules, fieldOptions);
|
|
|
|
return {
|
|
componentId,
|
|
mainStore,
|
|
errorMessage,
|
|
handleBlur,
|
|
handleChange,
|
|
validate,
|
|
meta,
|
|
errors,
|
|
resetField
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
isLoading: true,
|
|
selectableDatesData: [], // NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
|
|
selectableTimeSlotsData: [],
|
|
selectedTimeOfDayGrouping: null,
|
|
selectedDate: null,
|
|
selectedTime: null,
|
|
selectedTimeSlot: null,
|
|
activeDate: new Date(),
|
|
activePageIndex: 0,
|
|
todaysDate: new Date(),
|
|
mql: null,
|
|
daysInViewMobile: 3,
|
|
daysInViewStandard: 5,
|
|
daysLoaded: 0,
|
|
appointmentDurationMinutesMinimum: 90,
|
|
appointmentDurationMinutesMaximum: 120,
|
|
initialDaysToLoad: 15
|
|
};
|
|
},
|
|
computed: {
|
|
appointmentEstimate() {
|
|
return getDisplayTextForDurationLength(this.appointmentDurationMinutesMinimum, this.appointmentDurationMinutesMaximum);
|
|
},
|
|
calendarViewDirection() {
|
|
if (this.selectableDatesSetting === 'custom') return 'future';
|
|
return 'past';
|
|
},
|
|
daysToAdd() {
|
|
return this.isMobileView ? this.daysInViewMobile : this.daysInViewStandard;
|
|
},
|
|
displayErrorMessage() {
|
|
return errorMessages.TIME_REQUIRED;
|
|
},
|
|
headerDateString() {
|
|
const dayOffset = this.activePageIndex * this.daysToAdd;
|
|
const startDateToShow = new Date(this.activeDate);
|
|
startDateToShow.setDate(startDateToShow.getDate() + dayOffset);
|
|
const startMonthDisplay = convertDateToShortMonth(startDateToShow);
|
|
const endDateToShow = new Date(startDateToShow);
|
|
endDateToShow.setDate(endDateToShow.getDate() + (this.daysToAdd - 1));
|
|
const endMonthDisplay = convertDateToShortMonth(endDateToShow);
|
|
|
|
if (startMonthDisplay !== endMonthDisplay) {
|
|
return `${startMonthDisplay}. ${startDateToShow.getDate()} - ${endMonthDisplay}. ${endDateToShow.getDate()}`;
|
|
}
|
|
return `${startMonthDisplay}. ${startDateToShow.getDate()}-${endDateToShow.getDate()}`;
|
|
},
|
|
shortMonth() {
|
|
return this.todaysDate.toLocaleString('en-US', { month: 'short' });
|
|
},
|
|
todayString() {
|
|
return (
|
|
this.todayOverrideDateString
|
|
|| convertDateToDateString(new Date())
|
|
);
|
|
}
|
|
},
|
|
watch: {
|
|
isMobileView(newValue, oldValue) {
|
|
if (newValue !== oldValue) {
|
|
this.activePageIndex = 0;
|
|
this.selectedDate = null;
|
|
this.selectedTimeOfDayGrouping = null;
|
|
this.selectedTime = null;
|
|
this.selectedTimeSlot = null;
|
|
this.findFirstAvailableDateInView();
|
|
}
|
|
},
|
|
selectedDate(newValue, oldValue) {
|
|
if (newValue !== oldValue) {
|
|
this.$emit('dateSelected', newValue);
|
|
}
|
|
},
|
|
selectedTimeSlot(newValue, oldValue) {
|
|
if (newValue !== oldValue) {
|
|
const testObj = this.getSelectedTimeSlotInfoObject(newValue);
|
|
this.$emit('timeSlotSelected', testObj);
|
|
}
|
|
}
|
|
},
|
|
methods: {
|
|
initializeComponent(initialData) {
|
|
this.resetComponent();
|
|
this.setCalendarData(initialData);
|
|
},
|
|
resetComponent() {
|
|
this.isLoading = true;
|
|
this.selectableDatesData = []; // NOTE: uses monthNum (1-based), NOT monthIndex (0-based)
|
|
this.selectableTimeSlotsData = [];
|
|
this.selectedTimeOfDayGrouping = null;
|
|
this.selectedDate = null;
|
|
this.selectedTime = null;
|
|
this.selectedTimeSlot = null;
|
|
this.activeDate = new Date();
|
|
this.activePageIndex = 0;
|
|
this.todaysDate = new Date();
|
|
this.mql = null;
|
|
this.daysInViewMobile = 3;
|
|
this.daysInViewStandard = 5;
|
|
this.daysLoaded = 0;
|
|
this.appointmentDurationMinutesMinimum = 90;
|
|
this.appointmentDurationMinutesMaximum = 120;
|
|
this.initialDaysToLoad = 15;
|
|
},
|
|
addPremiumFlagToInput(routeCode) {
|
|
return (`${routeCode}${PREMIUM_TIME_SLOT_ID_FLAG}`);
|
|
},
|
|
checkIsSelectedDay(index, timeOfDayGrouping) {
|
|
const dateToShowString = this.getDateToShowString(index);
|
|
return this.selectedDate === dateToShowString && this.selectedTimeOfDayGrouping === timeOfDayGrouping;
|
|
},
|
|
checkIsSelectedTimeSlot(timeSlot) {
|
|
if (!this.selectedTimeSlot) {
|
|
return false;
|
|
}
|
|
return (this.selectedTimeSlot.isDropoff === timeSlot.isDropoff
|
|
&& this.selectedTimeSlot.startTime === timeSlot.startTime
|
|
&& this.selectedTimeSlot.timeOfDay === timeSlot.timeOfDay);
|
|
},
|
|
displayDropOffInformation(timeSlot) {
|
|
if (timeSlot.isDropoff) {
|
|
if (timeSlot.timeOfDay === 'morning') {
|
|
return this.getCmsContent(
|
|
'DropOffTimeSlotModal',
|
|
'BodyText'
|
|
);
|
|
}
|
|
return this.getCmsContent(
|
|
'OvernightDropOffTimeSlotModal',
|
|
'BodyText'
|
|
);
|
|
}
|
|
return '';
|
|
},
|
|
displayTimeSlotTime(timeSlot) {
|
|
const appointmentType = this.activeAppointmentType || AppointmentTypeStrings.IN_SHOP;
|
|
if (appointmentType === AppointmentTypeStrings.MOBILE || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
|
|
return `${militaryToTwelveHourTime(timeSlot.startTime)} - ${militaryToTwelveHourTime(timeSlot.endTime)}`;
|
|
}
|
|
if (timeSlot.isDropoff) {
|
|
if (timeSlot.timeOfDay === 'morning') {
|
|
return '<span class="dropoff-label">Drop & Go<sup>™</sup></span>';
|
|
}
|
|
return '<span class="dropoff-label">Overnight drop off</span>';
|
|
}
|
|
return militaryToTwelveHourTime(timeSlot.startTime);
|
|
},
|
|
findFirstAvailableDateInView() {
|
|
for (let i = 0; i < this.daysToAdd; i += 1) {
|
|
const dateInView = this.getDateToShow(i);
|
|
const dateInViewString = convertDateToDateString(dateInView);
|
|
const selectableDateObj = this.selectableDatesData.find((dateObj) => dateObj.date === dateInViewString);
|
|
if (selectableDateObj && selectableDateObj.timeSlots && selectableDateObj.timeSlots.length > 0) {
|
|
if (selectableDateObj.morningTimeSlots && selectableDateObj.morningTimeSlots.length > 0) {
|
|
this.showMorningTimes(i);
|
|
} else {
|
|
this.showAfternoonTimes(i);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
findInitialPageIndex(daysFromStart) {
|
|
if (daysFromStart !== null && daysFromStart !== undefined) {
|
|
return {
|
|
initialPageIndex: Math.floor(daysFromStart / this.daysToAdd),
|
|
initialDayIndex: daysFromStart % this.daysToAdd
|
|
};
|
|
}
|
|
return {
|
|
initialPageIndex: 0,
|
|
initialDayIndex: null
|
|
};
|
|
},
|
|
findInitialDate(myDateString, initialDayIndex) {
|
|
const dateInView = this.getDateToShow(initialDayIndex);
|
|
const dateInViewString = convertDateToDateString(dateInView);
|
|
if (dateInViewString === myDateString) {
|
|
const selectableDateObj = this.selectableDatesData.find((dateObj) => dateObj.date === dateInViewString);
|
|
if (selectableDateObj) {
|
|
const initialTimeSlot = this.mainStore.order.schedule;
|
|
const isAfternoonSlot = isAfternoon(initialTimeSlot?.startTime);
|
|
if (isAfternoonSlot) {
|
|
this.showAfternoonTimes(initialDayIndex);
|
|
this.findInitialTimeByStore(selectableDateObj.afternoonTimeSlots);
|
|
} else {
|
|
this.showMorningTimes(initialDayIndex);
|
|
this.findInitialTimeByStore(selectableDateObj.morningTimeSlots);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
findInitialTimeByStore(timeSlots) {
|
|
const initialTimeSlot = this.mainStore.order.schedule;
|
|
if (!initialTimeSlot || !initialTimeSlot.startTime) {
|
|
return;
|
|
}
|
|
const foundTimeSlot = timeSlots.find((ts) => ts.startTime === initialTimeSlot.startTime && ts.endTime === initialTimeSlot.endTime);
|
|
if (foundTimeSlot) {
|
|
this.selectTimeSlotForDay(foundTimeSlot);
|
|
}
|
|
},
|
|
getDateToShow(index) {
|
|
const dayOffset = this.activePageIndex * this.daysToAdd;
|
|
const dateToShow = new Date(this.activeDate);
|
|
dateToShow.setDate(dateToShow.getDate() + index + dayOffset);
|
|
return dateToShow;
|
|
},
|
|
getDateToShowString(index) {
|
|
const dateToShow = this.getDateToShow(index);
|
|
return convertDateToDateString(dateToShow);
|
|
},
|
|
getSelectedTimeSlotInfoObject(timeSlot) {
|
|
const routeCode = timeSlot?.id;
|
|
let routeCodeToUse = routeCode;
|
|
const routeCodeIncludesPremium = routeCode?.includes(PREMIUM_TIME_SLOT_ID_FLAG);
|
|
if (routeCodeIncludesPremium) {
|
|
routeCodeToUse = this.removePremiumFlagFromInput(routeCode);
|
|
}
|
|
|
|
const fullTimeSlot = this.selectableTimeSlotsData?.find((ts) => ts.id === routeCodeToUse);
|
|
|
|
if (fullTimeSlot) {
|
|
return {
|
|
timeSlot: {
|
|
date: this.selectedDate,
|
|
routeCode: fullTimeSlot.id,
|
|
startTime: fullTimeSlot.startTime,
|
|
endTime: fullTimeSlot.endTime,
|
|
isDropoff: fullTimeSlot.isDropoff,
|
|
jobMaxMinutes: this.appointmentDurationMinutesMaximum.toString(),
|
|
jobMinMinutes: this.appointmentDurationMinutesMinimum.toString()
|
|
},
|
|
isPremiumAppointment: !!routeCodeIncludesPremium
|
|
};
|
|
}
|
|
|
|
return {
|
|
timeSlot: {
|
|
date: null,
|
|
routeCode: null,
|
|
startTime: null,
|
|
endTime: null,
|
|
isDropoff: null,
|
|
jobMaxMinutes: null,
|
|
jobMinMinutes: null
|
|
},
|
|
isPremiumAppointment: null
|
|
};
|
|
},
|
|
async gotoNextPage() {
|
|
const maxPageIndex = Math.floor((this.daysLoaded - 1) / this.daysToAdd);
|
|
if (this.activePageIndex >= maxPageIndex) {
|
|
// do not go to next page right away if at end of loaded days
|
|
await this.loadMoreDays(maxPageIndex, this.daysToAdd, this.initialDaysToLoad).then(() => {
|
|
this.daysLoaded += this.initialDaysToLoad;
|
|
this.activePageIndex += 1;
|
|
this.selectedDate = null;
|
|
this.selectedTime = null;
|
|
this.selectedTimeSlot = null;
|
|
this.findFirstAvailableDateInView();
|
|
});
|
|
return;
|
|
}
|
|
this.activePageIndex += 1;
|
|
this.selectedDate = null;
|
|
this.selectedTime = null;
|
|
this.selectedTimeSlot = null;
|
|
this.findFirstAvailableDateInView();
|
|
},
|
|
gotoPreviousPage() {
|
|
this.activePageIndex -= 1;
|
|
this.selectedDate = null;
|
|
this.selectedTime = null;
|
|
this.selectedTimeSlot = null;
|
|
this.findFirstAvailableDateInView();
|
|
},
|
|
mapTimeSlot(timeSlot, timeOfDay) {
|
|
return {
|
|
...timeSlot,
|
|
isDropoff: timeSlot.id.endsWith('DROP OFF'),
|
|
timeOfDay
|
|
};
|
|
},
|
|
removePremiumFlagFromInput(routeCode) {
|
|
return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, '');
|
|
},
|
|
selectTimeSlotForDay(timeSlot) {
|
|
if (timeSlot) {
|
|
this.selectedTime = timeSlot.startTime;
|
|
this.selectedTimeSlot = timeSlot;
|
|
}
|
|
},
|
|
showDayOfWeekForIndex(index) {
|
|
const dateToShow = this.getDateToShow(index);
|
|
return dateToShow.toLocaleDateString('en-US', { weekday: 'short' });
|
|
},
|
|
showMonthAndDayForIndex(index) {
|
|
const dateToShow = this.getDateToShow(index);
|
|
return `${convertDateToTwoDigitMonth(dateToShow)}/${convertDateToTwoDigitDay(dateToShow)}`;
|
|
},
|
|
showMorningAvailabilityForIndex(index) {
|
|
const morningDateString = this.getDateToShowString(index);
|
|
const selectableDateObj = this.selectableDatesData.find((dateObj) => dateObj.date === morningDateString);
|
|
if (selectableDateObj && selectableDateObj.morningTimeSlots && selectableDateObj.morningTimeSlots.length > 0) {
|
|
return selectableDateObj.morningTimeSlots.length;
|
|
}
|
|
return 0;
|
|
},
|
|
showMorningTimes(index) {
|
|
const morningDateString = this.getDateToShowString(index);
|
|
const selectableDateObj = this.selectableDatesData.find((dateObj) => dateObj.date === morningDateString);
|
|
if (selectableDateObj && selectableDateObj.morningTimeSlots && selectableDateObj.morningTimeSlots.length > 0) {
|
|
if (this.selectedDate === morningDateString && this.selectedTimeOfDayGrouping === 'morning') {
|
|
// already selected, do nothing
|
|
return;
|
|
}
|
|
this.selectedDate = morningDateString;
|
|
this.selectedTimeOfDayGrouping = 'morning';
|
|
this.selectableTimeSlotsData = selectableDateObj.morningTimeSlots;
|
|
this.selectedTime = null;
|
|
this.selectedTimeSlot = null;
|
|
}
|
|
},
|
|
showAfternoonAvailabilityForIndex(index) {
|
|
const afternoonDateString = this.getDateToShowString(index);
|
|
const selectableDateObj = this.selectableDatesData.find((dateObj) => dateObj.date === afternoonDateString);
|
|
if (selectableDateObj && selectableDateObj.afternoonTimeSlots && selectableDateObj.afternoonTimeSlots.length > 0) {
|
|
return selectableDateObj.afternoonTimeSlots.length;
|
|
}
|
|
return 0;
|
|
},
|
|
showAfternoonTimes(index) {
|
|
const afternoonDateString = this.getDateToShowString(index);
|
|
const selectableDateObj = this.selectableDatesData.find((dateObj) => dateObj.date === afternoonDateString);
|
|
if (selectableDateObj && selectableDateObj.afternoonTimeSlots && selectableDateObj.afternoonTimeSlots.length > 0) {
|
|
if (this.selectedDate === afternoonDateString
|
|
&& this.selectedTimeOfDayGrouping === 'afternoon') {
|
|
// already selected, do nothing
|
|
return;
|
|
}
|
|
this.selectedDate = afternoonDateString;
|
|
this.selectedTimeOfDayGrouping = 'afternoon';
|
|
this.selectableTimeSlotsData = selectableDateObj.afternoonTimeSlots;
|
|
this.selectedTimeSlot = null;
|
|
this.selectedTime = null;
|
|
}
|
|
},
|
|
timeSlotFocused(timeSlot) {
|
|
if (timeSlot && this.selectedTime === null) {
|
|
this.selectTimeSlotForDay(timeSlot);
|
|
}
|
|
},
|
|
timeSlotInputChanged(timeSlot) {
|
|
this.selectTimeSlotForDay(timeSlot);
|
|
},
|
|
async setCalendarData(config = {}) {
|
|
// set selectable dates data
|
|
config.initialShopTimeSlotsResponse.days.forEach((selectableDate) => {
|
|
const dateObjectToPush = {
|
|
date: selectableDate.date,
|
|
timeSlots: selectableDate.timeSlots,
|
|
morningTimeSlots: selectableDate.timeSlots.filter((timeSlot) => {
|
|
const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10);
|
|
return timeHour < 12;
|
|
}).map((timeSlot) => this.mapTimeSlot(timeSlot, 'morning')),
|
|
afternoonTimeSlots: selectableDate.timeSlots.filter((timeSlot) => {
|
|
const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10);
|
|
return timeHour >= 12;
|
|
}).map((timeSlot) => this.mapTimeSlot(timeSlot, 'afternoon')),
|
|
isSelected: false
|
|
};
|
|
|
|
if (dateObjectToPush.morningTimeSlots.length > 0) {
|
|
const findIndex = dateObjectToPush.morningTimeSlots.findIndex((timeSlot) => timeSlot.id.endsWith('DROP OFF'));
|
|
if (findIndex !== -1) {
|
|
const dropOffSlot = dateObjectToPush.morningTimeSlots.splice(findIndex, 1)[0];
|
|
dateObjectToPush.morningTimeSlots.unshift(dropOffSlot);
|
|
}
|
|
}
|
|
if (dateObjectToPush.afternoonTimeSlots.length > 0) {
|
|
const findIndex = dateObjectToPush.afternoonTimeSlots.findIndex((timeSlot) => timeSlot.id.endsWith('DROP OFF'));
|
|
if (findIndex !== -1) {
|
|
const dropOffSlot = dateObjectToPush.afternoonTimeSlots.splice(findIndex, 1)[0];
|
|
dateObjectToPush.afternoonTimeSlots.push(dropOffSlot);
|
|
}
|
|
}
|
|
this.selectableDatesData.push(dateObjectToPush);
|
|
});
|
|
|
|
this.daysLoaded = config.initialDaysLoaded || this.initialDaysToLoad;
|
|
this.appointmentDurationMinutesMaximum = config.initialShopTimeSlotsResponse.estimatedServiceMinutesMaximum;
|
|
this.appointmentDurationMinutesMinimum = config.initialShopTimeSlotsResponse.estimatedServiceMinutesMinimum;
|
|
|
|
let pageIndexObj = { initialPageIndex: 0, initialDayIndex: null };
|
|
if (config.daysFromStart !== null && config.daysFromStart !== undefined) {
|
|
pageIndexObj = this.findInitialPageIndex(config.daysFromStart);
|
|
this.activePageIndex = pageIndexObj.initialPageIndex;
|
|
}
|
|
|
|
if (config.preSelectedDate) {
|
|
this.findInitialDate(config.preSelectedDate, pageIndexObj.initialDayIndex);
|
|
} else {
|
|
this.findFirstAvailableDateInView();
|
|
let attempts = 0;
|
|
while (!this.selectedDate && attempts < 5) {
|
|
await this.gotoNextPage();
|
|
attempts += 1;
|
|
}
|
|
}
|
|
|
|
this.isLoading = false;
|
|
},
|
|
async loadMoreDays(maxPageIndex, daysInView, daysToFetch) {
|
|
showIssLoadingModal(true);
|
|
|
|
const startDateForFetch = new Date(this.activeDate);
|
|
startDateForFetch.setDate(startDateForFetch.getDate() + (maxPageIndex + 1) * daysInView);
|
|
const startDateToFetchString = convertDateToDateString(startDateForFetch);
|
|
const endDateForFetch = new Date(startDateForFetch);
|
|
endDateForFetch.setDate(endDateForFetch.getDate() + (daysToFetch - 1));
|
|
const endDateToFetchString = convertDateToDateString(endDateForFetch);
|
|
// make new API call with this month's start and end dates
|
|
await this.updateSelectableDates(
|
|
startDateToFetchString,
|
|
endDateToFetchString
|
|
).catch(() => {
|
|
showIssLoadingModal(false);
|
|
});
|
|
|
|
showIssLoadingModal(false);
|
|
},
|
|
async updateSelectableDates(dateStart, dateEnd) {
|
|
const moreSelectableDates =
|
|
await this.customSelectableDatesCallback(
|
|
dateStart,
|
|
dateEnd
|
|
);
|
|
|
|
moreSelectableDates.days.forEach((selectableDate) => {
|
|
const index = this.selectableDatesData.findIndex((dateObj) => dateObj.date === selectableDate.date);
|
|
if (index === -1) {
|
|
const dateObjectToPush = {
|
|
date: selectableDate.date,
|
|
timeSlots: selectableDate.timeSlots,
|
|
morningTimeSlots: selectableDate.timeSlots.filter((timeSlot) => {
|
|
const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10);
|
|
return timeHour < 12;
|
|
}).map((timeSlot) => this.mapTimeSlot(timeSlot, 'morning')),
|
|
afternoonTimeSlots: selectableDate.timeSlots.filter((timeSlot) => {
|
|
const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10);
|
|
return timeHour >= 12;
|
|
}).map((timeSlot) => this.mapTimeSlot(timeSlot, 'afternoon')),
|
|
isSelected: false
|
|
};
|
|
|
|
if (dateObjectToPush.morningTimeSlots.length > 0) {
|
|
const findIndex = dateObjectToPush.morningTimeSlots.findIndex((timeSlot) => timeSlot.id.endsWith('DROP OFF'));
|
|
if (findIndex !== -1) {
|
|
const dropOffSlot = dateObjectToPush.morningTimeSlots.splice(findIndex, 1)[0];
|
|
dateObjectToPush.morningTimeSlots.unshift(dropOffSlot);
|
|
}
|
|
}
|
|
if (dateObjectToPush.afternoonTimeSlots.length > 0) {
|
|
const findIndex = dateObjectToPush.afternoonTimeSlots.findIndex((timeSlot) => timeSlot.id.endsWith('DROP OFF'));
|
|
if (findIndex !== -1) {
|
|
const dropOffSlot = dateObjectToPush.afternoonTimeSlots.splice(findIndex, 1)[0];
|
|
dateObjectToPush.afternoonTimeSlots.push(dropOffSlot);
|
|
}
|
|
}
|
|
this.selectableDatesData.push(dateObjectToPush);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@import '@/styles/ux-variables-svg-strings.scss';
|
|
|
|
.date-picker-hidden {
|
|
opacity: 0;
|
|
max-height: 0;
|
|
}
|
|
.date-picker {
|
|
position: relative;
|
|
|
|
.appointment-date-picker-header {
|
|
font-weight: 600;
|
|
color: #000;
|
|
font-size: 1rem;
|
|
text-align: left;
|
|
margin-top: 1.25rem;
|
|
}
|
|
|
|
.appointment-estimate-header {
|
|
font-weight: 400;
|
|
color: #525656;
|
|
font-size: 1rem;
|
|
text-align: left;
|
|
margin-top: 0.25rem;
|
|
|
|
span {
|
|
font-weight: 600;
|
|
color:#000;
|
|
}
|
|
}
|
|
|
|
.appointment-date-picker-container {
|
|
display: grid;
|
|
grid-column-gap: 0px;
|
|
grid-row-gap: 0px;
|
|
|
|
margin-top: 1.25rem;
|
|
border-radius: .3125rem;
|
|
box-shadow: 0 0 .625rem 0 rgba(0, 0, 0, 0.2);
|
|
|
|
@include media-breakpoint-up(xs) {
|
|
grid-template-columns: repeat(3, 33.33%);
|
|
}
|
|
@include media-breakpoint-up(xl) {
|
|
grid-template-columns: repeat(5, 20%);
|
|
}
|
|
.date-picker-row {
|
|
display: contents;
|
|
}
|
|
.header-cell {
|
|
font-size: 1rem;
|
|
padding: .3125rem;
|
|
|
|
.span-block-flex {
|
|
display: flex;
|
|
align-items: center;
|
|
border: none;
|
|
background-color: #fff;
|
|
color: #525656;
|
|
cursor: pointer;
|
|
|
|
&:hover {
|
|
text-decoration: underline;
|
|
}
|
|
}
|
|
}
|
|
.header-end {
|
|
text-align: end;
|
|
|
|
.span-block-flex {
|
|
justify-self: flex-end;
|
|
justify-content: flex-end;
|
|
|
|
img {
|
|
margin-left: 0.25rem;
|
|
}
|
|
}
|
|
}
|
|
.header-middle {
|
|
font-weight: 600;
|
|
color: #000;
|
|
@include media-breakpoint-up(xs) {
|
|
grid-column: 2 / span 1;
|
|
}
|
|
@include media-breakpoint-up(xl) {
|
|
grid-column: 2 / span 3;
|
|
}
|
|
|
|
text-align: center;
|
|
}
|
|
.header-start {
|
|
text-align: start;
|
|
|
|
.span-block-flex {
|
|
justify-self: flex-start;
|
|
justify-content: flex-start;
|
|
|
|
img {
|
|
margin-right: 0.25rem;
|
|
}
|
|
}
|
|
}
|
|
.day-of-week-header {
|
|
background-color: #0070d1;
|
|
color: $white;
|
|
padding: 0.5rem 0.25rem;
|
|
line-height: 1.5rem;
|
|
|
|
.day-of-week-caption {
|
|
font-weight: 600;
|
|
font-size: 1rem;
|
|
}
|
|
.month-day-caption {
|
|
font-weight: 300;
|
|
font-size: 0.8125rem;
|
|
}
|
|
}
|
|
.morning-row-cell, .afternoon-row-cell {
|
|
padding: 0.8125rem 0.25rem;
|
|
line-height: 1.5rem;
|
|
border: solid 1px #cacbcc;
|
|
|
|
.top-caption {
|
|
font-weight: 400;
|
|
font-size: 1rem;
|
|
color: #000;
|
|
}
|
|
.bottom-caption {
|
|
font-weight: 400;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
&.has-appointments {
|
|
&:hover {
|
|
border: solid 1px #0070d1;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.bottom-caption {
|
|
color: #1574a1;
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
|
|
&.selected-date {
|
|
background-color: $background-color-selected;
|
|
border: solid 1px #0070d1;
|
|
}
|
|
|
|
&:focus, &:focus-visible {
|
|
outline: none;
|
|
border: solid 2px #0070d1;
|
|
}
|
|
}
|
|
.span-block {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.appointment-time-picker-container {
|
|
margin-top: 1rem;
|
|
margin-bottom: 0.5rem;
|
|
|
|
.appointment-time-picker-header {
|
|
font-weight: 600;
|
|
color: #000;
|
|
font-size: 1rem;
|
|
text-align: left;
|
|
}
|
|
|
|
.time-slot-button {
|
|
width: 100%;
|
|
border: 1px solid #b3b4b5;
|
|
border-radius: $border-radius-list-button;
|
|
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, .2);
|
|
color: #525656;
|
|
padding: 0.625rem 0;
|
|
margin-top: 0.625rem;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
|
|
:deep(.dropoff-label) {
|
|
sup {
|
|
top: -0.375rem;
|
|
font-size: 0.75rem;
|
|
}
|
|
}
|
|
|
|
& + .time-slot-drop-off-information {
|
|
max-height: 0;
|
|
overflow: hidden;
|
|
transition: max-height 0.75s ease-in-out;
|
|
font-weight: 400;
|
|
text-align: left;
|
|
color: $darker-gray;
|
|
|
|
:deep(ul) {
|
|
margin: 0;
|
|
margin-bottom: 0.375rem;
|
|
|
|
> li {
|
|
padding-left: 0.625rem;
|
|
margin-top: 0.625rem;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.selected-time-slot {
|
|
background-color: $background-color-selected;
|
|
border: solid 1px #0070d1;
|
|
color:#000;
|
|
font-weight: 500;
|
|
|
|
:deep(.dropoff-label) {
|
|
sup {
|
|
top: .25rem;
|
|
font-size: 1.625rem;
|
|
}
|
|
}
|
|
|
|
& + .time-slot-drop-off-information {
|
|
max-height: 12rem;
|
|
transition: max-height 0.75s ease-in-out;
|
|
}
|
|
}
|
|
|
|
&.has-date-error {
|
|
border: 1px solid #d93025;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
fieldset {
|
|
flex-grow: 1;
|
|
position: relative;
|
|
}
|
|
|
|
.btn-link {
|
|
font-weight: 500;
|
|
text-underline-offset: 4px;
|
|
flex-grow: 0;
|
|
&:focus {
|
|
box-shadow: none;
|
|
}
|
|
}
|
|
}
|
|
.btn-link {
|
|
display: block;
|
|
position: relative;
|
|
height: 2rem;
|
|
width: 100%;
|
|
justify-content: center;
|
|
background: transparent;
|
|
border: none;
|
|
font-weight: 500;
|
|
letter-spacing: inherit;
|
|
text-underline-offset: 4px;
|
|
z-index: 2;
|
|
}
|
|
|
|
.form-test-error {
|
|
text-align: left;
|
|
margin-top: 0;
|
|
color: #db0020;
|
|
font-weight: 400;
|
|
font-size: 1rem;
|
|
}
|
|
</style>
|