Merge branch 'develop' into feature/CSR-886
This commit is contained in:
commit
9179506df8
31 changed files with 2264 additions and 159 deletions
|
|
@ -18,6 +18,7 @@ module.exports = {
|
|||
"!src/common-components/date-picker/**/*.vue", // Temp until unit tests completed
|
||||
"!src/layouts/vin-lookup/**/*.vue", //Temporary for Quote page testing
|
||||
"!src/common-components/funnel-header/menu-modal/**/*.vue",
|
||||
"!src/layouts/schedule/*.vue", // Temp test exclusion while in development
|
||||
// END
|
||||
], // ! means exclude from coverage.
|
||||
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ const endpoints = {
|
|||
url: "/parts/api/v1/parts/mobile-fee",
|
||||
method: "GET",
|
||||
},
|
||||
GetServiceabilityDetails: {
|
||||
url: "/location/api/v1/location/serviceability-details",
|
||||
method: "GET",
|
||||
},
|
||||
GetSupportingItems: {
|
||||
url: "/parts/api/v1/parts/supporting-items",
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ const errorMessages = {
|
|||
"Invalid VIN. Please make sure that you entered the correct 17-digit, alpha-numeric number. VINs do not contain the letters I, O, or Q",
|
||||
OPTION_REQUIRED: "Please select an option",
|
||||
VEHICLE_REQUIRED: "Please select a vehicle",
|
||||
MOBILE_LOCATION_REQUIRED: "Please enter your service address",
|
||||
DATE_REQUIRED: "Please select a date",
|
||||
};
|
||||
|
||||
export { errorMessages };
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const storeActions = {
|
|||
GET_PART_FROM_CAPABILITY_QUESTION_ANSWER: "getPartFromCapabilityQuestionAnswer",
|
||||
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
|
||||
GET_MOBILE_FEE_PART: "getMobileFeePart",
|
||||
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
|
||||
SAVE_SESSION: "saveSession",
|
||||
LOAD_SESSION: "loadSession",
|
||||
UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="date-picker" :class="calendarViewDirection">
|
||||
<div class="date-picker text-center" :class="calendarViewDirection">
|
||||
<fieldset v-if="months">
|
||||
<legend class="sr-only">Date Picker</legend>
|
||||
<div
|
||||
|
|
@ -56,10 +56,8 @@
|
|||
<button
|
||||
v-if="calendarViewDirection === 'future'"
|
||||
type="button"
|
||||
class="btn-link"
|
||||
@click="
|
||||
goForward();
|
||||
">
|
||||
class="btn btn-link"
|
||||
@click="goForward">
|
||||
View more dates
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -627,6 +625,12 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .btn-link {
|
||||
// font-weight: 500;
|
||||
// text-underline-offset: 4px;
|
||||
// }
|
||||
|
||||
.past {
|
||||
.calendar-grid-container {
|
||||
.month-year {
|
||||
|
|
@ -695,5 +699,7 @@ export default {
|
|||
border: none;
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
font-weight: 500;
|
||||
text-underline-offset: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
jest.mock("vee-validate", () => ({
|
||||
useForm: jest.fn(),
|
||||
useIsFormTouched: jest.fn(),
|
||||
useIsFormDirty: jest.fn(),
|
||||
useIsFormValid: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockValidate = (returnValue) => jest.fn(async () => Promise.resolve({ valid: returnValue }));
|
||||
const mockMeta = (returnValue) => jest.fn(async () => Promise.resolve(returnValue));
|
||||
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import modal from "./modal";
|
||||
import crypto from "crypto";
|
||||
import { useForm, useIsFormDirty, useIsFormTouched, useIsFormValid } from "vee-validate";
|
||||
import { useForm } from "vee-validate";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
const footerButtonText = "Sample footer text here.";
|
||||
|
|
@ -21,6 +19,18 @@ global.crypto = crypto;
|
|||
describe("modal.vue", () => {
|
||||
it("Should display modal header text when headerText is defined", async () => {
|
||||
// Arrange / Act
|
||||
const fakeMeta = {
|
||||
touched: true,
|
||||
dirty: true,
|
||||
valid: true,
|
||||
validated: true,
|
||||
};
|
||||
|
||||
useForm.mockReturnValue({
|
||||
meta: mockMeta(fakeMeta),
|
||||
validate: mockValidate(true),
|
||||
});
|
||||
|
||||
const wrapper = shallowMount(modal, {
|
||||
props: {
|
||||
headerText: headerText,
|
||||
|
|
@ -35,6 +45,17 @@ describe("modal.vue", () => {
|
|||
|
||||
it("Should display footer button text when footerButtonText is defined", async () => {
|
||||
// Arrange / Act
|
||||
const fakeMeta = {
|
||||
touched: true,
|
||||
dirty: true,
|
||||
valid: true,
|
||||
validated: true,
|
||||
};
|
||||
|
||||
useForm.mockReturnValue({
|
||||
meta: mockMeta(fakeMeta),
|
||||
validate: mockValidate(true),
|
||||
});
|
||||
const wrapper = shallowMount(modal, {
|
||||
props: {
|
||||
footerButtonText: footerButtonText,
|
||||
|
|
@ -48,7 +69,15 @@ describe("modal.vue", () => {
|
|||
|
||||
it("Should emit 'footer-button-event' if the form is valid", async () => {
|
||||
// Arrange
|
||||
const fakeMeta = {
|
||||
touched: true,
|
||||
dirty: true,
|
||||
valid: true,
|
||||
validated: true,
|
||||
};
|
||||
|
||||
useForm.mockReturnValue({
|
||||
meta: mockMeta(fakeMeta),
|
||||
validate: mockValidate(true),
|
||||
});
|
||||
|
||||
|
|
@ -63,7 +92,7 @@ describe("modal.vue", () => {
|
|||
wrapper.vm.resetButtonStyle = resetButtonStyle;
|
||||
|
||||
// Act
|
||||
const buttonMain = wrapper.findComponent({ ref: "buttonMain" });
|
||||
const buttonMain = wrapper.findComponent({ ref: "modalButtonMain" });
|
||||
await buttonMain.trigger("click-event");
|
||||
|
||||
// Assert
|
||||
|
|
@ -72,7 +101,15 @@ describe("modal.vue", () => {
|
|||
|
||||
it("Should not emit 'footer-button-event' if the form is invalid", async () => {
|
||||
// Arrange
|
||||
const fakeMeta = {
|
||||
touched: true,
|
||||
dirty: true,
|
||||
valid: false,
|
||||
validated: true,
|
||||
};
|
||||
|
||||
useForm.mockReturnValue({
|
||||
meta: mockMeta(fakeMeta),
|
||||
validate: mockValidate(false),
|
||||
});
|
||||
|
||||
|
|
@ -87,7 +124,7 @@ describe("modal.vue", () => {
|
|||
wrapper.vm.resetButtonStyle = resetButtonStyle;
|
||||
|
||||
// Act
|
||||
const buttonMain = wrapper.findComponent({ ref: "buttonMain" });
|
||||
const buttonMain = wrapper.findComponent({ ref: "modalButtonMain" });
|
||||
await buttonMain.trigger("click-event");
|
||||
|
||||
// Assert
|
||||
|
|
@ -96,11 +133,17 @@ describe("modal.vue", () => {
|
|||
|
||||
it("Should have a disabled footer button when the form has not been touched", async () => {
|
||||
// Arrange / Act
|
||||
useForm.mockReturnValue({
|
||||
validate: mockValidate(false),
|
||||
});
|
||||
const fakeMeta = {
|
||||
touched: true,
|
||||
dirty: true,
|
||||
valid: true,
|
||||
validated: true,
|
||||
};
|
||||
|
||||
useIsFormTouched.mockReturnValue(false);
|
||||
useForm.mockReturnValue({
|
||||
meta: mockMeta(fakeMeta),
|
||||
validate: mockValidate(true),
|
||||
});
|
||||
|
||||
const resetButtonStyle = jest.fn();
|
||||
const wrapper = shallowMount(modal, {
|
||||
|
|
@ -118,13 +161,17 @@ describe("modal.vue", () => {
|
|||
|
||||
it("Should have a disabled footer button when the form is invalid", async () => {
|
||||
// Arrange / Act
|
||||
useForm.mockReturnValue({
|
||||
validate: mockValidate(false),
|
||||
});
|
||||
const fakeMeta = {
|
||||
touched: true,
|
||||
dirty: true,
|
||||
valid: true,
|
||||
validated: true,
|
||||
};
|
||||
|
||||
useIsFormTouched.mockReturnValue(true);
|
||||
useIsFormDirty.mockReturnValue(true);
|
||||
useIsFormValid.mockReturnValue(false);
|
||||
useForm.mockReturnValue({
|
||||
meta: mockMeta(fakeMeta),
|
||||
validate: mockValidate(true),
|
||||
});
|
||||
|
||||
const resetButtonStyle = jest.fn();
|
||||
const wrapper = shallowMount(modal, {
|
||||
|
|
@ -142,12 +189,17 @@ describe("modal.vue", () => {
|
|||
|
||||
it("Should call bootstrap Modal method 'show' when calling 'openModal'", async () => {
|
||||
// Arrange
|
||||
useForm.mockReturnValue({
|
||||
validate: mockValidate(false),
|
||||
});
|
||||
const fakeMeta = {
|
||||
touched: true,
|
||||
dirty: true,
|
||||
valid: true,
|
||||
validated: true,
|
||||
};
|
||||
|
||||
useIsFormDirty.mockReturnValue(true);
|
||||
useIsFormValid.mockReturnValue(true);
|
||||
useForm.mockReturnValue({
|
||||
meta: mockMeta(fakeMeta),
|
||||
validate: mockValidate(true),
|
||||
});
|
||||
|
||||
const showMock = jest.spyOn(Modal.prototype, "show");
|
||||
|
||||
|
|
@ -170,13 +222,17 @@ describe("modal.vue", () => {
|
|||
|
||||
it("Should call bootstrap Modal method 'hide' when calling 'closeModal'", async () => {
|
||||
// Arrange
|
||||
const fakeMeta = {
|
||||
touched: true,
|
||||
dirty: true,
|
||||
valid: true,
|
||||
validated: true,
|
||||
};
|
||||
|
||||
useForm.mockReturnValue({
|
||||
validate: mockValidate(false),
|
||||
meta: mockMeta(fakeMeta),
|
||||
validate: mockValidate(true),
|
||||
});
|
||||
|
||||
useIsFormDirty.mockReturnValue(true);
|
||||
useIsFormValid.mockReturnValue(true);
|
||||
|
||||
const hideMock = jest.spyOn(Modal.prototype, "hide");
|
||||
|
||||
const resetButtonStyle = jest.fn();
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@
|
|||
<slot></slot>
|
||||
</div>
|
||||
<div class="modal-footer px-5 py-4">
|
||||
<buttonMain
|
||||
<modalButtonMain
|
||||
isPrimary
|
||||
class="w-100"
|
||||
ref="buttonMain"
|
||||
ref="modalButtonMain"
|
||||
loaderColor="white"
|
||||
:buttonText="footerButtonText"
|
||||
@click-event="validateAndEmit"
|
||||
|
|
@ -41,9 +41,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import buttonMain from "@/ux-components/button-main/button-main";
|
||||
import modalButtonMain from "@/digital-components/modal/ux-components/modal-button-main/modal-button-main";
|
||||
import { Modal } from "bootstrap";
|
||||
import { useForm, useIsFormTouched, useIsFormDirty, useIsFormValid } from "vee-validate";
|
||||
import { useForm } from "vee-validate";
|
||||
|
||||
export default {
|
||||
name: "modal",
|
||||
|
|
@ -60,22 +60,18 @@ export default {
|
|||
setup() {
|
||||
const modalId = `modal-${crypto.randomUUID()}`;
|
||||
|
||||
const form = useForm();
|
||||
const isFormTouched = useIsFormTouched();
|
||||
const isFormDirty = useIsFormDirty();
|
||||
const isFormValid = useIsFormValid();
|
||||
const { meta, validate, resetForm } = useForm();
|
||||
|
||||
return {
|
||||
modalId,
|
||||
form,
|
||||
isFormTouched,
|
||||
isFormDirty,
|
||||
isFormValid,
|
||||
meta,
|
||||
validate,
|
||||
resetForm,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async validateAndEmit() {
|
||||
const validationResult = await this.form.validate();
|
||||
const validationResult = await this.validate();
|
||||
if (validationResult.valid) {
|
||||
this.$emit("footer-button-event");
|
||||
} else {
|
||||
|
|
@ -83,7 +79,7 @@ export default {
|
|||
}
|
||||
},
|
||||
resetButtonStyle() {
|
||||
this.$refs.buttonMain.resetButtonStyle();
|
||||
this.$refs.modalButtonMain.resetButtonStyle();
|
||||
},
|
||||
onModalOpened() {
|
||||
this.onModalOpenedCallback?.();
|
||||
|
|
@ -103,14 +99,14 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
isFooterButtonDisabled() {
|
||||
if (!this.isFormTouched) {
|
||||
return !this.isFormValid;
|
||||
if (!this.meta.touched) {
|
||||
return !this.meta.valid;
|
||||
}
|
||||
return !this.isFormDirty || !this.isFormValid;
|
||||
return !this.meta.dirty || !this.meta.valid;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
buttonMain,
|
||||
modalButtonMain,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,187 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import modalButtonMain from "./modal-button-main";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import { nextTick } from "vue";
|
||||
|
||||
describe("modal-button-main.vue", () => {
|
||||
it("Should return btn-primary class", () => {
|
||||
// Arrange/Act
|
||||
const wrapper = shallowMount(
|
||||
modalButtonMain,
|
||||
setupMocks({
|
||||
propsData: {
|
||||
isPrimary: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
const button = wrapper.find("button");
|
||||
|
||||
// Assert
|
||||
expect(button.attributes("class")).toContain("btn-primary");
|
||||
});
|
||||
|
||||
it("Should return aria-disabled state", () => {
|
||||
// Arrange/Act
|
||||
const wrapper = shallowMount(
|
||||
modalButtonMain,
|
||||
setupMocks({
|
||||
propsData: {
|
||||
isDisabled: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
const button = wrapper.find("button");
|
||||
|
||||
// Assert
|
||||
expect(button.attributes()["aria-disabled"]).toEqual("true");
|
||||
});
|
||||
|
||||
it("Should return loader color", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(
|
||||
modalButtonMain,
|
||||
setupMocks({
|
||||
propsData: {
|
||||
loaderColor: "blue",
|
||||
loaderEnabled: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Act
|
||||
wrapper.vm.clicked();
|
||||
await nextTick();
|
||||
|
||||
// Assert
|
||||
const loader = wrapper.find("loader-stub");
|
||||
expect(loader.attributes("class")).toContain("blue");
|
||||
});
|
||||
|
||||
it("Should return loader position", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(
|
||||
modalButtonMain,
|
||||
setupMocks({
|
||||
propsData: {
|
||||
loaderPosition: "right",
|
||||
loaderEnabled: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Act
|
||||
wrapper.vm.clicked();
|
||||
await nextTick();
|
||||
|
||||
// Assert
|
||||
const loader = wrapper.find("loader-stub");
|
||||
expect(loader.attributes("class")).toContain("right");
|
||||
});
|
||||
|
||||
it("Should set 'isLoaderDisplayed' to false when calling 'removeLoader'", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(
|
||||
modalButtonMain,
|
||||
setupMocks({
|
||||
propsData: {
|
||||
loaderPosition: "right",
|
||||
loaderEnabled: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
wrapper.setData({
|
||||
isLoaderDisplayed: true,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.removeLoader();
|
||||
await nextTick();
|
||||
|
||||
// Assert
|
||||
const loader = wrapper.find("loader-stub");
|
||||
expect(wrapper.vm.isLoaderDisplayed).toBe(false);
|
||||
});
|
||||
|
||||
it("Should set 'isLoaderDisplayed' to false when calling 'resetButtonStyle'", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(
|
||||
modalButtonMain,
|
||||
setupMocks({
|
||||
propsData: {
|
||||
loaderPosition: "right",
|
||||
loaderEnabled: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
wrapper.setData({
|
||||
isLoaderDisplayed: true,
|
||||
});
|
||||
|
||||
// Act
|
||||
wrapper.vm.resetButtonStyle();
|
||||
await nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.isLoaderDisplayed).toBe(false);
|
||||
});
|
||||
|
||||
it("Should emit 'click-event' event when clicking if the button is enabled", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(
|
||||
modalButtonMain,
|
||||
setupMocks({
|
||||
propsData: {
|
||||
loaderPosition: "right",
|
||||
loaderEnabled: true,
|
||||
isDisabled: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const buttonElement = wrapper.find("button");
|
||||
|
||||
// Act
|
||||
buttonElement.trigger("click");
|
||||
|
||||
await nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("click-event")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Should not emit 'click-event' event when clicking if the button is disabled", async () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(
|
||||
modalButtonMain,
|
||||
setupMocks({
|
||||
propsData: {
|
||||
loaderPosition: "right",
|
||||
loaderEnabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const buttonElement = wrapper.find("button");
|
||||
|
||||
// Act
|
||||
buttonElement.trigger("click");
|
||||
|
||||
await nextTick();
|
||||
|
||||
// Assert
|
||||
expect(wrapper.emitted("click-event")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks(mountOptionsMockData = {}) {
|
||||
const defaultMountOptions = { route: { query: { fmgPage: "page-name" } } };
|
||||
const baseMountOptions = getMountOptions(
|
||||
Object.assign(defaultMountOptions, mountOptionsMockData)
|
||||
);
|
||||
const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions);
|
||||
|
||||
return allMountOptions;
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
<template>
|
||||
<button
|
||||
type="button"
|
||||
:aria-disabled="isDisabled"
|
||||
class="btn d-flex align-items-center justify-content-center py-3 px-4 delay"
|
||||
:class="[
|
||||
isPrimary ? 'btn-primary' : 'btn-secondary',
|
||||
isFloat ? 'float-end' : '',
|
||||
isLoaderDisplayed ? 'has-loader' : '',
|
||||
]"
|
||||
@click="clicked">
|
||||
<span class="m-0">{{ this.buttonText }}</span>
|
||||
<loader
|
||||
class="ms-2"
|
||||
v-if="isLoaderDisplayed && !suppressLoader"
|
||||
v-bind:class="[this.loaderColor, this.loaderPosition]" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import loader from "@/ux-components/loader/loader";
|
||||
|
||||
export default {
|
||||
name: "modalButtonMain",
|
||||
props: {
|
||||
isPrimary: Boolean,
|
||||
buttonText: String,
|
||||
isDisabled: Boolean,
|
||||
loaderColor: String,
|
||||
loaderPosition: String,
|
||||
isFloat: Boolean,
|
||||
suppressLoader: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isLoaderDisplayed: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
removeLoader() {
|
||||
this.isLoaderDisplayed = false;
|
||||
},
|
||||
clicked() {
|
||||
this.pushEventToGA(
|
||||
this.$route.query[this.queryStrings.FMG_PAGE],
|
||||
this.GaActions.CLICKED,
|
||||
this.buttonText,
|
||||
true
|
||||
);
|
||||
if (!this.isDisabled) {
|
||||
this.isLoaderDisplayed = true;
|
||||
this.$emit("click-event");
|
||||
}
|
||||
},
|
||||
resetButtonStyle() {
|
||||
this.isLoaderDisplayed = false;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
loader,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.btn {
|
||||
&.btn-primary {
|
||||
position: relative;
|
||||
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
|
||||
border: none;
|
||||
border-radius: $border-radius-lg;
|
||||
color: $white;
|
||||
justify-content: center;
|
||||
font-weight: 500;
|
||||
@media (hover: hover) {
|
||||
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||
}
|
||||
&:focus, // Mouse, touch, stylus focus
|
||||
&:focus-visible {
|
||||
// Keyboard focus for accessibility
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||
color: $white;
|
||||
background: linear-gradient(270deg, rgba(6, 87, 124, 1) 0%, rgba(6, 87, 124, 1) 100%);
|
||||
}
|
||||
&:disabled {
|
||||
background: $gray-200 !important;
|
||||
background: linear-gradient(270deg, $gray-200 0%, $gray-200 100%) !important;
|
||||
color: $gray-600 !important;
|
||||
font-weight: 400;
|
||||
height: 48px;
|
||||
border: none;
|
||||
border-radius: $border-radius-lg;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
&.has-loader {
|
||||
color: $white;
|
||||
background: $blue-700;
|
||||
box-shadow: 0 0 0 3px, 0 0 0 5.5px $blue-700;
|
||||
pointer-events: none;
|
||||
}
|
||||
&.delay {
|
||||
// fixes flicker while transitioning between states
|
||||
transition: background 0s 0s ease-in-out;
|
||||
}
|
||||
}
|
||||
&.btn-secondary {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
border: 1px solid $blue;
|
||||
border-radius: $border-radius-lg;
|
||||
color: $blue;
|
||||
font-weight: 500;
|
||||
transition: all 150ms linear;
|
||||
height: 3rem;
|
||||
&:hover {
|
||||
color: $white;
|
||||
@include blue-gradient;
|
||||
}
|
||||
&:focus, // Mouse, touch, stylus focus
|
||||
&:focus-visible {
|
||||
// Keyboard focus for accessibility
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px $white, 0 0 0 5.5px $blue-700;
|
||||
color: $white;
|
||||
@include blue-gradient;
|
||||
}
|
||||
&:disabled {
|
||||
background: transparent;
|
||||
color: $gray-550 !important;
|
||||
font-weight: 400;
|
||||
height: 48px;
|
||||
border: 1px solid $gray-550;
|
||||
border-radius: $border-radius-lg;
|
||||
cursor: pointer;
|
||||
pointer-events: all;
|
||||
}
|
||||
&.has-loader {
|
||||
color: $white;
|
||||
@include blue-gradient;
|
||||
pointer-events: none;
|
||||
}
|
||||
&.delay {
|
||||
// fixes flicker while transitioning between states
|
||||
transition: background 0s 0s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,11 +1,19 @@
|
|||
<template>
|
||||
<div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
|
||||
<div
|
||||
class="textbox-question"
|
||||
:class="[
|
||||
(errors && errors.length) || hasError ? 'has-error' : '',
|
||||
hideInput ? 'hide-input' : '',
|
||||
]">
|
||||
<label
|
||||
v-if="displayQuestionText"
|
||||
:for="inputId"
|
||||
:aria-label="questionText"
|
||||
class="form-label"
|
||||
:class="[questionAlignment === 'center' ? 'text-center w-100 mb-5' : '']"
|
||||
:class="[
|
||||
questionAlignment === 'center' ? 'text-center w-100 mb-5' : '',
|
||||
hideInput ? 'hide-input' : '',
|
||||
]"
|
||||
v-html="questionText"></label>
|
||||
<div class="input-wrapper" :class="[includeSearchIcon ? 'has-search-icon' : '']">
|
||||
<input
|
||||
|
|
@ -25,6 +33,7 @@
|
|||
hasIcon ? 'has-icon' : '',
|
||||
iconRight ? 'icon-right' : '',
|
||||
cornerStyle === 'rounded' ? 'rounded-pill' : '',
|
||||
hideInput ? 'hide-input' : '',
|
||||
]"
|
||||
:validationRules="validationRules"
|
||||
@change="handleChange"
|
||||
|
|
@ -34,7 +43,12 @@
|
|||
<button v-if="includeSearchIcon" type="submit" aria-label="Search button" />
|
||||
</div>
|
||||
<div v-show="errorMessage" class="row my-1 form-test-error">
|
||||
<span class="d-inline-flex small mt-0" role="alert">{{ errorMessage }}</span>
|
||||
<span
|
||||
class="d-inline-flex small mt-0"
|
||||
role="alert"
|
||||
:class="[centerErrorMessage ? 'center-error-message' : '']"
|
||||
>{{ errorMessage }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -74,6 +88,8 @@ export default {
|
|||
questionAlignment: String, // Left or center. Left is default.
|
||||
cornerStyle: String, // Rounded or square. Square is default.
|
||||
includeSearchIcon: Boolean,
|
||||
hideInput: Boolean,
|
||||
centerErrorMessage: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const inputId = !props.customInputId ? `input-${crypto.randomUUID()}` : props.customInputId;
|
||||
|
|
@ -142,6 +158,12 @@ export default {
|
|||
|
||||
<style lang="scss">
|
||||
.textbox-question {
|
||||
.hide-input {
|
||||
display: none;
|
||||
}
|
||||
.center-error-message {
|
||||
justify-content: center !important;
|
||||
}
|
||||
label {
|
||||
color: $black;
|
||||
font-weight: 500;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
class="menu-button"
|
||||
type="button"
|
||||
:class="[isActive ? 'active' : '']"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#footerModal"
|
||||
@click="toggleModal"
|
||||
aria-label="Hamburger Menu (modal window)">
|
||||
<div class="bar1"></div>
|
||||
<div class="bar2"></div>
|
||||
|
|
@ -22,19 +21,6 @@
|
|||
aria-hidden="true"
|
||||
v-on="{ 'show.bs.modal': show, 'hide.bs.modal': hide }"
|
||||
:style="`height: calc(100% - ${currentFooterAndHeaderHeight}px);`">
|
||||
<div class="menu-modal-container">
|
||||
<button
|
||||
class="menu-button"
|
||||
type="button"
|
||||
:class="[isActive ? 'active' : '']"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#footerModal"
|
||||
aria-label="Hamburger Menu (modal window)">
|
||||
<div class="bar1"></div>
|
||||
<div class="bar2"></div>
|
||||
<div class="bar3"></div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-dialog modal-fullscreen">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header visually-hidden">
|
||||
|
|
@ -79,6 +65,8 @@
|
|||
|
||||
<script>
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
export default {
|
||||
name: "menuModal",
|
||||
data() {
|
||||
|
|
@ -88,6 +76,19 @@ export default {
|
|||
};
|
||||
},
|
||||
methods: {
|
||||
toggleModal() {
|
||||
if (this.isActive) {
|
||||
this.closeModal();
|
||||
} else {
|
||||
this.openModal();
|
||||
}
|
||||
},
|
||||
openModal() {
|
||||
Modal.getOrCreateInstance(document.getElementById("footerModal")).show();
|
||||
},
|
||||
closeModal() {
|
||||
Modal.getInstance(document.getElementById("footerModal")).hide();
|
||||
},
|
||||
show() {
|
||||
this.currentFooterAndHeaderHeight = this.getFooterInfoBoxHeight() + 72;
|
||||
this.isActive = true;
|
||||
|
|
|
|||
|
|
@ -369,6 +369,9 @@ export default {
|
|||
console.log("Unable to load Google Places API script");
|
||||
});
|
||||
},
|
||||
resetAlerts() {
|
||||
this.displayVerificationWarning = false;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setupAddressLookup();
|
||||
|
|
|
|||
|
|
@ -271,7 +271,7 @@ export default {
|
|||
|
||||
// call saveSession here - navigateWithSaving saves too late in the flow
|
||||
await saveSession({});
|
||||
return this.$router.navigateWithoutSaving(
|
||||
return this.$router.navigateWithSaving(
|
||||
this.navigationScenarios.CLICKED_FORWARD_WITH_NO_QUESTIONS,
|
||||
this.$route
|
||||
);
|
||||
|
|
|
|||
136
src/layouts/schedule/schedule.vue
Normal file
136
src/layouts/schedule/schedule.vue
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<template>
|
||||
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
|
||||
<div class="page-container-grouped-styles">
|
||||
<loadingModal ref="loadingModal" />
|
||||
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" class="mt-5" />
|
||||
<div class="text-center mt-1 mb-3" v-if="ChangeShopLink.length">
|
||||
<span v-for="copy in ChangeShopLink" :key="copy">
|
||||
<span v-if="doesCopyContainRouterLink(copy)" class="text-body">
|
||||
<router-link
|
||||
:to="{
|
||||
query: { fmgPage: `${getRouterLinkRouteFromCopy(copy)}` },
|
||||
name: 'root',
|
||||
}"
|
||||
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
|
||||
>
|
||||
</span>
|
||||
<span v-else class="m-0 text-body" v-html="copy"></span>
|
||||
</span>
|
||||
</div>
|
||||
<date-picker
|
||||
selectableDates="custom"
|
||||
v-model="selectedDate"
|
||||
:customSelectableDatesCallback="getAvailableDates" />
|
||||
<funnel-footer
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="funnelFooter"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
@back-clicked="backButtonAction"
|
||||
@ForwardClicked="forwardButtonAction" />
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Components
|
||||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
|
||||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||
import { Form, defineRule } from "vee-validate";
|
||||
import datePicker from "@/digital-components/date-picker/date-picker";
|
||||
|
||||
// Supporting files
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||
import {
|
||||
doesCopyContainRouterLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
|
||||
defineRule("date-required", required(errorMessages.DATE_REQUIRED));
|
||||
|
||||
export default {
|
||||
name: "schedule",
|
||||
data() {
|
||||
return {
|
||||
selectedDate: null,
|
||||
mockSelectableDatesData: [
|
||||
{ year: 2023, month: 4, date: 13 },
|
||||
{ year: 2023, month: 4, date: 14 },
|
||||
{ year: 2023, month: 4, date: 26 },
|
||||
{ year: 2023, month: 5, date: 4 },
|
||||
{ year: 2023, month: 5, date: 5 },
|
||||
{ year: 2023, month: 5, date: 14 },
|
||||
{ year: 2023, month: 5, date: 21 },
|
||||
{ year: 2023, month: 5, date: 23 },
|
||||
{ year: 2023, month: 5, date: 25 },
|
||||
{ year: 2023, month: 6, date: 11 },
|
||||
],
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||
|
||||
// Settle promises and get results
|
||||
const promiseResultMap = [
|
||||
{
|
||||
resultKey: "cmsContent",
|
||||
promise: cmsContentPromise,
|
||||
},
|
||||
];
|
||||
|
||||
const resultMap = await settleAllPromises(promiseResultMap);
|
||||
|
||||
// Call the "next" function to complete the transition to this page.
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
ChangeShopLinkText() {
|
||||
return this.getCmsContent("ChangeShopLink", "Text");
|
||||
},
|
||||
ChangeShopLink() {
|
||||
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
|
||||
return this.splitCopyOnCMSPlaceHolder(this.ChangeShopLinkText);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
doesCopyContainRouterLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
arePagePrerequisitesValid() {
|
||||
return true;
|
||||
// NEED TODO - WHAT ARE PAGE REQ'S FOR THIS PAGE?
|
||||
},
|
||||
getAvailableDates(startDate, endDate) {
|
||||
return this.mockSelectableDatesData;
|
||||
},
|
||||
backButtonAction() {
|
||||
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
},
|
||||
forwardButtonAction() {
|
||||
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
|
||||
},
|
||||
},
|
||||
components: {
|
||||
funnelHeader,
|
||||
funnelFooter,
|
||||
funnelSubHeader,
|
||||
Form,
|
||||
loadingModal,
|
||||
datePicker,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
import appointmentTypeQuestion from "./appointment-type-question";
|
||||
|
||||
const mockCmsContent = {
|
||||
QuestionText: "Choose a service option:",
|
||||
Answers: [
|
||||
{
|
||||
AnswerImageUrl: "",
|
||||
Name: "Mobile",
|
||||
SubText: "",
|
||||
SubWidgetName: "",
|
||||
Text: "Mobile",
|
||||
},
|
||||
{
|
||||
AnswerImageUrl: "",
|
||||
Name: "Inshop",
|
||||
SubText: "",
|
||||
SubWidgetName: "",
|
||||
Text: "In-shop",
|
||||
},
|
||||
{
|
||||
AnswerImageUrl: "",
|
||||
Name: "DropOff",
|
||||
SubText: "",
|
||||
SubWidgetName: "",
|
||||
Text: "Drop-off",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const cmsWidgetName = "AppointmentTypeQuestionWidget";
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
|
||||
if (widgetName === cmsWidgetName) {
|
||||
return mockCmsContent[cmsFieldName];
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
describe("appointment-type-question.vue", () => {
|
||||
it("Should display all options if both in-shop and mobile are available", async () => {
|
||||
// Arrange/Act
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
cmsWidgetName: cmsWidgetName,
|
||||
isServiceableInshop: true,
|
||||
isServiceableMobile: true,
|
||||
},
|
||||
mountOptions: {
|
||||
attachTo: document.body,
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.answersToDisplay).toEqual([
|
||||
{
|
||||
AnswerImageUrl: "",
|
||||
Name: "Mobile",
|
||||
SubText: "",
|
||||
SubWidgetName: "",
|
||||
Text: "Mobile",
|
||||
},
|
||||
{
|
||||
AnswerImageUrl: "",
|
||||
Name: "Inshop",
|
||||
SubText: "",
|
||||
SubWidgetName: "",
|
||||
Text: "In-shop",
|
||||
},
|
||||
{
|
||||
AnswerImageUrl: "",
|
||||
Name: "DropOff",
|
||||
SubText: "",
|
||||
SubWidgetName: "",
|
||||
Text: "Drop-off",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("Should display only the In-Shop and Drop-Off answers when only in-shop service is available", async () => {
|
||||
// Arrange/Act
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
cmsWidgetName: cmsWidgetName,
|
||||
isServiceableInshop: true,
|
||||
isServiceableMobile: false,
|
||||
},
|
||||
mountOptions: {
|
||||
attachTo: document.body,
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.answersToDisplay).toEqual([
|
||||
{
|
||||
AnswerImageUrl: "",
|
||||
Name: "Inshop",
|
||||
SubText: "",
|
||||
SubWidgetName: "",
|
||||
Text: "In-shop",
|
||||
},
|
||||
{
|
||||
AnswerImageUrl: "",
|
||||
Name: "DropOff",
|
||||
SubText: "",
|
||||
SubWidgetName: "",
|
||||
Text: "Drop-off",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("Should display only the Mobile answer when only mobile service is available", async () => {
|
||||
// Arrange/Act
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
cmsWidgetName: cmsWidgetName,
|
||||
isServiceableInshop: false,
|
||||
isServiceableMobile: true,
|
||||
},
|
||||
mountOptions: {
|
||||
attachTo: document.body,
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.answersToDisplay).toEqual([
|
||||
{
|
||||
AnswerImageUrl: "",
|
||||
Name: "Mobile",
|
||||
SubText: "",
|
||||
SubWidgetName: "",
|
||||
Text: "Mobile",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("Should display no answers if neither in-shop nor mobile service are available", async () => {
|
||||
// Arrange/Act
|
||||
const { wrapper } = setupMocks({
|
||||
mixins: [mockMixin],
|
||||
props: {
|
||||
cmsWidgetName: cmsWidgetName,
|
||||
isServiceableInshop: false,
|
||||
isServiceableMobile: false,
|
||||
},
|
||||
mountOptions: {
|
||||
attachTo: document.body,
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.answersToDisplay).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function setupMocks({ mountOptions, mixins, props, isShallowMount = true }) {
|
||||
const resultingMountOptions = getMountOptions({
|
||||
...mountOptions,
|
||||
mixins,
|
||||
});
|
||||
|
||||
if (props) resultingMountOptions.propsData = props;
|
||||
|
||||
const wrapper = isShallowMount
|
||||
? shallowMount(appointmentTypeQuestion, resultingMountOptions)
|
||||
: mount(appointmentTypeQuestion, resultingMountOptions);
|
||||
|
||||
return { wrapper };
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<template>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="appointment-type-question" aria-live="polite">
|
||||
<buttonQuestion
|
||||
:questionText="questionText"
|
||||
:answers="answersToDisplay"
|
||||
:groupName="groupName"
|
||||
buttonTypeString="listCard"
|
||||
v-model="selectedValues"
|
||||
:suppressError="suppressError"
|
||||
:validationRules="validationRules"
|
||||
isRequired />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import buttonQuestion from "@/digital-components/button-question/button-question";
|
||||
|
||||
export default {
|
||||
name: "appointment-type-question",
|
||||
props: {
|
||||
modelValue: String,
|
||||
groupName: String,
|
||||
isAvailable: Boolean,
|
||||
suppressError: Boolean,
|
||||
validationRules: String,
|
||||
cmsWidgetName: String,
|
||||
isServiceableMobile: Boolean,
|
||||
isServiceableInshop: Boolean,
|
||||
},
|
||||
computed: {
|
||||
questionText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||
},
|
||||
answersFromCms() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "Answers");
|
||||
},
|
||||
answersToDisplay() {
|
||||
let filteredAnswers;
|
||||
if (this.isServiceableMobile && this.isServiceableInshop) {
|
||||
filteredAnswers = this.answersFromCms;
|
||||
} else if (this.isServiceableMobile) {
|
||||
filteredAnswers = this.answersFromCms.filter((answer) => answer.Name == "Mobile");
|
||||
} else if (this.isServiceableInshop) {
|
||||
filteredAnswers = this.answersFromCms.filter(
|
||||
(answer) => answer.Name == "Inshop" || answer.Name == "DropOff"
|
||||
);
|
||||
} else {
|
||||
filteredAnswers = [];
|
||||
}
|
||||
|
||||
return filteredAnswers;
|
||||
},
|
||||
selectedValues: {
|
||||
get: function () {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
},
|
||||
},
|
||||
isMobileOnly() {
|
||||
return this.isServiceableMobile && !this.isServiceableInshop;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isMobileOnly: {
|
||||
handler(newValue) {
|
||||
if (newValue) {
|
||||
this.selectedValues = "Mobile";
|
||||
} else {
|
||||
this.selectedValues = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
buttonQuestion,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.list-card img {
|
||||
height: auto;
|
||||
width: 3.417rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -28,3 +28,17 @@ export async function getPricedMobileFeePart(serviceZipCode) {
|
|||
|
||||
return Promise.resolve(pricingResults[0]);
|
||||
}
|
||||
|
||||
export async function getServiceabilityDetails(serviceZipCode, lineItems) {
|
||||
// Get the Mobile Fee Part
|
||||
const serviceabilityDetails = await baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.GET_SERVICEABILITY_DETAILS,
|
||||
{
|
||||
serviceZipCode: serviceZipCode,
|
||||
lineItems: lineItems,
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
return Promise.resolve(serviceabilityDetails);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,12 +110,26 @@ const mockGetPricedMobileFeePart = (mockServiceZipCode) => {
|
|||
return Promise.resolve(mobileFeePart);
|
||||
};
|
||||
|
||||
const mockGetServiceabilityDetails = (mockServiceZipCode) => {
|
||||
const serviceabilityDetails = {
|
||||
isGlassServiceableInshop: true,
|
||||
isRecalibrationServiceableInshop: true,
|
||||
isGlassServiceableMobile: true,
|
||||
isRecalibrationServiceableMobile: true,
|
||||
};
|
||||
|
||||
return Promise.resolve(serviceabilityDetails);
|
||||
};
|
||||
|
||||
jest.mock(
|
||||
"@/layouts/service-location/helpers/service-location-helper/service-location-helper",
|
||||
() => ({
|
||||
getPricedMobileFeePart: jest.fn((mockServiceZipCode) => {
|
||||
return mockGetPricedMobileFeePart(mockServiceZipCode);
|
||||
}),
|
||||
getServiceabilityDetails: jest.fn((mockServiceZipCode) => {
|
||||
return mockGetServiceabilityDetails(mockServiceZipCode);
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@
|
|||
:ref="modalName"
|
||||
:headerText="modalHeaderText"
|
||||
:footerButtonText="modalFooterText"
|
||||
:onModalOpenedCallback="onModalOpened"
|
||||
:onModalClosedCallback="onModalClosed"
|
||||
@footer-button-event="setMobileLocation">
|
||||
<addressQuestions
|
||||
ref="addressQuestions"
|
||||
|
|
@ -56,15 +58,18 @@ import modal from "@/digital-components/modal/modal";
|
|||
import alert from "@/ux-components/alert/alert";
|
||||
import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions";
|
||||
import vehicleProtectedQuestion from "@/layouts/service-location/mobile-location-modal-questions/vehicle-protected-question/vehicle-protected-question";
|
||||
import store from "@/store";
|
||||
|
||||
// Helpers
|
||||
import { deepClone } from "@/layouts/service-location/helpers/object-cloning-helper/object-cloning-helper";
|
||||
import { getPricedMobileFeePart } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
|
||||
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
getServiceabilityDetails,
|
||||
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
|
||||
|
||||
export default {
|
||||
name: "mobile-location-modal-questions",
|
||||
emits: ["update:modelValue", "set-mobile-fee-part"],
|
||||
emits: ["update:modelValue", "updated-mobile-fee-part", "updated-contains-military-base"],
|
||||
data() {
|
||||
return {
|
||||
internalModel: deepClone(this.modelValue),
|
||||
|
|
@ -89,8 +94,6 @@ export default {
|
|||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
isZipServiceableMobile: Boolean,
|
||||
isZipServiceableInShop: Boolean,
|
||||
linkWidgetName: String,
|
||||
modalWidgetName: String,
|
||||
alertNonServiceableZipWidgetName: String,
|
||||
|
|
@ -109,7 +112,8 @@ export default {
|
|||
this.addressModel.state &&
|
||||
this.addressModel.state !== "" &&
|
||||
this.addressModel.zipCode &&
|
||||
this.addressModel.zipCode !== ""
|
||||
this.addressModel.zipCode !== "" &&
|
||||
this.internalModel.isVehicleProtected !== null
|
||||
) {
|
||||
return `${this.addressModel.streetAddress}\n${this.addressModel.city}, ${this.addressModel.state} ${this.addressModel.zipCode}`;
|
||||
}
|
||||
|
|
@ -139,7 +143,6 @@ export default {
|
|||
modalFooterText() {
|
||||
return this.getCmsContent(this.modalWidgetName, "FooterText");
|
||||
},
|
||||
|
||||
addressModel: {
|
||||
get: function () {
|
||||
return this.modelValue.addressQuestions;
|
||||
|
|
@ -153,12 +156,21 @@ export default {
|
|||
closeModal() {
|
||||
this.$refs[this.modalName].closeModal();
|
||||
},
|
||||
onModalOpened() {
|
||||
this.internalModel = deepClone(this.modelValue);
|
||||
},
|
||||
onModalClosed() {
|
||||
this.internalModel = deepClone(this.modelValue);
|
||||
this.resetAlerts();
|
||||
},
|
||||
resetComponent(updatedServiceZipCodeInfo) {
|
||||
// Reset the validation form, setting the initial values
|
||||
// for the state and zipCode to those that were entered
|
||||
// on the service-zip-modal-question component
|
||||
this.$refs[this.modalName].form.resetForm({
|
||||
this.$refs[this.modalName].resetForm({
|
||||
values: {
|
||||
autocomplete: updatedServiceZipCodeInfo.streetAddress,
|
||||
city: updatedServiceZipCodeInfo.city,
|
||||
state: updatedServiceZipCodeInfo.state,
|
||||
zipCode: updatedServiceZipCodeInfo.zipCode,
|
||||
isVehicleProtected: updatedServiceZipCodeInfo.isVehicleProtected,
|
||||
|
|
@ -168,6 +180,9 @@ export default {
|
|||
resetModalButtonStyle() {
|
||||
this.$refs[this.modalName].resetButtonStyle();
|
||||
},
|
||||
resetAlerts() {
|
||||
this.$refs.addressQuestions.resetAlerts();
|
||||
},
|
||||
async setMobileLocation() {
|
||||
// Validate the Zip Code
|
||||
const zipCodeData = await this.getZipCodeData(
|
||||
|
|
@ -180,14 +195,19 @@ export default {
|
|||
} else {
|
||||
// retrieve mobile fee part
|
||||
const serviceZipCode = this.internalModel.addressQuestions.zipCode;
|
||||
|
||||
const mobileFeePart = await getPricedMobileFeePart(serviceZipCode);
|
||||
|
||||
// emit it to parent
|
||||
this.$emit("set-mobile-fee-part", mobileFeePart);
|
||||
// retrieve serviceability details
|
||||
const serviceabilityDetails = await getServiceabilityDetails(serviceZipCode);
|
||||
|
||||
// update content related to service zip code
|
||||
this.$emit("updated-mobile-fee-part", mobileFeePart);
|
||||
this.$emit("updated-serviceability", serviceabilityDetails.data);
|
||||
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
|
||||
|
||||
// Update the page level model
|
||||
this.$emit("update:modelValue", this.internalModel);
|
||||
|
||||
this.closeModal();
|
||||
}
|
||||
},
|
||||
|
|
@ -198,6 +218,8 @@ export default {
|
|||
this.internalModel = deepClone(newValue);
|
||||
|
||||
this.resetComponent({
|
||||
streetAddress: newValue.addressQuestions.streetAddress,
|
||||
city: newValue.addressQuestions.city,
|
||||
state: newValue.addressQuestions.state,
|
||||
zipCode: newValue.addressQuestions.zipCode,
|
||||
isVehicleProtected: newValue.isVehicleProtected,
|
||||
|
|
|
|||
|
|
@ -51,14 +51,11 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.question-text {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.625rem;
|
||||
|
||||
& > span {
|
||||
text-align: left;
|
||||
.modal-dialog {
|
||||
.question-text {
|
||||
& > span {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -8,25 +8,72 @@
|
|||
v-model="serviceZipCodeQuestion"
|
||||
ref="serviceZipCodeQuestion"
|
||||
:mobileFeePart="mobileFeePart"
|
||||
@set-mobile-fee-part="setMobileFeePart"
|
||||
@updated-mobile-fee-part="setMobileFeePart"
|
||||
@updated-serviceability="setServiceabilityDetails"
|
||||
@updated-contains-military-base="setContainsMilitaryBase"
|
||||
linkWidgetName="ServiceZipLinkWidget"
|
||||
modalWidgetName="ServiceZipModalWidget" />
|
||||
<alert
|
||||
ref="alertMilitaryBaseZip"
|
||||
class="my-4"
|
||||
cmsWidgetName="AlertMilitaryBaseZipWidget"
|
||||
v-if="zipContainsMilitaryBase"
|
||||
v-if="displayMilitaryZipAlert"
|
||||
alertClass="alert-warning" />
|
||||
<alert
|
||||
ref="alertMobileOnly"
|
||||
class="my-4"
|
||||
cmsWidgetName="AlertMobileOnlyWidget"
|
||||
v-if="displayServiceableMobileOnly"
|
||||
alertClass="alert-warning" />
|
||||
<alert
|
||||
ref="alertRecalNoMobile"
|
||||
class="my-4"
|
||||
cmsWidgetName="AlertRecalNoMobileWidget"
|
||||
v-if="displayRecalibrationWarning"
|
||||
@text-link-clicked="openModalAction"
|
||||
alertClass="alert-warning" />
|
||||
<alert
|
||||
ref="alertInshopOnly"
|
||||
class="my-4"
|
||||
cmsWidgetName="AlertInshopOnlyWidget"
|
||||
v-if="displayServiceableInshopOnly"
|
||||
alertClass="alert-warning" />
|
||||
<alert
|
||||
ref="alertNoShops"
|
||||
class="my-4"
|
||||
cmsWidgetName="AlertNoShopsWidget"
|
||||
v-if="displayNoShopsAlert"
|
||||
alertClass="alert-warning" />
|
||||
<appointmentTypeQuestion
|
||||
v-model="selectedAppointmentType"
|
||||
:isServiceableMobile="isServiceableMobile"
|
||||
:isServiceableInshop="isServiceableInshop"
|
||||
ref="appointmentTypeQuestion"
|
||||
groupName="appointmentTypeQuestion"
|
||||
cmsWidgetName="AppointmentTypeQuestionWidget"
|
||||
validationRules="option-required" />
|
||||
<mobileLocationModalQuestions
|
||||
v-show="selectedAppointmentType === 'Mobile'"
|
||||
v-model="mobileLocationQuestions"
|
||||
:mobileFeePart="mobileFeePart"
|
||||
@set-mobile-fee-part="setMobileFeePart"
|
||||
@updated-mobile-fee-part="setMobileFeePart"
|
||||
@updated-serviceability="setServiceabilityDetails"
|
||||
@updated-contains-military-base="setContainsMilitaryBase"
|
||||
ref="mobileLocationModalQuestions"
|
||||
linkWidgetName="MobileLocationLinkWidget"
|
||||
modalWidgetName="MobileLocationModalWidget" />
|
||||
<textboxQuestion
|
||||
v-show="selectedAppointmentType === 'Mobile'"
|
||||
ref="mobileLocationQuestionsError"
|
||||
v-model="mobileLocationValidationField"
|
||||
validationRules="mobile-location-required"
|
||||
hideInput
|
||||
centerErrorMessage />
|
||||
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
|
||||
<funnel-footer
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="funnelFooter"
|
||||
:isForwardActionDisabled="!meta.valid || shouldDisableForwardAction"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
@back-clicked="backButtonAction"
|
||||
@ForwardClicked="forwardButtonAction" />
|
||||
</div>
|
||||
|
|
@ -38,19 +85,30 @@
|
|||
import alert from "@/ux-components/alert/alert";
|
||||
import serviceZipModalQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-modal-question";
|
||||
import mobileLocationModalQuestions from "@/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions";
|
||||
import appointmentTypeQuestion from "@/layouts/service-location/appointment-type-question/appointment-type-question";
|
||||
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
||||
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
|
||||
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
||||
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
||||
import { Form } from "vee-validate";
|
||||
import { Form, defineRule } from "vee-validate";
|
||||
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
||||
import contentGroupModal from "@/fmg-components/content-group-modal/content-group-modal";
|
||||
|
||||
// Supporting files
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
||||
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
||||
import { settleAllPromises } from "@/helpers/layout-helper";
|
||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||
import { getPricedMobileFeePart } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
getServiceabilityDetails,
|
||||
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
|
||||
import store from "@/store";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
|
||||
defineRule("mobile-location-required", required(errorMessages.MOBILE_LOCATION_REQUIRED));
|
||||
|
||||
export default {
|
||||
name: "service-location",
|
||||
|
|
@ -62,26 +120,26 @@ export default {
|
|||
state: this.getServiceStateFromStore(),
|
||||
zipCode: this.getServiceZipCodeFromStore(),
|
||||
isVehicleProtected: null,
|
||||
isZipServiceableMobile: null,
|
||||
isZipServiceableInShop: null,
|
||||
isGlassServiceableInshop: null,
|
||||
isRecalibrationServiceableInshop: null,
|
||||
isGlassServiceableMobile: null,
|
||||
isRecalibrationServiceableMobile: null,
|
||||
mobileFeePart: null,
|
||||
zipContainsMilitaryBase: false,
|
||||
selectedAppointmentType: null,
|
||||
mobileLocationValidationField: null,
|
||||
};
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
// Call APIs
|
||||
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
|
||||
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
|
||||
const serviceType = store.getters.damage.isRepair ? "Repair" : "Replace";
|
||||
const parentAccountNumber = store.getters.payment.parentAccountNumber;
|
||||
const billToAccountNumber = 1;
|
||||
|
||||
const mobileFeePartPromise = getPricedMobileFeePart(
|
||||
serviceZipCode,
|
||||
serviceType,
|
||||
parentAccountNumber,
|
||||
billToAccountNumber
|
||||
);
|
||||
const serviceZipCode = store.getters.order.serviceLocation.zipCode;
|
||||
const getZipCodeData = baseMixin.methods.getZipCodeData(serviceZipCode);
|
||||
|
||||
const serviceabilityDetailsPromise = getServiceabilityDetails(serviceZipCode);
|
||||
|
||||
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode);
|
||||
|
||||
// Settle promises and get results
|
||||
const promiseResultMap = [
|
||||
|
|
@ -93,9 +151,13 @@ export default {
|
|||
resultKey: "mobileFeePart",
|
||||
promise: mobileFeePartPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "serviceabilityDetails",
|
||||
promise: serviceabilityDetailsPromise,
|
||||
},
|
||||
{
|
||||
resultKey: "zipCodeData",
|
||||
promise: baseMixin.methods.getZipCodeData(serviceZipCode),
|
||||
promise: getZipCodeData,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -104,9 +166,11 @@ export default {
|
|||
// Call the "next" function to complete the transition to this page.
|
||||
next((vm) => {
|
||||
vm.setCmsContent(resultMap.cmsContent);
|
||||
vm.setData(resultMap.mobileFeePart);
|
||||
vm.zipContainsMilitaryBase = resultMap.zipCodeData.containsMilitaryBase;
|
||||
vm.zipCode = serviceZipCode;
|
||||
vm.setData(
|
||||
resultMap.zipCodeData,
|
||||
resultMap.serviceabilityDetails,
|
||||
resultMap.mobileFeePart
|
||||
);
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -120,6 +184,7 @@ export default {
|
|||
set: function (newValue) {
|
||||
if (newValue.zipCode !== this.zipCode) {
|
||||
this.resetMobileLocation();
|
||||
this.selectedAppointmentType = null;
|
||||
}
|
||||
|
||||
this.state = newValue.state;
|
||||
|
|
@ -147,10 +212,56 @@ export default {
|
|||
this.state = newValue.addressQuestions.state;
|
||||
this.zipCode = newValue.addressQuestions.zipCode;
|
||||
this.isVehicleProtected = newValue.isVehicleProtected;
|
||||
|
||||
this.mobileLocationValidationField = "isValid";
|
||||
|
||||
if (
|
||||
newValue.zipCode !== this.zipCode &&
|
||||
!this.selectedAppointmentType == "Mobile"
|
||||
) {
|
||||
this.selectedAppointmentType = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
shouldDisableForwardAction() {
|
||||
return this.zipContainsMilitaryBase;
|
||||
isServiceableMobile() {
|
||||
if (this.isRecalibrationServiceableMobile !== null) {
|
||||
return this.isGlassServiceableMobile && this.isRecalibrationServiceableMobile;
|
||||
} else {
|
||||
return this.isGlassServiceableMobile;
|
||||
}
|
||||
},
|
||||
isServiceableInshop() {
|
||||
if (this.isRecalibrationServiceableInshop !== null) {
|
||||
return this.isGlassServiceableInshop && this.isRecalibrationServiceableInshop;
|
||||
} else {
|
||||
return this.isGlassServiceableInshop;
|
||||
}
|
||||
},
|
||||
isDualOrStaticRecalibration() {
|
||||
const supportingItems = store.getters.lineItems.supportingItems;
|
||||
|
||||
return supportingItems.some(
|
||||
(item) => item.partNumber === "RECAL STATIC" || item.partNumber === "RECAL DUAL"
|
||||
);
|
||||
},
|
||||
displayRecalibrationWarning() {
|
||||
return this.isDualOrStaticRecalibration;
|
||||
},
|
||||
displayServiceableInshopOnly() {
|
||||
return (
|
||||
!this.displayRecalibrationWarning &&
|
||||
this.isServiceableInshop &&
|
||||
!this.isServiceableMobile
|
||||
);
|
||||
},
|
||||
displayMilitaryZipAlert() {
|
||||
return this.zipContainsMilitaryBase && this.isServiceableMobile;
|
||||
},
|
||||
displayServiceableMobileOnly() {
|
||||
return this.isServiceableMobile && !this.isServiceableInshop;
|
||||
},
|
||||
displayNoShopsAlert() {
|
||||
return !this.isServiceableInshop && !this.isServiceableMobile;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -161,11 +272,24 @@ export default {
|
|||
store.getters.payment.isInsurance !== null
|
||||
);
|
||||
},
|
||||
setData(mobileFeePart) {
|
||||
setData(zipCodeData, serviceabilityDetails, mobileFeePart) {
|
||||
if (zipCodeData) {
|
||||
this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase;
|
||||
}
|
||||
|
||||
if (serviceabilityDetails) {
|
||||
this.setServiceabilityDetails(serviceabilityDetails);
|
||||
}
|
||||
|
||||
if (mobileFeePart) {
|
||||
this.mobileFeePart = mobileFeePart;
|
||||
}
|
||||
},
|
||||
setContainsMilitaryBase(val) {
|
||||
if (this.zipContainsMilitaryBase !== val) {
|
||||
this.zipContainsMilitaryBase = val;
|
||||
}
|
||||
},
|
||||
getServiceAddressFromStore() {
|
||||
return store.getters.order.serviceLocation.address;
|
||||
},
|
||||
|
|
@ -188,31 +312,47 @@ export default {
|
|||
|
||||
this.isVehicleProtected = null;
|
||||
},
|
||||
setServiceabilityDetails(serviceabilityDetails) {
|
||||
this.isGlassServiceableInshop = serviceabilityDetails.isGlassServiceableInshop;
|
||||
this.isRecalibrationServiceableInshop =
|
||||
serviceabilityDetails.isRecalibrationServiceableInshop;
|
||||
this.isGlassServiceableMobile = serviceabilityDetails.isGlassServiceableMobile;
|
||||
this.isRecalibrationServiceableMobile =
|
||||
serviceabilityDetails.isRecalibrationServiceableMobile;
|
||||
},
|
||||
backButtonAction() {
|
||||
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
},
|
||||
forwardButtonAction() {
|
||||
navigateToHeritageFunnel({ loadingModal: this.$refs.loadingModal });
|
||||
this.$router.navigateWithoutSaving(
|
||||
this.navigationScenarios.SELECTED_LOCATION,
|
||||
this.$route
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
zipCode(current, previous) {
|
||||
if (current !== previous) {
|
||||
baseMixin.methods.getZipCodeData(current).then((r) => {
|
||||
this.zipContainsMilitaryBase = r.containsMilitaryBase;
|
||||
});
|
||||
}
|
||||
openModalAction(modalName) {
|
||||
this.$refs[modalName].openModal();
|
||||
},
|
||||
},
|
||||
components: {
|
||||
alert,
|
||||
serviceZipModalQuestion,
|
||||
appointmentTypeQuestion,
|
||||
mobileLocationModalQuestions,
|
||||
funnelHeader,
|
||||
funnelFooter,
|
||||
funnelSubHeader,
|
||||
Form,
|
||||
loadingModal,
|
||||
textboxQuestion,
|
||||
contentGroupModal,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.question-text {
|
||||
& > span {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -33,12 +33,26 @@ const mockGetPricedMobileFeePart = (mockServiceZipCode) => {
|
|||
return Promise.resolve(mobileFeePart);
|
||||
};
|
||||
|
||||
const mockGetServiceabilityDetails = (mockServiceZipCode) => {
|
||||
const serviceabilityDetails = {
|
||||
isGlassServiceableInshop: true,
|
||||
isRecalibrationServiceableInshop: true,
|
||||
isGlassServiceableMobile: true,
|
||||
isRecalibrationServiceableMobile: true,
|
||||
};
|
||||
|
||||
return Promise.resolve(serviceabilityDetails);
|
||||
};
|
||||
|
||||
jest.mock(
|
||||
"@/layouts/service-location/helpers/service-location-helper/service-location-helper",
|
||||
() => ({
|
||||
getPricedMobileFeePart: jest.fn((mockServiceZipCode) => {
|
||||
return mockGetPricedMobileFeePart(mockServiceZipCode);
|
||||
}),
|
||||
getServiceabilityDetails: jest.fn((mockServiceZipCode) => {
|
||||
return mockGetServiceabilityDetails(mockServiceZipCode);
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
@footer-button-event="setZipCode">
|
||||
<serviceZipQuestion
|
||||
ref="serviceZipQuestion"
|
||||
customInputId="serviceZipCode"
|
||||
v-model="internalModel.zipCode"
|
||||
v-on="{ 'textboxQuestionEvent.inputIdAssigned': onInputIdAssigned }"
|
||||
:cmsWidgetName="textboxQuestionWidgetName" />
|
||||
|
|
@ -38,12 +39,15 @@ import textLink from "@/ux-components/text-link/text-link";
|
|||
import serviceZipQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-question/service-zip-question";
|
||||
import modal from "@/digital-components/modal/modal";
|
||||
import alert from "@/ux-components/alert/alert";
|
||||
import store from "@/store";
|
||||
import { getPricedMobileFeePart } from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
|
||||
|
||||
import {
|
||||
getPricedMobileFeePart,
|
||||
getServiceabilityDetails,
|
||||
} from "@/layouts/service-location/helpers/service-location-helper/service-location-helper";
|
||||
|
||||
export default {
|
||||
name: "service-zip-modal-question",
|
||||
emits: ["update:modelValue", "set-mobile-fee-part"],
|
||||
emits: ["update:modelValue", "updated-mobile-fee-part", "updated-contains-military-base"],
|
||||
data() {
|
||||
return {
|
||||
internalModel: this.copyModel(this.modelValue),
|
||||
|
|
@ -96,70 +100,70 @@ export default {
|
|||
resetAlerts() {
|
||||
this.displayInvalidZipAlert = false;
|
||||
},
|
||||
|
||||
resetsOnZipInput() {
|
||||
this.resetAlerts();
|
||||
},
|
||||
|
||||
focusOnZipInput() {
|
||||
const input = document.getElementById(this.serviceZipCodeTextInputId);
|
||||
input?.focus();
|
||||
},
|
||||
|
||||
copyModel(modelToCopy) {
|
||||
return {
|
||||
state: modelToCopy.state,
|
||||
zipCode: modelToCopy.zipCode,
|
||||
};
|
||||
},
|
||||
|
||||
openModal() {
|
||||
this.$refs[this.modalName].openModal();
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.$refs[this.modalName].closeModal();
|
||||
},
|
||||
|
||||
resetModalButtonStyle() {
|
||||
this.$refs[this.modalName].resetButtonStyle();
|
||||
},
|
||||
|
||||
onInputIdAssigned(inputId) {
|
||||
this.serviceZipCodeTextInputId = inputId;
|
||||
},
|
||||
|
||||
onModalOpened() {
|
||||
this.internalModel.zipCode = this.modelValue.zipCode;
|
||||
this.focusOnZipInput();
|
||||
},
|
||||
|
||||
onModalClosed() {
|
||||
this.internalModel.zipCode = this.modelValue.zipCode;
|
||||
this.resetsOnZipInput();
|
||||
},
|
||||
|
||||
async setZipCode() {
|
||||
this.resetAlerts();
|
||||
if (this.internalModel.zipCode !== this.modelValue.zipCode) {
|
||||
this.resetAlerts();
|
||||
|
||||
const zipCodeData = await this.getZipCodeData(this.internalModel.zipCode);
|
||||
const zipCodeData = await this.getZipCodeData(this.internalModel.zipCode);
|
||||
|
||||
if (!zipCodeData.isValid) {
|
||||
this.displayInvalidZipAlert = true;
|
||||
this.focusOnZipInput();
|
||||
this.resetModalButtonStyle();
|
||||
if (!zipCodeData.isValid) {
|
||||
this.displayInvalidZipAlert = true;
|
||||
this.focusOnZipInput();
|
||||
this.resetModalButtonStyle();
|
||||
} else {
|
||||
this.internalModel.state = zipCodeData.state;
|
||||
|
||||
// retrieve mobile fee part
|
||||
const serviceZipCode = this.internalModel.zipCode;
|
||||
const mobileFeePart = await getPricedMobileFeePart(serviceZipCode);
|
||||
|
||||
// retrieve serviceability details
|
||||
const serviceabilityDetails = await getServiceabilityDetails(serviceZipCode);
|
||||
|
||||
// update content related to service zip code
|
||||
this.$emit("updated-mobile-fee-part", mobileFeePart);
|
||||
this.$emit("updated-serviceability", serviceabilityDetails.data);
|
||||
this.$emit("updated-contains-military-base", zipCodeData.containsMilitaryBase);
|
||||
|
||||
// Update the page level model
|
||||
this.$emit("update:modelValue", this.internalModel);
|
||||
|
||||
this.closeModal();
|
||||
}
|
||||
} else {
|
||||
this.internalModel.state = zipCodeData.state;
|
||||
|
||||
// retrieve mobile fee part
|
||||
const serviceZipCode = this.internalModel.zipCode;
|
||||
const mobileFeePart = await getPricedMobileFeePart(serviceZipCode);
|
||||
|
||||
// emit it to parent
|
||||
this.$emit("set-mobile-fee-part", mobileFeePart);
|
||||
|
||||
// Update the page level model
|
||||
this.$emit("update:modelValue", this.internalModel);
|
||||
this.closeModal();
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -325,6 +325,17 @@ export default {
|
|||
false
|
||||
);
|
||||
|
||||
if (this.isWindshieldRepair) {
|
||||
const supportingItems = await this.dispatchStoreAction(
|
||||
storeActions.GET_SUPPORTING_ITEMS
|
||||
);
|
||||
this.dispatchStoreAction(
|
||||
this.storeActions.SAVE_SUPPORTING_ITEMS,
|
||||
supportingItems.data,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return this.navigateForward();
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import { experimentTriggers } from "../constants/experiments";
|
|||
import { applicationConfig } from "../constants/application-config";
|
||||
|
||||
// Components
|
||||
import quote from "@/layouts/quote/quote.vue";
|
||||
import datePicker from "@/digital-components/date-picker/date-picker.vue";
|
||||
import demoDatePicker from "@/layouts/demo-date-picker/demo-date-picker.vue";
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const fmgPageValues = {
|
|||
QUOTE: "quote",
|
||||
SERVICE_LOCATION: "service-location",
|
||||
HERITAGE: "heritage",
|
||||
SCHEDULE: "schedule",
|
||||
};
|
||||
|
||||
export { fmgPageValues };
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ const navigationScenarios = {
|
|||
CLICKED_BACK_WITH_CAPABILITY_QUESTIONS: "CLICKED_BACK_WITH_CAPABILITY_QUESTIONS",
|
||||
CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS: "CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS",
|
||||
CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS: "CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS",
|
||||
|
||||
// Scheduling
|
||||
SELECTED_LOCATION: "SELECTED_LOCATION",
|
||||
};
|
||||
|
||||
export { navigationScenarios };
|
||||
|
|
|
|||
|
|
@ -420,6 +420,19 @@ const routingTable = function (store) {
|
|||
scenario: navigationScenarios.CLICKED_BACK,
|
||||
destinationFmgPageValue: fmgPageValues.QUOTE,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.SELECTED_LOCATION,
|
||||
destinationFmgPageValue: fmgPageValues.SCHEDULE,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fmgPageValue: fmgPageValues.SCHEDULE,
|
||||
maps: [
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_BACK,
|
||||
destinationFmgPageValue: fmgPageValues.SERVICE_LOCATION,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ const getDefaultState = () => {
|
|||
state: null,
|
||||
zipCode: null,
|
||||
zipCodeCtu: null,
|
||||
appointmentType: null,
|
||||
},
|
||||
customer: {
|
||||
emailAddress: null,
|
||||
|
|
@ -926,6 +927,26 @@ export const actions = {
|
|||
});
|
||||
},
|
||||
|
||||
getServiceabilityDetails(context, { serviceZipCode }) {
|
||||
return globalMethods.callMockHttpClient({
|
||||
method: endpoints.GetServiceabilityDetails.method,
|
||||
//TODO: Remove Mocky Endpoints
|
||||
//endpoint: "https://run.mocky.io/v3/59e1a644-cf16-4f08-8069-1ab2a1e38f79", // NoShopsAvailable
|
||||
//endpoint: "https://run.mocky.io/v3/4fe1fb89-dd56-4e4a-9af2-96bd1ab77847", // ForcedInshop
|
||||
//endpoint: "https://run.mocky.io/v3/e2eaa097-6ea5-4906-af53-901edaa94939", // ForcedMobile
|
||||
endpoint: "https://run.mocky.io/v3/1811a1fe-12a7-48f3-939e-d10a9b77dd25", // All Options
|
||||
});
|
||||
|
||||
// TODO: Restore this when CSR-1104 is 100% complete
|
||||
// const lineItems = context.getters.order.lineItems;
|
||||
// const lineItemsToSend = [...lineItems.supportingItems];
|
||||
// const encodedLineItems = encodeURIComponent(JSON.stringify(lineItemsToSend));
|
||||
// return globalMethods.callHttpClient({
|
||||
// method: endpoints.GetServiceabilityDetails.method,
|
||||
// endpoint: `${endpoints.GetServiceabilityDetails.url}?zip=${serviceZipCode}&lineItems=${encodedLineItems}`,
|
||||
// });
|
||||
},
|
||||
|
||||
getSupportingItems(context) {
|
||||
const glassPartsArray = context.getters.lineItems.glassParts ?? [];
|
||||
const carId = context.getters.vehicle.carId;
|
||||
|
|
|
|||
|
|
@ -11,12 +11,11 @@
|
|||
<template v-for="paragraph in splitAlertCopyForParagraphTag" :key="paragraph">
|
||||
<p
|
||||
class="m-0 text-body small"
|
||||
v-if="!doesCopyContainRouterLink(paragraph)"
|
||||
v-if="!doesCopyContainRouterLink(paragraph) && !doesCopyContainTextLink(paragraph)"
|
||||
v-html="paragraph"></p>
|
||||
<p class="m-0 text-body small" v-else>
|
||||
<template v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)" :key="copy">
|
||||
<span v-if="!doesCopyContainRouterLink(copy)" v-html="copy"></span>
|
||||
<span v-else>
|
||||
<span v-if="doesCopyContainRouterLink(copy)">
|
||||
<router-link
|
||||
:to="{
|
||||
query: { [pageQueryString]: `${getRouterLinkRouteFromCopy(copy)}` },
|
||||
|
|
@ -25,6 +24,18 @@
|
|||
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
|
||||
>
|
||||
</span>
|
||||
<span v-else-if="doesCopyContainTextLink(copy)">
|
||||
<textLink
|
||||
linkType="text"
|
||||
:text="getRouterLinkDisplayTextFromCopy(copy)"
|
||||
href="#!"
|
||||
@click-event="
|
||||
$emit('textLinkClicked', getRouterLinkRouteFromCopy(copy))
|
||||
"
|
||||
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
|
||||
aria-label="Modal window" />
|
||||
</span>
|
||||
<span v-else v-html="copy"></span>
|
||||
</template>
|
||||
</p>
|
||||
</template>
|
||||
|
|
@ -40,11 +51,13 @@
|
|||
<script>
|
||||
import {
|
||||
doesCopyContainRouterLink,
|
||||
doesCopyContainTextLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
splitCMSCopyOnParagraphTag,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
import textLink from "@/ux-components/text-link/text-link";
|
||||
import { applicationConfig } from "@/constants/application-config";
|
||||
|
||||
export default {
|
||||
|
|
@ -96,6 +109,7 @@ export default {
|
|||
methods: {
|
||||
doesCopyContainRouterLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
doesCopyContainTextLink,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
ensureAlertIsInViewPort() {
|
||||
|
|
@ -120,6 +134,9 @@ export default {
|
|||
mounted() {
|
||||
this.ensureAlertIsInViewPort();
|
||||
},
|
||||
components: {
|
||||
textLink,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue