Merge branch 'develop' into feature/CSR-18

This commit is contained in:
CarlNation 2022-05-11 15:11:04 -04:00
commit 83c649fc8d
41 changed files with 1912 additions and 1186 deletions

View file

@ -30,6 +30,7 @@ resources:
variables:
- group: Digital-Infrastructure
- group: FixMyGlass-BuildBranches
stages:
# PR's
@ -47,6 +48,7 @@ stages:
# Dev Build/Deploy
- ${{ else }}:
- stage: Dev
condition: eq(variables['Build.SourceBranch'], variables['dev-branch'] )
variables:
- group: FixMyGlassDev
jobs:
@ -88,6 +90,7 @@ stages:
# QA Build/Deploy
- stage: Qa
condition: eq(variables['Build.SourceBranch'], variables['qa-branch'] )
variables:
- group: FixMyGlassQa
jobs:
@ -121,4 +124,7 @@ stages:
__VUE_APP_GOOGLE_PLACES_API_KEY__: $(__VUE_APP_GOOGLE_PLACES_API_KEY__)
__VUE_APP_HERITAGE_FUNNEL__: $(__VUE_APP_HERITAGE_FUNNEL__)
__VUE_APP_CURRENT_ENVIRONMENT__: $(__VUE_APP_CURRENT_ENVIRONMENT__)
indexDeployVariables:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
cfDistributionId: $(cfDistributionId)

View file

@ -11,11 +11,9 @@ module.exports = {
"!src/constants/*.js",
"!src/router/**/*.js",
"!src/helpers/unit-test-helper.js",
"!src/helpers/damage-helper.js",
"!src/layouts/component-test/component-test.vue",
"!src/layouts/form-test/form-test.vue",
"!src/layouts/vin-lookup/vin-lookup.vue",
"!src/layouts/license-plate-lookup/license-plate-lookup.vue",
"!src/layouts/vehicle-damage/windshield-damage-type-question/windshield-damage-type-question.vue",
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
"!src/layouts/part-questions/**/*.vue",
@ -28,7 +26,6 @@ module.exports = {
"!src/common-components/dropdown-question/dropdown-question.vue",
"!src/common-components/textbox-question/textbox-question.vue",
"!src/helpers/validation-rules.js",
"!src/helpers/damage-helper.js",
// END
], //! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],

1257
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,9 @@
<template>
<router-view></router-view>
<router-view v-slot="{ Component }">
<transition :duration="{ enter: 800, leave: 300 }" name="route-fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</template>
<style lang="scss">

BIN
src/assets/img/loader.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -162,7 +162,7 @@ export default {
<style lang="scss" scoped>
.button-question-overflow {
height: calc(100vh - 266px);
height: calc(100vh - 274px);
.overflow-scroll {
// Height will be determined by overall height of content above list

View file

@ -42,7 +42,8 @@ export default {
setup(props) {
const fieldOptions = {
type: "text",
value: props.modelValue,
value: props.modelValue,
initialValue: props.modelValue,
};
const {

View file

@ -0,0 +1,47 @@
import { shallowMount } from "@vue/test-utils";
import loadingModal from "./loading-modal";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
jest.mock('@/assets/img/loader.gif', () => 'loader.gif')
jest.mock('@/assets/img/windshield.png', () => 'windshield.png')
describe("loadingModal", () => {
test("showModal sets modal visible", async () => {
// Arrange
const { wrapper } = setupMocks();
wrapper.vm.isModalVisible = false;
//Act
wrapper.vm.showModal();
// Assert
expect(wrapper.vm.isModalVisible).toEqual(true);
wrapper.unmount();
});
});
function setupMocks() {
//Mock store
store.dispatch = jest.fn(() => {});
store.getters = { };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
});
const wrapper = shallowMount(loadingModal, mountOptions);
return { wrapper };
}

View file

@ -0,0 +1,97 @@
<template>
<div v-show="isModalVisible" class="loading-modal-backdrop">
<div class="loading-modal">
<section class="loading-modal-body">
<div class="modal-icon-container text-center">
<img class="loader-gif" alt="Loading" src="@/assets/img/loader.gif">
<img class="modal-icon" alt="" src="@/assets/img/windshield.png">
</div>
<div class="text-center fw-bold fs-5 loading-modal-text">
Please wait...
</div>
<div class="text-center fs-5 loading-modal-text">
This process can take up to 20 seconds.
</div>
</section>
</div>
</div>
</template>
<script>
export default {
name: 'Modal',
data() {
return {
isModalVisible: false,
};
},
methods: {
showModal() {
this.isModalVisible = true;
},
},
};
</script>
<style lang="scss">
.loading-modal-backdrop {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #e5e5e5;
display: flex;
justify-content: center;
align-items: center;
z-index: 1050;
width: 100vw;
height: 100vh;
}
.loading-modal {
position: absolute;
background: #ffffff;
padding: 0 0 32px 0;
box-shadow: 2px 2px 20px 1px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
border-radius: 4px;
top: 48px;
height: 246px;
width: 327px;
}
.loading-modal-body {
position: relative;
padding: 20px 10px;
}
.loading-modal-text {
padding: 0 24px 0 24px;
}
.modal-icon-container {
position: relative;
width: 75px;
height: 75px;
margin: 15px auto;
padding-bottom: 26px;
}
.modal-icon-container img {
position: absolute;
vertical-align: middle;
border: 0;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.loader-gif {
width: 75px;
height: 75px;
}
</style>

View file

@ -1,7 +1,8 @@
<template>
<div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
<input v-model="value"
<input
v-model="value"
v-maska="mask"
:type="type"
class="form-control"
@ -15,8 +16,7 @@
autocomplete="off"
:class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']"
:validationRules="validationRules"
@change="handleChange"
@blur="handleBlur" />
/>
<div v-show="errorMessage" class="row mt-2 form-test-error">
<span role="alert">{{ errorMessage }}</span>
</div>
@ -51,12 +51,26 @@ export default {
default: '',
},
validationRules: String,
cmsWidgetName: String,
cmsWidgetName: String
},
setup(props) {
const propsClone = Object.assign({}, props);
const modelValue = propsClone.modelValue;
let initialValue;
switch (typeof modelValue) {
case "number":
initialValue = modelValue;
break;
default:
initialValue = (modelValue && modelValue.length > 0) ? modelValue : "";
break;
}
const fieldOptions = {
type: "text",
value: props.modelValue,
value: modelValue,
initialValue: initialValue
};
const {
@ -98,10 +112,10 @@ export default {
words.forEach(function (word) {
const position = 1;
word = [word.toString().slice(0, position), noBreakChar, word.toString().slice(position)].join('');
questionText += `${word} `;
});
questionText = questionText.trimEnd();
questionText += `${word} `;
});
questionText = questionText.trimEnd();
} else {
questionText = this.questionText.toString();
}
@ -110,6 +124,11 @@ export default {
}
}
},
watch: {
value(newValue) {
this.handleChange(newValue);
}
}
};
</script>
@ -117,6 +136,7 @@ export default {
.textbox-question {
label {
color: $black;
font-weight: 500;
}
input {
&.has-icon {
@ -161,4 +181,4 @@ export default {
display: none;
}
}
</style>
</style>

View file

@ -13,6 +13,7 @@ const errorMessages = {
ZIP_REQUIRED: "Please enter your ZIP",
ZIP_FORMAT: "Please enter a valid ZIP",
LICENSE_PLATE_REQUIRED: "Please enter your license plate number",
REGISTRATION_ZIP_REQUIRED: "Please enter your registration ZIP code",
FIRST_NAME_REQUIRED: "Please enter your first name",
LAST_NAME_REQUIRED: "Please enter your last name",
EMAIL_ADDRESS_REQUIRED: "Please enter your email address",
@ -20,7 +21,7 @@ const errorMessages = {
SERVICE_ZIP_REQUIRED: "Please enter your Service ZIP",
SERVICE_ZIP_FORMAT: "Please enter a valid Service ZIP",
VIN_REQUIRED: "Please enter your VIN",
VIN_FORMAT: "Please enter a valid VIN",
VIN_FORMAT: "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",
};

View file

@ -3,9 +3,32 @@ import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
export function getDamageString() {
return store.getters.damage.glassToReplace.length > 1 ? "match" : store.getters.damage.glassToReplace[0].location;
const damageLocations = store.getters.damage.glassToReplace;
let returnString;
if(damageLocations.length > 1){
returnString = "match"
} else {
switch(damageLocations[0]?.location) {
case "Windshield":
returnString = "windshield"
break;
case "Driver":
case "Passenger":
returnString = "side window"
break;
case "Rear":
returnString = "rear window"
}
}
return returnString;
}
export function getIsWindshieldOnly () {
const damageLocations = store.getters.damage.glassToReplace;
const returnString = damageLocations.length === 1 && damageLocations[0]?.location === "Windshield" ? "windshield" : "glass";
return returnString;
}
export async function isGlassAvailableForCarId(carId){
const newGlassOptions = await baseMixin.methods.dispatchStoreAction(
storeActions.GET_DAMAGE_OPTIONS,

View file

@ -1,16 +1,91 @@
import {getDamageString, isGlassAvailableForCarId} from "./damage-helper";
//import baseMixin from "@/mixins/base-mixin.js";
import store from "@/store";
jest.mock("@/store", () => ({
getters: {damage: {
glassToReplace: [{location: "Windshield", name: "windshield"}]
}
}
}));
// Mock basemixin.
jest.mock("@/mixins/base-mixin.js", () => ({
methods: {
dispatchStoreAction: jest.fn().mockImplementation(() => { return {
data: {
windshieldOptions: {availableReplacementOptions: ["windshield"]}
}
} }),
},
}));
describe("damage-helper.js", () => {
it("Should return damage getter info", () => {
it("Should return match when multiple selected damage options are in the store", () => {
// Arrange / Act
store.getters.damage.glassToReplace = [{location: "Windshield", name: "windshield"}, {location: "Passenger", name: "sideWindow"}];
const damage = getDamageString();
expect(damage).toEqual("Windshield")
// Assert
expect(damage).toEqual("match");
});
});
});
describe("damage-helper.js", () => {
it("Should return windshield when Windshield is the only selected damage option in the store", () => {
// Arrange / Act
store.getters.damage.glassToReplace = [{location: "Windshield", name: "windshield"}];
const damage = getDamageString();
// Assert
expect(damage).toEqual("windshield");
});
});
describe("damage-helper.js", () => {
it("Should return side window when Driver or Passenger is the only selected damage option in the store", () => {
// Arrange / Act
store.getters.damage.glassToReplace = [{location: "Passenger", name: "sideWindow"}];
const damage = getDamageString();
// Assert
expect(damage).toEqual("side window");
});
});
describe("damage-helper.js", () => {
it("Should return rear window when Rear is the only selected damage option in the store", () => {
// Arrange / Act
store.getters.damage.glassToReplace = [{location: "Rear", name: "rear"}];
const damage = getDamageString();
// Assert
expect(damage).toEqual("rear window");
});
});
describe("damage-helper.js", () => {
it("Should return true if no mismatches between each array exist", async () => {
// Arrange
store.getters.damage.glassToReplace = [{location: "Windshield", name: "windshield"}];
// Act
const isGlassAvailable = await isGlassAvailableForCarId();
// Assert
expect(isGlassAvailable).toEqual(true);
});
});
describe("damage-helper.js", () => {
it("Should return false if any mismatches between each array exist", async () => {
// Arrange
store.getters.damage.glassToReplace = [{location: "Windshield", name: "sideWindow"}];
const isGlassAvailable = await isGlassAvailableForCarId();
// Assert
expect(isGlassAvailable).toEqual(false);
});
});

View file

@ -140,4 +140,4 @@ function isVinRelatedPage(toRoute) {
fmgPageValue === fmgPageValues.ADDRESS_LOOKUP ||
fmgPageValue === fmgPageValues.ADDRESS_VEHICLES ||
fmgPageValue === fmgPageValues.ESTIMATE;
}
}

View file

@ -367,4 +367,4 @@ describe("navigateToHeritageFunnel", () => {
})
);
});
});
});

View file

@ -5,54 +5,56 @@
ref="theForm"
v-slot="{ meta }"
autocomplete="off" >
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall px-5">
<div class="page-container-grouped-styles">
<loadingModal ref="loadingModal"/>
<funnelHeader cmsWidgetName="FunnelHeaderWidget" ref="funnelHeader" />
<vehicleBanner ref="vehicleBanner" :displayGenericVehicleImage=false />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" ref="funnelSubHeader" />
<customerQuestions ref="customerQuestions" v-model="customerQuestions" />
<alert ref="alertVinNotFound" v-show="displayVinNotFoundAlert"
class="my-3"
cmsWidgetName="AlertVinNotFoundWidget"
alertClass="alert-danger"
v-bind:isDismissible="false"
/>
<alert ref="alertMatchedDifferentVehicle" v-show="displayMatchedDifferentVehicleAlert"
class="my-3"
:manualHeadline="AlertMatchedDifferentVehicleHeader"
:manualCopy="AlertMatchedDifferentVehicleBody"
alertClass="alert-warning"
v-bind:isDismissible="false"
/>
<alert ref="alertNonServiceableZip" v-show="displayNonServiceableZipAlert"
class="my-3"
alertClass="alert-danger"
:manualHeadline="AlertNonServiceableZipHeader"
:manualCopy="AlertNonServiceableZipBody"
v-bind:isDismissible="false"
/>
<alert ref="alertVinLookupsByHomeAddressNotAllowed" v-show="displayVinLookupByHomeAddressNotAllowedAlert"
class="my-3"
cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget"
alertClass="alert-danger"
v-bind:isDismissible="false"
/>
<transition name="fade" mode="out-in">
<div class="service-zip-field" v-if="showServiceZipField" aria-live="polite">
<div class="row my-4">
<div class="col">
<textboxQuestion cmsWidgetName="ServiceZipQuestionWidget" v-model="serviceZip" ref="serviceZip" inputId="7add1b26df344f2caf1678de5797803f" aria-haspopup="" mask="#####" disableAutoFill validationRules="service-zip-required|service-zip-format" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" ref="funnelSubHeader" />
<div class="fade-on-route-transition sub-container make-tall">
<customerQuestions ref="customerQuestions" v-model="customerQuestions" />
<alert ref="alertVinNotFound" v-show="displayVinNotFoundAlert"
class="my-3"
cmsWidgetName="AlertVinNotFoundWidget"
alertClass="alert-danger"
v-bind:isDismissible="false"
/>
<alert ref="alertMatchedDifferentVehicle" v-show="displayMatchedDifferentVehicleAlert"
class="my-3"
:manualHeadline="AlertMatchedDifferentVehicleHeader"
:manualCopy="AlertMatchedDifferentVehicleBody"
alertClass="alert-warning"
v-bind:isDismissible="false"
/>
<alert ref="alertNonServiceableZip" v-show="displayNonServiceableZipAlert"
class="my-3"
alertClass="alert-danger"
:manualHeadline="AlertNonServiceableZipHeader"
:manualCopy="AlertNonServiceableZipBody"
v-bind:isDismissible="false"
/>
<alert ref="alertVinLookupsByHomeAddressNotAllowed" v-show="displayVinLookupByHomeAddressNotAllowedAlert"
class="my-3"
cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget"
alertClass="alert-danger"
v-bind:isDismissible="false"
/>
<transition name="fade" mode="out-in">
<div class="service-zip-field" v-if="showServiceZipField" aria-live="polite">
<div class="row my-4">
<div class="col">
<textboxQuestion cmsWidgetName="ServiceZipQuestionWidget" v-model="serviceZipCode" ref="serviceZip" inputId="7add1b26df344f2caf1678de5797803f" aria-haspopup="" mask="#####" disableAutoFill validationRules="service-zip-required|service-zip-format" />
</div>
</div>
</div>
</div>
</transition>
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction"
:isForwardActionDisabled="!meta.valid"
/>
</transition>
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction"
:isForwardActionDisabled="!meta.valid"
/>
</div>
</div>
</Form>
</template>
@ -67,6 +69,7 @@ import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-he
import customerQuestions from "@/layouts/address-lookup/customer-questions/customer-questions";
import alert from "@/ux-components/alert/alert";
import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
import { Form } from "vee-validate";
import { defineRule } from "vee-validate";
@ -115,19 +118,22 @@ export default {
streetAddress: this.getRegistrationAddressFromStore(),
city: this.getRegistrationCityFromStore(),
state: this.getRegistrationStateFromStore(),
zip: this.getRegistrationZipFromStore(),
zipCode: this.getRegistrationZipFromStore(),
},
firstName: this.getRegistrationFirstNameFromStore(),
lastName: this.getRegistrationLastNameFromStore(),
emailAddress: this.getEmailFromStore(),
},
serviceZip: this.getServiceZipFromStore(),
serviceZipCode: this.getServiceZipFromStore(),
displayNonServiceableZipAlert: false,
displayVinNotFoundAlert: false,
displayMatchedDifferentVehicleAlert: false,
displayVinLookupByHomeAddressNotAllowedAlert: false,
previousCarIdFound: "",
customAlertData: {},
previouslyEnteredCarId: "",
isSelectedGlassAvailableForVehicle: false,
customAlertData: {},
showServiceZipField: this.getServiceZipFromStore(),
isZipServicable: false,
}
},
methods: {
@ -173,65 +179,83 @@ export default {
this.resetWarningsAndErrors();
// Lookup VIN(s) with the provided address
const vinLookup = await this.lookupVin(
const vinLookup = this.lookupVin(
this.customerQuestions.lastName,
this.customerQuestions.addressQuestions.streetAddress,
this.customerQuestions.addressQuestions.zip,
this.customerQuestions.addressQuestions.zipCode,
this.customerQuestions.addressQuestions.state
);
if (!vinLookup.data.isStatePermissible) {
// Verify if the service zip code or registration zip code provided is serviceable
const zipValidation = this.serviceZipCode ? this.validateZip(this.serviceZipCode) : this.validateZip(this.customerQuestions.addressQuestions.zipCode);
const vinLookupResponse = await vinLookup;
const zipValidationResponse = await zipValidation;
if (!vinLookupResponse.data.isStatePermissible) {
// State Restrictions forbid lookup by address
this.displayVinLookupByHomeAddressNotAllowedAlert = true;
this.$refs.funnelFooter.removeLoader();
return;
}
// Validate if the original or service zip provided is serviceable
const zipValidation = this.serviceZip ? await this.validateZip(this.serviceZip) : await this.validateZip(this.customerQuestions.addressQuestions.zip);
if (!zipValidation.data.isServiceable) {
// if the neither the registration zip code or service zip code are not serviceable
this.isZipServicable = zipValidationResponse.data.isServiceable;
if (!this.isZipServicable) {
this.displayNonServiceableZipAlert = true;
this.showServiceZipField = true;
this.$refs.funnelFooter.removeLoader();
this.$refs.funnelFooter.removeLoader();
} else if (!this.serviceZipCode) {
// if the registration zip code is servicable and nothing was entered for the service zip code
// then set the service zip code to the registration zip code
this.serviceZipCode = this.customerQuestions.addressQuestions.zipCode;
}
const carEntered = store.getters.vehicle;
const carsFound = vinLookup.data.vinVehicles;
const carsFound = vinLookupResponse.data.vinVehicles;
if (carsFound.length == 0) {
// No VINs found
this.displayVinNotFoundAlert = true;
this.$refs.funnelFooter.removeLoader();
return;
} else if (carsFound.length == 1) {
var carFound = carsFound[0].vehicle;
const carFound = carsFound[0].vehicle;
this.isCarIdDifferent = carFound.carId !== carEntered.carId;
if (carEntered.carId == carFound.carId || carFound.carId == this.previousCarIdFound) {
// update data
this.updateVehicleInfo(carFound.vin, carFound);
this.updateCustomerInfo();
// navigate forward
this.navigateForward(carEntered, carsFound);
} else {
if (this.isCarIdDifferent && carFound.carId !== this.previouslyEnteredCarId) {
// Display Alert
this.previouslyEnteredCarId = carFound.carId;
this.customAlertData.vehicleInfo = carFound;
this.displayMatchedDifferentVehicleAlert = true;
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(carFound.carId);
// Update button "Continue with..."
this.$refs.funnelFooter.updateButtonText(`Continue with ${carFound.year} ${carFound.make} ${carFound.model}`);
this.$refs.funnelFooter.removeLoader();
}
return;
}
if (!this.isZipServicable) {
return;
}
this.previousCarIdFound = carFound.carId;
} else if (vinLookup.data.vinVehicles.length > 1) {
// update data if the zip or service zip is servicable
this.updateVehicleInfo(carsFound[0].vin, carFound);
this.updateCustomerInfo();
} else if (carsFound.length > 1) {
if (!this.isZipServicable) {
return;
}
// navigate forward
this.navigateForward(carEntered, carsFound);
// update data if the zip or service zip is servicable
this.updateCustomerInfo();
}
this.navigateForward(carEntered, carsFound);
},
resetWarningsAndErrors() {
this.displayVinNotFoundAlert = false;
@ -239,30 +263,35 @@ export default {
this.displayMatchedDifferentVehicleAlert = false;
this.displayVinLookupByHomeAddressNotAllowedAlert = false;
},
async navigateForward(carEntered, carsFound) {
navigateForward(carEntered, carsFound) {
if (carsFound.length == 1) {
// get the damage options for the car that was found
const carFound = carsFound[0].vehicle;
const glassOptions = await baseMixin.methods.dispatchStoreAction(
storeActions.GET_DAMAGE_OPTIONS,
{ carId: carFound.carId }
);
// if the car entered is the same as the car found OR the glass options for the found car match the users damage selections
if (carEntered.carId == carFound.carId || isGlassAvailableForCarId(carFound.carId)) {
navigateAfterSaveToHeritageFunnel(this.$route);
// if a different vehicle is found than the one entered and the selected glass
// is not available for that vehicle
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
this.$router.navigateAfterSave(
// then navigate back to "vehicle-damage", and display vehicle changed alert
// on that page
this.navigationScenarios.CONTINUING_WITH_DIFFERENT_GLASS,
this.$route, {}, {
displayVehicleChangeAlert: true
}, {}
);
} else {
// if not then navigate to the "vehicle-damage" page
this.$router.navigateAfterSave(this.navigationScenarios.CLICKED_FORWARD, this.$route, {}, { displayVehicleChangeAlert: true }, {});
}
// otherwise
this.$refs.loadingModal.showModal();
navigateAfterSaveToHeritageFunnel(this.$route);
}
} else if (carsFound.length > 1) {
// if multiple cars were found
if (carsFound.find(car => car.carId === carEntered.carId)) {
// and one of them matches the car id entered
this.$refs.loadingModal.showModal();
navigateAfterSaveToHeritageFunnel(this.$route);
} else {
// and there is no match, navigate to "address-vehicle" page
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, this.$route, {}, {}, carsFound);
// and there is no match, navigate to "address-vehicles" page
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES,
this.$route, {}, {},
carsFound);
}
}
@ -302,29 +331,29 @@ export default {
store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, this.customerQuestions.addressQuestions.zipCode);
store.commit(storeMutations.UPDATE_REGISTRATION_FIRST_NAME, this.customerQuestions.firstName);
store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, this.customerQuestions.lastName);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZip);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.customerQuestions.email);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZipCode);
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.customerQuestions.emailAddress);
},
},
computed: {
AlertNonServiceableZipHeader(){
let zip = this.serviceZip ? this.serviceZip : this.customerQuestions.addressQuestions.zip;
let text = this.getCmsContent("AlertNonServiceableZipWidget", "HeadlineText").replaceAll("{custom:serviceZip}", zip);
const zipCode = this.serviceZipCode ? this.serviceZipCode : this.customerQuestions.addressQuestions.zipCode;
const text = this.getCmsContent("AlertNonServiceableZipWidget", "HeadlineText").replaceAll("{custom:serviceZip}", zipCode);
return text;
},
AlertNonServiceableZipBody(){
return this.getCmsContent("AlertNonServiceableZipWidget", "BodyText");
},
AlertMatchedDifferentVehicleHeader(){
let text = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "HeadlineText").replaceAll("{custom:glassText}", getDamageString());
const text = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "HeadlineText").replaceAll("{custom:glassText}", getDamageString());
return text;
},
AlertMatchedDifferentVehicleBody(){
let content = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText");
content = content.replaceAll("{custom:glassText}", getDamageString());
let vinYmmFound = `${this.customAlertData?.vehicleInfo?.year} ${this.customAlertData?.vehicleInfo?.make} ${this.customAlertData?.vehicleInfo?.model}`;
let vinYmmExpected = `${store.getters.vehicle.year} ${store.getters.vehicle.make} ${store.getters.vehicle.model}`;
const vinYmmFound = `${this.customAlertData?.vehicleInfo?.year} ${this.customAlertData?.vehicleInfo?.make} ${this.customAlertData?.vehicleInfo?.model}`;
const vinYmmExpected = `${store.getters.vehicle.year} ${store.getters.vehicle.make} ${store.getters.vehicle.model}`;
content = content.replaceAll("{custom:vinYmmFound}", vinYmmFound);
content = content.replaceAll("{custom:vinYmmExpected}", vinYmmExpected);
@ -335,20 +364,27 @@ export default {
watch: {
customerQuestions: {
handler(newValue) {
// if they modify one of the lookup fields (address, city, state, zip, or lastName), then modify the button text back to Get my personalized quote
// if they modify one of the lookup fields (address, city, state, zipCode, or lastName), then modify the button text back to Get my personalized quote
this.$refs.funnelFooter.updateButtonText(this.getCmsContent("FunnelFooterWidget", "ForwardButtonText"));
this.showServiceZipField = false;
this.resetWarningsAndErrors();
},
deep: true
},
serviceZip: {
serviceZipCode: {
handler(newValue) {
// if they modify the service zip, then hide the error message
// if they modify the service zip code, then hide the error message
this.displayNonServiceableZipAlert = false;
},
},
showServiceZipField: {
handler(newValue) {
// if the Service Zip Code field is ever hidden, clear out it's value
if (!newValue) {
this.serviceZipCode = null;
}
},
}
},
components: {
funnelHeader,
@ -358,6 +394,7 @@ export default {
customerQuestions,
textboxQuestion,
alert,
loadingModal,
Form
},
};

View file

@ -17,7 +17,7 @@
<dropdownQuestion cmsWidgetName="StateQuestionWidget" v-model="addressModel.state" ref="state" inputId="8fdf9dc2e13e430eb57529499dceb3eb" :options="stateOptions" disableAutoFill validationRules="state-required" />
</div>
<div class="col">
<textboxQuestion cmsWidgetName="ZipQuestionWidget" v-model="addressModel.zip" ref="zip" inputId="01a9a1c2de0b4c9da8e023c9ae3be498" mask="#####" disableAutoFill validationRules="zip-required|zip-format"/>
<textboxQuestion cmsWidgetName="ZipQuestionWidget" v-model="addressModel.zipCode" ref="zipCode" inputId="01a9a1c2de0b4c9da8e023c9ae3be498" mask="#####" disableAutoFill validationRules="zip-code-required|zip-code-format"/>
</div>
</div>
</transition>
@ -48,8 +48,8 @@ import { errorMessages } from "@/constants/error-messages";
defineRule("street-address-required", required(errorMessages.STREET_ADDRESS_REQUIRED));
defineRule("city-required", required(errorMessages.CITY_REQUIRED));
defineRule("state-required", required(errorMessages.STATE_REQUIRED));
defineRule("zip-required", required(errorMessages.ZIP_REQUIRED));
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.ZIP_FORMAT));
defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED));
defineRule("zip-code-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.ZIP_FORMAT));
export default ({
name: "address-questions",
@ -61,7 +61,7 @@ export default ({
streetAddress: "",
city: "",
state: "",
zip: "",
zipCode: "",
}),
},
validationRules: String,
@ -146,6 +146,15 @@ export default ({
},
methods: {
setupAddressLookup() {
if (this.addressModel.streetAddress !== null &
this.addressModel.city !== null &
this.addressModel.state !== null &
this.addressModel.zipCode !== null) {
this.showAddressFields = true;
}
const addressField1 = document.getElementById("autocomplete");
const self = this;
@ -166,7 +175,7 @@ export default ({
// Standard place_changed event handling
autocomplete.addListener('place_changed', fillInAddress);
addressField1.onblur = function() {
addressField1.onchange = function() {
const hover = document.querySelector(".pac-container .pac-item:hover");
// if an item has been clicked, do nothing, otherwise get first solution and use Geocoder to get the place
if (hover === null) {
@ -187,7 +196,7 @@ export default ({
else {
self.addressModel.city = "";
self.addressModel.state = "";
self.addressModel.zip = "";
self.addressModel.zipCode = "";
self.showAddressFields = true;
self.displayVerificationWarning = false;
self.displayNoMatchWarning = true;
@ -225,7 +234,7 @@ export default ({
break;
}
case "postal_code": {
self.addressModel.zip = component.long_name;
self.addressModel.zipCode = component.long_name;
break;
}

View file

@ -43,7 +43,7 @@ export default ({
streetAddress: "",
city: "",
state: "",
zip: "",
zipCode: "",
},
firstName: "",
lastName: "",

View file

@ -5,29 +5,34 @@
ref="theForm"
v-slot="{ meta }"
>
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall px-5">
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner cmsWidgetName="VehicleBannerWidget" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<buttonQuestion
cmsWidgetName="VinLookupMethod"
class="button-question-overflow"
:questionText="questionText"
:answers="answersFromCms"
groupName="vinLookupMethodOption"
buttonType="listButton"
v-model="selectedValues"
isRequired
validationRules="option-required"
/>
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@isDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
<div class="fade-on-route-transition sub-container make-tall">
<alert
class="vinLookupMethodHeading"
v-model="customAlertData"
alertClass=""
cmsWidgetName="AlertVinLookupQuestion"
/>
<buttonQuestion
cmsWidgetName="VinLookupMethod"
:answers="answersFromCms"
groupName="vinLookupMethodOption"
buttonType="listButton"
v-model="selectedValues"
isRequired
validationRules="option-required"
/>
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@isDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
</div>
</Form>
</template>
@ -39,6 +44,7 @@ import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import buttonQuestion from "@/common-components/button-question/button-question";
import alert from "@/ux-components/alert/alert";
//Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
@ -126,6 +132,18 @@ export default {
funnelFooter,
buttonQuestion,
Form,
alert,
},
};
</script>
</script>
<style lang="scss">
.vinLookupMethodHeading p {
font-size: 1rem;
font-weight: 500;
color: $black;
}
div .current_car_info-text {
padding-bottom: 16px;
}
</style>

View file

@ -1,17 +1,18 @@
// Components
import vehicleDamage from "@/layouts/license-plate-lookup/license-plate-lookup.vue";
import licensePlateLookup from "@/layouts/license-plate-lookup/license-plate-lookup.vue";
// Supporting Files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper";
import baseMixin from "@/mixins/base-mixin";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { shallowMount, flushPromises } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import store from "@/store";
import { validate } from "vee-validate";
jest.mock('@/assets/img/loader.gif', () => 'loader.gif')
jest.mock('@/assets/img/windshield.png', () => 'windshield.png')
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
@ -29,25 +30,15 @@ jest.mock("@/store", () => ({
dispatch: jest.fn(),
getters: {
order: {
customer: {
emailAddress: "test@test.com"
},
serviceLocation: {
zipCode: "43443"
}
customer: { emailAddress: "test@test.com"},
serviceLocation: {zip: "11111"},
},
vehicle: {
carId: "C00000000",
image: "test.jpg",
payment: {
insuranceCoverage: {
isVerified: false
}
},
carId: "TESTID",
registration: {
licensePlate: "HWV4445",
zipCode: "43224"
}
licensePlate: "TESTPLATE",
zipCode: "12345",
},
},
eventBusItem: jest.fn(),
damage: {
@ -62,7 +53,7 @@ describe("license-plate-lookup.vue", () => {
const { wrapper } = setupMocks({});
//Act
vehicleDamage.beforeRouteEnter.call(
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
@ -84,7 +75,7 @@ describe("license-plate-lookup.vue", () => {
const { wrapper } = setupMocks({});
//Act
vehicleDamage.beforeRouteEnter.call(
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
@ -99,6 +90,340 @@ describe("license-plate-lookup.vue", () => {
});
});
describe("license-plate-lookup.vue", () => {
test("getLicensePlateFromStore returns store license plate", async () => {
// Arrange
const { wrapper } = setupMocks({});
// ACT
const licensePlate = wrapper.vm.getLicensePlateFromStore();
// Assert
expect(licensePlate).toEqual("TESTPLATE");
});
});
describe("license-plate-lookup.vue", () => {
test("getRegistrationZipFromStore returns store registration zip", async () => {
// Arrange
const { wrapper } = setupMocks({});
// ACT
const registrationZip = wrapper.vm.getRegistrationZipFromStore();
// Assert
expect(registrationZip).toEqual("12345");
});
});
describe("license-plate-lookup.vue", () => {
test("getEmailFromStore returns store customer email", async () => {
// Arrange
const { wrapper } = setupMocks({});
// ACT
const customerEmail = wrapper.vm.getEmailFromStore();
// Assert
expect(customerEmail).toEqual("test@test.com");
});
});
describe("license-plate-lookup.vue", () => {
test("getServiceZipFromStore returns store service zip", async () => {
// Arrange
const { wrapper } = setupMocks({});
// ACT
const serviceZip = wrapper.vm.getServiceZipFromStore();
// Assert
expect(serviceZip).toEqual("11111");
});
});
describe("license-plate-lookup.vue", () => {
test("Navigate forward should be called and isCarId should be set to false when data entered matches store data on forwardButtonAction click", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return {data: {isServiceable: true}};
});
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
const vinLookup = {data: {vehicle: {carId: "TESTID"}}}
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => {
return {catch: () => vinLookup};
});
wrapper.vm.navigateForward = jest.fn();
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
expect(wrapper.vm.isCarIdDifferent).toEqual(false);
});
});
describe("license-plate-lookup.vue", () => {
test("Function should stop and datam isRegistrationZipServicable should be set to false when service zip entered returns false on forwardButtonAction click", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return {data: {isServiceable: false}};
});
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.isRegistrationZipServicable).toEqual(false);
});
});
describe("license-plate-lookup.vue", () => {
test("Function should stop and datam isCarIdDifferent should be set to true when carId entered doesn't match store carId or previously entered carId on forwardButtonAction click", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return {data: {isServiceable: true}};
});
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
const vinLookup = {data: {vehicle: {carId: "TESTID1"}}}
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => {
return {catch: () => vinLookup};
});
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.isCarIdDifferent).toEqual(true);
});
});
describe("license-plate-lookup.vue", () => {
test("Navigate forward should be called and isCarId should be set to true when carId entered matches previously entered carId and rest of data entered matches store data on forwardButtonAction click", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.validateZip = jest.fn().mockImplementation(() => {
return {data: {isServiceable: true}};
});
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
const vinLookup = {data: {vehicle: {carId: "TESTID1"}}}
wrapper.vm.lookupVin = jest.fn().mockImplementation(() => {
return {catch: () => vinLookup};
});
wrapper.vm.previouslyEnteredCarId = "TESTID1";
wrapper.vm.navigateForward = jest.fn();
//Act
licensePlateLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "license-plate-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
expect(wrapper.vm.isCarIdDifferent).toEqual(true);
});
});
describe("license-plate-lookup.vue", () => {
test("Button Text should revert to initial value when licensePlate textfield has new text", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.licensePlate = "NEWPLATE";
wrapper.vm.getCmsContent = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toHaveBeenCalled();
});
});
describe("license-plate-lookup.vue", () => {
test("Button Text should revert to initial value when registrationZip textfield has new text", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.registrationZip = "55555";
wrapper.vm.getCmsContent = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toHaveBeenCalled();
});
});
describe("license-plate-lookup.vue", () => {
test("Button Text should revert to initial value when serviceZip textfield has new text", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.serviceZip = "55555";
wrapper.vm.getCmsContent = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toHaveBeenCalled();
});
});
describe("license-plate-lookup.vue", () => {
test("Dispatch reset damage and dependencies should be called if isCarIdDifferent is true and isSelectedGlassAvailableForVehicle is false when updateCustomerInfo is called", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.isCarIdDifferent = true;
wrapper.vm.isSelectedGlassAvailableForVehicle = false;
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
store.commit = jest.fn();
store.dispatch = jest.fn();
const vehicleInfo = {year: "2020", make: "honda", model: "civic", style: "2 door", carId: "TestId", category: "testCat", imageUrl: "image.jpg", imageVifNumber: "123", imageColor: "blue"}
await wrapper.vm.updateCustomerInfo('vin', vehicleInfo, 'registrationState');
//Assert
expect(store.dispatch).toHaveBeenCalled();
});
});
describe("license-plate-lookup.vue", () => {
test("NavigateAfterSave should be called if isCarIdDifferent is true and isSelectedGlassAvailableForVehicle is false when navigateForward is called", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.isCarIdDifferent = true;
wrapper.vm.isSelectedGlassAvailableForVehicle = false;
wrapper.vm.$router.navigateAfterSave = jest.fn();
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
store.dispatch = jest.fn();
await wrapper.vm.navigateForward();
//Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalled();
});
});
describe("license-plate-lookup.vue", () => {
test("navigateAfterSaveToHeritageFunnel should be called if isCarIdDifferent is false or isSelectedGlassAvailableForVehicle is true when navigateForward is called", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.vm.isCarIdDifferent = false;
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {
return '';
});
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn();
await wrapper.vm.navigateForward();
//Assert
expect(navigateToHeritage.navigateAfterSaveToHeritageFunnel).toHaveBeenCalled();
});
});
describe("license-plate-lookup.vue", () => {
test("dispatch non blocking store action called on validate zip", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
await wrapper.vm.validateZip("12345");
//Assert
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalled();
});
});
describe("license-plate-lookup.vue", () => {
test("dispatch non blocking store action called on lookup vin", async () => {
// Arrange
const { wrapper } = setupMocks({});
//Act
await wrapper.vm.lookupVin("zzz123fqsfwg");
//Assert
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalled();
});
});
function setupMocks({
pageHeaderWidgetHeaderText = {},
@ -106,12 +431,8 @@ function setupMocks({
router: {
navigate: jest.fn(),
},
store: {
getters: {
vehicle: {},
payment: { insuranceCoverage: { isVerified: false } },
},
},
licensePlate: "TESTPLATE",
registrationZip: "12345"
},
}) {
//Mock api responses
@ -139,7 +460,7 @@ function setupMocks({
const mountOptions = getMountOptions(mountOptionsMockData);
mountOptions['attachTo'] = document.body; // append wrapper to document.body to test DOM methods
const wrapper = shallowMount(vehicleDamage, mountOptions);
const wrapper = shallowMount(licensePlateLookup, mountOptions);
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;

View file

@ -1,108 +1,41 @@
<template>
<Form
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
>
<div
class="container-fluid shadow rounded-3 p-2 position-relative make-tall px-5"
>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles">
<loadingModal ref="loadingModal"/>
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false"
/>
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="row my-2">
<div class="col">
<textboxQuestion
cmsWidgetName="LicensePlateNumber"
v-model="licensePlate"
isRequired
inputId="license_plate"
validationRules="license-plate-required"
/>
<div class="fade-on-route-transition sub-container make-tall">
<div class="row my-2">
<div class="col">
<textboxQuestion cmsWidgetName="LicensePlateNumber" v-model="licensePlate" isRequired inputId="license_plate" validationRules="license-plate-required" />
</div>
</div>
</div>
<div class="row my-2">
<div class="col">
<textboxQuestion
cmsWidgetName="RegistrationZip"
v-model="registrationZip"
inputId="zip"
mask="#####"
validationRules="zip-required"
/>
<textboxQuestion
cmsWidgetName="RegistrationZip"
v-model="registrationZip"
isRequired
inputId="zip"
mask="#####"
validationRules="zip-required"
/>
<div class="row my-2">
<div class="col">
<textboxQuestion cmsWidgetName="RegistrationZip" v-model="registrationZip" inputId="zip" mask="#####" validationRules="zip-required" />
</div>
</div>
</div>
<div class="row my-2">
<div class="col">
<textboxQuestion
cmsWidgetName="EmailAddress"
v-model="email"
inputId="email"
validationRules="email-address-required|email-address-format"
/>
<textboxQuestion
cmsWidgetName="EmailAddress"
v-model="email"
isRequired
inputId="email"
validationRules="email-address-required|email-address-format"
/>
<div class="row my-2">
<div class="col">
<textboxQuestion cmsWidgetName="EmailAddress" v-model="email" inputId="email" validationRules="email-address-required|email-address-format" />
</div>
</div>
</div>
<alert
class="my-3"
:manualHeadline="NoServiceZipHeader"
:manualCopy="NoServiceZipBody"
v-if="!isRegistrationZipServicable && isVinValid && !isCarIdDifferent"
alertClass="alert-danger"
/>
<div class="row my-2">
<div class="col">
<textboxQuestion
v-if="!isRegistrationZipServicable"
cmsWidgetName="ServiceZip"
v-model="serviceZip"
inputId="serviceZip"
validationRules="zip-required"
/>
<alert class="my-3" :manualHeadline="NoServiceZipHeader" :manualCopy="NoServiceZipBody" v-if="!isRegistrationZipServicable && isVinValid && !isCarIdDifferent" alertClass="alert-danger" />
<div class="row my-2">
<div class="col">
<textboxQuestion v-if="!isRegistrationZipServicable" cmsWidgetName="ServiceZip" v-model="serviceZip" inputId="serviceZip" validationRules="zip-required" />
</div>
</div>
<alert class="my-3" cmsWidgetName="NoMatchAlertWidget" v-if="!isVinValid" />
<alert class="my-3" :manualHeadline="MatchedDifferentVehicleAlertHeader" :manualCopy="MatchedDifferentVehicleAlertBody" v-if="isCarIdDifferent" alertClass="alert-warning" />
<funnelFooter ref="funnelFooter" cmsWidgetName="FunnelFooterWidget" :isForwardActionDisabled="!meta.valid" @back-clicked="backButtonAction" @ForwardClicked="forwardButtonAction" />
</div>
<alert
class="my-3"
cmsWidgetName="NoMatchAlertWidget"
v-if="!isVinValid"
alertClass="alert-danger"
/>
<alert
class="my-3"
:manualHeadline="MatchedDifferentVehicleAlertHeader"
:manualCopy="MatchedDifferentVehicleAlertBody"
v-if="isCarIdDifferent"
alertClass="alert-warning"
/>
<funnelFooter
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
</Form>
</template>
<script>
// Components
import funnelHeader from "@/common-components/funnel-header/funnel-header";
@ -111,29 +44,47 @@ import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import alert from "@/ux-components/alert/alert";
import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import {
fetchCmsContentForPage
} from "@/helpers/cms-content-helper";
import {
settleAllPromises
} from "@/helpers/layout-helper";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { errorMessages } from "@/constants/error-messages";
import {
storeActions
} from "@/constants/store-actions";
import {
storeMutations
} from "@/constants/store-mutations";
import {
errorMessages
} from "@/constants/error-messages";
import {
navigateAfterSaveToHeritageFunnel
} from "@/helpers/heritage-integration/navigation-helper";
import {
getDamageString,
isGlassAvailableForCarId,
} from "@/helpers/damage-helper";
import { required, regex } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import {
required,
regex,
} from "@/helpers/validation-rules";
import {
Form,
defineRule,
} from "vee-validate";
// DEFINE VALIDATION RULES
defineRule(
"license-plate-required",
required(errorMessages.LICENSE_PLATE_REQUIRED)
);
defineRule("zip-required", required(errorMessages.ZIP_REQUIRED));
defineRule("zip-required", required(errorMessages.REGISTRATION_ZIP_REQUIRED));
defineRule(
"email-address-required",
required(errorMessages.EMAIL_ADDRESS_REQUIRED)
@ -145,7 +96,6 @@ defineRule(
errorMessages.EMAIL_ADDRESS_FORMAT
)
);
export default {
name: "license-plate-lookup",
async beforeRouteEnter(to, from, next) {
@ -159,9 +109,7 @@ export default {
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
@ -182,6 +130,7 @@ export default {
previouslyEnteredCarId: "",
customAlertData: {},
isSelectedGlassAvailableForVehicle: true,
zipToDisplay: this.getRegistrationZipFromStore(),
};
},
mounted() {
@ -193,7 +142,6 @@ export default {
"MatchedDifferentVehicleAlertWidget",
"HeadlineText"
).replaceAll("{custom:damage}", getDamageString());
return text;
},
MatchedDifferentVehicleAlertBody() {
@ -214,15 +162,13 @@ export default {
"{custom:plateLookupModel}",
this.customAlertData?.vehicleInfo?.model
);
return text;
},
NoServiceZipHeader() {
let text = this.getCmsContent(
"NoServiceZipWidget",
"HeadlineText"
).replaceAll("{custom:zip}", this.registrationZip);
).replaceAll("{custom:zip}", this.zipToDisplay);
return text;
},
NoServiceZipBody() {
@ -259,24 +205,24 @@ export default {
getEmailFromStore() {
return store.getters.order.customer.emailAddress;
},
getServiceZipFromStore(){
return store.getters.order.serviceLocation.zipCode
getServiceZipFromStore() {
return store.getters.order.serviceLocation.zip;
},
backButtonAction() {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {
const zipValidation = this.serviceZip
? await this.validateZip(this.serviceZip)
: await this.validateZip(this.registrationZip);
const zipValidation = this.serviceZip ?
await this.validateZip(this.serviceZip) :
await this.validateZip(this.registrationZip);
if (!zipValidation.data.isServiceable) {
this.$refs.funnelFooter.removeLoader();
this.isVinValid = true;
this.isRegistrationZipServicable = false;
this.isCarIdDifferent = false;
this.zipToDisplay = this.serviceZip ? this.serviceZip : this.registrationZip;
return;
}
const vinLookup = await this.lookupVin(
this.licensePlate,
zipValidation.data.state
@ -286,7 +232,6 @@ export default {
this.isCarIdDifferent = false;
return;
});
this.isCarIdDifferent =
vinLookup.data.vehicle.carId !== store.getters.vehicle.carId;
@ -311,33 +256,21 @@ export default {
vinLookup.data.vehicle,
zipValidation.data.state
);
const partsData = await baseMixin.methods.dispatchStoreAction(
this.storeActions.GET_PARTS_OR_QUESTIONS,
{
carId: vinLookup.data.vehicle.carId,
glassArray:
this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle
? []
: store.getters.damage.glassToReplace,
zipCode: this.serviceZip ? this.serviceZip : this.registrationZip,
vin: vinLookup.data.vin,
},
false
);
this.navigateForward(partsData);
this.navigateForward();
},
navigateForward(partsData) {
navigateForward() {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
this.$router.navigateAfterSave(
this.navigationScenarios.CLICKED_FORWARD,
this.$route,
{},
{ displayVehicleChangeAlert: true },
partsData.data
{}
);
return;
} else {
this.$refs.loadingModal.showModal();
navigateAfterSaveToHeritageFunnel(this.$route);
return;
}
@ -355,7 +288,7 @@ export default {
},
updateCustomerInfo(vin, vehicleInfo, registrationState) {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
}
store.commit(storeMutations.UPDATE_VEHICLE_VIN, vin);
store.commit(storeMutations.UPDATE_YEAR, vehicleInfo.year);
@ -363,11 +296,11 @@ export default {
store.commit(storeMutations.UPDATE_MODEL, vehicleInfo.model);
store.commit(storeMutations.UPDATE_STYLE, vehicleInfo.style);
store.commit(storeMutations.UPDATE_CAR_ID, vehicleInfo.carId);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY,vehicleInfo.category);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL,vehicleInfo.imageUrl);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER,vehicleInfo.imageVifNumber);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR,vehicleInfo.imageColor);
store.commit(storeMutations.UPDATE_REGISTRATION_LICENSE_PLATE,this.licensePlate);
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, vehicleInfo.category);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, vehicleInfo.imageUrl);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicleInfo.imageVifNumber);
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicleInfo.imageVifColor);
store.commit(storeMutations.UPDATE_REGISTRATION_LICENSE_PLATE, this.licensePlate);
store.commit(storeMutations.UPDATE_REGISTRATION_STATE, registrationState);
store.commit(storeMutations.UPDATE_REGISTRATION_ZIP_CODE, this.registrationZip);
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZip);
@ -399,6 +332,7 @@ export default {
textboxQuestion,
alert,
funnelFooter,
loadingModal,
},
};
</script>
</script>

View file

@ -1,12 +1,14 @@
<template>
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall">
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage=false />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<h1>Part Questions Page Placeholder</h1>
<funnel-footer
cmsWidgetName="FunnelFooterWidget" @back-clicked="backButtonAction"
/>
<div class="fade-on-route-transition sub-container make-tall">
<h1>Part Questions Page Placeholder</h1>
<funnel-footer
cmsWidgetName="FunnelFooterWidget" @back-clicked="backButtonAction"
/>
</div>
</div>
</template>

View file

@ -5,61 +5,63 @@
ref="theForm"
v-slot="{ meta }"
>
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall px-5">
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage=false />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<alert
ref="vehicleChangeAlert"
class="mt-5 mb-0"
cmsWidgetName="VehicleChangeAlert"
v-show="shouldDisplayVehicleChangeAlert"
alertClass="alert-warning"
:isDismissible="false"
/>
<damageLocationQuestion
ref="damageLocation"
cmsWidgetName="DamageLocationQuestion"
v-model="selectedDamageLocations"
groupName="DamageLocationQuestion"
/>
<windshieldOptions
ref="windshieldOptions"
v-model="selectedWindshieldOptions"
:hasRepairReplaceConflict="hasRepairReplaceConflict"
:hasSplitSingleConflict="hasSplitSingleConflict"
:selectedDamageLocations="selectedDamageLocations"
/>
<alert
class="my-3"
cmsWidgetName="HasReplacementConflict"
v-show="hasRepairReplaceConflict"
alertClass="alert-danger"
:isDismissible="false"
/>
<sideDoorOptions
ref="sideDoorOptions"
cmsWidgetName="SideDoorSideQuestion"
groupName="SideDoorSideQuestion"
v-model="sideDoorOptionsData"
v-show="!hasRepairReplaceConflict"
:selectedDamageLocations="selectedDamageLocations"
/>
<replaceOptionsQuestion
ref="backGlassOptions"
cmsWidgetName="RearReplaceOptionsQuestion"
:isAvailable="isRearWindowDamageLocation && !hasRepairReplaceConflict"
v-model="selectedRearReplaceOptions"
groupName="BackGlassReplaceOptionsQuestion"
validationRules="replace-options-required"
/>
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
:isBackButtonHidden=shouldHideBackButton
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
<div class="fade-on-route-transition sub-container make-tall">
<alert
ref="vehicleChangeAlert"
class="mt-5 mb-0"
cmsWidgetName="VehicleChangeAlert"
v-show="shouldDisplayVehicleChangeAlert"
alertClass="alert-warning"
:isDismissible="false"
/>
<damageLocationQuestion
ref="damageLocation"
cmsWidgetName="DamageLocationQuestion"
v-model="selectedDamageLocations"
groupName="DamageLocationQuestion"
/>
<windshieldOptions
ref="windshieldOptions"
v-model="selectedWindshieldOptions"
:hasRepairReplaceConflict="hasRepairReplaceConflict"
:hasSplitSingleConflict="hasSplitSingleConflict"
:selectedDamageLocations="selectedDamageLocations"
/>
<alert
class="my-3"
cmsWidgetName="HasReplacementConflict"
v-show="hasRepairReplaceConflict"
alertClass="alert-danger"
:isDismissible="false"
/>
<sideDoorOptions
ref="sideDoorOptions"
cmsWidgetName="SideDoorSideQuestion"
groupName="SideDoorSideQuestion"
v-model="sideDoorOptionsData"
v-show="!hasRepairReplaceConflict"
:selectedDamageLocations="selectedDamageLocations"
/>
<replaceOptionsQuestion
ref="backGlassOptions"
cmsWidgetName="RearReplaceOptionsQuestion"
:isAvailable="isRearWindowDamageLocation && !hasRepairReplaceConflict"
v-model="selectedRearReplaceOptions"
groupName="BackGlassReplaceOptionsQuestion"
validationRules="replace-options-required"
/>
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
:isBackButtonHidden=shouldHideBackButton
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
</div>
</Form>
</template>

View file

@ -1,6 +1,6 @@
<template>
<buttonQuestion
class="radioQuestion"
class="radioQuestion"
isOverflowScrollable
selectingInitiatesLoad
:questionText="questionText"

View file

@ -1,5 +1,5 @@
<template>
<div class="container-fluid shadow rounded-3 p-0 position-relative make-tall">
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="select-car">
<div class="select-car-form rounded text-center">
@ -10,7 +10,9 @@
backButtonAccessibleText="Change Vehicle Year"
@click-event="backButtonAction"
/>
<makeQuestion v-model="selectedMake" ref="makeQuestion" cmsWidgetName="VehicleMakeQuestion" />
<div class="fade-on-route-transition">
<makeQuestion v-model="selectedMake" ref="makeQuestion" cmsWidgetName="VehicleMakeQuestion" />
</div>
</div>
</div>
</div>

View file

@ -1,5 +1,5 @@
<template>
<div class="container-fluid shadow rounded-3 p-0 position-relative">
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="select-car">
<div class="select-car-form rounded text-center">
@ -10,7 +10,9 @@
backButtonAccessibleText="Change Vehicle Make"
@click-event="backButtonAction"
/>
<modelQuestion v-model="selectedModel" ref="modelQuestion" cmsWidgetName="VehicleModelQuestion" />
<div class="fade-on-route-transition">
<modelQuestion v-model="selectedModel" ref="modelQuestion" cmsWidgetName="VehicleModelQuestion" />
</div>
</div>
</div>
</div>

View file

@ -1,36 +1,38 @@
<template>
<div class="container-fluid shadow rounded-3 p-2 position-relative make-tall">
<div class="page-container-grouped-styles">
<funnelHeader ref="funnelHeader" cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner ref="vehicleBanner" cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
<funnelSubHeader ref="funnelSubHeader" cmsWidgetName="FunnelSubHeaderWidget" />
<div class="container-fluid prevent-squish my-5">
<div class="row">
<div class="col">
<alert
class="rounded border-0 shadow-sm"
alertClass="alert-warning"
cmsWidgetName="AlertWidget"
:isDismissible="false"
/>
<div class="fade-on-route-transition sub-container make-tall">
<div class="container-fluid prevent-squish my-5">
<div class="row">
<div class="col">
<alert
class="rounded border-0 shadow-sm"
alertClass="alert-warning"
cmsWidgetName="AlertWidget"
:isDismissible="false"
/>
</div>
</div>
</div>
</div>
<div v-for="(item, i) in PartsForQuestions" :key="i">
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->
<div class="container-fluid">
<hr v-if="i > 0" />
<div v-for="(item, i) in PartsForQuestions" :key="i">
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->
<div class="container-fluid">
<hr v-if="i > 0" />
</div>
<glassPartQuestion
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
v-model="glassParts[item.glassLocation + '-' + item.glassName]"
:glassLocation="item.glassLocation"
:glassName="item.glassName"
:colorAnswers="item.colorAnswers"
/>
</div>
<glassPartQuestion
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
v-model="glassParts[item.glassLocation + '-' + item.glassName]"
:glassLocation="item.glassLocation"
:glassName="item.glassName"
:colorAnswers="item.colorAnswers"
/>
<funnelFooter cmsWidgetName="FunnelFooterWidget" ref="funnelFooter" @back-clicked="backButtonAction" @ForwardClicked="forwardButtonAction" />
</div>
<funnelFooter cmsWidgetName="FunnelFooterWidget" ref="funnelFooter" @back-clicked="backButtonAction" @ForwardClicked="forwardButtonAction" />
</div>
</template>

View file

@ -1,5 +1,5 @@
<template>
<div class="container-fluid shadow rounded-3 p-0 position-relative">
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="select-car">
<div class="select-car-form rounded text-center">
@ -10,7 +10,9 @@
backButtonAccessibleText="Change Vehicle Model"
@click-event="backButtonAction"
/>
<styleQuestion v-model="selectedStyle" ref="styleQuestion" cmsWidgetName="VehicleStyleQuestion" />
<div class="fade-on-route-transition">
<styleQuestion v-model="selectedStyle" ref="styleQuestion" cmsWidgetName="VehicleStyleQuestion" />
</div>
</div>
</div>
</div>

View file

@ -1,11 +1,18 @@
<template>
<div class="container-fluid shadow rounded-3 p-0 position-relative">
<div class="page-container-grouped-styles">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="select-car">
<div class="select-car-form rounded text-center">
<vehicleBanner cmsWidgetName="VehicleBannerWidget" displayGenericVehicleImage />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<yearQuestion v-model="selectedYear" ref="yearQuestion" cmsWidgetName="VehicleYearQuestion" />
<div class="fade-on-route-transition">
<yearQuestion
class="fade-on-route-transition"
v-model="selectedYear"
ref="yearQuestion"
cmsWidgetName="VehicleYearQuestion"
/>
</div>
</div>
</div>
</div>

View file

@ -5,109 +5,118 @@
ref="theForm"
v-slot="{ meta }"
>
<div
class="container-fluid shadow rounded-3 p-2 position-relative make-tall px-5"
>
<div class="page-container-grouped-styles">
<loadingModal ref="loadingModal"/>
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false"
/>
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="row my-2">
<div class="col">
<textboxQuestion
cmsWidgetName="VinNumber"
v-model="vin"
inputId="vin"
isRequired
disableAutoFill
validationRules="vin-required|vin-format"
:isDisabled="isVinFieldReadOnly"
/>
<div class="fade-on-route-transition sub-container make-tall">
<div class="row my-2">
<div class="col">
<textboxQuestion
cmsWidgetName="VinNumber"
v-model="vin"
inputId="vin"
isRequired
disableAutoFill
validationRules="vin-required|vin-format"
:isDisabled="isVinFieldReadOnly"
/>
</div>
</div>
</div>
<div class="row my-2">
<div class="col">
<vinInformation />
<div class="row my-2">
<div class="col">
<vinInformation />
</div>
</div>
</div>
<div class="row my-2">
<div class="col">
<textboxQuestion
cmsWidgetName="ServiceZIP"
v-model="zip"
inputId="zip"
mask="#####"
isRequired
disableAutoFill
validationRules="zip-required"
/>
<div class="row my-2">
<div class="col">
<textboxQuestion
cmsWidgetName="ServiceZIP"
v-model="zip"
inputId="zip"
mask="#####"
isRequired
disableAutoFill
validationRules="zip-required"
/>
</div>
</div>
</div>
<div class="row my-2">
<div class="col">
<textboxQuestion
cmsWidgetName="EmailAddress"
v-model="email"
inputId="email"
isRequired
disableAutoFill
validationRules="email-address-required|email-address-format"
/>
<div class="row my-2">
<div class="col">
<textboxQuestion
cmsWidgetName="EmailAddress"
v-model="email"
inputId="email"
isRequired
disableAutoFill
validationRules="email-address-required|email-address-format"
/>
</div>
</div>
<alert
class="my-3"
v-model="customAlertData"
v-if="noMatchAlert"
alertClass="alert-warning"
cmsWidgetName="NoMatchAlertWidget"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="vinNotFound"
alertClass="alert-warning"
cmsWidgetName="VinNotFound"
/>
<alert
class="my-3"
:manualHeadline="PerfectMatchNewVinAlertReadOnlyHeader"
:manualCopy="PerfectMatchNewVinAlertReadOnlyBody"
v-model="customAlertData"
v-if="isVinFieldReadOnly"
alertClass="alert-success"
/>
<alert
class="my-3"
:manualHeadline="MatchedDifferentVehicleAlertHeader"
:manualCopy="MatchedDifferentVehicleAlertBody"
v-model="customAlertData"
v-if="isCarIdDifferent"
alertClass="alert-warning"
/>
<alert
class="my-3"
:manualHeadline="NoServiceZipHeader"
:manualCopy="NoServiceZipBody"
v-model="customAlertData"
v-if="noServiceZip"
alertClass="alert-danger"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="vinNotFound"
alertClass="alert-warning"
cmsWidgetName="VinNotFound"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="perfectMatchNewVinAlert"
alertClass="alert-success"
cmsWidgetName="PerfectMatchNewVinAlert"
/>
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
<alert
class="my-3"
:manualHeadline="MatchedDifferentVehicleAlertHeader"
:manualCopy="MatchedDifferentVehicleAlertBody"
v-model="customAlertData"
v-if="matchedDifferentVehicle"
alertClass="alert-danger"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="noMatchAlert"
alertClass="alert-warning"
cmsWidgetName="NoMatchAlertWidget"
/>
<alert
class="my-3"
:manualHeadline="NoServiceZipHeader"
:manualCopy="NoServiceZipBody"
v-model="customAlertData"
v-if="noServiceZip"
alertClass="alert-warning"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="foundWindshieldAlert"
alertClass="alert-warning"
cmsWidgetName="FoundWindshieldAlert"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="vinNotFound"
alertClass="alert-warning"
cmsWidgetName="VinNotFound"
/>
<alert
class="my-3"
v-model="customAlertData"
v-if="perfectMatchNewVinAlert"
alertClass="alert-warning"
cmsWidgetName="PerfectMatchNewVinAlert"
/>
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
ref="funnelFooter"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
</Form>
</template>
@ -121,6 +130,7 @@ import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-he
import alert from "@/ux-components/alert/alert";
import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import vinInformation from "@/layouts/vin-lookup/vin-information/vin-information";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
@ -130,13 +140,13 @@ import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { errorMessages } from "@/constants/error-messages";
import { getDamageString, isGlassAvailableForCarId } from "@/helpers/damage-helper";
import { getDamageString, getIsWindshieldOnly, isGlassAvailableForCarId } from "@/helpers/damage-helper";
import { required, regex } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
// DEFINE VALIDATION RULES
defineRule("zip-required", required(errorMessages.ZIP_REQUIRED));
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
defineRule(
"email-address-required",
required(errorMessages.EMAIL_ADDRESS_REQUIRED)
@ -175,44 +185,39 @@ export default {
vm.setCmsContent(resultMap.cmsContent);
});
},
props: {
validationRules: String,
},
data() {
return {
matchedDifferentVehicle: false,
noMatchAlert: false,
isCarIdDifferent: false,
noServiceZip: false,
vinFound: false,
vinFoundReadOnly: false,
foundWindshieldAlert: false,
vinNotFound: false,
perfectMatchNewVinAlert: false,
vin: this.getVinFromStore(),
zip: this.getZipFromStore(),
email: this.getEmailFromStore(),
customAlertData: {},
isCarIdDifferent: false,
previouslyEnteredCarId: '',
invalidZip: '',
vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0,
};
},
computed: {
perfectMatchNewVinAlert() {
return this.vinPopulatedOnPageLoad && this.vin === this.getVinFromStore();
},
MatchedDifferentVehicleAlertHeader(){
let text = this.getCmsContent("MatchedDifferentVehicle",
const text = this.getCmsContent("MatchedDifferentVehicle",
"HeadlineText").replaceAll("{custom:damage}", getDamageString());
return text;
},
MatchedDifferentVehicleAlertBody(){
let text = this.getCmsContent("MatchedDifferentVehicle",
const text = this.getCmsContent("MatchedDifferentVehicle",
"BodyText").replaceAll("{custom:damage}", getDamageString()).replaceAll("{custom:vinlookupYear}", this.customAlertData?.vehicleInfo?.year).replaceAll("{custom:vinlookupMake}", this.customAlertData?.vehicleInfo?.make).replaceAll("{custom:vinlookupModel}",
this.customAlertData?.vehicleInfo?.model);
return text;
},
NoServiceZipHeader(){
let text = this.getCmsContent("NoServiceZipWidget", "HeadlineText").replaceAll("{custom:serviceZip}", this.invalidZip);
const text = this.getCmsContent("NoServiceZipWidget", "HeadlineText").replaceAll("{custom:serviceZip}", this.invalidZip);
return text;
},
@ -226,6 +231,13 @@ export default {
return this.getCmsContent("PerfectMatchNewVinAlert", "BodyText").replaceAll("{custom:damage}",
getDamageString())
},
PerfectMatchNewVinAlertReadOnlyHeader () {
return this.getCmsContent("PerfectMatchNewVinAlertReadOnly", "HeadlineText");
},
PerfectMatchNewVinAlertReadOnlyBody () {
return this.getCmsContent("PerfectMatchNewVinAlertReadOnly", "BodyText").replaceAll("{custom:damage}",
getIsWindshieldOnly())
},
isVinFieldReadOnly(){
return this.$store.getters.payment.insuranceCoverage.isVerified;
},
@ -242,13 +254,13 @@ export default {
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
},
getEmailFromStore(){
return store.getters.order.customer.emailAddress
return store.getters.order.customer.emailAddress;
},
getVinFromStore(){
return store.getters.vehicle.vin
return store.getters.vehicle.vin;
},
getZipFromStore(){
return store.getters.vehicle.registration.zipCode
return store.getters.order.serviceLocation.zipCode;
},
attachCustomEvents() {
this.prependActionToMethod(this, this.forwardButtonAction, () => {
@ -264,34 +276,36 @@ export default {
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {
const zipValidation = await this.validateZip(this.zip);
if (!zipValidation.data.isServiceable) {
const zipValidation = this.validateZip(this.zip);
const vehicleLookup = this.lookupVehicle(this.vin);
const zipValidationResponse = await zipValidation;
const vehicleLookupResponse = await vehicleLookup.catch(() => {
this.vinNotFound = true;
this.$refs.funnelFooter.removeLoader();
this.noServiceZip = false;
return;
});
if (!zipValidationResponse.data.isServiceable) {
this.customAlertData.zip = this.zip;
this.$refs.funnelFooter.removeLoader();
this.noServiceZip = true;
this.invalidZip = this.zip;
return;
}
const vehicleLookup = await this.lookupVehicle(this.vin).catch(() => {
this.vinNotFound = true;
this.$refs.funnelFooter.removeLoader();
this.noServiceZip = false;
return;
});
this.isCarIdDifferent = vehicleLookup.data.carId !== store.getters.vehicle.carId;
this.isCarIdDifferent = vehicleLookupResponse.data.carId !== store.getters.vehicle.carId;
if (this.isCarIdDifferent && (vehicleLookup.data.carId !== this.previouslyEnteredCarId)) {
this.previouslyEnteredCarId = vehicleLookup.data.carId;
if (this.isCarIdDifferent && (vehicleLookupResponse.data.carId !== this.previouslyEnteredCarId)) {
this.previouslyEnteredCarId = vehicleLookupResponse.data.carId;
this.noServiceZip = false;
this.customAlertData.vehicleInfo = vehicleLookup.data;
this.$refs.funnelFooter.updateButtonText(`Continue with ${vehicleLookup.data.year} ${vehicleLookup.data.make} ${vehicleLookup.data.model}`);
this.customAlertData.vehicleInfo = vehicleLookupResponse.data;
this.$refs.funnelFooter.updateButtonText(`Continue with ${vehicleLookupResponse.data.year} ${vehicleLookupResponse.data.make} ${vehicleLookupResponse.data.model}`);
this.isVinValid = true;
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(vehicleLookup.data.carId);
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(vehicleLookupResponse.data.carId);
this.$refs.funnelFooter.removeLoader();
this.matchedDifferentVehicle = true;
this.isCarIdDifferent = true;
return;
}
this.updateStore(vehicleLookup.data);
this.updateStore(vehicleLookupResponse.data);
this.navigateForward();
},
navigateForward(){
@ -299,6 +313,7 @@ export default {
this.$router.navigateAfterSave(this.navigationScenarios.CLICKED_FORWARD, this.$route, {}, { displayVehicleChangeAlert: true }, {});
return;
} else {
this.$refs.loadingModal.showModal();
navigateAfterSaveToHeritageFunnel(this.$route);
return;
}
@ -341,6 +356,7 @@ export default {
alert,
funnelFooter,
vinInformation,
loadingModal,
},
};
</script>

View file

@ -12,6 +12,7 @@ const navigationScenarios = {
CONTINUING_WITH_MULTIPLE_PARTS: "CONTINUING_WITH_MULTIPLE_PARTS",
CONTINUING_WITH_SINGLE_PART: "CONTINUING_WITH_SINGLE_PART",
CONTINUING_WITH_MULTIPLE_VEHICLES: "CONTINUING_WITH_MULTIPLE_VEHICLES",
CONTINUING_WITH_DIFFERENT_GLASS: "CONTINUING_WITH_DIFFERENT_GLASS",
CLICKED_FORWARD_WITHOUT_VIN: "CLICKED_FORWARD_WITHOUT_VIN",
SELECTED_MANUAL_VIN: "SELECTED_MANUAL_VIN",
SELECTED_LICENSE_PLATE: "SELECTED_LICENSE_PLATE",

View file

@ -102,19 +102,11 @@ const routingTable = [
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.REVEAL,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
{
scenario: navigationScenarios.VIN_LOOKUP,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS,
destinationFmgPageValue: fmgPageValues.ESTIMATE,
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.ESTIMATE,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
}
],
},
@ -125,18 +117,6 @@ const routingTable = [
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.ESTIMATE,
},
{
scenario: navigationScenarios.CONTINUING_WITH_PARTS_QUESTION,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS,
},
{
scenario: navigationScenarios.CONTINUING_WITH_MULTIPLE_PARTS,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS,
},
{
scenario: navigationScenarios.CONTINUING_WITH_SINGLE_PART,
destinationFmgPageValue: fmgPageValues.REVEAL,
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
@ -155,9 +135,9 @@ const routingTable = [
destinationFmgPageValue: fmgPageValues.ADDRESS_VEHICLES,
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
scenario: navigationScenarios.CONTINUING_WITH_DIFFERENT_GLASS,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
},
],
},
{fmgPageValue: fmgPageValues.ESTIMATE,

View file

@ -135,26 +135,26 @@ export const mutations = {
updateRegistrationLicensePlate(state, licensePlate){
state.order.vehicle.registration.licensePlate = licensePlate;
},
updateRegistrationAddress(state, registrationAddress){
state.order.vehicle.registration.address = registrationAddress;
},
updateRegistrationCity(state, registrationCity){
state.order.vehicle.registration.city = registrationCity;
},
updateRegistrationState(state, registrationState){
state.order.vehicle.registration.state = registrationState;
},
updateRegistrationZipCode(state, registrationZipCode){
state.order.vehicle.registration.zipCode = registrationZipCode;
},
updateRegistrationAddress(state, registrationAddress){
state.order.vehicle.registration.address = registrationAddress;
},
updateServiceLocationZipCode(state, serviceLocationZipCode){
state.order.serviceLocation.zipCode = serviceLocationZipCode;
},
updateRegistrationCity(state, serviceCity){
state.order.serviceLocation.city = serviceCity;
},
updateServiceLocationZipCode(state, serviceLocationZip){
state.order.serviceLocation.zipCode = serviceLocationZip;
},
updateRegistrationFirstName(state, firstName){
state.order.serviceLocation.firstName = firstName;
state.order.vehicle.registration.firstName = firstName;
},
updateRegistrationLastName(state, lastName){
state.order.serviceLocation.lastName = lastName;
state.order.vehicle.registration.lastName = lastName;
},
updateCustomerEmailAddress(state, customerEmailAddress){
state.order.customer.emailAddress = customerEmailAddress;

View file

@ -9,4 +9,22 @@
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.route-fade-enter-active .fade-on-route-transition {
transition: opacity 0.8s ease;
}
.route-fade-leave-active .fade-on-route-transition {
transition: opacity 0.3s ease;
}
.route-fade-enter-from .fade-on-route-transition,
.route-fade-leave-to .fade-on-route-transition {
/*
Hack around a Chrome 96 bug in handling nested opacity transitions.
This is not needed in other browsers or Chrome 99+ where the bug
has been fixed.
*/
opacity: 0.001;
}

View file

@ -1,7 +1,8 @@
html {
.has-error {
&.list-button,
&.list-card {
&.list-card,
&.list-card.list-button {
border: 1px solid $red;
color: $red;
input[type=checkbox]:focus + label,
@ -15,6 +16,14 @@ html {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px;
}
label {
border: 1px solid transparent;
}
label:hover {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px;
border: 1px solid $red;
}
}
&.list-button-horizontal {
color: $red;
@ -106,14 +115,17 @@ html {
color: $red;
font-size: .875rem;
font-weight: 500;
height: 1.5rem;
margin-top: .25rem !important;
}
.form-test-invalid {
&.btn.btn-primary {
color: $gray;
color: $gray-600;
background: $gray-200;
cursor: pointer;
pointer-events: all;
font-weight: $font-weight-normal;
}
&.btn.btn-primary:hover,
&.btn.btn-primary:focus,

View file

@ -26,6 +26,15 @@ body {
overflow-x: hidden;
}
.sub-container{
&.make-tall {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}
}
.sr-only {
position: absolute;
left: -10000px;
@ -34,4 +43,8 @@ body {
height: 1px;
overflow: hidden;
}
}
.page-container-grouped-styles {
@extend .container-fluid, .shadow, .rounded-3, .p-2, .position-relative, .make-tall, .px-5;
}
}

View file

@ -47,6 +47,21 @@ describe("alert.vue", () => {
expect(wrapperDiv.classes()).toContain('warning')
});
it("Should update alert Headline to manualHeadline datam entered and alert copy to manualCopy datam entered when no cmsWidgetName entered", async () => {
// Arrange
const wrapper = shallowMount(alert, {
propsData: {
manualHeadline: 'testHeader',
manualCopy: 'testCopy'
},
mixins: [mockMixin]
});
// Assert
expect(wrapper.vm.alertHeadline).toBe("testHeader");
expect(wrapper.vm.alertCopy).toBe("testCopy");
});
});
const mockMixin = {

View file

@ -4,16 +4,16 @@
role="alert"
:class="[isDismissible ? 'alert-dismissible' : '', this.alertClass]"
>
<p class="m-0 fw-bold small alert-heading">{{ alertHeadline }}</p>
<p class="m-0 fw-bold alert-heading">{{ alertHeadline }}</p>
<p v-if="splitAlertCopyForLink.length">
<template v-for="copy in splitAlertCopyForLink" :key="copy">
<span v-if="copy.includes('routerLink:')" class="m-0 text-body small">
<span v-if="copy.includes('routerLink:')" class="m-0 text-body">
<router-link :to="{query: {fmgPage: `${copy.split(':')[1].split(',')[0]}`}, name: 'root'}">{{ copy.split(':')[1].split(',')[1] }}</router-link>
</span>
<span v-else class="m-0 text-body small" v-html="copy"></span>
<span v-else class="m-0 text-body" v-html="copy"></span>
</template>
</p>
<p v-else class="m-0 text-body small" v-html="alertCopy"></p>
<p v-else class="m-0 text-body" v-html="alertCopy"></p>
<button
type="button"
class="btn-close p-2"
@ -129,8 +129,8 @@ export default {
}
}
& p {
font-size: 14px;
margin-bottom: 0px;
font-size: .875rem;
margin-bottom: 0.25rem !important;
}
}
</style>

View file

@ -1,6 +1,6 @@
<template>
<div
class="list-group list-button d-flex flex-column w-100 mb-2"
class="list-group list-button rounded-3 d-flex flex-column w-100 mb-2"
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
@keyup.space="triggerButton()"
@keyup.up="handleKeyupArrow()"

View file

@ -44,7 +44,6 @@ a {
&.navigation-link {
color: $black;
line-height: 26px;
text-transform: capitalize;
white-space: nowrap;
}
&.footer-link {