Merge pull request #2512 from Safelite/rlsmerge/2025.05.22-to-develop
Rlsmerge/2025.05.22 to develop
This commit is contained in:
commit
d53b88ee80
30 changed files with 512 additions and 277 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -35,4 +35,9 @@ export const inputButtonProps = {
|
|||
default: false,
|
||||
},
|
||||
suppressError: Boolean,
|
||||
labelBold: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
|
|||
v-if="questionText && answers && answers.length > 0"
|
||||
class="question-text d-flex"
|
||||
:class="{ 'small-question-text': isSmallQuestionText }">
|
||||
<span class="fw-bold w-100">{{ questionText }}</span>
|
||||
<span class="fw-bold w-100" :class="[labelBold ? 'span-bold' : '']">
|
||||
{{ questionText }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="w-100 d-flex justify-content-center">
|
||||
|
|
@ -57,6 +59,7 @@ https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Com
|
|||
:setLastValuePushedToGa="setLastValuePushedToGa"
|
||||
:isInsuranceSelected="isInsuranceSelected"
|
||||
:suppressError="suppressError"
|
||||
:labelBold="labelBold"
|
||||
@buttonEvent="handleButtonEvent"
|
||||
v-model="selectedValues" />
|
||||
<!-- For nested questions -->
|
||||
|
|
@ -141,6 +144,11 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
isInsuranceSelected: Boolean,
|
||||
labelBold: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const propsClone = Object.assign({}, props);
|
||||
|
|
@ -360,6 +368,9 @@ export default {
|
|||
& > span {
|
||||
text-align: center;
|
||||
}
|
||||
.span-bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.small-question-text {
|
||||
&.question-text {
|
||||
|
|
|
|||
|
|
@ -1042,6 +1042,9 @@ export default {
|
|||
min-width: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
&:hover {
|
||||
border: 2px solid $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.unavailable-day {
|
||||
|
|
@ -1083,7 +1086,6 @@ export default {
|
|||
}
|
||||
input[type="radio"]:checked + label:after {
|
||||
background-color: $black;
|
||||
top: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
:for="dropdownId"
|
||||
:aria-label="questionText"
|
||||
class="form-label"
|
||||
:class="[labelBold ? 'label-bold' : '']"
|
||||
v-html="questionText"></label>
|
||||
<select
|
||||
v-model="selectedOption"
|
||||
|
|
@ -61,6 +62,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
labelBold: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const uuid = uuidv4();
|
||||
|
|
@ -133,6 +139,10 @@ export default {
|
|||
color: $black;
|
||||
font-weight: 500;
|
||||
}
|
||||
.label-bold {
|
||||
color: $black;
|
||||
font-weight: 600;
|
||||
}
|
||||
.form-label {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,12 +119,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)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<template>
|
||||
<span class="salesforce-webchat"></span>
|
||||
<span class="salesforce-webchat">
|
||||
<button v-show="hideSalesforceWebchatLaunchButton" class="salesforce-chat-button"></button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -49,12 +51,121 @@ export default {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
.embeddedServiceHelpButton .helpButton .uiButton {
|
||||
background-color: #167cac;
|
||||
font-family: "Arial", sans-serif;
|
||||
<style lang="scss">
|
||||
.salesforce-webchat {
|
||||
.salesforce-chat-button {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xml:space='preserve' id='Layer_2' x='0' y='0' style='enable-background:new 0 0 24 24' version='1.1' viewBox='0 0 24 24'%3E%3Cstyle%3E .st0%7Bfill:%232e6fb6%7D %3C/style%3E%3Ccircle cx='12' cy='12' r='12' class='st0'/%3E%3Cpath d='M18.3 12c0 3.5-2.8 6.3-6.3 6.3-1.2 0-2.4-.4-3.4-1l-2.3.7s-.5.1-.3-.4.5-1.7.7-2.2c-.6-1-1-2.1-1-3.3 0-3.5 2.8-6.3 6.3-6.3 3.5-.1 6.3 2.7 6.3 6.2z' style='fill:%23fff'/%3E%3Ccircle cx='10.2' cy='12' r='.6' class='st0'/%3E%3Ccircle cx='12' cy='12' r='.6' class='st0'/%3E%3Ccircle cx='13.8' cy='12' r='.6' class='st0'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.5rem 1.5rem;
|
||||
background-position: center;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.embeddedServiceHelpButton .helpButton .uiButton:focus {
|
||||
outline: 1px solid #167cac;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.embeddedServiceHelpButton {
|
||||
.helpButton {
|
||||
.uiButton {
|
||||
background-color: $blue !important;
|
||||
}
|
||||
}
|
||||
.embeddedServiceIcon {
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
.modalContainer {
|
||||
&.sidebarMinimized {
|
||||
&.layout-docted {
|
||||
&.embeddedServiceSidebar {
|
||||
.embeddedServiceSidebarMinimizedDefaultUI {
|
||||
.helpButton,
|
||||
.sidebarHeader {
|
||||
&.minimizedContainer {
|
||||
.embeddedServiceSidebarMinimizedDefaultUI {
|
||||
background-color: $blue;
|
||||
border: 1px solid $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
:class="[
|
||||
questionAlignment === 'center' ? 'text-center w-100 mb-5' : '',
|
||||
hideInput ? 'hide-input' : '',
|
||||
labelBold ? 'label-bold' : '',
|
||||
]"
|
||||
v-html="questionText"></label>
|
||||
<div
|
||||
|
|
@ -132,6 +133,11 @@ export default {
|
|||
isRequired: false,
|
||||
},
|
||||
autocomplete: String,
|
||||
labelBold: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const uuid = uuidv4();
|
||||
|
|
@ -251,6 +257,9 @@ export default {
|
|||
color: $black;
|
||||
font-weight: 500;
|
||||
}
|
||||
.label-bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
&.has-search-icon {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
hasIcon
|
||||
validationRules="street-address-required"
|
||||
autocomplete="street-address"
|
||||
@keydown.enter.prevent />
|
||||
@keydown.enter.prevent
|
||||
:labelBold="labelBold" />
|
||||
</div>
|
||||
</div>
|
||||
<transition name="fade" mode="out-in">
|
||||
|
|
@ -27,7 +28,8 @@
|
|||
customInputId="apartmentNumberOrBusinessName"
|
||||
cmsWidgetName="ApartmentNumberOrBusinessNameQuestionWidget"
|
||||
v-model="addressModel.apartmentNumberOrBusinessName"
|
||||
ref="apartmentNumberOrBusinessName" />
|
||||
ref="apartmentNumberOrBusinessName"
|
||||
:labelBold="labelBold" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
|
@ -40,7 +42,8 @@
|
|||
v-model="addressModel.city"
|
||||
ref="city"
|
||||
validationRules="city-required"
|
||||
autocomplete="address-level2" />
|
||||
autocomplete="address-level2"
|
||||
:labelBold="labelBold" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
|
@ -54,7 +57,8 @@
|
|||
ref="state"
|
||||
:options="stateOptions"
|
||||
validationRules="state-required"
|
||||
autocomplete="address-level1" />
|
||||
autocomplete="address-level1"
|
||||
:labelBold="labelBold" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<textboxQuestion
|
||||
|
|
@ -64,7 +68,8 @@
|
|||
ref="zipCode"
|
||||
mask="#####"
|
||||
validationRules="zip-code-required|zip-code-format"
|
||||
autocomplete="postal-code" />
|
||||
autocomplete="postal-code"
|
||||
:labelBold="labelBold" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
|
@ -124,6 +129,11 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
labelBold: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
<template>
|
||||
<div
|
||||
class="funnel-header d-flex justify-content-center align-items-center flex-column"
|
||||
v-if="imageSrc">
|
||||
<!-- HIDDEN FOR 05.08 <salesforceWebchat hideSalesforceWebchatLaunchButton />-->
|
||||
<img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" />
|
||||
<!-- // menu-modal temporarily not in use until the hamburger menu is re-implemented for use with the progress bar. See display: none below. -->
|
||||
<menuModal />
|
||||
<!-- // menu-modal temporarily not in use until the hamburger menu is re-implemented for use with the progress bar. See display: none below. -->
|
||||
<progressBar />
|
||||
<div class="funnel-header" v-if="imageSrc">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="button-container">
|
||||
<!-- HIDDEN FOR 05.08 <salesforceWebchat hideSalesforceWebchatLaunchButton /> -->
|
||||
</div>
|
||||
<div class="site-logo">
|
||||
<img class="logo-image img-fluid" :src="imageSrc" alt="Safelite logo" />
|
||||
</div>
|
||||
<div class="button-container">
|
||||
<!-- // menu-modal temporarily not in use until the hamburger menu is re-implemented for use with the progress bar. See display: none below. -->
|
||||
<menuModal />
|
||||
<!-- // menu-modal temporarily not in use until the hamburger menu is re-implemented for use with the progress bar. See display: none below. -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex w-100">
|
||||
<progressBar />
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="globalAlert in globalAlertMessages" :key="globalAlert.id">
|
||||
<transition name="fade" mode="out-in">
|
||||
|
|
@ -45,7 +53,7 @@ export default {
|
|||
cmsWidgetName: String,
|
||||
hideSalesforceWebchatLaunchButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -100,16 +108,23 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.menu-modal-container) {
|
||||
display: none; // This is temporary until the hamburger menu is re-implemented for use with the progress bar
|
||||
}
|
||||
.funnel-header {
|
||||
position: relative;
|
||||
padding-top: 0.97rem;
|
||||
}
|
||||
padding: 1rem 0;
|
||||
|
||||
.logo-image {
|
||||
width: auto;
|
||||
height: 1.1875rem;
|
||||
.button-container {
|
||||
width: 1.5rem;
|
||||
height: 1.625rem;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: auto;
|
||||
height: 1.1875rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
:deep(.menu-modal-container) {
|
||||
display: none; // This is temporary until the hamburger menu is re-implemented for use with the progress bar
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -11,21 +11,6 @@
|
|||
<div class="bar3"></div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="menu-modal-container">
|
||||
<button
|
||||
aria-hidden="true"
|
||||
tabindex="-1"
|
||||
class="menu-button"
|
||||
type="button"
|
||||
:class="[isActive ? 'active' : '']"
|
||||
@click="toggleModal"
|
||||
aria-label="Hamburger Menu (modal window)">
|
||||
<div class="bar1"></div>
|
||||
<div class="bar2"></div>
|
||||
<div class="bar3"></div>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Modal -->
|
||||
<div
|
||||
class="modal menu-modal fade"
|
||||
data-bs-backdrop="false"
|
||||
|
|
@ -123,9 +108,6 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.menu-modal-container {
|
||||
position: absolute;
|
||||
padding: 1.47rem 0 1.47rem 1.47rem;
|
||||
right: 0;
|
||||
button {
|
||||
border: none;
|
||||
&.menu-button {
|
||||
|
|
@ -164,7 +146,7 @@ export default {
|
|||
}
|
||||
.modal {
|
||||
&.menu-modal {
|
||||
left: auto;
|
||||
//left: auto;
|
||||
height: calc(100% - 56px);
|
||||
top: 56px;
|
||||
border-top: 1px solid $gray-300;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export default {
|
|||
cmsWidgetName: String,
|
||||
lineItems: Array,
|
||||
isAfterpayBreakoutDisplay: Boolean,
|
||||
isInsuranceSelected: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
|
|
@ -72,9 +73,14 @@ export default {
|
|||
afterpayCopyTokens() {
|
||||
if (this.isAfterpayBreakoutDisplay) {
|
||||
// Text for Afterpay Breakout experiment
|
||||
return splitCopyOnCMSPlaceHolder(
|
||||
this.getCmsContent(this.cmsWidgetName, "BodyText2")
|
||||
);
|
||||
var afterpayBannerCopy = this.getCmsContent(this.cmsWidgetName, "BodyText2");
|
||||
afterpayBannerCopy =
|
||||
afterpayBannerCopy &&
|
||||
afterpayBannerCopy.replaceAll(
|
||||
"{custom:paymentType}",
|
||||
this.isInsuranceSelected ? "deductible" : "spending"
|
||||
);
|
||||
return splitCopyOnCMSPlaceHolder(afterpayBannerCopy);
|
||||
} else {
|
||||
return splitCopyOnCMSPlaceHolder(
|
||||
this.getCmsContent(this.cmsWidgetName, "HeaderText")
|
||||
|
|
|
|||
|
|
@ -12,8 +12,30 @@ import baseMixin from "@/mixins/base-mixin.js";
|
|||
import { containsLineItemWithPartType } from "../../helpers/service-package-helper";
|
||||
|
||||
jest.mock("@/store", () => ({
|
||||
commit: jest.fn(),
|
||||
commit: jest.fn(), // Mock store.commit
|
||||
dispatch: jest.fn(),
|
||||
getters: {
|
||||
order: {
|
||||
lineItems: {
|
||||
glassParts: [],
|
||||
supportingItems: [],
|
||||
vaps: [],
|
||||
promos: [],
|
||||
},
|
||||
payment: {
|
||||
insuranceCoverage: {},
|
||||
isInsurance: false,
|
||||
parentAccountNumber: 167132,
|
||||
},
|
||||
},
|
||||
payment: {
|
||||
insuranceCoverage: {},
|
||||
isInsurance: false,
|
||||
},
|
||||
customer: {
|
||||
emailAddress: "test@test.com",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock fetchCmsContentForPage
|
||||
|
|
@ -157,6 +179,7 @@ afterEach(() => {
|
|||
describe("quote.vue", () => {
|
||||
test("IsInsurance false should navigateWithSaving", async () => {
|
||||
//Arrange
|
||||
store.commit = jest.fn(); // Ensure store.commit is mocked
|
||||
store.getters.payment = {
|
||||
insuranceCoverage: {},
|
||||
isInsurance: false,
|
||||
|
|
@ -785,6 +808,7 @@ describe("quote.vue", () => {
|
|||
});
|
||||
test("On forward button action save promos", async () => {
|
||||
//Arrange
|
||||
store.commit = jest.fn(); // Ensure store.commit is mocked
|
||||
store.getters.payment = {
|
||||
insuranceCoverage: {},
|
||||
isInsurance: false,
|
||||
|
|
@ -809,7 +833,8 @@ describe("quote.vue", () => {
|
|||
expect(wrapper.vm.lineItems.promos !== null).toBe(true);
|
||||
});
|
||||
test("On forward button action save supporting lineItems", async () => {
|
||||
//Arrange
|
||||
// Arrange
|
||||
store.commit = jest.fn(); // Ensure store.commit is mocked
|
||||
store.getters.payment = {
|
||||
insuranceCoverage: {},
|
||||
isInsurance: false,
|
||||
|
|
@ -820,6 +845,7 @@ describe("quote.vue", () => {
|
|||
parentAccountNumber: 167132,
|
||||
},
|
||||
};
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
customMountOptions: {
|
||||
router: {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
v-if="showAfterpayBanner"
|
||||
cmsWidgetName="AfterpayModalWidget"
|
||||
:isAfterpayBreakoutDisplay="isAfterpayBreakoutDisplay"
|
||||
:isInsuranceSelected="isInsuranceSelected"
|
||||
:lineItems="lineItems" />
|
||||
|
||||
<!-- If vehicle requires recal AND we are hiding recal pricing info, then show recal disclaimer banner. -->
|
||||
|
|
@ -168,6 +169,7 @@ import {
|
|||
} from "@/helpers/heritage-integration/cookie-helper";
|
||||
import { showFmgLoadingModal } from "@/helpers/loading-modal-helper";
|
||||
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
||||
import { storeMutations } from "@/constants/store-mutations";
|
||||
|
||||
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
|
||||
|
||||
|
|
@ -588,7 +590,7 @@ export default {
|
|||
},
|
||||
showAfterpayBanner() {
|
||||
return (
|
||||
!this.isInsuranceSelected &&
|
||||
(this.isAfterpayBreakoutDisplay || !this.isInsuranceSelected) &&
|
||||
(!this.isRecalibrationOnOrder || !this.shouldHideRecalibration)
|
||||
);
|
||||
},
|
||||
|
|
@ -766,6 +768,22 @@ export default {
|
|||
this.$route
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
attachCustomEventsForAnalytics() {
|
||||
this.prependActionToMethod(this, this.forwardButtonAction, () => {
|
||||
|
|
@ -789,8 +807,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) {
|
||||
|
|
@ -815,14 +835,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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
v-model="selectedValue"
|
||||
:suppressError="suppressError"
|
||||
:validationRules="validationRules"
|
||||
isRequired />
|
||||
isRequired
|
||||
:labelBold="labelBold" />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
|
@ -34,6 +35,12 @@ export default {
|
|||
isServiceableInshop: Boolean,
|
||||
isServiceableDropoff: Boolean,
|
||||
mobileFeeApplies: Boolean,
|
||||
labelBold: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
zipCode: String,
|
||||
},
|
||||
computed: {
|
||||
questionText() {
|
||||
|
|
@ -47,6 +54,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 +95,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 +111,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;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: [{}],
|
||||
},
|
||||
});
|
||||
}
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -189,41 +189,7 @@ describe("mobile-location-modal-questions.vue", () => {
|
|||
expect(wrapper.vm.internalModel.serviceZipCode).toEqual("55555");
|
||||
});
|
||||
|
||||
it("Should open the modal when the 'Enter your service address' link is clicked", async () => {
|
||||
// Arrange
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
streetAddress: "555 Some St",
|
||||
apartmentNumberOrBusinessName: "Apt 1",
|
||||
city: "Funkytown",
|
||||
state: "OH",
|
||||
zipCode: "55555",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
serviceZipCode: "55555",
|
||||
};
|
||||
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
modelValue: mobileLocationQuestions,
|
||||
},
|
||||
mountOptions: {
|
||||
attachTo: document.body,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.vm.$refs.MobileLocationModalWidget.openModal = jest.fn();
|
||||
const mobileLocationLink = wrapper.findComponent({ ref: "mobileLocationLink" });
|
||||
|
||||
// // Act
|
||||
await mobileLocationLink.trigger("click-event");
|
||||
|
||||
// // Assert
|
||||
expect(wrapper.vm.$refs.MobileLocationModalWidget.openModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Should emit setMobileLocationAndProceed on setMobileLocation for a valid address and vehicle protection answer", async () => {
|
||||
it("Should emit setMobileLocation on setMobileLocation for a valid address and vehicle protection answer", async () => {
|
||||
// Arrange
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
|
|
@ -258,7 +224,6 @@ describe("mobile-location-modal-questions.vue", () => {
|
|||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
wrapper.vm.$refs.MobileLocationModalWidget.closeModal = jest.fn();
|
||||
|
||||
// Act
|
||||
wrapper.vm.internalModel = newMobileLocationQuestions;
|
||||
|
|
@ -266,13 +231,13 @@ describe("mobile-location-modal-questions.vue", () => {
|
|||
await wrapper.vm.setMobileLocation();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted()["setMobileLocationAndProceed"]).toBeTruthy();
|
||||
expect(wrapper.emitted()["setMobileLocationAndProceed"][0][0]).toStrictEqual(
|
||||
expect(wrapper.emitted()["setMobileLocation"]).toBeTruthy();
|
||||
expect(wrapper.emitted()["setMobileLocation"][0][0]).toStrictEqual(
|
||||
newMobileLocationQuestions
|
||||
);
|
||||
});
|
||||
|
||||
it("Should not emit setMobileLocationAndProceed on setMobileLocation for an invalid address zip code", async () => {
|
||||
it("Should not emit setMobileLocation on setMobileLocation for an invalid address zip code", async () => {
|
||||
// Arrange
|
||||
const mobileLocationQuestions = {
|
||||
addressQuestions: {
|
||||
|
|
@ -307,15 +272,13 @@ describe("mobile-location-modal-questions.vue", () => {
|
|||
},
|
||||
attachTo: document.body,
|
||||
});
|
||||
wrapper.vm.$refs.MobileLocationModalWidget.closeModal = jest.fn();
|
||||
wrapper.vm.resetModalButtonStyle = jest.fn();
|
||||
|
||||
// Act
|
||||
wrapper.vm.internalModel = newMobileLocationQuestions;
|
||||
await wrapper.vm.setMobileLocation();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("setMobileLocationAndProceed")).not.toBeTruthy();
|
||||
expect(wrapper.emitted("setMobileLocation")).not.toBeTruthy();
|
||||
});
|
||||
|
||||
it("Should display invalid zip alert for invalid address zip inputs", async () => {
|
||||
|
|
|
|||
|
|
@ -1,52 +1,48 @@
|
|||
<template>
|
||||
<div class="mobile-location-questions">
|
||||
<div class="text-center" :id="componentId">
|
||||
<label
|
||||
for="mobileLocationLinkPromptId"
|
||||
:aria-label="mobileLocationLinkPromptText"
|
||||
class="form-label fw-bold w-100 ps-4 pe-4 pt-4 text-black"
|
||||
v-html="mobileLocationLinkPromptText"></label>
|
||||
<div class="update-mobile-location-text-link">
|
||||
<textLink
|
||||
ref="mobileLocationLink"
|
||||
id="mobileLocationLinkPromptId"
|
||||
linkType="text"
|
||||
:text="mobileLocationLinkText"
|
||||
href="#!"
|
||||
@click-event="openModal" />
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="mobile-location-questions">
|
||||
<div class="text-center" :id="componentId">
|
||||
<label
|
||||
for="mobileLocationLinkPromptId"
|
||||
:aria-label="mobileLocationLinkPromptText"
|
||||
class="form-label fw-bold w-100 ps-4 pe-4 pt-4 text-black"
|
||||
v-html="mobileLocationLinkPromptText"></label>
|
||||
<div class="update-mobile-location-text-link">
|
||||
<textLink
|
||||
ref="mobileLocationLink"
|
||||
id="mobileLocationLinkPromptId"
|
||||
linkType="text"
|
||||
:text="mobileLocationLinkText"
|
||||
href="#!"
|
||||
@click-event="toggleMobileLocation" />
|
||||
</div>
|
||||
<div v-show="errorMessage" class="row my-1 form-test-error">
|
||||
<span
|
||||
class="d-inline-flex small mt-0 center-error-message"
|
||||
aria-atomic="true"
|
||||
aria-live="polite">
|
||||
{{ errorMessage }}
|
||||
</span>
|
||||
</div>
|
||||
<textBlock
|
||||
v-if="mobileFeeApplies"
|
||||
:customText="mobileFeeText"
|
||||
cmsWidgetName="MobileFeeDisclaimerWidget"
|
||||
typeStyle="caption" />
|
||||
</div>
|
||||
<div v-show="errorMessage" class="row my-1 form-test-error">
|
||||
<span
|
||||
class="d-inline-flex small mt-0 center-error-message"
|
||||
aria-atomic="true"
|
||||
aria-live="polite">
|
||||
{{ errorMessage }}
|
||||
</span>
|
||||
</div>
|
||||
<textBlock
|
||||
v-if="mobileFeeApplies"
|
||||
:customText="mobileFeeText"
|
||||
cmsWidgetName="MobileFeeDisclaimerWidget"
|
||||
typeStyle="caption" />
|
||||
</div>
|
||||
<modal
|
||||
:ref="modalName"
|
||||
:headerText="modalHeaderText"
|
||||
:footerButtonText="modalFooterText"
|
||||
:onModalOpenedCallback="onModalOpened"
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
@isModalOpened="setModalStatus"
|
||||
@footer-button-event="setMobileLocation">
|
||||
<template v-if="isModalOpened">
|
||||
<div v-if="isMobileLocationOpened" class="address-questions-container">
|
||||
<div v-html="headerText" class="HeaderText" />
|
||||
<addressQuestions
|
||||
ref="addressQuestions"
|
||||
v-model="internalModel.addressQuestions"
|
||||
captureApartmentNumberOrBusinessName="true"
|
||||
preserveCityAndStateOnReset="true" />
|
||||
preserveCityAndStateOnReset="true"
|
||||
labelBold="true" />
|
||||
<vehicleProtectedQuestion
|
||||
ref="vehicleProtectedQuestion"
|
||||
v-model="internalModel.isVehicleProtected"
|
||||
cmsWidgetName="VehicleProtectedQuestionWidget" />
|
||||
cmsWidgetName="VehicleProtectedQuestionWidget"
|
||||
labelBold="true" />
|
||||
<textBlock cmsWidgetName="WorkspaceRequirementsWidget" typeStyle="caption" />
|
||||
<alert
|
||||
ref="alertInvalidZip"
|
||||
|
|
@ -62,16 +58,14 @@
|
|||
cmsWidgetName="AlertMismatchStateAndZipWidget"
|
||||
alertClass="alert-danger"
|
||||
v-bind:isDismissible="false" />
|
||||
</template>
|
||||
</modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Components
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import textBlock from "@/digital-components/text-block/text-block";
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import alert from "@/ux-components/alert/alert";
|
||||
import addressQuestions from "@/fmg-components/address-questions/address-questions";
|
||||
import vehicleProtectedQuestion from "@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question";
|
||||
|
|
@ -96,13 +90,14 @@ export default {
|
|||
"updated-mobile-fee-part",
|
||||
"updated-contains-military-base",
|
||||
"updated-bill-to-account-number",
|
||||
"setMobileLocationAndProceed",
|
||||
"setMobileLocation",
|
||||
"set-mobile-location-invalid",
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
internalModel: deepClone(this.modelValue),
|
||||
displayInvalidZipAlert: false,
|
||||
isModalOpened: false,
|
||||
isMobileLocationOpened: false,
|
||||
displayMismatchStateAndZipAlert: false,
|
||||
};
|
||||
},
|
||||
|
|
@ -188,23 +183,29 @@ export default {
|
|||
this.mobileFeePart.kitPrice
|
||||
);
|
||||
},
|
||||
modalName() {
|
||||
return "MobileLocationModalWidget";
|
||||
},
|
||||
modalHeaderText() {
|
||||
headerText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "HeaderText");
|
||||
},
|
||||
modalFooterText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText");
|
||||
},
|
||||
modal() {
|
||||
return this.$refs[this.modalName];
|
||||
},
|
||||
addressModel: {
|
||||
get: function () {
|
||||
return this.modelValue.addressQuestions;
|
||||
},
|
||||
},
|
||||
isMobileAddressAvailable: {
|
||||
get: function () {
|
||||
return (
|
||||
this.addressModel.streetAddress &&
|
||||
this.addressModel.streetAddress !== "" &&
|
||||
this.addressModel.city &&
|
||||
this.addressModel.city !== "" &&
|
||||
this.addressModel.state &&
|
||||
this.addressModel.state !== "" &&
|
||||
this.addressModel.zipCode &&
|
||||
this.addressModel.zipCode !== "" &&
|
||||
this.internalModel.isVehicleProtected !== null
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isMobileAddressComplete() {
|
||||
|
|
@ -220,24 +221,14 @@ export default {
|
|||
this.internalModel.isVehicleProtected !== null
|
||||
);
|
||||
},
|
||||
openModal() {
|
||||
this.modal.openModal();
|
||||
getIsMobleLocationOpened() {
|
||||
if (this.isServiceAddressFromStoreAvailable) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onModalOpened() {
|
||||
this.internalModel = deepClone(this.modelValue);
|
||||
},
|
||||
setModalStatus(isOpened) {
|
||||
this.isModalOpened = isOpened;
|
||||
},
|
||||
closeModal() {
|
||||
this.modal.closeModal();
|
||||
},
|
||||
onModalClosed() {
|
||||
this.resetAlerts();
|
||||
this.internalModel = deepClone(this.modelValue);
|
||||
},
|
||||
resetModalButtonStyle() {
|
||||
this.modal.resetButtonStyle();
|
||||
setMobileLocationInvalid(isFormInvalid) {
|
||||
this.$emit("set-mobile-location-invalid", isFormInvalid);
|
||||
},
|
||||
resetAlerts() {
|
||||
this.displayInvalidZipAlert = false;
|
||||
|
|
@ -246,6 +237,10 @@ export default {
|
|||
async setMobileLocation() {
|
||||
this.resetAlerts();
|
||||
// Validate the Zip Code
|
||||
if (this.internalModel.addressQuestions.zipCode === "") {
|
||||
this.setMobileLocationInvalid(true);
|
||||
return;
|
||||
}
|
||||
const zipCodeData = await this.getZipCodeData(
|
||||
this.internalModel.addressQuestions.zipCode,
|
||||
"service-location"
|
||||
|
|
@ -253,11 +248,11 @@ export default {
|
|||
|
||||
if (!zipCodeData.isValid) {
|
||||
this.displayInvalidZipAlert = true;
|
||||
this.resetModalButtonStyle();
|
||||
this.setMobileLocationInvalid(true);
|
||||
return;
|
||||
} else if (zipCodeData.state != this.internalModel.addressQuestions.state) {
|
||||
this.displayMismatchStateAndZipAlert = true;
|
||||
this.resetModalButtonStyle();
|
||||
this.setMobileLocationInvalid(true);
|
||||
return;
|
||||
} else if (
|
||||
this.internalModel.addressQuestions.zipCode !==
|
||||
|
|
@ -296,10 +291,21 @@ export default {
|
|||
this.$emit("updated-bill-to-account-number", billToAccountNumber);
|
||||
}
|
||||
// update the page level model
|
||||
this.$emit("setMobileLocationAndProceed", this.internalModel);
|
||||
this.$emit("setMobileLocation", this.internalModel);
|
||||
this.setMobileLocationInvalid(false);
|
||||
},
|
||||
toggleMobileLocation() {
|
||||
this.isMobileLocationOpened = !this.isMobileLocationOpened;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
internalModel: {
|
||||
handler() {
|
||||
this.setMobileLocation();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
|
||||
modelValue: {
|
||||
handler(newValue) {
|
||||
this.internalModel = deepClone(newValue);
|
||||
|
|
@ -308,15 +314,11 @@ export default {
|
|||
deep: true,
|
||||
},
|
||||
},
|
||||
unmounted() {
|
||||
if (this.isModalOpened) this.closeModal();
|
||||
},
|
||||
components: {
|
||||
textLink,
|
||||
modal,
|
||||
addressQuestions,
|
||||
vehicleProtectedQuestion,
|
||||
textBlock,
|
||||
textLink,
|
||||
alert,
|
||||
},
|
||||
};
|
||||
|
|
@ -327,28 +329,34 @@ export default {
|
|||
.center-error-message {
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
.update-zip-text-link::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 13px;
|
||||
height: 16px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.49635 1.00142e-07C5.64734 -0.000153295 4.80722 0.175918 4.0274 0.517444C3.24757 0.858969 2.54443 1.35877 1.96099 1.98626C0.765713 3.27588 0.0999756 4.98141 0.0999756 6.75394C0.0999756 8.52646 0.765713 10.232 1.96099 11.5216L5.98324 15.777C6.04954 15.8475 6.12918 15.9036 6.21736 15.9419C6.30555 15.9802 6.40045 16 6.49635 16C6.59225 16 6.68716 15.9802 6.77534 15.9419C6.86353 15.9036 6.94317 15.8475 7.00946 15.777L11.0317 11.52C12.2391 10.2383 12.909 8.52914 12.8999 6.75394C12.9094 4.97818 12.2394 3.26832 11.0317 1.98626C10.4481 1.35899 9.74493 0.859347 8.96514 0.517839C8.18535 0.176331 7.34532 0.000130509 6.49635 1.00142e-07V1.00142e-07ZM6.49635 9.13131C6.02507 9.13131 5.56437 8.98913 5.17251 8.72275C4.78065 8.45637 4.47524 8.07776 4.29488 7.63479C4.11453 7.19181 4.06734 6.70438 4.15928 6.23412C4.25123 5.76387 4.47817 5.33191 4.81142 4.99287C5.14467 4.65384 5.56925 4.42295 6.03148 4.32941C6.49371 4.23587 6.97282 4.28388 7.40823 4.46736C7.84364 4.65085 8.21579 4.96157 8.47762 5.36023C8.73945 5.7589 8.87921 6.2276 8.87921 6.70707C8.87921 7.34974 8.62837 7.96611 8.18185 8.4207C7.73532 8.87528 7.12964 9.13088 6.49794 9.13131H6.49635Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
.update-zip-text-link::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 13px;
|
||||
height: 16px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 13 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M6.49635 1.00142e-07C5.64734 -0.000153295 4.80722 0.175918 4.0274 0.517444C3.24757 0.858969 2.54443 1.35877 1.96099 1.98626C0.765713 3.27588 0.0999756 4.98141 0.0999756 6.75394C0.0999756 8.52646 0.765713 10.232 1.96099 11.5216L5.98324 15.777C6.04954 15.8475 6.12918 15.9036 6.21736 15.9419C6.30555 15.9802 6.40045 16 6.49635 16C6.59225 16 6.68716 15.9802 6.77534 15.9419C6.86353 15.9036 6.94317 15.8475 7.00946 15.777L11.0317 11.52C12.2391 10.2383 12.909 8.52914 12.8999 6.75394C12.9094 4.97818 12.2394 3.26832 11.0317 1.98626C10.4481 1.35899 9.74493 0.859347 8.96514 0.517839C8.18535 0.176331 7.34532 0.000130509 6.49635 1.00142e-07V1.00142e-07ZM6.49635 9.13131C6.02507 9.13131 5.56437 8.98913 5.17251 8.72275C4.78065 8.45637 4.47524 8.07776 4.29488 7.63479C4.11453 7.19181 4.06734 6.70438 4.15928 6.23412C4.25123 5.76387 4.47817 5.33191 4.81142 4.99287C5.14467 4.65384 5.56925 4.42295 6.03148 4.32941C6.49371 4.23587 6.97282 4.28388 7.40823 4.46736C7.84364 4.65085 8.21579 4.96157 8.47762 5.36023C8.73945 5.7589 8.87921 6.2276 8.87921 6.70707C8.87921 7.34974 8.62837 7.96611 8.18185 8.4207C7.73532 8.87528 7.12964 9.13088 6.49794 9.13131H6.49635Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.address-questions {
|
||||
margin-top: 0em;
|
||||
}
|
||||
.update-mobile-location-text-link {
|
||||
white-space: pre-line;
|
||||
}
|
||||
.address-questions-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.update-mobile-location-text-link {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.text-black {
|
||||
color: $black;
|
||||
.text-black {
|
||||
color: $black;
|
||||
}
|
||||
.HeaderText {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.625rem;
|
||||
color: $black;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
textPosition="text-center"
|
||||
v-model="selectedValue"
|
||||
validationRules="option-required"
|
||||
isRequired />
|
||||
isRequired
|
||||
:labelBold="labelBold" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -27,6 +28,11 @@ export default {
|
|||
isVehicleProtected: Boolean,
|
||||
},
|
||||
cmsWidgetName: String,
|
||||
labelBold: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
buttonQuestion,
|
||||
|
|
|
|||
|
|
@ -343,7 +343,6 @@ describe("service-location.vue", () => {
|
|||
zipCode: "43054",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
isMobileSelected: true,
|
||||
};
|
||||
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
|
||||
|
||||
|
|
@ -356,7 +355,6 @@ describe("service-location.vue", () => {
|
|||
zipCode: "61606",
|
||||
},
|
||||
isVehicleProtected: null,
|
||||
isMobileSelected: false,
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -459,7 +457,6 @@ describe("service-location.vue", () => {
|
|||
zipCode: "",
|
||||
},
|
||||
isVehicleProtected: null,
|
||||
isMobileSelected: false,
|
||||
};
|
||||
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
|
||||
const newMobileLocationQuestions = {
|
||||
|
|
@ -471,15 +468,14 @@ describe("service-location.vue", () => {
|
|||
zipCode: "43054",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
isMobileSelected: true,
|
||||
};
|
||||
|
||||
wrapper.vm.closeModalAction = jest.fn();
|
||||
//wrapper.vm.closeModalAction = jest.fn();
|
||||
// Act
|
||||
|
||||
// Trigger the event
|
||||
await mobileLocationQuestionsComponent.vm.$emit(
|
||||
"setMobileLocationAndProceed",
|
||||
"setMobileLocation",
|
||||
newMobileLocationQuestions
|
||||
);
|
||||
|
||||
|
|
@ -521,7 +517,7 @@ describe("service-location.vue", () => {
|
|||
zipCode: "",
|
||||
},
|
||||
isVehicleProtected: null,
|
||||
isMobileSelected: false,
|
||||
isMobileSelectedAndModalClosed: false,
|
||||
mobileFeePart: null,
|
||||
};
|
||||
wrapper.vm.mobileLocationQuestions = mobileLocationQuestions;
|
||||
|
|
@ -535,7 +531,7 @@ describe("service-location.vue", () => {
|
|||
zipCode: "43081",
|
||||
},
|
||||
isVehicleProtected: true,
|
||||
isMobileSelected: true,
|
||||
isMobileSelectedAndModalClosed: true,
|
||||
mobileFeePart: null,
|
||||
};
|
||||
|
||||
|
|
@ -556,7 +552,7 @@ describe("service-location.vue", () => {
|
|||
|
||||
// Trigger the event
|
||||
await mobileLocationQuestionsComponent.vm.$emit(
|
||||
"setMobileLocationAndProceed",
|
||||
"setMobileLocation",
|
||||
newMobileLocationQuestions
|
||||
);
|
||||
|
||||
|
|
@ -599,15 +595,12 @@ describe("service-location.vue", () => {
|
|||
zipCode: "43081",
|
||||
},
|
||||
isVehicleProtected: "YesAnswer",
|
||||
isMobileSelected: true,
|
||||
isMobileSelectedAndModalClosed: true,
|
||||
};
|
||||
|
||||
wrapper.vm.closeModalAction = jest.fn();
|
||||
// Act
|
||||
mobileLocationQuestionsComponent.vm.$emit(
|
||||
"setMobileLocationAndProceed",
|
||||
mobileLocationQuestions
|
||||
);
|
||||
mobileLocationQuestionsComponent.vm.$emit("setMobileLocation", mobileLocationQuestions);
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.selectedAppointmentType).toStrictEqual("Mobile");
|
||||
|
|
|
|||
|
|
@ -79,10 +79,12 @@
|
|||
:isServiceableDropoff="isServiceableDropoff"
|
||||
:isDisplayed="isAppointmentTypeDisplayed"
|
||||
:mobileFeeApplies="mobileFeeApplies"
|
||||
:zipCode="zipCode"
|
||||
ref="appointmentTypeQuestion"
|
||||
groupName="appointmentTypeQuestion"
|
||||
cmsWidgetName="AppointmentTypeQuestionWidget"
|
||||
validationRules="option-required" />
|
||||
validationRules="option-required"
|
||||
labelBold="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
|
|
@ -102,7 +104,8 @@
|
|||
ref="mobileLocationQuestions"
|
||||
linkWidgetName="MobileLocationLinkWidget"
|
||||
modalWidgetName="MobileLocationModalWidget"
|
||||
@setMobileLocationAndProceed="setMobileLocationQuestions" />
|
||||
@setMobileLocation="setMobileLocationQuestions"
|
||||
@set-mobile-location-invalid="setMobileLocationInValid" />
|
||||
|
||||
<shopQuestion
|
||||
ref="shopQuestion"
|
||||
|
|
@ -111,15 +114,14 @@
|
|||
:selectedAppointmentType="selectedAppointmentType"
|
||||
:shopProviderData="shopProviderData"
|
||||
:isDisplayed="isShopQuestionDisplayed"
|
||||
cmsWidgetName="ShopQuestionWidget"
|
||||
:isInshopOnly="isInshopOnly" />
|
||||
cmsWidgetName="ShopQuestionWidget" />
|
||||
|
||||
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
|
||||
|
||||
<navbar
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="navbar"
|
||||
:isForwardActionDisabled="!meta.valid || displayNoShopsAlert"
|
||||
:isForwardActionDisabled="!meta.valid || isForwardActionDisabled"
|
||||
@back-clicked="backButtonAction"
|
||||
@ForwardClicked="forwardButtonAction" />
|
||||
</div>
|
||||
|
|
@ -173,12 +175,12 @@ const MOBILE_FEE_PART_TYPE = "MOBILE FEE";
|
|||
// DEFINE VALIDATION RULES
|
||||
defineRule("mobile-location-required", (value) => {
|
||||
if (
|
||||
value.isMobileSelected &&
|
||||
(!value.addressQuestions.streetAddress ||
|
||||
!value.addressQuestions.city ||
|
||||
!value.addressQuestions.state ||
|
||||
!value.addressQuestions.zipCode ||
|
||||
!value.isVehicleProtected)
|
||||
!value.isVehicleProtected) &&
|
||||
value.isMobileSelected
|
||||
) {
|
||||
return errorMessages.MOBILE_LOCATION_REQUIRED;
|
||||
}
|
||||
|
|
@ -212,6 +214,7 @@ export default {
|
|||
billToAccountNumber: null,
|
||||
shopProviderData: null,
|
||||
navigatingForward: false,
|
||||
isMobileAddressValid: true,
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
|
|
@ -302,7 +305,6 @@ export default {
|
|||
zipCode: this.zipCode,
|
||||
},
|
||||
isVehicleProtected: this.isVehicleProtected,
|
||||
isMobileSelected: this.selectedAppointmentType === "Mobile",
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
@ -416,10 +418,8 @@ export default {
|
|||
isNoComp() {
|
||||
return store.getters.order.policy.isNoComp;
|
||||
},
|
||||
isInshopOnly() {
|
||||
return (
|
||||
this.isServiceableInshop && !this.isServiceableMobile && !this.isServiceableDropoff
|
||||
);
|
||||
isForwardActionDisabled() {
|
||||
return this.displayNoShopsAlert || !this.isMobileAddressValid;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -523,10 +523,7 @@ export default {
|
|||
}
|
||||
if (this.isServiceableMobile) {
|
||||
this.setMobileLocation(newValue);
|
||||
//proceed to next page
|
||||
this.forwardButtonAction();
|
||||
} else {
|
||||
this.closeModalAction("mobileLocationQuestions");
|
||||
await getZipCodeData(newZipCode).then((zipCodeData) => {
|
||||
this.serviceZipCodeQuestion = {
|
||||
state: zipCodeData.state,
|
||||
|
|
@ -694,7 +691,7 @@ export default {
|
|||
},
|
||||
async forwardButtonAction() {
|
||||
this.navigatingForward = true;
|
||||
|
||||
var ctu = this.zipCodeCtu;
|
||||
if (
|
||||
this.selectedAppointmentType == AppointmentTypeStrings.MOBILE &&
|
||||
this.selectedProvider &&
|
||||
|
|
@ -705,18 +702,13 @@ export default {
|
|||
this.selectedProvider.address.state = null;
|
||||
this.selectedProvider.address.zipCode = null;
|
||||
this.selectedProvider.address.zipCodeCtu = null;
|
||||
}
|
||||
|
||||
var ctu = this.zipCodeCtu;
|
||||
if (this.selectedAppointmentType == AppointmentTypeStrings.IN_SHOP) {
|
||||
} else {
|
||||
// if we have a provider.zipCodeCtu that's different than the servicelocation.zipCodeCtu then they
|
||||
// may have selected a shop in a different ctu. change the servicelocation zip/ctu if different
|
||||
if (this.zipCodeCtu != this.selectedProvider.address.zipCodeCtu) {
|
||||
ctu = this.selectedProvider.address.zipCodeCtu;
|
||||
}
|
||||
|
||||
this.city = null;
|
||||
this.streetAddress = null;
|
||||
this.resetMobileLocation();
|
||||
}
|
||||
|
||||
await this.dispatchStoreAction(
|
||||
|
|
@ -756,11 +748,15 @@ export default {
|
|||
this.updateAndSaveIsMSRFeeApplicable();
|
||||
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
|
||||
},
|
||||
openModalAction(modalName) {
|
||||
this.$refs[modalName].openModal();
|
||||
setMobileLocationInValid(isMobileLocationInValid) {
|
||||
this.isMobileAddressValid = !isMobileLocationInValid;
|
||||
},
|
||||
closeModalAction(modalName) {
|
||||
this.$refs[modalName].closeModal();
|
||||
displayMobileAddressQuestion(openMobileLocation) {
|
||||
var mobileLocationQuestionsRef = this.$refs.mobileLocationQuestions;
|
||||
var isMobileAddressComplete = mobileLocationQuestionsRef.isMobileAddressComplete;
|
||||
if (isMobileAddressComplete) {
|
||||
mobileLocationQuestionsRef.isMobileLocationOpened = openMobileLocation;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -773,8 +769,10 @@ export default {
|
|||
this.selectedProvider = new Provider(
|
||||
this.shopProviderData.mobileProviderNumber
|
||||
);
|
||||
this.displayMobileAddressQuestion(true);
|
||||
} else {
|
||||
this.selectedProvider = new Provider();
|
||||
this.displayMobileAddressQuestion(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -786,12 +784,10 @@ export default {
|
|||
this.selectedProvider = new Provider(
|
||||
this.shopProviderData.mobileProviderNumber
|
||||
);
|
||||
var mobileLocationQs = this.$refs.mobileLocationQuestions;
|
||||
if (!mobileLocationQs.isMobileAddressComplete()) {
|
||||
mobileLocationQs.openModal();
|
||||
}
|
||||
this.displayMobileAddressQuestion(true);
|
||||
} else {
|
||||
this.selectedProvider = new Provider();
|
||||
this.displayMobileAddressQuestion(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ export default {
|
|||
cmsWidgetName: String,
|
||||
validationRules: String,
|
||||
isDisplayed: Boolean,
|
||||
isInshopOnly: Boolean,
|
||||
},
|
||||
computed: {
|
||||
questionText() {
|
||||
|
|
@ -234,13 +233,9 @@ 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() {
|
||||
//We do not need to run this handler in case of Inshop only else it will ended running twice function getNextShopsFromList and will cause pagination
|
||||
if (!this.isInshopOnly) {
|
||||
this.resetAnswers();
|
||||
|
||||
await nextTick();
|
||||
this.getNextShopsFromList();
|
||||
}
|
||||
await nextTick();
|
||||
this.resetAnswers();
|
||||
this.getNextShopsFromList();
|
||||
},
|
||||
},
|
||||
shopProviders: {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
|||
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
||||
import router from "@/router/index.js";
|
||||
import { getQuerystringParameter } from "@/helpers/querystring-helper";
|
||||
import { Variables } from "../constants/analytics";
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
|
|
@ -120,6 +121,10 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
async pushVariableToDataLayer(data) {
|
||||
pushToDataLayerIfDefined(data);
|
||||
},
|
||||
|
||||
async pushPageViewToGA() {
|
||||
const currentPageName = getPageNameByQueryString();
|
||||
const pageViewEvent = {
|
||||
|
|
@ -306,6 +311,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 &&
|
||||
|
|
@ -770,6 +787,9 @@ export default {
|
|||
ValueToLogTypes() {
|
||||
return ValueToLogTypes;
|
||||
},
|
||||
Variables() {
|
||||
return Variables;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -340,6 +340,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;
|
||||
|
|
@ -1732,7 +1736,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;
|
||||
|
|
@ -1750,6 +1757,7 @@ export const actions = {
|
|||
order.serviceLocation?.zipCode;
|
||||
|
||||
var providerNumber =
|
||||
mobileProviderNumber ??
|
||||
serviceZipCodeCtu ??
|
||||
order.serviceLocation?.provider?.providerNumber ??
|
||||
order.serviceLocation?.zipCodeCtu;
|
||||
|
|
@ -2339,6 +2347,7 @@ export const actions = {
|
|||
lockToken: order.lockToken,
|
||||
isRecalAckOptIn: order.isRecalAckOptIn,
|
||||
isMSRFeeApplicable: order.isMSRFeeApplicable,
|
||||
cashPriceSubTotal: order.cashPriceSubTotal,
|
||||
},
|
||||
},
|
||||
logApiCall: true,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<div
|
||||
:aria-label="buttonLabel"
|
||||
class="button-content list-button-content d-flex flex-column justify-content-center py-3 px-4 p-md-4">
|
||||
<span class="m-0" :class="textPosition">
|
||||
<span class="m-0" :class="[textPosition, labelBold ? 'span-bold' : '']">
|
||||
{{ buttonLabel }}
|
||||
</span>
|
||||
<span v-if="buttonLabelSubCopy" class="m-0 small" :class="textPosition">
|
||||
|
|
@ -88,6 +88,9 @@ export default {
|
|||
&:checked + .list-button-content span {
|
||||
font-weight: 500;
|
||||
}
|
||||
&:checked + .list-button-content .span-bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
&:checked + .list-button-content span:nth-child(2) {
|
||||
font-weight: 400;
|
||||
color: $gray-600;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@
|
|||
:class="!isWide ? 'order-1' : 'ms-auto order-3'"
|
||||
:src="buttonImage"
|
||||
:alt="altText" />
|
||||
<p v-if="!isWide" class="small order-3" :class="isMultiSelect ? 'm-0' : 'mt-2 mb-0'">
|
||||
<p
|
||||
v-if="!isWide"
|
||||
class="small order-3"
|
||||
:class="[isMultiSelect ? 'm-0' : 'mt-2 mb-0', labelBold ? 'p-bold' : '']">
|
||||
{{ buttonLabel }}
|
||||
</p>
|
||||
<p v-if="buttonLabelSubCopy && !isWide" class="fs-7 m-0 order-4 sub-copy">
|
||||
|
|
@ -122,6 +125,9 @@ export default {
|
|||
color: $black;
|
||||
font-weight: 500;
|
||||
}
|
||||
.p-bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sub-copy {
|
||||
color: $gray-600;
|
||||
|
|
|
|||
Loading…
Reference in a new issue