Merge pull request #1071 from Safelite/feature/richardson/INSR-drop.off

drop off updates for schedule page
This commit is contained in:
brich1212safe 2026-02-13 11:47:15 -05:00 committed by GitHub
commit 43dff7ac16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 182 additions and 75 deletions

View file

@ -99,9 +99,9 @@
</div> </div>
<template <template
v-for="timeSlot in selectableTimeSlotsData" v-for="timeSlot in selectableTimeSlotsData"
:key="timeSlot.startTime"> :key="`${timeSlot.startTime}-${timeSlot.isDropoff}`">
<input <input
:id="`timeslot${timeSlot.startTime}`" :id="`timeslot${timeSlot.startTime}-${timeSlot.isDropoff}`"
v-model="selectedTime" v-model="selectedTime"
type="radio" type="radio"
name="time-slot" name="time-slot"
@ -114,9 +114,14 @@
class="time-slot-button" class="time-slot-button"
:class="[checkIsSelectedTimeSlot(timeSlot) ? 'selected-time-slot' : '' :class="[checkIsSelectedTimeSlot(timeSlot) ? 'selected-time-slot' : ''
, {'has-date-error': showTimeSlotError}]" , {'has-date-error': showTimeSlotError}]"
@click="selectTimeSlotForDay(timeSlot)"> @click="selectTimeSlotForDay(timeSlot)"
{{ displayTimeSlotTime(timeSlot) }} v-html="displayTimeSlotTime(timeSlot)">
</label> </label>
<div
v-if="timeSlot.isDropoff"
class="time-slot-drop-off-information"
v-html="displayDropOffInformation(timeSlot)">
</div>
</template> </template>
</div> </div>
<div class="row form-test-error"> <div class="row form-test-error">
@ -256,6 +261,10 @@ export default {
appointmentEstimate() { appointmentEstimate() {
return getDisplayTextForDurationLength(this.appointmentDurationMinutesMinimum, this.appointmentDurationMinutesMaximum); return getDisplayTextForDurationLength(this.appointmentDurationMinutesMinimum, this.appointmentDurationMinutesMaximum);
}, },
calendarViewDirection() {
if (this.selectableDatesSetting === 'custom') return 'future';
return 'past';
},
daysToAdd() { daysToAdd() {
return this.isMobileView ? this.daysInViewMobile : this.daysInViewStandard; return this.isMobileView ? this.daysInViewMobile : this.daysInViewStandard;
}, },
@ -284,10 +293,6 @@ export default {
this.todayOverrideDateString this.todayOverrideDateString
|| convertDateToDateString(new Date()) || convertDateToDateString(new Date())
); );
},
calendarViewDirection() {
if (this.selectableDatesSetting === 'custom') return 'future';
return 'past';
} }
}, },
watch: { watch: {
@ -301,16 +306,16 @@ export default {
this.findFirstAvailableDateInView(); this.findFirstAvailableDateInView();
} }
}, },
selectedDate(newValue, oldValue) {
if (newValue !== oldValue) {
this.$emit('dateSelected', newValue);
}
},
selectedTimeSlot(newValue, oldValue) { selectedTimeSlot(newValue, oldValue) {
if (newValue !== oldValue) { if (newValue !== oldValue) {
const testObj = this.getSelectedTimeSlotInfoObject(newValue); const testObj = this.getSelectedTimeSlotInfoObject(newValue);
this.$emit('timeSlotSelected', testObj); this.$emit('timeSlotSelected', testObj);
} }
},
selectedDate(newValue, oldValue) {
if (newValue !== oldValue) {
this.$emit('dateSelected', newValue);
}
} }
}, },
methods: { methods: {
@ -340,50 +345,44 @@ export default {
addPremiumFlagToInput(routeCode) { addPremiumFlagToInput(routeCode) {
return (`${routeCode}${PREMIUM_TIME_SLOT_ID_FLAG}`); return (`${routeCode}${PREMIUM_TIME_SLOT_ID_FLAG}`);
}, },
removePremiumFlagFromInput(routeCode) { checkIsSelectedDay(index, timeOfDayGrouping) {
return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, ''); const dateToShowString = this.getDateToShowString(index);
return this.selectedDate === dateToShowString && this.selectedTimeOfDayGrouping === timeOfDayGrouping;
}, },
getSelectedTimeSlotInfoObject(timeSlot) { checkIsSelectedTimeSlot(timeSlot) {
const routeCode = timeSlot?.id; if (!this.selectedTimeSlot) {
let routeCodeToUse = routeCode; return false;
const routeCodeIncludesPremium = routeCode?.includes(PREMIUM_TIME_SLOT_ID_FLAG);
if (routeCodeIncludesPremium) {
routeCodeToUse = this.removePremiumFlagFromInput(routeCode);
} }
return (this.selectedTimeSlot.isDropoff === timeSlot.isDropoff
const fullTimeSlot = this.selectableTimeSlotsData?.find((ts) => ts.id === routeCodeToUse); && this.selectedTimeSlot.startTime === timeSlot.startTime
&& this.selectedTimeSlot.timeOfDay === timeSlot.timeOfDay);
if (fullTimeSlot) { },
return { displayDropOffInformation(timeSlot) {
timeSlot: { if (timeSlot.isDropoff) {
date: this.selectedDate, if (timeSlot.timeOfDay === 'morning') {
routeCode: fullTimeSlot.id, return this.getCmsContent(
startTime: fullTimeSlot.startTime, 'DropOffTimeSlotModal',
endTime: fullTimeSlot.endTime, 'BodyText'
jobMaxMinutes: this.appointmentDurationMinutesMaximum.toString(), );
jobMinMinutes: this.appointmentDurationMinutesMinimum.toString() }
}, return this.getCmsContent(
isPremiumAppointment: !!routeCodeIncludesPremium 'OvernightDropOffTimeSlotModal',
}; 'BodyText'
);
} }
return '';
return {
timeSlot: {
date: null,
startTime: null,
endTime: null,
routeCode: null,
jobMaxMinutes: null,
jobMinMinutes: null
},
isPremiumAppointment: null
};
}, },
displayTimeSlotTime(timeSlot) { displayTimeSlotTime(timeSlot) {
const appointmentType = this.activeAppointmentType || AppointmentTypeStrings.IN_SHOP; const appointmentType = this.activeAppointmentType || AppointmentTypeStrings.IN_SHOP;
if (appointmentType === AppointmentTypeStrings.MOBILE || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) { if (appointmentType === AppointmentTypeStrings.MOBILE || appointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) {
return `${militaryToTwelveHourTime(timeSlot.startTime)} - ${militaryToTwelveHourTime(timeSlot.endTime)}`; return `${militaryToTwelveHourTime(timeSlot.startTime)} - ${militaryToTwelveHourTime(timeSlot.endTime)}`;
} }
if (timeSlot.isDropoff) {
if (timeSlot.timeOfDay === 'morning') {
return '<span class="dropoff-label">Drop & Go<sup>&trade;</sup></span>';
}
return '<span class="dropoff-label">Overnight drop off</span>';
}
return militaryToTwelveHourTime(timeSlot.startTime); return militaryToTwelveHourTime(timeSlot.startTime);
}, },
findFirstAvailableDateInView() { findFirstAvailableDateInView() {
@ -436,8 +435,7 @@ export default {
if (!initialTimeSlot || !initialTimeSlot.startTime) { if (!initialTimeSlot || !initialTimeSlot.startTime) {
return; return;
} }
const initialStartTime = initialTimeSlot.startTime; const foundTimeSlot = timeSlots.find((ts) => ts.startTime === initialTimeSlot.startTime && ts.endTime === initialTimeSlot.endTime);
const foundTimeSlot = timeSlots.find((ts) => ts.startTime === initialStartTime);
if (foundTimeSlot) { if (foundTimeSlot) {
this.selectTimeSlotForDay(foundTimeSlot); this.selectTimeSlotForDay(foundTimeSlot);
} }
@ -452,6 +450,44 @@ export default {
const dateToShow = this.getDateToShow(index); const dateToShow = this.getDateToShow(index);
return convertDateToDateString(dateToShow); 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() { async gotoNextPage() {
const maxPageIndex = Math.floor((this.daysLoaded - 1) / this.daysToAdd); const maxPageIndex = Math.floor((this.daysLoaded - 1) / this.daysToAdd);
if (this.activePageIndex >= maxPageIndex) { if (this.activePageIndex >= maxPageIndex) {
@ -479,15 +515,15 @@ export default {
this.selectedTimeSlot = null; this.selectedTimeSlot = null;
this.findFirstAvailableDateInView(); this.findFirstAvailableDateInView();
}, },
checkIsSelectedDay(index, timeOfDayGrouping) { mapTimeSlot(timeSlot, timeOfDay) {
const dateToShowString = this.getDateToShowString(index); return {
return this.selectedDate === dateToShowString && this.selectedTimeOfDayGrouping === timeOfDayGrouping; ...timeSlot,
isDropoff: timeSlot.id.endsWith('DROP OFF'),
timeOfDay
};
}, },
checkIsSelectedTimeSlot(timeSlot) { removePremiumFlagFromInput(routeCode) {
if (!this.selectedTimeSlot) { return routeCode.replace(PREMIUM_TIME_SLOT_ID_FLAG, '');
return false;
}
return this.selectedTimeSlot.startTime === timeSlot.startTime;
}, },
selectTimeSlotForDay(timeSlot) { selectTimeSlotForDay(timeSlot) {
if (timeSlot) { if (timeSlot) {
@ -567,13 +603,28 @@ export default {
morningTimeSlots: selectableDate.timeSlots.filter((timeSlot) => { morningTimeSlots: selectableDate.timeSlots.filter((timeSlot) => {
const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10); const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10);
return timeHour < 12; return timeHour < 12;
}), }).map((timeSlot) => this.mapTimeSlot(timeSlot, 'morning')),
afternoonTimeSlots: selectableDate.timeSlots.filter((timeSlot) => { afternoonTimeSlots: selectableDate.timeSlots.filter((timeSlot) => {
const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10); const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10);
return timeHour >= 12; return timeHour >= 12;
}), }).map((timeSlot) => this.mapTimeSlot(timeSlot, 'afternoon')),
isSelected: false 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.selectableDatesData.push(dateObjectToPush);
}); });
@ -635,14 +686,28 @@ export default {
morningTimeSlots: selectableDate.timeSlots.filter((timeSlot) => { morningTimeSlots: selectableDate.timeSlots.filter((timeSlot) => {
const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10); const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10);
return timeHour < 12; return timeHour < 12;
}), }).map((timeSlot) => this.mapTimeSlot(timeSlot, 'morning')),
afternoonTimeSlots: selectableDate.timeSlots.filter((timeSlot) => { afternoonTimeSlots: selectableDate.timeSlots.filter((timeSlot) => {
const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10); const timeHour = parseInt(timeSlot.startTime.split(':')[0], 10);
return timeHour >= 12; return timeHour >= 12;
}), }).map((timeSlot) => this.mapTimeSlot(timeSlot, 'afternoon')),
isSelected: false 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.selectableDatesData.push(dateObjectToPush);
} }
}); });
@ -832,16 +897,55 @@ export default {
text-align: center; text-align: center;
cursor: pointer; 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 { &.selected-time-slot {
background-color: $background-color-selected; background-color: $background-color-selected;
border: solid 1px #0070d1; border: solid 1px #0070d1;
color:#000; color:#000;
font-weight: 500; 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 { &.has-date-error {
border: 1px solid #d93025; border: 1px solid #d93025;
} }
} }
} }

View file

@ -6,7 +6,6 @@
<div class="fade-on-route-transition"> <div class="fade-on-route-transition">
<div class="justify-content-center"> <div class="justify-content-center">
<siteHeader <siteHeader
class="header"
cmsWidgetName="SiteHeaderWidget" /> cmsWidgetName="SiteHeaderWidget" />
</div> </div>
<div class="iss-heritage-container-width"> <div class="iss-heritage-container-width">
@ -176,9 +175,7 @@ const getAvailableDates = async (
storeAction.payload.endDate, storeAction.payload.endDate,
storeAction.payload.shopAppointmentType, storeAction.payload.shopAppointmentType,
storeAction.payload.providerNumber storeAction.payload.providerNumber
).catch(() => { );
console.warn('Error fetching shop time slots...');
});
} else if (storeAction.payload?.zipCodeOverride) { } else if (storeAction.payload?.zipCodeOverride) {
timeSlotsResponse = await useMainStore().getMobileTimeSlots( timeSlotsResponse = await useMainStore().getMobileTimeSlots(
storeAction.payload.startDate, storeAction.payload.startDate,
@ -343,7 +340,7 @@ export default {
&& (((this.selectedAppointmentType === AppointmentTypeStrings.MOBILE && (((this.selectedAppointmentType === AppointmentTypeStrings.MOBILE
|| this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP) || this.selectedAppointmentType === AppointmentTypeStrings.MOBILE_NOT_ITAC_AND_NOT_NOCOMP)
&& serviceLocationToUse.mobileProviderNumber) && serviceLocationToUse.mobileProviderNumber)
|| (this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP || ((this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP || this.selectedAppointmentType === AppointmentTypeStrings.DROP_OFF)
&& (this.selectedProvider?.providerNumber || serviceLocationToUse.provider?.providerNumber))); && (this.selectedProvider?.providerNumber || serviceLocationToUse.provider?.providerNumber)));
const damageInfo = useMainStore().order.damage.isRepair const damageInfo = useMainStore().order.damage.isRepair
@ -652,8 +649,13 @@ export default {
return; return;
} }
// Save service location then schedule and navigate forward let appointmentTypeToUse = this.selectedAppointmentType;
await this.$refs.serviceLocation.forwardButtonAction(); if (appointmentTypeToUse === AppointmentTypeStrings.IN_SHOP && this.selectedTimeSlotInfo.timeSlot?.isDropoff === true) {
appointmentTypeToUse = AppointmentTypeStrings.DROP_OFF;
}
// Save service location then save schedule then navigate forward
await this.$refs.serviceLocation.forwardButtonAction(appointmentTypeToUse);
this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot); this.mainStore.saveSchedule(this.selectedTimeSlotInfo.timeSlot);
this.$router.navigate( this.$router.navigate(
this.navigationScenarios.CLICKED_FORWARD, this.navigationScenarios.CLICKED_FORWARD,

View file

@ -308,7 +308,7 @@ export default {
} }
}, },
selectedAppointmentType(newValue, oldValue) { selectedAppointmentType(newValue, oldValue) {
if (this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP && this.selectedProvider && this.availabilityRating === null) { if (this.isInshop && this.selectedProvider && this.availabilityRating === null) {
this.refreshAvailabilityRating(); this.refreshAvailabilityRating();
const appointmenTypeServiceLocationObj = { const appointmenTypeServiceLocationObj = {
@ -336,7 +336,7 @@ export default {
}, },
selectedProvider(newProvider, oldProvider) { selectedProvider(newProvider, oldProvider) {
if (newProvider?.providerNumber !== oldProvider?.providerNumber || this.availabilityRating === null) { if (newProvider?.providerNumber !== oldProvider?.providerNumber || this.availabilityRating === null) {
const appointmentIsInshop = this.selectedAppointmentType === AppointmentTypeStrings.IN_SHOP; const appointmentIsInshop = this.isInshop;
const returnedProvider = { const returnedProvider = {
provider: newProvider, provider: newProvider,
refreshDatePicker: appointmentIsInshop && oldProvider?.providerNumber !== null refreshDatePicker: appointmentIsInshop && oldProvider?.providerNumber !== null
@ -358,7 +358,7 @@ export default {
await this.setData(initialData); await this.setData(initialData);
this.initializingData = false; this.initializingData = false;
}, },
async forwardButtonAction() { async forwardButtonAction(appointmentType) {
let provider = this.selectedProvider; let provider = this.selectedProvider;
this.mainStore.updateMobileFee(null); this.mainStore.updateMobileFee(null);
if (this.isMobile) { if (this.isMobile) {
@ -382,7 +382,7 @@ export default {
state: this.state, state: this.state,
zipCode: this.zipCode, zipCode: this.zipCode,
zipCodeCtu: this.zipCodeCtu, zipCodeCtu: this.zipCodeCtu,
appointmentType: this.selectedAppointmentType, appointmentType: appointmentType || this.selectedAppointmentType,
provider provider
}); });
}, },

View file

@ -965,7 +965,8 @@ export const useMainStore = defineStore({
providerNumber, providerNumber,
startDate, startDate,
endDate, endDate,
shopAppointmentType, shopAppointmentType: (shopAppointmentType === AppointmentTypeStrings.IN_SHOP
|| shopAppointmentType === AppointmentTypeStrings.DROP_OFF) ? 'InShopOrDropoff' : shopAppointmentType,
applicationName: applicationConfig.APPLICATION_NAME, applicationName: applicationConfig.APPLICATION_NAME,
billToAccountNumber: this.billToAccountNumber, billToAccountNumber: this.billToAccountNumber,
parentAccountNumber: this.order.parentAccountNumber, // this.payment.parentAccountNumber, parentAccountNumber: this.order.parentAccountNumber, // this.payment.parentAccountNumber,
@ -1605,7 +1606,7 @@ export const useMainStore = defineStore({
order.customer.firstName = data?.customer?.firstName; order.customer.firstName = data?.customer?.firstName;
order.customer.lastName = data?.customer?.lastName; order.customer.lastName = data?.customer?.lastName;
order.customer.emailAddress = data?.customer?.emailAddress; order.customer.emailAddress = data?.customer?.emailAddress;
order.contactInfo.extension = data?.customer?.homePhone?.slice(10); order.contactInfo.extension = data?.customer?.homePhone?.slice(10);
order.contactInfo.homePhone = data?.customer?.homePhone?.slice(0, 10); order.contactInfo.homePhone = data?.customer?.homePhone?.slice(0, 10);
order.contactInfo.servicePhone = data?.customer?.servicePhone; order.contactInfo.servicePhone = data?.customer?.servicePhone;