+ :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();
@@ -690,7 +683,8 @@ export default {
isMobileStaticRecalibrationApplicable() {
return (
this.displayMSR &&
- this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE
+ this.mobileFeePart?.partNumber == partNumberStrings.MOBILE_STATIC_RECAL_FEE &&
+ (this.isCashItacNoComp || this.mobileFeePart?.isInsurable)
);
},
displayMSR() {
@@ -700,28 +694,6 @@ 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;
},
@@ -934,6 +906,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 =
@@ -988,6 +965,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,
@@ -1273,7 +1262,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();
@@ -1307,7 +1297,7 @@ export default {
},
async initializeDatePicker() {
- this.isShowMobileFirstAppt && this.showLoadingModal();
+ this.isShowMultiLocationPopup && this.showLoadingModal();
const includeMobileTimeSlots = this.isServiceableMobile;
const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff;
@@ -1819,7 +1809,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();
@@ -1934,10 +1925,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;
}
@@ -2032,7 +2023,7 @@ export default {
if (this.isShowMobileFirstAppt) {
if (pmMobileDays == 0) {
// Selects the first available time slot
- preSelectedMobileAppointment =
+ promotedMobileAppointment =
numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate
? firstMobileAMAppt
: firstMobilePMAppt;
@@ -2041,20 +2032,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();
}
@@ -2062,11 +2053,176 @@ 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
+ );
+ let experimentSettingsString =
+ "ShowPm:" +
+ maxDayRangeToShowPmTimeslot +
+ "|" +
+ "NoPm:" +
+ maxDayRangeToShowNoPmTimeslot +
+ "|" +
+ "NoAvail:" +
+ maxDayRangeToShowAnyTimeslot;
+ 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();
+ this.$refs.multiLocationModal.captureExperimentSettings(
+ experimentSettingsString
+ );
+ }
+ }
+ }
+ }
+ },
+ 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,
@@ -2074,6 +2230,7 @@ export default {
},
timeSlot: null,
date: null,
+ addressCopy: null,
};
const isMatchingApptDay = dateObj.timeSlots.some(
(slot) =>
@@ -2081,6 +2238,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;
}
@@ -2088,7 +2255,7 @@ export default {
return null;
}
},
- updateTimeSlotandNavigateForward(timeSlotObj) {
+ updateMobileFirstTimeSlotandNavigateForward(timeSlotObj) {
this.selectedMobileFirstAppointment = true;
this.appointmentType = AppointmentTypeStrings.MOBILE;
this.selectedDate = timeSlotObj.date;
@@ -2096,6 +2263,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();
@@ -2119,6 +2298,7 @@ export default {
textBlock,
timeSlotQuestion,
mobileFirstModal,
+ multiLocationModal,
recalAckModal,
alert,
serviceZipModalQuestion,
diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js
index 813a3189a..4ca139275 100644
--- a/src/mixins/base-mixin.js
+++ b/src/mixins/base-mixin.js
@@ -209,6 +209,15 @@ export default {
userAgent.includes("webOS")
);
},
+ isAppleBrowser() {
+ const userAgent = navigator.userAgent;
+ return (
+ userAgent.includes("iPod") ||
+ userAgent.includes("iPad") ||
+ userAgent.includes("iPhone") ||
+ userAgent.includes("Mac")
+ );
+ },
getSubmittedOrder() {
return JSON.parse(
window.sessionStorage.getItem(sessionStorageKeyConstants.SUBMITTED_STATE)
diff --git a/src/router/methods/route-logic/vehicle.js b/src/router/methods/route-logic/vehicle.js
index 39fa107ab..e7ff41fc0 100644
--- a/src/router/methods/route-logic/vehicle.js
+++ b/src/router/methods/route-logic/vehicle.js
@@ -3,6 +3,7 @@ import { getBoolFromString } from "@/helpers/boolean-helper";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import { routeData } from "@/router/constants/routes";
import { isVerifiedInsurance } from "@/router/methods/helpers/requires-verified-redirect";
+import { hasVehicleInfo } from "@/helpers/vehicle-helper";
import store from "@/store";
import { queryStrings } from "@/constants/query-strings";
import baseMixin from "@/mixins/base-mixin";
@@ -33,7 +34,7 @@ export async function vehicleBeforeEnter(to, from) {
}
}
- if (isVerifiedInsurance()) {
+ if (isVerifiedInsurance() && hasVehicleInfo()) {
return {
name: routeData.VEHICLE_DAMAGE.name,
replace: true,
diff --git a/src/store/index.js b/src/store/index.js
index cd349e351..e91ec6c0b 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -3115,6 +3115,8 @@ export const actions = {
serviceLocationState,
serviceLocationZipCode,
pricedLineItems,
+ serviceLocationAddressLine1,
+ serviceLocationAddressLine2,
},
pageNameToLog,
}
@@ -3148,6 +3150,8 @@ export const actions = {
appointmentType: appointmentType,
pricedLineItems: lineItemsWithOnlyPriceInfo,
serviceLocation: {
+ addressLine1: appointmentType == "Mobile" ? serviceLocationAddressLine1 : null,
+ addressLine2: appointmentType == "Mobile" ? serviceLocationAddressLine2 : null,
city: appointmentType == "Mobile" ? serviceLocationCity : null,
state: appointmentType == "Mobile" ? serviceLocationState : null,
zipCode: appointmentType == "Mobile" ? serviceLocationZipCode : null,
diff --git a/src/styles/mixins/customMixins.scss b/src/styles/mixins/customMixins.scss
index 62abcc82d..4ee56d5f1 100644
--- a/src/styles/mixins/customMixins.scss
+++ b/src/styles/mixins/customMixins.scss
@@ -20,3 +20,9 @@
}
// Usage: @include responsive-font-size-md(mobile-font-size-in-rem, desktop-font-size-in-rem);
// Example: @include responsive-font-size-md(0.875rem, 1rem);
+
+// Filter to use on SVG elements that are background
+@mixin blue-filter {
+ filter: brightness(0) saturate(100%) invert(31%) sepia(68%) saturate(1676%) hue-rotate(186deg)
+ brightness(93%) contrast(110%);
+}
diff --git a/src/styles/ux-variables.scss b/src/styles/ux-variables.scss
index 76035c6ca..9ad9381a7 100644
--- a/src/styles/ux-variables.scss
+++ b/src/styles/ux-variables.scss
@@ -127,6 +127,12 @@ $body-color: $gray-600;
$font-family-sans-serif: UrbanistRegular, Arial, Helvetica, sans-serif;
$font-family-monospace:
UrbanistRegular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+$font-family-sans-serif-semibold: UrbanistSemibold, Arial, Helvetica, sans-serif;
+$font-family-sans-serif-bold: UrbanistBold, Arial, Helvetica, sans-serif;
+$font-family-sans-serif-extrabold: UrbanistExtraBold, Arial, Helvetica, sans-serif;
+$font-family-sans-serif-medium-italic: UrbanistMedium_Italic, Arial, Helvetica, sans-serif;
+$font-family-sans-serif-bold-italic: UrbanistBold_Italic, Arial, Helvetica, sans-serif;
+
// stylelint-enable value-keyword-case
$font-family-base: $font-family-sans-serif;
$font-family-code: $font-family-monospace;
diff --git a/src/ux-components/alert/alert.vue b/src/ux-components/alert/alert.vue
index deb7bca8b..5c78853d4 100644
--- a/src/ux-components/alert/alert.vue
+++ b/src/ux-components/alert/alert.vue
@@ -7,15 +7,9 @@
isDismissible ? 'alert-dismissible' : '',
this.alertClass,
this.cssClassNameForCmsWidget,
- this.hasBorder ? 'bordered' : '',
]">
-
{{ alertHeadline }}
@@ -69,9 +63,7 @@ export default {
name: "alert",
props: {
showInfoIcon: Boolean,
- showWarningIcon: Boolean,
showHorizontalRow: Boolean,
- hasBorder: Boolean,
isDismissible: Boolean,
alertClass: String,
/*
@@ -104,9 +96,6 @@ 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;
},
@@ -222,19 +211,12 @@ 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;
@@ -247,9 +229,6 @@ export default {
height: 1rem;
}
}
- &.bordered {
- border: 1px solid;
- }
& p {
font-size: 0.875rem;
}