From 7af19bba2acc5224691772b5efe36b72d9942ba0 Mon Sep 17 00:00:00 2001 From: Minojhini Valaiyapathi Date: Tue, 3 Mar 2026 12:20:20 -0500 Subject: [PATCH 01/16] CASH-2264 - Checking if we have vehicle before routing to Vehicle Damage --- src/helpers/vehicle-helper.js | 11 +++++++++++ src/router/methods/route-logic/vehicle.js | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 src/helpers/vehicle-helper.js diff --git a/src/helpers/vehicle-helper.js b/src/helpers/vehicle-helper.js new file mode 100644 index 000000000..cd94d7bd5 --- /dev/null +++ b/src/helpers/vehicle-helper.js @@ -0,0 +1,11 @@ +import store from "@/store"; + +export function hasVehicleInfo() { + return ( + store.getters.order?.vehicle.year && + store.getters.order?.vehicle?.make && + store.getters.order?.vehicle?.model && + store.getters.order?.vehicle?.style && + store.getters.order?.vehicle?.carId + ); +} 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, From 664a7d289d7d7e612dc9eb21c47187496066e9a4 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 4 Mar 2026 07:59:51 -0500 Subject: [PATCH 02/16] CASH-2378: add experiment constants --- src/constants/experiments.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/constants/experiments.js b/src/constants/experiments.js index c76b1ea1c..f8691fc46 100644 --- a/src/constants/experiments.js +++ b/src/constants/experiments.js @@ -5,6 +5,7 @@ const experimentUniverses = { IGQ_SkipQuote: "NextGen_IGQSkipToInsurance", AFTERPAY_BREAKOUT_DISPLAY: "AfterpayBreakoutDisplay", MOBILE_FIRST_APPOINTMENT: "MobileFirstAppointment", + MULTI_LOCATION_POPUP: "MultiLocationPopup", }; const experimentSettings = { @@ -40,6 +41,10 @@ const experimentSettings = { SHOW_MOBILE_FIRST_POPUP_REPLACE_WITHOUT_MSR: "Show_Mobile_First_Popup_Replace_Without_MSR", SHOW_MOBILE_FIRST_POPUP_REPLACE_WITH_MSR: "Show_Mobile_First_Popup_Replace_With_MSR", OFFER_OEM: "OfferOem", + SHOW_MULTI_LOCATION_APPT: "ShowMultiLocationAppt", + SHOW_NO_AVAILABILE_DAYS_MULTI_LOCATION: "Show_NoAvailableDaysMultiLocation", + SHOW_NO_PM_DAYS_MULTI_LOCATION: "Show_NoPMDaysMultiLocation", + SHOW_PM_DAYS_MULTI_LOCATION: "Show_PMDaysMultiLocation", USE_ADYEN_PAYMENT: "UseAdyenPayment", SHOW_UNVERIFIED_DEDUCT_ENTRY: "ShowUnverifiedDeducEntry", SERVICE_PACKAGE_ORDER: "ServicePackageOrder", From 0c32eecdccf47ae54fd6852c0430a9d456cf8e69 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 4 Mar 2026 08:00:39 -0500 Subject: [PATCH 03/16] CASH-2378: create new radio button for multi location modal usage --- .../button-question/button-question.vue | 3 + .../multi-location-radio.spec.js | 121 +++++++ .../multi-location-radio.vue | 309 ++++++++++++++++++ 3 files changed, 433 insertions(+) create mode 100644 src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.spec.js create mode 100644 src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.vue diff --git a/src/digital-components/button-question/button-question.vue b/src/digital-components/button-question/button-question.vue index 9862633ad..417c88809 100644 --- a/src/digital-components/button-question/button-question.vue +++ b/src/digital-components/button-question/button-question.vue @@ -226,6 +226,7 @@ export default { let classes; switch (this.buttonTypeString) { case "timeSlotModalListButton": + case "multiLocationRadioButton": case "listButton": classes = "w-100"; break; @@ -265,6 +266,8 @@ export default { classes += " radio-button-container"; } else if (this.buttonTypeString == "servicePackageRadio") { classes = "package-wrapper col"; + } else if (this.buttonTypeString == "multiLocationRadioButton") { + classes = "multi-location-wrapper col"; } if (this.buttonTypeString == "listCard" && this.buttonsInfo.length > 2) { diff --git a/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.spec.js b/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.spec.js new file mode 100644 index 000000000..f305d4e2e --- /dev/null +++ b/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.spec.js @@ -0,0 +1,121 @@ +import { mount } from "@vue/test-utils"; +import multiLocationRadio from "./multi-location-radio"; +import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin"; + +describe("multi-location-radio.vue", () => { + it("Should include buttonLabel in html", async () => { + // Arrange + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: mockProps, + }, + }); + + // Act + const outputHtml = wrapper.html(); + + // Assert + expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabel"])); + }); + + it("Should include buttonLabelSubCopy in html", async () => { + // Arrange + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: mockProps, + }, + }); + + // Act + const outputHtml = wrapper.html(); + + // Assert + expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelSubCopy"])); + }); + it("Should include buttonFooterCopy in html", async () => { + // Arrange + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: mockProps, + }, + }); + + // Act + const outputHtml = wrapper.html(); + + // Assert + expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonFooterCopy"])); + }); + it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when provided with a
  • ...
  • (x5)
buttonBodyCopy", async () => { + // Arrange + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: mockProps, + }, + }); + // Act + const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy); + + // Assert + expect(results.length).toBe(5); + }); + it("Should get strings from getArrayOfListItemsFromRawCmsCopy without
    or
  • tags when provided with a
    • ...
    buttonBodyCopy", async () => { + // Arrange + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: mockProps, + }, + }); + + // Act + const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy); + + // Assert + const fileredResults = results.filter((result) => { + return ( + result.includes("
      ") || + result.includes("
    ") || + result.includes("
  • ") || + result.includes("
  • ") + ); + }); + expect(fileredResults.length).toBe(0); + }); + it("Should get 5 strings from getArrayOfListItemsFromRawCmsCopy when buttonBodyCopy has 6 total
  • ...
  • , but one is empty ", async () => { + // Arrange + const moddedProps = mockProps; + moddedProps["buttonBodyCopy"] = + "
    • buttonBodyCopy test copy
    • 2
    • 3
    • 4
    • 5
    "; + let { wrapper } = setupMocks({ + mountOptionsMockData: { + propsData: moddedProps, + }, + }); + + // Act + const results = wrapper.vm.getArrayOfListItemsFromRawCmsCopy(wrapper.vm.buttonBodyCopy); + + // Assert + expect(results.length).toBe(5); + }); +}); + +const mockProps = { + groupName: "group-name", + buttonLabel: "buttonLabel test copy", + buttonLabelAuxillaryCopy: "buttonLabelAuxillaryCopy test copy", + buttonLabelSubCopy: "buttonLabelSubCopy test copy", + buttonBodyCopy: + "
    • buttonBodyCopy test copy
    • 2
    • 3
    • 4
    • 5
    ", + buttonFooterCopy: "buttonFooterCopy test copy", + buttonAuxillaryCopy: "buttonAuxillaryCopy test copy", +}; + +function setupMocks({ mountOptionsMockData = {} }) { + const wrapper = mount(multiLocationRadio, { + ...mountOptionsMockData, + mixins: [inputButtonWrapperMixin], + }); + + return { wrapper }; +} diff --git a/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.vue b/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.vue new file mode 100644 index 000000000..64c8e3ce2 --- /dev/null +++ b/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.vue @@ -0,0 +1,309 @@ + + + + + From b3fb548813eddfdebc96e1569f033ddc71fd3c35 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 4 Mar 2026 08:01:13 -0500 Subject: [PATCH 04/16] CASH-2378: new component for multi location modal --- .../multi-location-modal.vue | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 src/layouts/schedule/multi-location-modal/multi-location-modal.vue diff --git a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue new file mode 100644 index 000000000..78dbe6ba5 --- /dev/null +++ b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue @@ -0,0 +1,317 @@ + + + + From e5aa47673b3f55df137291819e4d9b3bd8fcc7c7 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 4 Mar 2026 08:05:52 -0500 Subject: [PATCH 05/16] CASH-2378: updates to mobile first modal (renaming, change in helper method) --- src/layouts/schedule/schedule.vue | 54 +++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 07330b5d6..e1dd86e06 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -5,7 +5,7 @@ + @confirm-appointment="updateMobileFirstTimeSlotandNavigateForward" /> 0; } }, + isShowMobileFirstAppt() { + return experimentMixin.methods.hasSettingEqualTo( + experimentSettings.SHOW_MOBILE_FIRST_APPT, + "true" + ); + }, }, methods: { splitCopyOnCMSPlaceHolder, @@ -1934,10 +1936,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 +2034,7 @@ export default { if (this.isShowMobileFirstAppt) { if (pmMobileDays == 0) { // Selects the first available time slot - preSelectedMobileAppointment = + promotedMobileAppointment = numberOfDaysToFirstMobileAMDate <= numberOfDaysToFirstMobilePMDate ? firstMobileAMAppt : firstMobilePMAppt; @@ -2041,20 +2043,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 +2064,19 @@ export default { } } }, - async getFirstAvailableMobileApptByTOD(timeOfDay) { - if (!this.selectableDatesMobile?.days?.length) { + 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, @@ -2081,6 +2091,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 +2108,7 @@ export default { return null; } }, - updateTimeSlotandNavigateForward(timeSlotObj) { + updateMobileFirstTimeSlotandNavigateForward(timeSlotObj) { this.selectedMobileFirstAppointment = true; this.appointmentType = AppointmentTypeStrings.MOBILE; this.selectedDate = timeSlotObj.date; From 364b5d29bf961af42e4371632941064a2591ba97 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 4 Mar 2026 08:06:27 -0500 Subject: [PATCH 06/16] CASH-2378: implement new multi location modal --- src/layouts/schedule/schedule.vue | 198 +++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 5 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index e1dd86e06..dbdaaa90f 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -6,6 +6,11 @@ modalWidgetName="MobileFirstModalWidget" ref="mobileFirstModal" @confirm-appointment="updateMobileFirstTimeSlotandNavigateForward" /> +
    + :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(); @@ -930,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 = @@ -990,6 +1005,12 @@ export default { "true" ); }, + isShowMultiLocationPopup() { + return experimentMixin.methods.hasSettingEqualTo( + experimentSettings.SHOW_MULTI_LOCATION_APPT, + "true" + ); + }, }, methods: { splitCopyOnCMSPlaceHolder, @@ -1275,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(); @@ -1309,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; @@ -1821,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(); @@ -2064,6 +2087,157 @@ export default { } } }, + 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) { + 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") { @@ -2084,6 +2258,7 @@ export default { }, timeSlot: null, date: null, + addressCopy: null, }; const isMatchingApptDay = dateObj.timeSlots.some( (slot) => @@ -2116,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(); @@ -2139,6 +2326,7 @@ export default { textBlock, timeSlotQuestion, mobileFirstModal, + multiLocationModal, recalAckModal, alert, serviceZipModalQuestion, From e5b00fde3ed6239f4b232caf9ed3a3d7474f011c Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 4 Mar 2026 08:41:38 -0500 Subject: [PATCH 07/16] CASH-2378: implement copilot PR review suggestions (better guards, removing superfluous emit) --- .../schedule/multi-location-modal/multi-location-modal.vue | 3 +-- src/layouts/schedule/schedule.vue | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue index 78dbe6ba5..3ca0693a8 100644 --- a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue +++ b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue @@ -90,14 +90,13 @@ export default { }, onModalClosed() { this.pushEvent(); - this.$emit("close-mobile-first-modal"); this.confirmedAppointment = false; }, closeModal() { this.modal.closeModal(); }, confirmAppointment() { - if (!this.selectedAppointment) return; + if (!this.selectedAppointment.timeSlot.id) return; const selectedTimeSlot = { timeSlot: this.selectedAppointment.timeSlot, diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index dbdaaa90f..f4a9c09ac 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -2152,7 +2152,7 @@ export default { (firstInshopPMDate && numberOfDaysToFirstInshopPMDate <= maxDayRangeToShowAnyTimeslot); - if (showMultiLocationAppointment) { + if (showMultiLocationAppointment?.toLowerCase() === "true") { return isMobileAppointmentInDateRange && isInshopAppointmentInDateRange; } return false; From 4b3c80c62b510249ce45250aabfd6a08553d896a Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 4 Mar 2026 14:47:28 -0500 Subject: [PATCH 08/16] CASH-2378 - remove hard coded values --- src/layouts/schedule/schedule.vue | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index f4a9c09ac..e55274d01 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -2102,11 +2102,6 @@ export default { 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(); From de4384a6c3050dee9b63cf82e88b5bf93b4477f1 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 4 Mar 2026 16:32:27 -0500 Subject: [PATCH 09/16] CASH-2378: add GA event changes --- src/constants/analytics.js | 2 ++ .../multi-location-modal.vue | 31 +++++++++++++++++-- src/layouts/schedule/schedule.vue | 11 ++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/constants/analytics.js b/src/constants/analytics.js index e156de11b..51b0c24b6 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -15,6 +15,8 @@ const GaCategories = { APPOINTMENT: "Appointment", SERVICE_LOCATION: "service-location", CONFIRMATION_CLICKED: "confirmation clicked", + INSHOP_CONFIRMATION_CLICKED: "inshop confirmation clicked", + MOBILE_CONFIRMATION_CLICKED: "mobile confirmation clicked", RECAL_DISCLAIMER: "Recalibration disclaimer", }; diff --git a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue index 3ca0693a8..012e8a15d 100644 --- a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue +++ b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue @@ -80,6 +80,7 @@ export default { confirmedAppointment: false, multiLocationRadioButton: multiLocationRadioButton, selectedValue: null, + experimentSettings: "", }; }, methods: { @@ -122,18 +123,42 @@ export default { setIsModalOpen(isOpen) { this.isModalOpen = isOpen; }, + captureExperimentSettings(settingsString) { + this.experimentSettings = settingsString; + }, pushEvent() { let gaAction = this.confirmedAppointment ? this.GaActions.YES : this.GaActions.NO; - if (this.mobileAppointmentOption && this.mobileAppointmentOption.timeSlot) { + if ( + this.selectedValue?.toLowerCase() === "mobile" && + this.mobileAppointmentOption?.timeSlot + ) { this.pushEventToGA( - this.GaCategories.CONFIRMATION_CLICKED, + this.GaCategories.MOBILE_CONFIRMATION_CLICKED, gaAction, this.mobileAppointmentOption.timeSlot.startTime + "-" + this.mobileAppointmentOption.timeSlot.endTime + " " + this.mobileAppointmentOption.date, - true + true, + null, + this.experimentSettings + ); + } else if ( + this.selectedValue?.toLowerCase() === "inshop" && + this.inshopAppointmentOption?.timeSlot + ) { + this.pushEventToGA( + this.GaCategories.INSHOP_CONFIRMATION_CLICKED, + gaAction, + this.inshopAppointmentOption.timeSlot.startTime + + "-" + + this.inshopAppointmentOption.timeSlot.endTime + + " " + + this.inshopAppointmentOption.date, + true, + null, + this.experimentSettings ); } }, diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index e55274d01..f771a2363 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -2101,7 +2101,15 @@ export default { 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(); @@ -2228,6 +2236,7 @@ export default { promotedInshopAppointment ); this.$refs.multiLocationModal.openModal(); + this.$refs.multiLocationModal.captureExperimentSettings(experimentSettingsString); } } } From 3bced163dcd4480512d1c0ccc01b10770b68c969 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Wed, 4 Mar 2026 16:36:44 -0500 Subject: [PATCH 10/16] CASH-2378 prettier fix --- src/layouts/schedule/schedule.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index f771a2363..a11045ce2 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -2236,7 +2236,9 @@ export default { promotedInshopAppointment ); this.$refs.multiLocationModal.openModal(); - this.$refs.multiLocationModal.captureExperimentSettings(experimentSettingsString); + this.$refs.multiLocationModal.captureExperimentSettings( + experimentSettingsString + ); } } } From 27c7da1eb5891d761ae8267a690ee44dedc28697 Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Thu, 5 Mar 2026 08:56:16 -0500 Subject: [PATCH 11/16] Revert "Merge pull request #3075 from Safelite/feature/CASH-1684" This reverts commit cbb02bf4ae8ec30300b6b8fcbbf3c76cc7b424a7, reversing changes made to e44acb4e617d6bb82afe8872b44c66dc4fd2fe0c. --- src/assets/img/icons/alert-circle-yellow.svg | 3 -- src/helpers/cms-content-helper.js | 11 ----- src/layouts/schedule/schedule.vue | 42 ++------------------ src/ux-components/alert/alert.vue | 21 ---------- 4 files changed, 4 insertions(+), 73 deletions(-) delete mode 100644 src/assets/img/icons/alert-circle-yellow.svg diff --git a/src/assets/img/icons/alert-circle-yellow.svg b/src/assets/img/icons/alert-circle-yellow.svg deleted file mode 100644 index 904aefb98..000000000 --- a/src/assets/img/icons/alert-circle-yellow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js index 450875d0f..727add296 100644 --- a/src/helpers/cms-content-helper.js +++ b/src/helpers/cms-content-helper.js @@ -343,14 +343,3 @@ export function splitCMSCopyOnParagraphTag(copy) { export function splitCMSCopyOnBR(copy) { return copy.split("
    "); } - -// This function takes in a number and returns it formatted as USD currency, with or without cents depending on if the number is an integer or not. -export function formatToUSDollar(amount) { - const isInteger = amount % 1 === 0; - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - minimumFractionDigits: isInteger ? 0 : 2, - maximumFractionDigits: 2, - }).format(amount); -} diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index a11045ce2..89ed21a08 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -81,16 +81,6 @@ cmsWidgetName="AlertNoShopsWidget" v-if="displayNoShopsAlert" alertClass="alert-warning" /> -

    Info Icon - Warning Icon {{ 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; } From e1bd79ef8e2a2ac0c5c99f9853012d5a37a3b575 Mon Sep 17 00:00:00 2001 From: CarlNation Date: Thu, 5 Mar 2026 10:19:38 -0500 Subject: [PATCH 12/16] CASH-2442 ZipPlus4 changes CASH-2442 pass address line 1/2 to the tax endpoint so zipPlus4 can be requested for tax purposes --- .../promo-modal-question/promo-modal-question.vue | 4 ++++ src/layouts/payment-method/payment-method.vue | 8 ++++++++ src/store/index.js | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/src/fmg-components/promo-modal-question/promo-modal-question.vue b/src/fmg-components/promo-modal-question/promo-modal-question.vue index 1e87d5bb5..bb3b390a8 100644 --- a/src/fmg-components/promo-modal-question/promo-modal-question.vue +++ b/src/fmg-components/promo-modal-question/promo-modal-question.vue @@ -317,6 +317,10 @@ export default { serviceLocationZipCode: store.getters.order.serviceLocation.zipCode, pricedLineItems: pricedLineItemsToTax, + serviceLocationAddressLine1: + store.getters.order.serviceLocation?.address, + serviceLocationAddressLine2: + store.getters.order.serviceLocation?.address2, }, "payment-method", false diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 4d80f84c0..d333b4bbf 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -322,6 +322,8 @@ export default { serviceLocationState: store.getters.order.serviceLocation.state, serviceLocationZipCode: store.getters.order.serviceLocation.zipCode, pricedLineItems: lineItemsForCart, + serviceLocationAddressLine1: store.getters.order.serviceLocation?.address, + serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2, }, "payment-method", false @@ -526,6 +528,10 @@ export default { serviceLocationState: this.$store.getters.order.serviceLocation.state, serviceLocationZipCode: this.$store.getters.order.serviceLocation.zipCode, pricedLineItems: this.lineItems, + serviceLocationAddressLine1: + this.$store.getters.order.serviceLocation?.address, + serviceLocationAddressLine2: + this.$store.getters.order.serviceLocation?.address2, }, "payment-method", false @@ -650,6 +656,8 @@ export default { serviceLocationState: store.getters.order.serviceLocation.state, serviceLocationZipCode: store.getters.order.serviceLocation.zipCode, pricedLineItems: this.lineItems, + serviceLocationAddressLine1: store.getters.order.serviceLocation?.address, + serviceLocationAddressLine2: store.getters.order.serviceLocation?.address2, }, "payment-method", false 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, From ef8ff581d5a65025f78f7465592dfe1893c6dcfc Mon Sep 17 00:00:00 2001 From: Adam Caouette Date: Thu, 5 Mar 2026 15:05:35 -0500 Subject: [PATCH 13/16] CASH-2378 - CSS updates --- .../multi-location-modal.vue | 10 +- .../multi-location-radio.vue | 155 +++++++----------- src/styles/mixins/customMixins.scss | 5 + src/styles/ux-variables.scss | 6 + 4 files changed, 82 insertions(+), 94 deletions(-) diff --git a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue index 012e8a15d..2ca7176e9 100644 --- a/src/layouts/schedule/multi-location-modal/multi-location-modal.vue +++ b/src/layouts/schedule/multi-location-modal/multi-location-modal.vue @@ -300,13 +300,15 @@ export default { .modal-content { .modal-header { flex-direction: column; + text-align: center; + padding: 0 1rem; + & > span { color: $red; - font-size: $font-size-14; font-weight: $font-weight-600; text-transform: uppercase; + font-family: $font-family-sans-serif-semibold; } - .modal-title { font-size: $font-size-20; font-weight: $font-weight-normal; @@ -315,6 +317,10 @@ export default { .modal-body { padding: 0.5rem 1rem; + .button-question { + margin-top: 0.75rem; + } + .modal-body-inner { ul { list-style: none; diff --git a/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.vue b/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.vue index 64c8e3ce2..0adabf9cc 100644 --- a/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.vue +++ b/src/layouts/schedule/multi-location-modal/multi-location-radio/multi-location-radio.vue @@ -1,26 +1,25 @@