diff --git a/src/constants/analytics.js b/src/constants/analytics.js index f6534cda4..fe43be06d 100644 --- a/src/constants/analytics.js +++ b/src/constants/analytics.js @@ -23,7 +23,6 @@ const GaActions = { SUBMITTED: "Submitted", DISPLAYED: "Displayed", CASH_QUOTE_DISPLAYED: "Cash quote displayed", - SERVICE_PACKAGE_PRICE: "Service package price", MOBILE_AVAILABLE: "mobile_available", SHOPS_FIRST_DISPLAYED: "shops_first_displayed", MORE_LOCATIONS_CLICKED: "more_locations_clicked", diff --git a/src/constants/store-mutations.js b/src/constants/store-mutations.js index a61720e83..f7e8d9f4b 100644 --- a/src/constants/store-mutations.js +++ b/src/constants/store-mutations.js @@ -69,6 +69,7 @@ const storeMutations = { UPDATE_SETTLED_TENDER_AMOUNT: "updateSettledTenderAmount", UPDATE_IS_RECAL_ACK_OPT_IN: "updateIsRecalAckOptIn", UPDATE_IS_MSR_FEE_APPLICABLE: "updateIsMSRFeeApplicable", + UPDATE_CASH_PRICE_SUBTOTAL: "updateCashPriceSubTotal", // EVENT BUS MUTATIONS ADD_EVENT_TO_BUS: "addEventToBus", diff --git a/src/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue index 6ba5f576b..4cc75f701 100644 --- a/src/digital-components/modal/modal.vue +++ b/src/digital-components/modal/modal.vue @@ -113,12 +113,14 @@ export default { }, onModalOpened() { this.onModalOpenedCallback?.(); + this.$emit("isModalOpened", true); }, onModalClosed() { this.resetButtonStyle(); this.onModalClosedCallback?.(); if (this.suppressPageScroll) document.querySelector("body").classList.remove("prevent-modal-scroll"); + this.$emit("isModalOpened", false); }, openModal() { if (this.suppressPageScroll) diff --git a/src/digital-components/salesforce-webchat/salesforce-webchat.vue b/src/digital-components/salesforce-webchat/salesforce-webchat.vue index 975c639a8..8cffc143f 100644 --- a/src/digital-components/salesforce-webchat/salesforce-webchat.vue +++ b/src/digital-components/salesforce-webchat/salesforce-webchat.vue @@ -65,77 +65,107 @@ export default { } } -.dockableContainer.showDockableContainer .sidebarBody { - button { - background: #0070d1; - color: #fff; - &:hover { - background: #06577c !important; +.dockableContainer { + &.showDockableContainer { + font-family: AvertaRegular; + color: $gray-600; + .sidebarBody { + button { + background: $blue; + color: $white; + &:hover { + background: $blue-700 !important; + } + } + .form-element__help { + color: $red; + } + .embeddedServiceSidebarFormField { + .uiInput { + .uiLabel-left { + color: $gray-600; + } + } + } + .embeddedServiceSidebarButton { + &.uiButton--inverse { + .label { + color: $white; + text-decoration: underline; + } + &:not(:disabled):focus { + background-color: $blue; + } + } + } + .embeddedServiceLiveAgentStateChatAvatar { + &.isLightningOutContext { + .agentIconColor0 { + background-color: $blue; + } + } + } + .embeddedServiceLiveAgentStateChatInputFooter { + .footerMenuWrapper { + .footer-menu-items { + .slds-dropdown__item { + > a { + padding-left: 0; + max-width: 100%; + display: flex; + justify-content: center; + } + } + } + } + } + .embeddedServiceLiveAgentStateChatInputFooter { + .footerMenuWrapper { + .footer-menu-items { + a { + margin: 0 auto; + } + } + } + } + } + .chatHeaderBranding { + background-color: $blue; + } + span { + &.required { + color: $red; + } } } - .embeddedServiceSidebarFormField .uiInput input { - border: 1px solid #8e9292; +} +.embeddedServiceHelpButton { + .helpButton { + .uiButton { + background-color: $blue !important; + } } - .embeddedServiceSidebarButton.uiButton--inverse .label { - color: #fff; - text-decoration: underline; - } - .embeddedServiceLiveAgentStateChatAvatar.isLightningOutContext .agentIconColor0 { - background-color: #0070d1; - } - .embeddedServiceSidebarFormField .uiInput .uiLabel-left { - font-family: AvertaRegular; - color: #727676; - } - .form-element__help { - color: #db0020; - } - .embeddedServiceSidebarFormField .uiInput input { - border: 1px solid #06577c; - } - .embeddedServiceLiveAgentStateChatInputFooter - .footerMenuWrapper - .footer-menu-items - .slds-dropdown__item - > a { - padding-left: 0; - max-width: 100%; - display: flex; - justify-content: center; - } - .embeddedServiceLiveAgentStateChatInputFooter .footerMenuWrapper .footer-menu-items a { - margin: 0 auto; - } - .embeddedServiceSidebarButton.uiButton--inverse:not(:disabled):focus { - background-color: #0070d1; + .embeddedServiceIcon { + display: inline-block !important; } } - -.embeddedServiceHelpButton .helpButton .uiButton { - background-color: #0070d1 !important; -} - -.modalContainer.sidebarMinimized.layout-docked.embeddedServiceSidebar - .embeddedServiceSidebarMinimizedDefaultUI.helpButton, -.modalContainer.sidebarMinimized.layout-docked.embeddedServiceSidebar - .sidebarHeader.minimizedContainer.embeddedServiceSidebarMinimizedDefaultUI { - background-color: #0070d1; - border: 1px solid #0070d1; -} - -.dockableContainer.showDockableContainer .chatHeaderBranding { - background-color: #0070d1; -} - -.dockableContainer.showDockableContainer { - font-family: AvertaRegular; - color: #727676; -} - -.dockableContainer.showDockableContainer span.required { - color: #db0020; -} - -.dockableContainer.showDockableContainer .sidebarBody button:hover { +.modalContainer { + &.sidebarMinimized { + &.layout-docted { + &.embeddedServiceSidebar { + .embeddedServiceSidebarMinimizedDefaultUI { + .helpButton, + .sidebarHeader { + &.minimizedContainer { + .embeddedServiceSidebarMinimizedDefaultUI { + background-color: $blue; + border: 1px solid $blue; + } + } + } + } + } + } + } } diff --git a/src/helpers/heritage-integration/order-helper.js b/src/helpers/heritage-integration/order-helper.js index 1c0922ba0..b74757b1e 100644 --- a/src/helpers/heritage-integration/order-helper.js +++ b/src/helpers/heritage-integration/order-helper.js @@ -73,6 +73,22 @@ export async function saveSession({ if (!store.getters.applicationUser.savedSessionId || shouldAwaitSaveSessionQueue) { await saveSessionPromise; } + + const lineItems = store.getters.order?.lineItems ?? {}; + const combinedLineItems = [ + ...(lineItems.glassParts || []), + ...(lineItems.supportingItems || []), + ...(lineItems.vaps || []), + ...(lineItems.promos || []), + ]; + + const cashPriceSubtotal = combinedLineItems.length + ? baseMixin.methods + .getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, false) + .toFixed(2) + : ""; + + store.commit(storeMutations.UPDATE_CASH_PRICE_SUBTOTAL, cashPriceSubtotal); } export async function submitWorkOrder({ diff --git a/src/layouts/quote/quote.vue b/src/layouts/quote/quote.vue index cf4c8c84e..3ada84dde 100644 --- a/src/layouts/quote/quote.vue +++ b/src/layouts/quote/quote.vue @@ -800,8 +800,10 @@ export default { }); this.$nextTick(() => { const availablePackageNames = this.$refs.servicePackage?.servicePackageAnswers; - var eventLabel = ""; - availablePackageNames?.forEach((tier) => { + var priceLabel = ""; + + if (availablePackageNames?.length > 1 || availablePackageNames?.length == 1) { + var tier = availablePackageNames[0]; if (tier) { let lineItemsToPrice = this.availableLineItems; if (this.isRecalibrationOnOrder && this.shouldHideRecalibration) { @@ -826,14 +828,11 @@ export default { baseMixin.methods.filterOutFees(lineItemsToPrice) ); price += this.$refs.servicePackage.getVapsPrice(tier.value); - const formattedPrice = parseFloat(price).toFixed(2); - eventLabel += tier.value + "_" + formattedPrice; + priceLabel = price.toFixed(2); } - eventLabel += ","; - }); - eventLabel = eventLabel.slice(0, -1); - - this.pushEventToGA("quote", this.GaActions.SERVICE_PACKAGE_PRICE, eventLabel, true); + } + const subTotalLabel = this.Variables?.CASH_SUBTOTAL; + this.pushVariableToDataLayer?.({ [subTotalLabel]: priceLabel }); }); }, async skip(insuranceSelection, packageSelection) { diff --git a/src/layouts/quote/service-package-question/service-package-question.vue b/src/layouts/quote/service-package-question/service-package-question.vue index 64c075408..546bb573a 100644 --- a/src/layouts/quote/service-package-question/service-package-question.vue +++ b/src/layouts/quote/service-package-question/service-package-question.vue @@ -21,6 +21,8 @@ import servicePackageRadioExperiment from "@/experiment-components/service-packa import servicePackageRadioAfterpayExperiment from "@/experiment-components/service-package-radio-for-afterpay"; import { partTypeStrings } from "@/constants/part-type-strings"; import { packageNames } from "@/constants/package-names"; +import store from "@/store"; +import { storeMutations } from "@/constants/store-mutations"; import { shouldFrontWipersBeAvailable, shouldRearWipersBeAvailable, @@ -161,6 +163,7 @@ export default { isInsuranceSelected: this.isInsuranceSelected, }, })); + this.$emit("currentNumberOfPackages", modifiedAnswers.length); return modifiedAnswers; }, diff --git a/src/layouts/schedule/schedule.vue b/src/layouts/schedule/schedule.vue index 3d0b29e36..b47de1fa8 100644 --- a/src/layouts/schedule/schedule.vue +++ b/src/layouts/schedule/schedule.vue @@ -375,6 +375,10 @@ export default { setTimeout(() => { this.$refs.datePicker.showAnotherMonth().then((moreSelectableDatesData) => { this.selectableDatesData = moreSelectableDatesData; + if (selectableDatesData?.days?.length > 0) { + this.selectedDate = selectableDatesData.days[0].date; + } + this.setDisplayWaitList(); }); }, 50); diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue index 4c1c0d031..cc702529a 100644 --- a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue +++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue @@ -34,6 +34,7 @@ export default { isServiceableInshop: Boolean, isServiceableDropoff: Boolean, mobileFeeApplies: Boolean, + zipCode: String, }, computed: { questionText() { @@ -47,6 +48,7 @@ export default { const shouldShowInshop = this.isServiceableInshop; const shouldShowDropoff = this.isServiceableDropoff && !this.$store.getters.damage.isRepair; + const zipCode = this.zipCode; var answers = this.answersFromCms ? this.answersFromCms.filter((answer) => { @@ -87,12 +89,15 @@ export default { watch: { answersToDisplay: { handler(newValue) { - // If there is only one option to display and that option is 'Mobile' then select it - if ( - newValue.length == 1 && - newValue.findIndex((answer) => answer.Name == "Mobile") != -1 - ) { - this.selectedValue = "Mobile"; + // If there is only one option to display and that option is 'Mobile' or 'Inshop' then select it + if (newValue.length == 1) { + const name = newValue[0].Name; + if ( + name === AppointmentTypeStrings.MOBILE || + name === AppointmentTypeStrings.IN_SHOP + ) { + this.selectedValue = name; + } } }, immediate: true, @@ -100,14 +105,14 @@ export default { isMobileOnly: { handler(newValue) { if (newValue) { - this.selectedValue = "Mobile"; + this.selectedValue = AppointmentTypeStrings.MOBILE; } }, }, isInshopOnly: { handler(newValue) { if (newValue) { - this.selectedValue = "Inshop"; + this.selectedValue = AppointmentTypeStrings.IN_SHOP; } }, }, diff --git a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js index 106ed892c..a85b180f9 100644 --- a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js +++ b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.js @@ -8,6 +8,7 @@ export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) { } const zipCodeData = await getZipCodeData(serviceZipCode); + const providerData = await getShopProviderData(serviceZipCode); // Get the Mobile Fee Part const mobileFeePart = await baseMixin.methods.dispatchStoreActionWithLogging( @@ -15,6 +16,7 @@ export async function getPricedMobileFeePart(serviceZipCode, pageNameToLog) { { serviceZipCode: serviceZipCode, serviceZipCodeCtu: zipCodeData.zipCodeCtu, + mobileProviderNumber: providerData.data.mobileProviderNumber, }, pageNameToLog, false diff --git a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.spec.js b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.spec.js index 5bb41c657..06f34f125 100644 --- a/src/layouts/service-location/helpers/service-location-helper/service-location-helper.spec.js +++ b/src/layouts/service-location/helpers/service-location-helper/service-location-helper.spec.js @@ -91,6 +91,7 @@ const mockStoreActionPriceOrderItemsAndSaveServerData = storeActions.PRICE_ORDER_ITEMS_AND_SAVE_SERVER_DATA; const mockStoreActionGetServiceabilityDetails = storeActions.GET_SERVICEABILITY_DETAILS; const mockStoreActionGetShopTimeSlots = storeActions.GET_SHOP_TIME_SLOTS; +const mockStoreActionGetProviders = storeActions.GET_PROVIDERS; const mockGetShopTimeSlotsGoodAvailability = { data: { @@ -236,6 +237,15 @@ jest.mock("@/mixins/base-mixin.js", () => ({ return Promise.resolve(mockGetShopTimeSlotsLowAvailability); } + + if (actionName === mockStoreActionGetProviders) { + return Promise.resolve({ + data: { + mobileProviderNumber: "001820", + shopProviders: [{}], + }, + }); + } }), }, })); diff --git a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue index ac597c72b..c72f7d4dd 100644 --- a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue +++ b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue @@ -97,6 +97,7 @@ export default { "updated-contains-military-base", "updated-bill-to-account-number", "setMobileLocationAndProceed", + "updated-modal-opened", ], data() { return { @@ -228,6 +229,7 @@ export default { }, setModalStatus(isOpened) { this.isModalOpened = isOpened; + this.$emit("updated-modal-opened", isOpened); }, closeModal() { this.modal.closeModal(); diff --git a/src/layouts/service-location/service-location.spec.js b/src/layouts/service-location/service-location.spec.js index 3b20c69f0..3a499b2ca 100644 --- a/src/layouts/service-location/service-location.spec.js +++ b/src/layouts/service-location/service-location.spec.js @@ -343,7 +343,7 @@ describe("service-location.vue", () => { zipCode: "43054", }, isVehicleProtected: true, - isMobileSelected: true, + isMobileSelectedAndModalClosed: true, }; wrapper.vm.mobileLocationQuestions = mobileLocationQuestions; @@ -356,7 +356,7 @@ describe("service-location.vue", () => { zipCode: "61606", }, isVehicleProtected: null, - isMobileSelected: false, + isMobileSelectedAndModalClosed: false, }; // Act @@ -459,7 +459,7 @@ describe("service-location.vue", () => { zipCode: "", }, isVehicleProtected: null, - isMobileSelected: false, + isMobileSelectedAndModalClosed: false, }; wrapper.vm.mobileLocationQuestions = mobileLocationQuestions; const newMobileLocationQuestions = { @@ -471,7 +471,7 @@ describe("service-location.vue", () => { zipCode: "43054", }, isVehicleProtected: true, - isMobileSelected: true, + isMobileSelectedAndModalClosed: true, }; wrapper.vm.closeModalAction = jest.fn(); @@ -521,7 +521,7 @@ describe("service-location.vue", () => { zipCode: "", }, isVehicleProtected: null, - isMobileSelected: false, + isMobileSelectedAndModalClosed: false, mobileFeePart: null, }; wrapper.vm.mobileLocationQuestions = mobileLocationQuestions; @@ -535,7 +535,7 @@ describe("service-location.vue", () => { zipCode: "43081", }, isVehicleProtected: true, - isMobileSelected: true, + isMobileSelectedAndModalClosed: true, mobileFeePart: null, }; @@ -599,7 +599,7 @@ describe("service-location.vue", () => { zipCode: "43081", }, isVehicleProtected: "YesAnswer", - isMobileSelected: true, + isMobileSelectedAndModalClosed: true, }; wrapper.vm.closeModalAction = jest.fn(); diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index fbc02e08f..0ab6531c4 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -79,6 +79,7 @@ :isServiceableDropoff="isServiceableDropoff" :isDisplayed="isAppointmentTypeDisplayed" :mobileFeeApplies="mobileFeeApplies" + :zipCode="zipCode" ref="appointmentTypeQuestion" groupName="appointmentTypeQuestion" cmsWidgetName="AppointmentTypeQuestionWidget" @@ -98,6 +99,7 @@ @updated-serviceability="setServiceabilityDetails" @updated-contains-military-base="setContainsMilitaryBase" @updated-mobile-ctu="setCtuForMobile" + @updated-modal-opened="setMobileLocationModalOpened" validationRules="mobile-location-required" ref="mobileLocationQuestions" linkWidgetName="MobileLocationLinkWidget" @@ -111,14 +113,17 @@ :selectedAppointmentType="selectedAppointmentType" :shopProviderData="shopProviderData" :isDisplayed="isShopQuestionDisplayed" - cmsWidgetName="ShopQuestionWidget" /> + cmsWidgetName="ShopQuestionWidget" + :isInshopOnly="isInshopOnly" /> @@ -172,7 +177,7 @@ const MOBILE_FEE_PART_TYPE = "MOBILE FEE"; // DEFINE VALIDATION RULES defineRule("mobile-location-required", (value) => { if ( - value.isMobileSelected && + value.isMobileSelectedAndModalClosed && (!value.addressQuestions.streetAddress || !value.addressQuestions.city || !value.addressQuestions.state || @@ -211,6 +216,7 @@ export default { billToAccountNumber: null, shopProviderData: null, navigatingForward: false, + mobileLocationModalOpened: false, }; }, async beforeRouteEnter(to, from, next) { @@ -301,7 +307,9 @@ export default { zipCode: this.zipCode, }, isVehicleProtected: this.isVehicleProtected, - isMobileSelected: this.selectedAppointmentType === "Mobile", + isMobileSelectedAndModalClosed: + this.selectedAppointmentType === "Mobile" && + !this.mobileLocationModalOpened, }; }, }, @@ -415,6 +423,11 @@ export default { isNoComp() { return store.getters.order.policy.isNoComp; }, + isInshopOnly() { + return ( + this.isServiceableInshop && !this.isServiceableMobile && !this.isServiceableDropoff + ); + }, }, methods: { arePagePrerequisitesValid() { @@ -502,6 +515,9 @@ export default { setRecycleFeePart(recycleFeePart) { this.recycleFeePart = recycleFeePart; }, + setMobileLocationModalOpened(isModalOpened) { + this.mobileLocationModalOpened = isModalOpened; + }, //creating async function as the computed property can not directly handle asynchronous operations or promises. async setMobileLocationQuestions(newValue) { var newZipCode = newValue.addressQuestions.zipCode; diff --git a/src/layouts/service-location/shop-question/shop-question.vue b/src/layouts/service-location/shop-question/shop-question.vue index 417a3b4c4..5e85b752b 100644 --- a/src/layouts/service-location/shop-question/shop-question.vue +++ b/src/layouts/service-location/shop-question/shop-question.vue @@ -77,6 +77,7 @@ export default { cmsWidgetName: String, validationRules: String, isDisplayed: Boolean, + isInshopOnly: Boolean, }, computed: { questionText() { @@ -233,9 +234,8 @@ export default { // nothing to do with this component. We could mitigate this by showing / hiding this component with v-if but that messes // up the component initialization on page load. async handler() { - this.resetAnswers(); - await nextTick(); + this.resetAnswers(); this.getNextShopsFromList(); }, }, diff --git a/src/mixins/analytics-mixin.js b/src/mixins/analytics-mixin.js index ce18efb5c..05baa9b0b 100644 --- a/src/mixins/analytics-mixin.js +++ b/src/mixins/analytics-mixin.js @@ -29,6 +29,7 @@ import { applicationConfig } from "../constants/application-config"; import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper"; import { routeData } from "@/router/constants/routes"; import { getQuerystringParameter } from "@/helpers/querystring-helper"; +import { Variables } from "../constants/analytics"; export default { methods: { @@ -119,6 +120,10 @@ export default { } }, + async pushVariableToDataLayer(data) { + pushToDataLayerIfDefined(data); + }, + async pushPageViewToGA() { const currentPageName = getPageNameByQueryString(); const pageViewEvent = { @@ -305,6 +310,18 @@ export default { } else { payload.priceSubTotal = ""; } + + // Cash Quote or Cash Price Sub Total + if (isPricingAvailable) { + const subtotal = baseMixin.methods + .getTotalPriceOfAllLineItemsAndChildParts(combinedLineItems, false) + .toFixed(2); + + payload.cashPriceSubTotal = parseFloat(subtotal); + } else { + payload.cashPriceSubTotal = ""; + } + //unverified (in scenarios we don’t display the price) if ( order.payment.isInsurance && @@ -769,6 +786,9 @@ export default { ValueToLogTypes() { return ValueToLogTypes; }, + Variables() { + return Variables; + }, }, }; diff --git a/src/store/index.js b/src/store/index.js index 7832cba42..e3cb7ca2c 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -341,6 +341,10 @@ export const mutations = { updateIsMSRFeeApplicable(state, isMSRFeeApplicable) { state.order.isMSRFeeApplicable = isMSRFeeApplicable; }, + updateCashPriceSubTotal(state, cashPriceSubTotal) { + state.order.cashPriceSubTotal = + cashPriceSubTotal === "" ? null : cashPriceSubTotal.toString(); + }, updateCCToken(state, ccToken) { state.order.payment.ccToken.subscriptionId = ccToken.subscriptionId; state.order.payment.ccToken.expMonth = ccToken.expMonth; @@ -1736,7 +1740,10 @@ export const actions = { return response; }, - getMobileFeePart(context, { payload: { serviceZipCode, serviceZipCodeCtu }, pageNameToLog }) { + getMobileFeePart( + context, + { payload: { serviceZipCode, serviceZipCodeCtu, mobileProviderNumber }, pageNameToLog } + ) { const serviceType = context.getters.damage.isRepair ? "Repair" : "Install"; const facilityType = "Mobile"; const parentAccountNumber = context.getters.payment.parentAccountNumber; @@ -1754,6 +1761,7 @@ export const actions = { order.serviceLocation?.zipCode; var providerNumber = + mobileProviderNumber ?? serviceZipCodeCtu ?? order.serviceLocation?.provider?.providerNumber ?? order.serviceLocation?.zipCodeCtu; @@ -2343,6 +2351,7 @@ export const actions = { lockToken: order.lockToken, isRecalAckOptIn: order.isRecalAckOptIn, isMSRFeeApplicable: order.isMSRFeeApplicable, + cashPriceSubTotal: order.cashPriceSubTotal, }, }, logApiCall: true,