+ :class="[
+ isMobileFirstModalOpen || isMultiLocationModalOpen ? 'hidden-background' : '',
+ ]">
{
- vm.showMobileFirstModal();
+ // vm.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
+ vm.showMultiLocationModal();
vm.hideLoadingModal();
vm.calendarLoadingStatus = "none";
if (!vm.preSelectedDate) vm.setSelectedDateToFirstAvailable();
@@ -677,8 +696,7 @@ export default {
isMobileStaticRecalibrationApplicable() {
return (
this.displayMSR &&
- this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE &&
- (this.isCashItacNoComp || this.mobileFeePart?.isInsurable)
+ this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE
);
},
displayMSR() {
@@ -688,6 +706,28 @@ export default {
?.toLowerCase() === "true"
);
},
+ isMSRFeeNotCoveredByInsurance() {
+ return (
+ this.isMobileSelected &&
+ this.isMobileStaticRecalibrationApplicable &&
+ this.mobileFeeHasPrice &&
+ this.isInsurance &&
+ !this.mobileFeePart?.isInsurable
+ );
+ },
+ MSRFeeAlertHeadlineText() {
+ return this.getCmsContent("AlertMSRNotCoveredByInsuranceWidget", "HeadlineText");
+ },
+ MSRFeeAlertBodyText() {
+ const cmsContentText = this.getCmsContent(
+ "AlertMSRNotCoveredByInsuranceWidget",
+ "BodyText"
+ );
+ return cmsContentText.replaceAll(
+ "{custom:mobileFee}",
+ formatToUSDollar(this.mobileFee)
+ );
+ },
isCashItacNoComp() {
return !this.isInsurance || this.isITAC || this.isNoComp;
},
@@ -900,6 +940,11 @@ export default {
? this.$refs.mobileFirstModal?.getIsModalOpen()
: false;
},
+ isMultiLocationModalOpen() {
+ return this.selectableDatesMobile.days && this.selectableDatesInshop.days
+ ? this.$refs.multiLocationModal?.getIsModalOpen()
+ : false;
+ },
displayWaitList() {
if (!this.appointmentType) return false;
const firstMobileDateString =
@@ -954,6 +999,18 @@ export default {
return this.selectableDatesInshop?.days?.length > 0;
}
},
+ isShowMobileFirstAppt() {
+ return experimentMixin.methods.hasSettingEqualTo(
+ experimentSettings.SHOW_MOBILE_FIRST_APPT,
+ "true"
+ );
+ },
+ isShowMultiLocationPopup() {
+ return experimentMixin.methods.hasSettingEqualTo(
+ experimentSettings.SHOW_MULTI_LOCATION_APPT,
+ "true"
+ );
+ },
},
methods: {
splitCopyOnCMSPlaceHolder,
@@ -1239,7 +1296,8 @@ export default {
}
}
this.initializeDatePicker().then(() => {
- this.showMobileFirstModal();
+ // this.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
+ this.showMultiLocationModal();
this.hideLoadingModal();
this.calendarLoadingStatus = "none";
this.setSelectedDateToFirstAvailable();
@@ -1273,7 +1331,7 @@ export default {
},
async initializeDatePicker() {
- this.isShowMobileFirstAppt && this.showLoadingModal();
+ this.isShowMultiLocationPopup && this.showLoadingModal();
const includeMobileTimeSlots = this.isServiceableMobile;
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
@@ -1785,7 +1843,8 @@ export default {
this.zipCodeCtu = newZipCode.zipCodeCtu;
this.selectedDate = null;
this.initializeDatePicker().then(() => {
- this.showMobileFirstModal();
+ // this.showMobileFirstModal(); // KEEP IN CASE WE WANT TO REINSTATE MOBILE FIRST
+ this.showMultiLocationModal();
this.hideLoadingModal();
this.calendarLoadingStatus = "none";
this.setSelectedDateToFirstAvailable();
@@ -1900,10 +1959,10 @@ export default {
const order = this.$store.getters.order;
const isRepair = order.damage.isRepair;
- let preSelectedMobileAppointment = null;
+ let promotedMobileAppointment = null;
const todaysDate = getTodayDate();
- let firstMobileAMAppt = await this.getFirstAvailableMobileApptByTOD("AM");
- let firstMobilePMAppt = await this.getFirstAvailableMobileApptByTOD("PM");
+ let firstMobileAMAppt = await this.getFirstAvailableApptByTOD("AM", "mobile");
+ let firstMobilePMAppt = await this.getFirstAvailableApptByTOD("PM", "mobile");
if (!firstMobileAMAppt && !firstMobilePMAppt) {
return;
}
@@ -1998,7 +2057,7 @@ export default {
if (this.isShowMobileFirstAppt) {
if (pmMobileDays == 0) {
// Selects the first available time slot
- preSelectedMobileAppointment =
+ promotedMobileAppointment =
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
? firstMobileAMAppt
: firstMobilePMAppt;
@@ -2007,20 +2066,20 @@ export default {
numberOfDaysToFirstMobilePMDate <= pmMobileDays
) {
// Selects the first available PM time slot
- preSelectedMobileAppointment = firstMobilePMAppt;
+ promotedMobileAppointment = firstMobilePMAppt;
} else if (
firstMobilePMDate &&
numberOfDaysToFirstMobilePMDate >= noPMMobileDays
) {
// Selects the first available AM time slot. If none, selects the first available PM time slot
- preSelectedMobileAppointment = firstMobileAMAppt
+ promotedMobileAppointment = firstMobileAMAppt
? firstMobileAMAppt
: firstMobilePMAppt;
}
- if (preSelectedMobileAppointment) {
+ if (promotedMobileAppointment) {
this.$refs.mobileFirstModal.setSelectedAppointment(
- preSelectedMobileAppointment
+ promotedMobileAppointment
);
this.$refs.mobileFirstModal.openModal();
}
@@ -2028,11 +2087,170 @@ export default {
}
}
},
- async getFirstAvailableMobileApptByTOD(timeOfDay) {
- if (!this.selectableDatesMobile?.days?.length) {
+ async showMultiLocationModal() {
+ if (!this.selectedRouteCodeData?.routeCode) {
+ let maxDayRangeToShowPmTimeslot = experimentMixin.methods.getSettingValue(
+ experimentSettings.SHOW_PM_DAYS_MULTI_LOCATION
+ );
+ let maxDayRangeToShowNoPmTimeslot = experimentMixin.methods.getSettingValue(
+ experimentSettings.SHOW_NO_PM_DAYS_MULTI_LOCATION
+ );
+ let maxDayRangeToShowAnyTimeslot = experimentMixin.methods.getSettingValue(
+ experimentSettings.SHOW_NO_AVAILABILE_DAYS_MULTI_LOCATION
+ );
+ let showMultiLocationAppointment = experimentMixin.methods.getSettingValue(
+ experimentSettings.SHOW_MULTI_LOCATION_APPT
+ );
+
+ // temp vars to use for testing; TO BE REMOVED
+ maxDayRangeToShowPmTimeslot = 2;
+ maxDayRangeToShowNoPmTimeslot = 2;
+ maxDayRangeToShowAnyTimeslot = 5;
+
+ let promotedMobileAppointment = null;
+ let promotedInshopAppointment = null;
+ const todaysDate = getTodayDate();
+
+ // Helper function to calculate days until appointment (handles null/undefined)
+ const calculateDaysUntilDate = (appointmentObj) => {
+ const dateString = appointmentObj?.date;
+ return dateString
+ ? (new Date(dateString) - todaysDate) / (1000 * 60 * 60 * 24)
+ : null;
+ };
+
+ // Fetch all appointments
+ const [firstMobileAMAppt, firstMobilePMAppt, firstInshopAMAppt, firstInshopPMAppt] =
+ await Promise.all([
+ this.getFirstAvailableApptByTOD("AM", "mobile"),
+ this.getFirstAvailableApptByTOD("PM", "mobile"),
+ this.getFirstAvailableApptByTOD("AM", "inshop"),
+ this.getFirstAvailableApptByTOD("PM", "inshop"),
+ ]);
+
+ // Extract dates
+ const firstMobileAMDate = firstMobileAMAppt?.date;
+ const firstMobilePMDate = firstMobilePMAppt?.date;
+ const firstInshopAMDate = firstInshopAMAppt?.date;
+ const firstInshopPMDate = firstInshopPMAppt?.date;
+
+ // Calculate days until appointments
+ const numberOfDaysToFirstMobileAMDate = calculateDaysUntilDate(firstMobileAMAppt);
+ const numberOfDaysToFirstMobilePMDate = calculateDaysUntilDate(firstMobilePMAppt);
+ const numberOfDaysToFirstInshopAMDate = calculateDaysUntilDate(firstInshopAMAppt);
+ const numberOfDaysToFirstInshopPMDate = calculateDaysUntilDate(firstInshopPMAppt);
+
+ const shouldExposeMultiLocationModal = () => {
+ const isMobileAppointmentInDateRange =
+ (firstMobileAMDate &&
+ numberOfDaysToFirstMobileAMDate <= maxDayRangeToShowAnyTimeslot) ||
+ (firstMobilePMDate &&
+ numberOfDaysToFirstMobilePMDate <= maxDayRangeToShowAnyTimeslot);
+ const isInshopAppointmentInDateRange =
+ (firstInshopAMDate &&
+ numberOfDaysToFirstInshopAMDate <= maxDayRangeToShowAnyTimeslot) ||
+ (firstInshopPMDate &&
+ numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowAnyTimeslot);
+
+ if (showMultiLocationAppointment?.toLowerCase() === "true") {
+ return isMobileAppointmentInDateRange && isInshopAppointmentInDateRange;
+ }
+ return false;
+ };
+
+ if (shouldExposeMultiLocationModal()) {
+ const multiLocationPopupExperiment =
+ store.getters.applicationUser.experiments.find(
+ (e) => e.universeName === experimentUniverses.MULTI_LOCATION_POPUP
+ );
+ const hasExposedMultiLocationPopup = multiLocationPopupExperiment?.isExposed;
+
+ if (!hasExposedMultiLocationPopup && multiLocationPopupExperiment) {
+ baseMixin.methods.dispatchStoreActionWithLogging(
+ storeActions.LOG_EXPERIMENT_EXPOSURE_AND_UPDATE_STORE,
+ {
+ userId: getUserIdValue(),
+ deviceId: getDeviceIdValue(),
+ sessionKey: getSessionKeyValue(),
+ pageName: "schedule",
+ experiment: multiLocationPopupExperiment,
+ },
+ "schedule",
+ false
+ );
+ }
+ if (this.isShowMultiLocationPopup) {
+ if (maxDayRangeToShowPmTimeslot == 0) {
+ // Selects the first available mobile time slot
+ promotedMobileAppointment =
+ numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
+ ? firstMobileAMAppt
+ : firstMobilePMAppt;
+ // Selects the first available inshop time slot
+ promotedInshopAppointment =
+ numberOfDaysToFirstInshopAMDate <= numberOfDaysToFirstInshopPMDate
+ ? firstInshopAMAppt
+ : firstInshopPMAppt;
+ } else {
+ if (
+ firstMobilePMDate &&
+ numberOfDaysToFirstMobilePMDate <= maxDayRangeToShowPmTimeslot
+ ) {
+ // Selects the first available PM time slot
+ promotedMobileAppointment = firstMobilePMAppt;
+ } else if (
+ firstMobilePMDate &&
+ numberOfDaysToFirstMobilePMDate >= maxDayRangeToShowNoPmTimeslot
+ ) {
+ // Selects the first available AM time slot. If none, selects the first available PM time slot
+ promotedMobileAppointment = firstMobileAMAppt
+ ? firstMobileAMAppt
+ : firstMobilePMAppt;
+ }
+
+ if (
+ firstInshopPMDate &&
+ numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowPmTimeslot
+ ) {
+ // Selects the first available PM time slot
+ promotedInshopAppointment = firstInshopPMAppt;
+ } else if (
+ firstInshopPMDate &&
+ numberOfDaysToFirstInshopPMDate >= maxDayRangeToShowNoPmTimeslot
+ ) {
+ // Selects the first available AM time slot. If none, selects the first available PM time slot
+ promotedInshopAppointment = firstInshopAMAppt
+ ? firstInshopAMAppt
+ : firstInshopPMAppt;
+ }
+ }
+
+ if (promotedMobileAppointment && promotedInshopAppointment) {
+ this.$refs.multiLocationModal.setPromotedMobileAppointment(
+ promotedMobileAppointment
+ );
+ this.$refs.multiLocationModal.setPromotedInshopAppointment(
+ promotedInshopAppointment
+ );
+ this.$refs.multiLocationModal.openModal();
+ }
+ }
+ }
+ }
+ },
+ async getFirstAvailableApptByTOD(timeOfDay, appointmentType) {
+ let selectableDays;
+ if (appointmentType?.toLowerCase() == "mobile") {
+ selectableDays = this.selectableDatesMobile?.days;
+ } else if (appointmentType?.toLowerCase() == "inshop") {
+ selectableDays = this.selectableDatesInshop?.days;
+ } else {
+ return;
+ }
+ if (!selectableDays?.length) {
return null;
} else {
- for (const dateObj of this.selectableDatesMobile.days) {
+ for (const dateObj of selectableDays) {
let matchingTimeSlot = {
estimatedServiceMinutes: {
minimum: this.estimatedServiceMinutesMinimum,
@@ -2040,6 +2258,7 @@ export default {
},
timeSlot: null,
date: null,
+ addressCopy: null,
};
const isMatchingApptDay = dateObj.timeSlots.some(
(slot) =>
@@ -2047,6 +2266,16 @@ export default {
(matchingTimeSlot.timeSlot = slot) &&
(matchingTimeSlot.date = dateObj.date)
);
+ if (
+ appointmentType?.toLowerCase() == "inshop" &&
+ this.selectedShopAnswer?.address1 &&
+ this.selectedShopAnswer?.address2
+ ) {
+ matchingTimeSlot.addressCopy =
+ this.selectedShopAnswer?.address1 +
+ ", " +
+ this.selectedShopAnswer?.address2;
+ }
if (isMatchingApptDay && matchingTimeSlot) {
return matchingTimeSlot;
}
@@ -2054,7 +2283,7 @@ export default {
return null;
}
},
- updateTimeSlotandNavigateForward(timeSlotObj) {
+ updateMobileFirstTimeSlotandNavigateForward(timeSlotObj) {
this.selectedMobileFirstAppointment = true;
this.appointmentType = AppointmentTypeStrings.MOBILE;
this.selectedDate = timeSlotObj.date;
@@ -2062,6 +2291,18 @@ export default {
this.updateTimeSlot(timeSlotObj);
this.forwardButtonAction();
},
+ updateMultiLocationTimeSlotandNavigateForward({ selectedTimeSlot, appointmentType }) {
+ if (appointmentType?.toLowerCase() == "mobile") {
+ this.appointmentType = AppointmentTypeStrings.MOBILE;
+ } else if (appointmentType?.toLowerCase() == "inshop") {
+ this.appointmentType = AppointmentTypeStrings.IN_SHOP;
+ }
+ this.selectedMultiLocationAppointment = true;
+ this.selectedDate = selectedTimeSlot.date;
+ this.updateSelectedProvider();
+ this.updateTimeSlot(selectedTimeSlot);
+ this.forwardButtonAction();
+ },
updateRecalAcknowledgedAndNavigateForward(isAcknowledged) {
this.isRecalAcknowledgedForScheduling = isAcknowledged;
this.forwardButtonAction();
@@ -2085,6 +2326,7 @@ export default {
textBlock,
timeSlotQuestion,
mobileFirstModal,
+ multiLocationModal,
recalAckModal,
alert,
serviceZipModalQuestion,
diff --git a/src/ux-components/alert/alert.vue b/src/ux-components/alert/alert.vue
index 5c78853d4..deb7bca8b 100644
--- a/src/ux-components/alert/alert.vue
+++ b/src/ux-components/alert/alert.vue
@@ -7,9 +7,15 @@
isDismissible ? 'alert-dismissible' : '',
this.alertClass,
this.cssClassNameForCmsWidget,
+ this.hasBorder ? 'bordered' : '',
]">
+
{{ alertHeadline }}
@@ -63,7 +69,9 @@ export default {
name: "alert",
props: {
showInfoIcon: Boolean,
+ showWarningIcon: Boolean,
showHorizontalRow: Boolean,
+ hasBorder: Boolean,
isDismissible: Boolean,
alertClass: String,
/*
@@ -96,6 +104,9 @@ export default {
infoIcon() {
return require(`@/assets/img/icons/info-circle-blue.svg`);
},
+ warningIcon() {
+ return require(`@/assets/img/icons/alert-circle-yellow.svg`);
+ },
pageQueryString() {
return applicationConfig.PAGE_QUERYSTRING;
},
@@ -211,12 +222,19 @@ export default {
background-color: $yellow-100;
.alert-heading {
color: $yellow-600;
+
+ .warning-icon {
+ height: 1rem;
+ }
}
svg {
fill: $yellow-600;
width: 1rem;
height: 1rem;
}
+ hr {
+ border-color: $yellow-800;
+ }
}
&.alert-success {
background-color: $green-100;
@@ -229,6 +247,9 @@ export default {
height: 1rem;
}
}
+ &.bordered {
+ border: 1px solid;
+ }
& p {
font-size: 0.875rem;
}