diff --git a/src/constants/save-progress-cms-widgets.js b/src/constants/save-progress-cms-widgets.js
new file mode 100644
index 000000000..d80960ace
--- /dev/null
+++ b/src/constants/save-progress-cms-widgets.js
@@ -0,0 +1,10 @@
+const saveProgressCmsWidgets = {
+ PHONE_SPECIFIC: "SaveProgressPopupWidget_PhoneSpecific",
+ EMAIL_SPECIFIC: "SaveProgressPopupWidget_EmailSpecific",
+ MODAL_PHONE_QUESTION: "SaveProgressPhoneQuestionWidget",
+ MODAL_EMAIL_QUESTION: "SaveProgressEmailQuestionWidget",
+ POPUP_PHONE_QUESTION: "SaveProgressPopupPhoneQuestionWidget",
+ POPUP_EMAIL_QUESTION: "SaveProgressPopupEmailQuestionWidget",
+};
+
+export { saveProgressCmsWidgets };
diff --git a/src/fmg-components/save-progress-modal-question/save-progress-modal-question.spec.js b/src/fmg-components/save-progress-modal-question/save-progress-modal-question.spec.js
index 15fb1481d..0f27858de 100644
--- a/src/fmg-components/save-progress-modal-question/save-progress-modal-question.spec.js
+++ b/src/fmg-components/save-progress-modal-question/save-progress-modal-question.spec.js
@@ -1,43 +1,261 @@
-import { mount, shallowMount } from "@vue/test-utils";
+import { mount } from "@vue/test-utils";
import saveProgressModalQuestion from "./save-progress-modal-question";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
+import { storeActions } from "@/constants/store-actions";
+import { saveProgressCmsWidgets } from "@/constants/save-progress-cms-widgets";
+import { saveQuote } from "@/helpers/heritage-integration/order-helper.js";
+import experimentMixin from "@/mixins/experiment-mixin.js";
+import { experimentSettings } from "@/constants/experiments";
+
+jest.mock("@/helpers/heritage-integration/order-helper.js", () => ({
+ saveQuote: jest.fn().mockResolvedValue(undefined),
+}));
+
+jest.mock("@/helpers/save-progress-sms-consent/save-progress-sms-consent-helper", () => ({
+ ...jest.requireActual("@/helpers/save-progress-sms-consent/save-progress-sms-consent-helper"),
+ getSaveProgressSmsConsentConfig: jest.fn().mockReturnValue({
+ copy: {
+ transactional: "Sign me up for updates about my upcoming service.",
+ marketing: "Sign me up for promotional and product offers.",
+ },
+ value: { transactional: false, marketing: false },
+ }),
+}));
jest.mock("@/digital-components/modal/modal", () => ({
methods: {
openModal: jest.fn(),
+ closeModal: jest.fn(),
resetButtonStyle: jest.fn(),
+ resetForm: jest.fn(),
+ validate: jest.fn().mockResolvedValue({ valid: true }),
},
}));
-describe("save-progress-modal-question ", () => {
- describe("when openModal is run ", () => {
- test("the modal should open ", () => {
- // Arrange
+jest.mock("@/ux-components/alert/alert", () => ({
+ name: "alert",
+ template: "
+
+
@@ -96,13 +139,30 @@ import mobileSchedulingCard from "@/layouts/scheduling/mobile-scheduling-card/mo
import inshopSchedulingCard from "@/layouts/scheduling/inshop-scheduling-card/inshop-scheduling-card";
import interceptOverlay from "@/ux-components/intercept-overlay/intercept-overlay";
import waitlistQuestion from "@/layouts/scheduling/waitlist-question/waitlist-question";
+import schedulingZipSearch from "@/layouts/scheduling/scheduling-zip-search/scheduling-zip-search";
+import recalAckModal from "@/layouts/schedule/recal-ack-modal/recal-ack-modal.vue";
import textLink from "@/ux-components/text-link/text-link";
import store from "@/store";
-import { AppointmentTypeStrings, PREMIUM_FEE_PART_TYPE } from "@/constants/schedule-constants";
+import { partNumberStrings } from "@/constants/part-number-strings";
+import baseMixin from "@/mixins/base-mixin";
+import experimentMixin from "@/mixins/experiment-mixin.js";
+import { experimentSettings } from "@/constants/experiments";
+import { storeActions } from "@/constants/store-actions";
+import {
+ AppointmentTypeStrings,
+ PREMIUM_FEE_PART_TYPE,
+ RECAL_ACK_YES,
+} from "@/constants/schedule-constants";
+import { isDropOffRouteCode } from "@/layouts/schedule/helpers/schedule-helper";
+import {
+ getPricedMobileFeePart,
+ getPricedRecycleFeePart,
+} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { getDisplayTextForDurationLength } from "@/helpers/duration-length-helper";
+import { partTypeStrings } from "@/constants/part-type-strings";
import {
flushPagePrereqsLogs,
hasServiceZipInfo,
@@ -115,6 +175,8 @@ import {
getAvailableDatesFromProviderDays,
} from "@/layouts/schedule/helpers/schedule-helper";
+const MOBILE_FEE_PART_TYPE = "MOBILE FEE";
+
/**
* Returns a YYYY-MM-DD date string offset by the given number of days from a base date.
* @param {number} offsetDays - Number of days to offset (positive or negative).
@@ -132,6 +194,7 @@ const SCHEDULING_RADIO_GROUP_NAME = "schedulingTimeSlot";
const INITIAL_INSHOP_PROVIDER_COUNT = 3;
const MAX_INSHOP_PROVIDERS = 9;
const VIEW_MORE_SHOPS_BATCH_SIZE = 3;
+const INSHOP_MAPS_PLACE_NAME = "safelite,Safelite Autoglass";
/**
* Appends days from newSlots into entry.timeSlots, initializing it if absent.
@@ -150,14 +213,23 @@ function appendDays(entry, newSlots) {
/**
* Assigns time slots from a v2 multi-provider response onto in-shop provider entries.
* @param {Array<{ provider: { providerNumber: string }, timeSlots: any }>} entries
- * @param {{ providerTimeSlots?: Array<{ providerNumber: string, days: any[], provisionalTriggers?: string[] }> } | null | undefined} multiProviderResponse
+ * @param {{ providerTimeSlots?: Array<{ providerNumber: string, days: any[], provisionalTriggers?: string[] }>, estimatedServiceMinutesMinimum?: number, estimatedServiceMinutesMaximum?: number } | null | undefined} multiProviderResponse
*/
function assignInshopTimeSlotsFromV2Response(entries, multiProviderResponse) {
const providerTimeSlots = multiProviderResponse?.providerTimeSlots ?? [];
entries.forEach((entry) => {
- entry.timeSlots =
+ const matchedSlots =
providerTimeSlots.find((pts) => pts.providerNumber === entry.provider.providerNumber) ??
null;
+ if (matchedSlots) {
+ matchedSlots.estimatedServiceMinutesMinimum =
+ matchedSlots.estimatedServiceMinutesMinimum ??
+ multiProviderResponse?.estimatedServiceMinutesMinimum;
+ matchedSlots.estimatedServiceMinutesMaximum =
+ matchedSlots.estimatedServiceMinutesMaximum ??
+ multiProviderResponse?.estimatedServiceMinutesMaximum;
+ }
+ entry.timeSlots = matchedSlots;
});
}
@@ -259,6 +331,24 @@ function fetchTimeSlotsBatch({
]);
}
+/**
+ * Fetches serviceability details for a given zip code.
+ * @param {{ serviceZipCode: string, lineItems: any, pageNameToLog: string }} params
+ * @returns {Promise<{ isGlassServiceableInshop: boolean, isRecalibrationServiceableInshop: boolean, isGlassServiceableDropoff: boolean, isRecalibrationServiceableDropoff: boolean, isGlassServiceableMobile: boolean, isRecalibrationServiceableMobile: boolean }>}
+ */
+function getServiceabilityDetails(serviceZipCode, pageNameToLog) {
+ const lineItems = store.getters.lineItems;
+ return baseMixin.methods.dispatchStoreActionWithLogging(
+ storeActions.GET_SERVICEABILITY_DETAILS,
+ {
+ serviceZipCode: serviceZipCode,
+ lineItems: lineItems,
+ },
+ pageNameToLog,
+ false
+ );
+}
+
export default {
name: "scheduling",
async beforeRouteEnter(to, from, next) {
@@ -282,52 +372,25 @@ export default {
pageNameToLog: to.name,
}),
},
+ {
+ resultKey: "mobileFeePart",
+ promise: getPricedMobileFeePart(serviceZipCode, to.name),
+ },
+ {
+ resultKey: "recycleFeePart",
+ promise: getPricedRecycleFeePart(serviceZipCode, to.name),
+ },
];
const resultMap = await settleAllPromises(promiseResultMap);
- const allShopProviders = resultMap.providers?.shopProviders ?? [];
- const providers = allShopProviders.slice(0, INITIAL_INSHOP_PROVIDER_COUNT);
- const mobileProviderNumber = resultMap.providers?.mobileProviderNumber ?? null;
next(async (vm) => {
vm.setCmsContent(resultMap.cmsContent);
- vm.allShopProviders = allShopProviders;
- vm.inshopProvidersAndTimeSlots = providers.map((provider) => ({
- provider,
- timeSlots: null,
- }));
- vm.mobileProviderAndTimeSlot = mobileProviderNumber
- ? { providerNumber: mobileProviderNumber, timeSlots: null }
- : null;
- const startDate = toDateString(0);
- const endDate = toDateString(SCHEDULE_FETCH_DAYS - 1);
- const providerNumbers = providers.map((provider) => provider.providerNumber);
- const timeSlotsResultMap = await fetchTimeSlotsBatch({
- startDate,
- endDate,
- providerNumbers,
- zipCode: serviceZipCode,
- includeMobile: Boolean(mobileProviderNumber),
+ await vm.loadSchedulingData(serviceZipCode, {
+ providersResult: resultMap.providers,
pageNameToLog: to.name,
});
- if (timeSlotsResultMap.inshopTimeSlots) {
- vm.estimatedServiceMinutesMinimum =
- timeSlotsResultMap.inshopTimeSlots.estimatedServiceMinutesMinimum;
- vm.estimatedServiceMinutesMaximum =
- timeSlotsResultMap.inshopTimeSlots.estimatedServiceMinutesMaximum;
- } else if (timeSlotsResultMap.mobileTimeSlots) {
- vm.estimatedServiceMinutesMinimum =
- timeSlotsResultMap.mobileTimeSlots.estimatedServiceMinutesMinimum;
- vm.estimatedServiceMinutesMaximum =
- timeSlotsResultMap.mobileTimeSlots.estimatedServiceMinutesMaximum;
- }
- assignInshopTimeSlotsFromV2Response(
- vm.inshopProvidersAndTimeSlots,
- timeSlotsResultMap.inshopTimeSlots
- );
- if (vm.mobileProviderAndTimeSlot) {
- vm.mobileProviderAndTimeSlot.timeSlots = timeSlotsResultMap.mobileTimeSlots ?? null;
- }
- vm.datesLoaded = true;
vm.mobilePremiumAppointmentFee = resultMap.mobilePremiumFee ?? null;
+ vm.mobileFeePart = resultMap.mobileFeePart ?? null;
+ vm.recycleFeePart = resultMap.recycleFeePart ?? null;
vm.isLoadingDates = false;
});
},
@@ -340,8 +403,27 @@ export default {
schedulingRadioGroupName() {
return SCHEDULING_RADIO_GROUP_NAME;
},
- serviceLocationText() {
- return this.getCmsContent("ServiceLocationText", "Text");
+ mobileSectionHeaderText() {
+ return this.getCmsContent("MobileCardWidget", "SubheaderText");
+ },
+ mobileSectionHeaderMainText() {
+ const text = this.mobileSectionHeaderText?.trim();
+ if (!text) {
+ return "";
+ }
+ const freeTextIndex = text.indexOf("(");
+ return freeTextIndex >= 0 ? text.slice(0, freeTextIndex).trimEnd() : text;
+ },
+ mobileSectionHeaderFreeText() {
+ const text = this.mobileSectionHeaderText?.trim();
+ if (!text) {
+ return "";
+ }
+ const freeTextIndex = text.indexOf("(");
+ return freeTextIndex >= 0 ? text.slice(freeTextIndex) : "";
+ },
+ inshopSectionHeaderText() {
+ return this.getCmsContent("InshopCardWidget", "HeaderText");
},
viewMoreShopsText() {
return this.getCmsContent("ViewMoreShopsWidget", "Text");
@@ -350,13 +432,16 @@ export default {
if (!this.estimatedServiceMinutesMinimum || !this.estimatedServiceMinutesMaximum) {
return " ";
}
- const durationText = getDisplayTextForDurationLength(
+ // "1 - 2 hours" → "[1-2] hours"
+ const formattedDuration = getDisplayTextForDurationLength(
this.estimatedServiceMinutesMinimum,
this.estimatedServiceMinutesMaximum
- );
- return this.getCmsContent("FunnelSubHeaderWidget", "HeaderSubText").replace(
+ )
+ .replace(" - ", "-")
+ .replace(/(\S+) (hours|minutes)/i, "[$1] $2");
+ return (this.getCmsContent("FunnelSubHeaderWidget", "HeaderSubText") || "").replace(
"{custom:DURATION}",
- durationText
+ formattedDuration
);
},
serviceZipCode() {
@@ -394,9 +479,6 @@ export default {
}
return this.getTotalLineItemPrice(this.mobilePremiumAppointmentFee);
},
- showMobileFreeFlag() {
- return true;
- },
availableDates() {
if (!this.datesLoaded) return null;
const inshopDates = this.inshopProvidersAndTimeSlots.flatMap(({ timeSlots }) =>
@@ -411,6 +493,45 @@ export default {
const maxVisible = Math.min(MAX_INSHOP_PROVIDERS, this.allShopProviders.length);
return this.inshopProvidersAndTimeSlots.length < maxVisible;
},
+ isInsurance() {
+ return store.getters.order.payment?.isInsurance;
+ },
+ isITAC() {
+ return store.getters.order.policy?.isItac;
+ },
+ isNoComp() {
+ return store.getters.order.policy?.isNoComp;
+ },
+ isCashItacNoComp() {
+ return !this.isInsurance || this.isITAC || this.isNoComp;
+ },
+ displayMSR() {
+ return (
+ experimentMixin.methods
+ .getSettingValue(experimentSettings.DISPLAY_MSR)
+ ?.toLowerCase() === "true"
+ );
+ },
+ enableMSRSplitPay() {
+ return (
+ experimentMixin.methods
+ .getSettingValue(experimentSettings.ENABLE_MSR_SPLIT_PAY)
+ ?.toLowerCase() === "true"
+ );
+ },
+ isMSRFeePartNumber() {
+ return (
+ this.mobileFeePart?.partNumber === partNumberStrings.MOBILE_STATIC_RECAL_FEE ||
+ this.mobileFeePart?.partNumber === partNumberStrings.MOBILE_DUAL_RECAL_FEE
+ );
+ },
+ isMobileStaticRecalibrationApplicable() {
+ return (
+ this.displayMSR &&
+ this.isMSRFeePartNumber &&
+ (this.enableMSRSplitPay || this.isCashItacNoComp || this.mobileFeePart?.isInsurable)
+ );
+ },
},
data() {
return {
@@ -425,12 +546,119 @@ export default {
allShopProviders: [],
mobileProviderAndTimeSlot: null,
mobilePremiumAppointmentFee: null,
+ mobileFeePart: null,
+ recycleFeePart: null,
selectedScheduling: null,
isWaitlistRequested: false,
isLoadingMoreShops: false,
+ zipSearchCode: store.getters.order.serviceLocation.zipCode ?? "",
+ datePickerKey: 0,
+ billToAccountNumber: store.getters.order.payment?.billToAccountNumber ?? null,
+ isRecalAcknowledgedForScheduling:
+ store.getters.order.isRecalAcknowledgedForScheduling ?? "",
+ serviceabilityDetails: {
+ isGlassServiceableInshop: false,
+ isRecalibrationServiceableInshop: false,
+ isGlassServiceableDropoff: false,
+ isRecalibrationServiceableDropoff: false,
+ isGlassServiceableMobile: false,
+ isRecalibrationServiceableMobile: false,
+ },
};
},
methods: {
+ async loadFeeParts(serviceZipCode, pageNameToLog = this.pageName) {
+ const [mobileFeePart, recycleFeePart] = await Promise.all([
+ getPricedMobileFeePart(serviceZipCode, pageNameToLog),
+ getPricedRecycleFeePart(serviceZipCode, pageNameToLog),
+ ]);
+ this.mobileFeePart = mobileFeePart ?? null;
+ this.recycleFeePart = recycleFeePart ?? null;
+ },
+ async loadSchedulingData(
+ serviceZipCode,
+ { providersResult = null, pageNameToLog = this.pageName } = {}
+ ) {
+ let providersData = providersResult;
+ if (!providersData) {
+ providersData = await store.dispatch("getProviders", {
+ payload: { serviceZipCode },
+ pageNameToLog,
+ });
+ }
+ if (providersData?.shopProviders === undefined) {
+ providersData = providersData?.data ?? {};
+ }
+
+ const allShopProviders = providersData.shopProviders ?? [];
+ const providers = allShopProviders.slice(0, INITIAL_INSHOP_PROVIDER_COUNT);
+ const mobileProviderNumber = providersData?.mobileProviderNumber ?? null;
+
+ this.allShopProviders = allShopProviders;
+ this.inshopProvidersAndTimeSlots = providers.map((provider) => ({
+ provider,
+ timeSlots: null,
+ }));
+ this.mobileProviderAndTimeSlot = mobileProviderNumber
+ ? { providerNumber: mobileProviderNumber, timeSlots: null }
+ : null;
+
+ var gaLabel = this.GaLabels.NO;
+ if (this.mobileProviderAndTimeSlot) {
+ gaLabel = this.GaLabels.YES;
+ }
+ this.pushEventToGA(
+ this.GaCategories.APPOINTMENT,
+ this.GaActions.MOBILE_AVAILABLE,
+ gaLabel,
+ true
+ );
+
+ const startDate = toDateString(0);
+ const endDate = toDateString(SCHEDULE_FETCH_DAYS - 1);
+ const providerNumbers = providers.map((provider) => provider.providerNumber);
+ const timeSlotsResultMap = await fetchTimeSlotsBatch({
+ startDate,
+ endDate,
+ providerNumbers,
+ zipCode: serviceZipCode,
+ includeMobile: Boolean(mobileProviderNumber),
+ pageNameToLog,
+ });
+
+ if (timeSlotsResultMap.inshopTimeSlots) {
+ this.estimatedServiceMinutesMinimum =
+ timeSlotsResultMap.inshopTimeSlots.estimatedServiceMinutesMinimum;
+ this.estimatedServiceMinutesMaximum =
+ timeSlotsResultMap.inshopTimeSlots.estimatedServiceMinutesMaximum;
+ } else if (timeSlotsResultMap.mobileTimeSlots) {
+ this.estimatedServiceMinutesMinimum =
+ timeSlotsResultMap.mobileTimeSlots.estimatedServiceMinutesMinimum;
+ this.estimatedServiceMinutesMaximum =
+ timeSlotsResultMap.mobileTimeSlots.estimatedServiceMinutesMaximum;
+ } else {
+ this.estimatedServiceMinutesMinimum = null;
+ this.estimatedServiceMinutesMaximum = null;
+ }
+
+ assignInshopTimeSlotsFromV2Response(
+ this.inshopProvidersAndTimeSlots,
+ timeSlotsResultMap.inshopTimeSlots
+ );
+ if (this.mobileProviderAndTimeSlot) {
+ this.mobileProviderAndTimeSlot.timeSlots =
+ timeSlotsResultMap.mobileTimeSlots ?? null;
+ }
+ this.datesLoaded = true;
+
+ const serviceabilityDetailsResultMap = await getServiceabilityDetails(
+ serviceZipCode,
+ pageNameToLog
+ );
+ if (serviceabilityDetailsResultMap?.data) {
+ this.serviceabilityDetails = serviceabilityDetailsResultMap.data;
+ }
+ },
getInshopTimeSlotsForSelectedDate(providerNumber) {
if (!this.selectedDate) {
return [];
@@ -441,6 +669,26 @@ export default {
const day = providerEntry?.timeSlots?.days?.find((d) => d.date === this.selectedDate);
return day?.timeSlots ?? [];
},
+ buildInshopMapsQuery(address) {
+ const streetAddress = address?.streetAddress?.trim();
+ if (!streetAddress) {
+ return null;
+ }
+ const street = [streetAddress, address.streetAddress2?.trim()]
+ .filter(Boolean)
+ .join(" ");
+ return `${INSHOP_MAPS_PLACE_NAME}, ${street}, ${address.city}, ${address.state} ${address.zipCode}`;
+ },
+ buildInshopMapsUrl(address) {
+ const query = this.buildInshopMapsQuery(address);
+ if (!query) {
+ return null;
+ }
+ if (this.isAppleBrowser()) {
+ return `https://maps.apple.com/?q=${encodeURIComponent(query)}`;
+ }
+ return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`;
+ },
getInshopAvailableDateChips(providerNumber) {
if (
!this.selectedDate ||
@@ -460,11 +708,48 @@ export default {
this.selectedDate = date;
},
onInshopAddressClicked(provider) {
- // TODO: open Google Maps when address link functionality is implemented
- console.log("onInshopAddressClicked", provider?.providerNumber);
+ const url = this.buildInshopMapsUrl(provider?.address);
+ if (!url) {
+ return;
+ }
+ window.open(url, "_blank", "noopener,noreferrer");
},
onMobileZipCodeClicked() {
- // TODO: open service zip modal when zip edit is implemented for scheduling page
+ this.$refs.schedulingZipSearch?.focusZipInput();
+ },
+ async onZipSearched({ zipCode, billToAccountNumber }) {
+ // When recal ack applies, changing zip must re-run parts selection via service-zip.
+ if (this.shouldShowRecalAckModal()) {
+ this.$router.navigateWithSaving(
+ this.navigationScenarios.ZIP_CODE_CHANGED_RECAL_ACK,
+ this.pageName
+ );
+ return;
+ }
+
+ const serviceabilityDetailsResultMap = await getServiceabilityDetails(
+ zipCode,
+ this.pageName
+ );
+ if (serviceabilityDetailsResultMap?.data) {
+ this.serviceabilityDetails = serviceabilityDetailsResultMap.data;
+ }
+
+ this.billToAccountNumber = billToAccountNumber;
+ this.isLoadingDates = true;
+ try {
+ this.selectedDate = null;
+ this.selectedScheduling = null;
+ this.isWaitlistRequested = false;
+ this.isLoadingMoreShops = false;
+ this.datesLoaded = false;
+ this.datePickerKey += 1;
+ this.datePickerStartDate = toDateString(0);
+ this.datePickerEndDate = toDateString(SCHEDULE_FETCH_DAYS - 1);
+ await Promise.all([this.loadSchedulingData(zipCode), this.loadFeeParts(zipCode)]);
+ } finally {
+ this.isLoadingDates = false;
+ }
},
async onViewMoreShopsClick() {
if (this.isLoadingDates || this.isLoadingMoreShops) return;
@@ -566,9 +851,298 @@ export default {
this.datePickerEndDate = newEndDate;
this.isLoadingDates = false;
},
- forwardButtonAction() {
- const appointmentType = store.getters.order.serviceLocation.appointmentType; // TODO: remove this once we have a proper appointment type
+ getInShopOrDropOffApptType(selectedScheduling = this.selectedScheduling) {
+ if (selectedScheduling?.appointmentType === AppointmentTypeStrings.MOBILE) {
+ return AppointmentTypeStrings.MOBILE;
+ }
+ return isDropOffRouteCode(selectedScheduling?.routeCodeId)
+ ? AppointmentTypeStrings.DROP_OFF
+ : AppointmentTypeStrings.IN_SHOP;
+ },
+ getSelectedProvider(appointmentType, selectedScheduling = this.selectedScheduling) {
if (appointmentType === AppointmentTypeStrings.MOBILE) {
+ return {
+ providerNumber: selectedScheduling?.providerNumber,
+ address: {
+ streetAddress: null,
+ city: null,
+ state: null,
+ zipCode: null,
+ zipCodeCtu: null,
+ },
+ };
+ }
+ return (
+ this.inshopProvidersAndTimeSlots.find(
+ ({ provider }) => provider.providerNumber === selectedScheduling?.providerNumber
+ )?.provider ?? null
+ );
+ },
+ getSelectedTimeSlot(
+ appointmentType,
+ selectedScheduling = this.selectedScheduling,
+ selectedDate = this.selectedDate
+ ) {
+ const routeCodeId = selectedScheduling?.routeCodeId;
+ if (!routeCodeId || !selectedDate) {
+ return null;
+ }
+
+ let timeSlots;
+ if (appointmentType === AppointmentTypeStrings.MOBILE) {
+ const day = this.mobileProviderAndTimeSlot?.timeSlots?.days?.find(
+ (d) => d.date === selectedDate
+ );
+ timeSlots = day?.timeSlots ?? [];
+ } else {
+ const providerEntry = this.inshopProvidersAndTimeSlots.find(
+ ({ provider }) => provider.providerNumber === selectedScheduling.providerNumber
+ );
+ const day = providerEntry?.timeSlots?.days?.find((d) => d.date === selectedDate);
+ timeSlots = day?.timeSlots ?? [];
+ }
+
+ return timeSlots.find((timeSlot) => timeSlot.id === routeCodeId) ?? null;
+ },
+ getEstimatedServiceMinutes(appointmentType, selectedScheduling = this.selectedScheduling) {
+ if (appointmentType === AppointmentTypeStrings.MOBILE) {
+ return {
+ minimum:
+ this.mobileProviderAndTimeSlot?.timeSlots?.estimatedServiceMinutesMinimum,
+ maximum:
+ this.mobileProviderAndTimeSlot?.timeSlots?.estimatedServiceMinutesMaximum,
+ };
+ }
+
+ const providerEntry = this.inshopProvidersAndTimeSlots.find(
+ ({ provider }) => provider.providerNumber === selectedScheduling?.providerNumber
+ );
+ return {
+ minimum: providerEntry?.timeSlots?.estimatedServiceMinutesMinimum,
+ maximum: providerEntry?.timeSlots?.estimatedServiceMinutesMaximum,
+ };
+ },
+ updateAndSaveSupportingItems(appointmentType) {
+ let supportingItems = store.getters.lineItems?.supportingItems;
+ let shouldSaveSupportingItems = false;
+
+ supportingItems =
+ !supportingItems && this.isMobileStaticRecalibrationApplicable
+ ? []
+ : supportingItems;
+
+ // for insurance orders, fees can get removed causing supportingitems to be null or empty
+ if (!supportingItems) {
+ return;
+ }
+
+ // update recycle fee price
+ if (this.recycleFeePart) {
+ const recycleFeeIndex = supportingItems.findIndex(
+ (item) => item.partNumber == partNumberStrings.RECYCLE_FEE
+ );
+ if (recycleFeeIndex >= 0) {
+ supportingItems[recycleFeeIndex].laborAmount = this.recycleFeePart.laborAmount;
+ supportingItems[recycleFeeIndex].sellingPrice =
+ this.recycleFeePart.sellingPrice;
+ supportingItems[recycleFeeIndex].kitPrice = this.recycleFeePart.kitPrice;
+
+ shouldSaveSupportingItems = true;
+ }
+ }
+
+ // if we have a mobile fee, then save/update supporting items
+ if (appointmentType === AppointmentTypeStrings.MOBILE) {
+ if (this.mobileFeePart) {
+ const mobileFeeIndex = supportingItems.findIndex(
+ (item) => item.partType == MOBILE_FEE_PART_TYPE
+ );
+ if (mobileFeeIndex >= 0) {
+ supportingItems[mobileFeeIndex].laborAmount =
+ this.mobileFeePart.laborAmount;
+ supportingItems[mobileFeeIndex].sellingPrice =
+ this.mobileFeePart.sellingPrice;
+ supportingItems[mobileFeeIndex].kitPrice = this.mobileFeePart.kitPrice;
+ supportingItems[mobileFeeIndex].isInsurable =
+ this.mobileFeePart.isInsurable;
+ } else {
+ supportingItems.push(this.mobileFeePart);
+ }
+ shouldSaveSupportingItems = true;
+ }
+ } else {
+ const removeMobileFeeIndex = supportingItems.findIndex(
+ (item) => item.partType == MOBILE_FEE_PART_TYPE
+ );
+ if (removeMobileFeeIndex >= 0) {
+ supportingItems.splice(removeMobileFeeIndex, 1);
+ shouldSaveSupportingItems = true;
+ }
+ }
+
+ if (shouldSaveSupportingItems) {
+ this.dispatchStoreAction(
+ this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
+ supportingItems,
+ false
+ );
+ }
+ },
+ updateAndSaveIsMSRFeeApplicable(appointmentType) {
+ const isMSRFeeApplicable =
+ this.isMobileStaticRecalibrationApplicable &&
+ appointmentType === AppointmentTypeStrings.MOBILE;
+ this.dispatchStoreAction(
+ this.storeActions.SAVE_IS_MSR_FEE_APPLICABLE,
+ isMSRFeeApplicable
+ );
+ },
+ updateAndSaveIsMSRFeeCoveredByInsurance() {
+ const isMSRFeeCoveredByInsurance = this.mobileFeePart?.isInsurable;
+ this.dispatchStoreAction(
+ this.storeActions.SAVE_IS_MSR_FEE_COVERED_BY_INSURANCE,
+ isMSRFeeCoveredByInsurance
+ );
+ },
+ updateSupportingItems(selectedScheduling = this.selectedScheduling) {
+ const supportingItems = store.getters.lineItems?.supportingItems;
+ if (!supportingItems) {
+ return;
+ }
+
+ const isPremiumMobile =
+ selectedScheduling?.appointmentType === AppointmentTypeStrings.MOBILE &&
+ selectedScheduling?.isPremiumAppointment;
+
+ if (isPremiumMobile && this.mobilePremiumAppointmentFee) {
+ const premiumFeeIndex = supportingItems.findIndex(
+ (item) => item.partType == PREMIUM_FEE_PART_TYPE
+ );
+
+ if (premiumFeeIndex > -1) {
+ supportingItems[premiumFeeIndex].laborAmount =
+ this.mobilePremiumAppointmentFee.laborAmount;
+ supportingItems[premiumFeeIndex].sellingPrice =
+ this.mobilePremiumAppointmentFee.sellingPrice;
+ supportingItems[premiumFeeIndex].kitPrice =
+ this.mobilePremiumAppointmentFee.kitPrice;
+ } else {
+ supportingItems.push(this.mobilePremiumAppointmentFee);
+ }
+ } else {
+ const removePremiumFeeIndex = supportingItems.findIndex(
+ (item) => item.partType == PREMIUM_FEE_PART_TYPE
+ );
+ if (removePremiumFeeIndex >= 0) {
+ supportingItems.splice(removePremiumFeeIndex, 1);
+ }
+ }
+
+ this.dispatchStoreAction(
+ this.storeActions.SAVE_SUPPORTING_ITEMS_SUPPRESSING_STATE_RESETTING,
+ supportingItems,
+ false
+ );
+ },
+ async forwardButtonAction() {
+ if (this.shouldShowRecalAckModal()) {
+ this.$refs.navbar.removeLoader();
+ this.$refs.recalAckModal.openModal();
+ return;
+ }
+
+ const selectedScheduling = this.selectedScheduling;
+ const selectedDate = this.selectedDate;
+
+ await store.dispatch(
+ storeActions.SAVE_IS_RECAL_ACKNOWLEDGED_FOR_SCHEDULING,
+ this.isRecalAcknowledgedForScheduling
+ );
+
+ const appointmentType = this.getInShopOrDropOffApptType(selectedScheduling);
+ const selectedProvider = this.getSelectedProvider(appointmentType, selectedScheduling);
+ const serviceLocation = store.getters.order.serviceLocation;
+ const isMobile = appointmentType === AppointmentTypeStrings.MOBILE;
+
+ let zipCodeCtu = serviceLocation.zipCodeCtu;
+ if (
+ !isMobile &&
+ selectedProvider?.address?.zipCodeCtu &&
+ zipCodeCtu !== selectedProvider.address.zipCodeCtu
+ ) {
+ zipCodeCtu = selectedProvider.address.zipCodeCtu;
+ }
+
+ await this.dispatchStoreAction(
+ this.storeActions.SAVE_SERVICE_LOCATION,
+ {
+ address: isMobile ? serviceLocation.address : "",
+ address2: isMobile ? serviceLocation.address2 : "",
+ city: isMobile ? serviceLocation.city : "",
+ state: serviceLocation.state,
+ zipCode: serviceLocation.zipCode,
+ zipCodeCtu: zipCodeCtu,
+ appointmentType,
+ isVehicleProtected: isMobile ? serviceLocation.isVehicleProtected : null,
+ provider: {
+ providerNumber: selectedProvider?.providerNumber,
+ address: {
+ streetAddress: selectedProvider?.address?.streetAddress ?? null,
+ city: selectedProvider?.address?.city ?? null,
+ state: selectedProvider?.address?.state ?? null,
+ zipCode: selectedProvider?.address?.zipCode ?? null,
+ zipCodeCtu: selectedProvider?.address?.zipCodeCtu ?? null,
+ },
+ },
+ },
+ false
+ );
+
+ if (this.billToAccountNumber) {
+ this.dispatchStoreAction(
+ this.storeActions.SAVE_BILL_TO_ACCOUNT_NUMBER,
+ this.billToAccountNumber,
+ false
+ );
+ }
+
+ this.updateAndSaveSupportingItems(appointmentType);
+ this.updateAndSaveIsMSRFeeApplicable(appointmentType);
+ this.updateAndSaveIsMSRFeeCoveredByInsurance();
+ this.updateSupportingItems(selectedScheduling);
+
+ const selectedTimeSlot = this.getSelectedTimeSlot(
+ appointmentType,
+ selectedScheduling,
+ selectedDate
+ );
+ const estimatedServiceMinutes = this.getEstimatedServiceMinutes(
+ appointmentType,
+ selectedScheduling
+ );
+
+ this.dispatchStoreAction(
+ this.storeActions.SAVE_SCHEDULE,
+ {
+ date: selectedDate,
+ routeCode: selectedScheduling?.routeCodeId ?? null,
+ startTime: selectedTimeSlot?.startTime ?? null,
+ endTime: selectedTimeSlot?.endTime ?? null,
+ jobMinMinutes: estimatedServiceMinutes.minimum?.toString() ?? null,
+ jobMaxMinutes: estimatedServiceMinutes.maximum?.toString() ?? null,
+ },
+ false
+ );
+
+ if (this.isWaitlistRequested !== null && this.isWaitlistRequested !== undefined) {
+ this.dispatchStoreAction(
+ this.storeActions.SAVE_WAITLIST_REQUESTED,
+ this.isWaitlistRequested,
+ false
+ );
+ }
+
+ if (isMobile) {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_MOBILE_SERVICE,
this.pageName
@@ -580,6 +1154,23 @@ export default {
);
}
},
+ shouldShowRecalAckModal() {
+ if (this.isRecalAcknowledgedForScheduling === RECAL_ACK_YES) {
+ return false;
+ }
+
+ const glassParts = store.getters.order.lineItems?.glassParts ?? [];
+ return glassParts.some(
+ (part) =>
+ part.partType === partTypeStrings.WINDSHIELD &&
+ part.requiresRecalibration === true &&
+ part.canSafeliteRecalibrate === false
+ );
+ },
+ onRecalAcknowledged(isAcknowledged) {
+ this.isRecalAcknowledgedForScheduling = isAcknowledged;
+ this.forwardButtonAction();
+ },
},
components: {
funnelHeader,
@@ -591,17 +1182,37 @@ export default {
inshopSchedulingCard,
interceptOverlay,
waitlistQuestion,
+ schedulingZipSearch,
+ recalAckModal,
textLink,
},
};