diff --git a/src/constants/part-type-strings.js b/src/constants/part-type-strings.js index 17b96d2fb..0118c0b2c 100644 --- a/src/constants/part-type-strings.js +++ b/src/constants/part-type-strings.js @@ -12,6 +12,7 @@ const partTypeStrings = { SERVICE_PACKAGE_DISCOUNT: "SERVICE PACKAGE DISCOUNT", QUOTE_PAGE_DISCOUNT: "QUOTE PAGE DISCOUNT", DONATION: "DONATION", + WINDSHIELD: "WINDSHIELD", }; export { partTypeStrings }; diff --git a/src/constants/store-actions.js b/src/constants/store-actions.js index e09a4c78b..2fc0c88eb 100644 --- a/src/constants/store-actions.js +++ b/src/constants/store-actions.js @@ -111,6 +111,7 @@ const storeActions = { SAVE_PAYPAL_TOKEN: "savePaypalToken", SAVE_NEXTGEN_SETTLED_AMOUNT: "saveNextGenSettledAmount", SAVE_IS_RECAL_ACK_OPT_IN: "saveIsRecalAckOptIn", + SAVE_IS_OEM_GLASS_SELECTED: "saveIsOemGlassSelected", SAVE_IS_MSR_FEE_APPLICABLE: "saveIsMSRFeeApplicable", CREATE_SUBMITTED_STATE: "createSubmittedState", diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index 2112c789c..5167581e0 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -69,6 +69,7 @@ const storeMutations = { LOCK_TOKEN: "updateLockToken", UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount", UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn", + UPDATE_IS_OEM_GLASS_SELECTED: "updateIsOemGlassSelected", UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable", UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal", diff --git a/src/digital-components/date-picker/date-picker.vue b/src/digital-components/date-picker/date-picker.vue index f38df0372..f48b1f43b 100644 --- a/src/digital-components/date-picker/date-picker.vue +++ b/src/digital-components/date-picker/date-picker.vue @@ -281,7 +281,7 @@ export default { if (event.screenX === 0 && event.screenY === 0) { return; } - this.$emit("date-selected", date); + this.$emit("date-clicked", date); }, getWeekStartDate(dateString) { const date = convertDateStringToDate(dateString); @@ -846,6 +846,12 @@ export default { }, }, watch: { + isLoading(newValue) { + // if done loading dates, then set to first available date + if (newValue === false && this.firstAvailableSelectableDate) { + this.selectedDate = this.firstAvailableSelectableDate; + } + }, modelValue(newValue) { this.resetField({ value: newValue, diff --git a/src/layouts/part-questions/part-questions.spec.js b/src/layouts/part-questions/part-questions.spec.js index 268f1febd..4dcbb669c 100644 --- a/src/layouts/part-questions/part-questions.spec.js +++ b/src/layouts/part-questions/part-questions.spec.js @@ -306,6 +306,12 @@ describe("partQuestions.vue...", () => { //Assert expect(spy).toHaveBeenNthCalledWith( 1, + storeActions.SAVE_IS_OEM_GLASS_SELECTED, + false, + false + ); + expect(spy).toHaveBeenNthCalledWith( + 2, "savePartQuestionAnswers", [ { diff --git a/src/layouts/part-questions/part-questions.vue b/src/layouts/part-questions/part-questions.vue index 5456d868f..958a38095 100644 --- a/src/layouts/part-questions/part-questions.vue +++ b/src/layouts/part-questions/part-questions.vue @@ -188,6 +188,8 @@ export default { } }); + this.dispatchStoreAction(this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, false, false); + // save to vuex store as order.damage.partQuestionAnswers (array) // used in GET_PARTS call following this one await this.dispatchStoreAction( diff --git a/src/layouts/quote/oem-glass-question/oem-glass-question.vue b/src/layouts/quote/oem-glass-question/oem-glass-question.vue new file mode 100644 index 000000000..24bcc0f28 --- /dev/null +++ b/src/layouts/quote/oem-glass-question/oem-glass-question.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/src/layouts/quote/quote.spec.js b/src/layouts/quote/quote.spec.js index 5d9f3bacb..60dd548ad 100644 --- a/src/layouts/quote/quote.spec.js +++ b/src/layouts/quote/quote.spec.js @@ -118,6 +118,9 @@ jest.mock("@/mixins/experiment-mixin.js", () => ({ if (settingName === mockExperimentSettings.DISPLAY_AFTERPAY_BREAKOUT_DISPLAY) { return false; } + if (settingName === mockExperimentSettings.OFFER_OEM) { + return false; + } return "test"; }, }, diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index b08e9ed72..0bd095b77 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -86,6 +86,13 @@ isInsuranceSelected ? isInsuranceContinueButtonText : '' " /> + + 0 + ? oemGlassPieceParts[0].parts + : null; + const isOemGlassSelected = store.getters.order.damage?.installOemGlass ?? false; + + let alternateGlassPartsForOem; + if (oemGlassParts) alternateGlassPartsForOem = oemGlassParts; + const quotePageDiscountPartsInfo = getAvailableQuotePageDiscounts(); const quotePageDiscountParts = quotePageDiscountPartsInfo.map((partInfo) => ({ @@ -342,6 +362,18 @@ export default { ...quotePageDiscountParts, ]; + let pricedAlternateGlassParts = null; + if (alternateGlassPartsForOem && alternateGlassPartsForOem.length > 0) { + pricedAlternateGlassParts = await baseMixin.methods.dispatchStoreActionWithLogging( + storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA, + { + availableLineItems: alternateGlassPartsForOem, + }, + "quote", + false + ); + } + // When calling pricing from the quote page, always use the cash parent account but save the existing one in case it's unverified insurance navigating backwards var holdParentAccountNumber = store.getters.order.payment?.parentAccountNumber; var holdBillToAccountNumber = store.getters.order.payment?.billToAccountNumber; @@ -403,6 +435,41 @@ export default { vm.holdParentAccountNumber = holdParentAccountNumber; vm.holdBillToAccountNumber = holdBillToAccountNumber; vm.skipToInsurance = shouldSkipToInsurance; + vm.offerOem = offerOem; + vm.alternateGlassPartsForOem = pricedAlternateGlassParts; // oemParts + vm.isOemGlassSelected = isOemGlassSelected; + + const getOEMGlassPartPrice = () => { + let price = 0; + if (vm.alternateGlassPartsForOem) { + let oemGlassPart = vm.isOemGlassSelected + ? vm.lineItems.glassParts + : vm.alternateGlassPartsForOem; + if (oemGlassPart) { + price = + oemGlassPart.find( + (altPart) => altPart.partType === partTypeStrings.WINDSHIELD + )?.sellingPrice || 0; + } + } + return price; + }; + const getAftermarketGlassPartPrice = () => { + let price = 0; + if (vm.alternateGlassPartsForOem) { + let aftermarketGlassPart = vm.isOemGlassSelected + ? vm.alternateGlassPartsForOem + : vm.lineItems.glassParts; + if (aftermarketGlassPart) { + price = aftermarketGlassPart.find( + (altPart) => altPart.partType === partTypeStrings.WINDSHIELD + )?.sellingPrice; + } + } + return price; + }; + vm.oemGlassPartPrice = getOEMGlassPartPrice(); + vm.aftermarketGlassPartPrice = getAftermarketGlassPartPrice(); if (revalidatePromoResponse) { const revalidateAlerts = buildToastMessagesFromRevalidateOrValidatePromoResponse( @@ -570,6 +637,8 @@ export default { availableLineItems: null, lineItems: [], addableVaps: [], + offerOem: null, + alternateGlassPartsForOem: null, servicePackage: null, holdParentAccountNumber: null, holdBillToAccountNumber: null, @@ -579,6 +648,7 @@ export default { packageNumber: null, skipToInsurance: null, servicePackageSelected: null, + isOemGlassSelected: false, }; }, async mounted() { @@ -743,6 +813,20 @@ export default { this.lineItems.supportingItems = this.filterOutFees(this.lineItems.supportingItems); } + if ( + this.lineItems.glassParts && + this.alternateGlassPartsForOem && + this.alternateGlassPartsForOem.length > 0 + ) { + let isOEMGlassOnOrder = this.isOEMGlassOnOrder(this.lineItems.glassParts); + if ( + (this.isOemGlassSelected && !isOEMGlassOnOrder) || + (!this.isOemGlassSelected && isOEMGlassOnOrder) + ) { + this.swapOEMGlassPartsOnOrder(); + } + } + if (this.lineItems.glassParts?.length > 0) { this.dispatchStoreAction( this.storeActions.SAVE_GLASS_PARTS_SUPPRESSING_STATE_RESETTING, @@ -775,6 +859,11 @@ export default { }, false ); + this.dispatchStoreAction( + this.storeActions.SAVE_IS_OEM_GLASS_SELECTED, + this.isOemGlassSelected, + false + ); await savePageData( this.pageName, @@ -893,6 +982,26 @@ export default { const alert = createPromoSuccessAlert(lineItems.promos[0].promoCode); this.$refs.funnelHeader.pushGlobalAlert(alert, alert.shouldAutoFade); }, + isOEMGlassOnOrder(glassParts) { + if (glassParts && glassParts.length > 0) { + return glassParts.some((part) => part.partNumber.endsWith("OEM")); + } + return false; + }, + swapOEMGlassPartsOnOrder() { + this.lineItems.glassParts.forEach((part, index) => { + if (part.partType === partTypeStrings.WINDSHIELD) { + let altPartToUse = this.alternateGlassPartsForOem.find( + (altPart) => altPart.partType === partTypeStrings.WINDSHIELD + ); + if (altPartToUse) { + let childParts = this.lineItems.glassParts[index].childParts; + altPartToUse.childParts = childParts; + this.lineItems.glassParts[index] = altPartToUse; + } + } + }); + }, }, components: { funnelHeader, @@ -909,6 +1018,7 @@ export default { recalDisclaimer, saveProgressModalQuestion, saveProgressPopupQuestion, + oemGlassQuestion, }, }; diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 948a7de19..1ebf47c7e 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -22,7 +22,6 @@ cmsWidgetName="ScheduleYourServiceWidget" id="schedule-your-service" /> 0; - } else { - return this.selectableDatesInshop?.days?.length > 0; - } - }, }, methods: { splitCopyOnCMSPlaceHolder, @@ -1140,6 +1134,29 @@ export default { ); } }, + getTimeSlotInfo() { + // Get the current date + const currentDate = getTodayDate(); + + // Add 10 days to the current date + currentDate.setDate(currentDate.getDate() + 10); + + // Format the date as yyyy-mm-dd + const year = currentDate.getFullYear(); + const month = String(currentDate.getMonth() + 1).padStart(2, "0"); + const day = String(currentDate.getDate()).padStart(2, "0"); + + const formattedDate = `${year}-${month}-${day}`; + + return { + date: formattedDate, + endTime: "12:00", + jobMaxMinutes: "180", + jobMinMinutes: "120", + routeCode: "3357I-03357-M-I*20988*AM", + startTime: "08:00", + }; + }, onShopSelected(shopQuestionPopUpData) { this.resetWaitlist(); this.preSelectedDate = null; @@ -1192,6 +1209,8 @@ export default { async initializeDatePicker() { this.isShowMobileFirstAppt && this.showLoadingModal(); + this.selectedDate = null; + const includeMobileTimeSlots = this.isServiceableMobile; const includeInshopTimeSlots = this.isServiceableInshop || this.isServiceableDropoff; const datePickerInitialData = await this.$refs.datePicker.loadInitialData({ @@ -1582,40 +1601,28 @@ export default { handleWaitListRequested(value) { this.waitListRequested = value; }, - handleDateSelected(date) { - const previousDate = this.selectedDate; - - // check if date actually changed - if (previousDate !== date.dateString) { - this.selectedTimeSlotInfo = this.getEmptyTimeSlot(); - this.updateFooterButtonText(this.selectedTimeSlotInfo); + handleDateClicked(date) { + // do something to mark this as upcharge day or not... + if (date.isPricingByDayUpchargeDay) { + this.includePricingByDayUpcharge = true; + } else { + this.includePricingByDayUpcharge = false; } - - // TODO: REMOVE AS PART OF CASH-1634 - // // do something to mark this as upcharge day or not... - // if (date.isPricingByDayUpchargeDay) { - // this.includePricingByDayUpcharge = true; - // } else { - // this.includePricingByDayUpcharge = false; - // } - }, - getEmptyTimeSlot() { - return { - timeSlot: { - date: null, - routeCode: null, - startTime: null, - endTime: null, - jobMaxMinutes: null, - jobMinMinutes: null, - }, - isPremiumAppointment: null, - }; }, updateTimeSlot(timeSlotObj) { if (!timeSlotObj?.routeCode) { this.appointmentType = AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF; - this.selectedTimeSlotInfo = this.getEmptyTimeSlot(); + this.selectedTimeSlotInfo = { + timeSlot: { + date: null, + routeCode: null, + startTime: null, + endTime: null, + jobMaxMinutes: null, + jobMinMinutes: null, + }, + isPremiumAppointment: null, + }; return; } const timeSlot = this.timeSlotsForSelectedDate?.timeSlots?.find( @@ -1714,9 +1721,6 @@ export default { }, handleAppointmentTypeChange(newAppointmentType) { this.updateFooterButtonText(); - // if time appointment type changes, clear any selected time slot - this.selectedTimeSlotInfo = this.getEmptyTimeSlot(); - if (newAppointmentType === AppointmentTypeStrings.MOBILE) { // Remember last shop selected if previous selection was inshop/dropoff if ( @@ -1725,6 +1729,17 @@ export default { AppointmentTypeStrings.IN_SHOP_OR_DROP_OFF) && this.selectedProvider ) { + this.selectedTimeSlotInfo = { + timeSlot: { + date: null, + routeCode: null, + startTime: null, + endTime: null, + jobMaxMinutes: null, + jobMinMinutes: null, + }, + isPremiumAppointment: null, + }; this.lastSelectedInshopOrDropoffProvider = this.selectedProvider; } this.appointmentType = AppointmentTypeStrings.MOBILE; @@ -1751,8 +1766,8 @@ export default { } else { this.appointmentType = null; } - this.setSelectedDateToFirstAvailable(); // v-if on appointmentTypeQuestion ensures dates have been loaded by now - this.setDisplayWaitList(); // v-if on appointmentTypeQuestion ensures dates have been loaded by now + this.setSelectedDateToFirstAvailable(); + this.setDisplayWaitList(); }, setSelectedDateToFirstAvailable() { this.selectedDate = this.getFirstAvailableDate(); @@ -1884,9 +1899,35 @@ export default { watch: { appointmentTypeFromAppointmentTypeQuestion: { handler(newValue, oldValue) { - this.handleAppointmentTypeChange(newValue); // v-if on appointmentTypeQuestion ensures dates have been loaded by now + this.handleAppointmentTypeChange(newValue); }, }, + selectedDate(newValue, oldValue) { + // Clear time slot selection if date selected changes + const selectedDate = this.getSelectedDateFromStore(); + if (oldValue && this.appointmentType === AppointmentTypeStrings.MOBILE) { + oldValue = `${oldValue}-mobile`; + } + const hasValueChanged = newValue !== oldValue; + const isDateDifferent = (newValue || oldValue) !== selectedDate; + + if (hasValueChanged && isDateDifferent && !this.selectedMobileFirstAppointment) { + this.selectedTimeSlotInfo = { + timeSlot: { + date: null, + routeCode: null, + startTime: null, + endTime: null, + jobMaxMinutes: null, + jobMinMinutes: null, + }, + isPremiumAppointment: null, + }; + } + }, + selectedTimeSlotInfo(newValue) { + this.updateFooterButtonText(newValue); + }, }, components: { funnelHeader, @@ -1899,6 +1940,7 @@ export default { textBlock, timeSlotQuestion, mobileFirstModal, + alert, serviceZipModalQuestion, appointmentTypeQuestion, diff --git a/src/layouts/schedule/time-slot-question/time-slot-question.vue b/src/layouts/schedule/time-slot-question/time-slot-question.vue index d12757571..1b8a2cb2e 100644 --- a/src/layouts/schedule/time-slot-question/time-slot-question.vue +++ b/src/layouts/schedule/time-slot-question/time-slot-question.vue @@ -1,7 +1,6 @@