514 lines
21 KiB
Vue
514 lines
21 KiB
Vue
<template>
|
|
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
|
|
<loadingModal ref="loadingModal" />
|
|
<div class="container-fluid page-container-grouped-styles">
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-6">
|
|
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
|
|
</div>
|
|
</div>
|
|
<div class="row justify-content-md-center">
|
|
<div class="col-md-6 col-xl-4">
|
|
<vehicleBanner
|
|
cmsWidgetName="VehicleBannerWidget"
|
|
:displayGenericVehicleImage="false" />
|
|
|
|
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
|
|
|
|
<textboxQuestion
|
|
class="mb-4 mt-5"
|
|
cmsWidgetName="VinNumberQuestionWidget"
|
|
v-model="vin"
|
|
customInputId="vin"
|
|
isRequired
|
|
:validationRules="!vinPopulatedOnPageLoad ? 'vin-required|vin-format' : ''"
|
|
:isDisabled="vinPopulatedOnPageLoad"
|
|
maxLength="17"
|
|
:mask="vinMask"
|
|
includeImageQuestion
|
|
:imageQuestionSubmitHandler="getVinFromImage"
|
|
:maxFileSize="imageUploadMaxFileSize"
|
|
@image-lookup-error="displayVinScanAlert"
|
|
@image-validity-error="displayVinScanAlert"
|
|
data-test="vin-lookup-field"
|
|
ref="vinLookupQuestion" />
|
|
|
|
<alert
|
|
cmsWidgetName="AlertVinScanFailed"
|
|
v-if="displayVinScanFailedAlert"
|
|
alertClass="alert-danger" />
|
|
|
|
<vinInformation class="mb-4" />
|
|
|
|
<textboxQuestion
|
|
class="mb-4"
|
|
cmsWidgetName="ServiceZipQuestionWidget"
|
|
v-model="serviceZipCode"
|
|
customInputId="serviceZipCode"
|
|
mask="#####"
|
|
isRequired
|
|
validationRules="zip-required|zip-format" />
|
|
|
|
<textboxQuestion
|
|
cmsWidgetName="EmailAddressQuestionWidget"
|
|
v-model="emailAddress"
|
|
customInputId="emailAddress"
|
|
isRequired
|
|
validationRules="email-address-required|email-address-format" />
|
|
|
|
<textBlock cmsWidgetName="QuoteEmailTextBlockWidget" typeStyle="caption" />
|
|
|
|
<alert
|
|
ref="alertInvalidZip"
|
|
v-if="displayInvalidZipAlert"
|
|
class="my-4"
|
|
cmsWidgetName="AlertInvalidZipWidget"
|
|
alertClass="alert-danger"
|
|
v-bind:isDismissible="false" />
|
|
|
|
<alert
|
|
class="my-4"
|
|
:manualHeadline="AlertPerfectMatchInsuranceVerifiedHeader"
|
|
:manualCopy="AlertPerfectMatchInsuranceVerifiedBody"
|
|
v-model="customAlertData"
|
|
v-if="
|
|
vinPopulatedOnPageLoad &&
|
|
isInsuranceVerified &&
|
|
!displayInvalidZipAlert &&
|
|
!displayNonServiceableZipAlert
|
|
"
|
|
alertClass="alert-success" />
|
|
|
|
<alert
|
|
class="my-4"
|
|
:manualHeadline="AlertMatchedDifferentVehicleHeader"
|
|
:manualCopy="AlertMatchedDifferentVehicleBody"
|
|
v-model="customAlertData"
|
|
v-if="displayMatchedDifferentVehicleAlert"
|
|
alertClass="alert-warning" />
|
|
|
|
<alert
|
|
class="my-4"
|
|
:manualHeadline="AlertNonServiceableZipHeader"
|
|
:manualCopy="AlertNonServiceableZipBody"
|
|
v-model="customAlertData"
|
|
v-if="displayNonServiceableZipAlert"
|
|
alertClass="alert-danger" />
|
|
|
|
<alert
|
|
class="my-4"
|
|
v-model="customAlertData"
|
|
v-if="displayVinNotFoundAlert"
|
|
alertClass="alert-danger"
|
|
cmsWidgetName="AlertVinNotFoundWidget" />
|
|
|
|
<alert
|
|
class="my-4"
|
|
:manualHeadline="AlertPerfectMatchInsuranceNotVerifiedHeader"
|
|
:manualCopy="AlertPerfectMatchInsuranceNotVerifiedBody"
|
|
v-model="customAlertData"
|
|
v-if="
|
|
vinPopulatedOnPageLoad &&
|
|
!isInsuranceVerified &&
|
|
!displayInvalidZipAlert &&
|
|
!displayNonServiceableZipAlert
|
|
"
|
|
alertClass="alert-success" />
|
|
|
|
<navbar
|
|
cmsWidgetName="FunnelFooterWidget"
|
|
ref="navbar"
|
|
:isForwardActionDisabled="!meta.valid"
|
|
@back-clicked="backButtonAction"
|
|
@ForwardClicked="forwardButtonAction" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Form>
|
|
</template>
|
|
|
|
<script>
|
|
// Components
|
|
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
|
|
import navbar from "@/fmg-components/nav-bar/nav-bar";
|
|
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
|
|
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
|
|
import alert from "@/ux-components/alert/alert";
|
|
import textboxQuestion from "@/digital-components/textbox-question/textbox-question";
|
|
import vinInformation from "@/layouts/vin-lookup/vin-information/vin-information";
|
|
import textBlock from "@/digital-components/text-block/text-block";
|
|
import loadingModal from "@/fmg-components/loading-modal/loading-modal.vue";
|
|
|
|
// Supporting files
|
|
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
|
|
import { settleAllPromises } from "@/helpers/layout-helper";
|
|
import { storeActions } from "@/constants/store-actions";
|
|
import { errorMessages } from "@/constants/error-messages";
|
|
import {
|
|
getDamageString,
|
|
getIsWindshieldOnly,
|
|
isGlassAvailableForCarId,
|
|
} from "@/helpers/damage-helper";
|
|
import { required, regex } from "@/helpers/validation-rules";
|
|
import { Form, defineRule } from "vee-validate";
|
|
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
|
|
import { routerParams } from "@/router/router-constants/router-params";
|
|
|
|
import store from "@/store";
|
|
import vinPagesMixin from "@/mixins/vin-pages-mixin";
|
|
|
|
// DEFINE VALIDATION RULES
|
|
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
|
|
defineRule("zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
|
|
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
|
|
defineRule(
|
|
"email-address-format",
|
|
regex(
|
|
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$/,
|
|
errorMessages.EMAIL_ADDRESS_FORMAT
|
|
)
|
|
);
|
|
defineRule("vin-required", required(errorMessages.VIN_REQUIRED));
|
|
defineRule("vin-format", regex(/^[a-hA-Hj-nJ-NpPr-zR-Z0-9]{17}$/, errorMessages.VIN_FORMAT));
|
|
|
|
export default {
|
|
name: "vin-lookup",
|
|
mixins: [vinPagesMixin],
|
|
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);
|
|
});
|
|
},
|
|
data() {
|
|
return {
|
|
vin: this.getVinFromStore(),
|
|
serviceZipCode: this.getZipFromStore() ?? this.$route.query.zipcode,
|
|
emailAddress: this.getEmailFromStore(),
|
|
isCarIdDifferent: false,
|
|
customAlertData: {},
|
|
previouslyEnteredCarId: "",
|
|
vinPopulatedOnPageLoad: this.getVinFromStore()?.length > 0,
|
|
isSelectedGlassAvailableForVehicle: true,
|
|
displayInvalidZipAlert: false,
|
|
displayNonServiceableZipAlert: false,
|
|
displayVinNotFoundAlert: false,
|
|
displayMatchedDifferentVehicleAlert: false,
|
|
displayVinScanFailedAlert: false,
|
|
};
|
|
},
|
|
methods: {
|
|
arePagePrerequisitesValid() {
|
|
return store.getters.vehicle.carId !== null;
|
|
},
|
|
getEmailFromStore() {
|
|
return this.$store.getters.order.customer.emailAddress;
|
|
},
|
|
getVinFromStore() {
|
|
return this.$store.getters.vehicle.vin;
|
|
},
|
|
getZipFromStore() {
|
|
return this.$store.getters.order.serviceLocation.zipCode;
|
|
},
|
|
attachCustomEvents() {
|
|
this.prependActionToMethod(this, this.forwardButtonAction, () => {
|
|
this.pushEventToGA(
|
|
this.$route.query[this.queryStrings.FMG_PAGE],
|
|
this.GaActions.SUBMITTED,
|
|
this.GaLabels.VIN_LOOKUP,
|
|
true
|
|
);
|
|
});
|
|
},
|
|
backButtonAction() {
|
|
if (this.$store.getters.vehicle.vin) {
|
|
this.$router.navigateWithoutSaving(
|
|
this.navigationScenarios.CLICKED_BACK_WITH_VIN,
|
|
this.$route
|
|
);
|
|
} else {
|
|
this.$router.navigateWithoutSaving(
|
|
this.navigationScenarios.CLICKED_BACK,
|
|
this.$route
|
|
);
|
|
}
|
|
},
|
|
async forwardButtonAction() {
|
|
this.resetAlerts();
|
|
|
|
// If this is a new VIN Lookup, do both a Vehicle Lookup and a Zip Validation
|
|
if (!this.vinPopulatedOnPageLoad) {
|
|
const vehicleLookupResponse = this.dispatchStoreActionWithLogging(
|
|
storeActions.LOOKUP_VEHICLE_BY_VIN,
|
|
{ vin: this.vin },
|
|
"vin-lookup"
|
|
);
|
|
|
|
// Settle promises and get results
|
|
const promiseResultMap = [
|
|
{
|
|
resultKey: "vehicleLookupResponse",
|
|
promise: vehicleLookupResponse,
|
|
},
|
|
{
|
|
resultKey: "zipCodeData",
|
|
promise: this.getZipCodeData(this.serviceZipCode),
|
|
},
|
|
];
|
|
|
|
const resultMap = await settleAllPromises(promiseResultMap);
|
|
|
|
// If a Service Zip is entered and it is an invalid zip code (ex. 11111) then show an alert
|
|
const isZipValid = resultMap.zipCodeData.isValid;
|
|
if (this.serviceZipCode && !isZipValid) {
|
|
this.displayInvalidZipAlert = true;
|
|
return this.$refs.navbar.removeLoader();
|
|
}
|
|
this.displayInvalidZipAlert = false;
|
|
|
|
// If either lookup fails, remove the loader and stop processing the page.
|
|
if (!resultMap.vehicleLookupResponse || !resultMap.zipCodeData.isServiceable) {
|
|
// If the vehicle result is undefined, the vin entered was invalid.
|
|
if (!resultMap.vehicleLookupResponse) {
|
|
this.displayVinNotFoundAlert = true;
|
|
}
|
|
|
|
// Check if Service Zip entered is serviceable, if not display an alert
|
|
if (!resultMap.zipCodeData.isServiceable) {
|
|
this.displayNonServiceableZipAlert = true;
|
|
}
|
|
|
|
// Remove loader and stop processing the page.
|
|
return this.$refs.navbar.removeLoader();
|
|
}
|
|
|
|
// Check if the CarId is different from the lookup vs what is in state currently.
|
|
this.isCarIdDifferent =
|
|
resultMap.vehicleLookupResponse.carId !== this.$store.getters.vehicle.carId;
|
|
|
|
if (
|
|
this.isCarIdDifferent &&
|
|
resultMap.vehicleLookupResponse.carId !== this.previouslyEnteredCarId
|
|
) {
|
|
this.previouslyEnteredCarId = resultMap.vehicleLookupResponse.carId;
|
|
this.customAlertData.vehicleInfo = resultMap.vehicleLookupResponse;
|
|
this.displayMatchedDifferentVehicleAlert = true;
|
|
|
|
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(
|
|
resultMap.vehicleLookupResponse.carId,
|
|
"vin-lookup"
|
|
);
|
|
|
|
// Update button "Continue with..."
|
|
this.$refs.navbar.updateButtonText(
|
|
`Continue with ${resultMap.vehicleLookupResponse.year} ${resultMap.vehicleLookupResponse.make} ${resultMap.vehicleLookupResponse.model}`
|
|
);
|
|
return this.$refs.navbar.removeLoader();
|
|
}
|
|
|
|
// Save vin, vehicle, customer and service information
|
|
await this.dispatchStoreAction(
|
|
storeActions.SAVE_VIN_LOOKUP,
|
|
{
|
|
isSelectedGlassAvailableForVehicle: this.isSelectedGlassAvailableForVehicle,
|
|
vehicleInfo: Object.assign(resultMap.vehicleLookupResponse, {
|
|
vin: this.vin,
|
|
}),
|
|
},
|
|
false
|
|
);
|
|
|
|
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);
|
|
await this.dispatchStoreAction(
|
|
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
|
{
|
|
state: resultMap.zipCodeData.state,
|
|
zipCode: this.serviceZipCode,
|
|
zipCodeCtu: resultMap.zipCodeData.zipCodeCtu,
|
|
},
|
|
false
|
|
);
|
|
|
|
return await this.navigateForward();
|
|
}
|
|
|
|
// If a VIN has already been found. Validate the Service Zip (in case of changes)
|
|
const zipCodeData = await this.getZipCodeData(this.serviceZipCode);
|
|
|
|
// Check if Service Zip entered is serviceable then save the ZIP info
|
|
if (zipCodeData.isServiceable) {
|
|
//Only save the zipCode, state, and zipCodeCtu if the zip changed or we lack zipCodeCtu
|
|
if (
|
|
this.$store.getters.order.serviceLocation.zipCode != this.serviceZipCode ||
|
|
!this.$store.getters.order.serviceLocation.zipCodeCtu
|
|
) {
|
|
await this.dispatchStoreAction(
|
|
storeActions.SAVE_SERVICE_ZIP_CODE_INFO,
|
|
{
|
|
state: zipCodeData.state,
|
|
zipCode: this.serviceZipCode,
|
|
zipCodeCtu: zipCodeData.zipCodeCtu,
|
|
},
|
|
false
|
|
);
|
|
}
|
|
|
|
await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.emailAddress, false);
|
|
|
|
return await this.navigateForward();
|
|
}
|
|
|
|
// If the Service Zip is NOT serviceable then show an alert
|
|
if (zipCodeData.isValid) {
|
|
this.displayNonServiceableZipAlert = true;
|
|
} else {
|
|
this.displayInvalidZipAlert = true;
|
|
}
|
|
|
|
return this.$refs.navbar.removeLoader();
|
|
},
|
|
async navigateForward() {
|
|
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
|
|
this.$router.navigateWithSaving(
|
|
this.navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
|
|
this.$route,
|
|
{},
|
|
{ [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true }
|
|
);
|
|
} else {
|
|
await this.navigateForwardWithSingleCarMatch();
|
|
}
|
|
},
|
|
displayVinScanAlert() {
|
|
this.displayVinScanFailedAlert = true;
|
|
},
|
|
getVinFromImage(image) {
|
|
return new Promise((resolve, reject) => {
|
|
this.dispatchStoreActionWithLogging(
|
|
storeActions.LOOKUP_VIN_BY_IMAGE,
|
|
image,
|
|
"vin-lookup"
|
|
)
|
|
.then((response) => {
|
|
if (response.data.length > 0) {
|
|
resolve(response.data[0]);
|
|
} else {
|
|
reject("No VINs detected.");
|
|
}
|
|
})
|
|
.catch(() => {
|
|
reject("An error occurred during the lookup.");
|
|
});
|
|
});
|
|
},
|
|
resetAlerts() {
|
|
this.displayMatchedDifferentVehicleAlert = false;
|
|
this.displayNonServiceableZipAlert = false;
|
|
this.displayInvalidZipAlert = false;
|
|
this.displayVinNotFoundAlert = false;
|
|
this.displayVinScanFailedAlert = false;
|
|
},
|
|
},
|
|
mounted() {
|
|
this.attachCustomEvents();
|
|
},
|
|
computed: {
|
|
AlertMatchedDifferentVehicleHeader() {
|
|
return this.getCmsContent(
|
|
"AlertMatchedDifferentVehicleWidget",
|
|
"HeadlineText"
|
|
).replaceAll("{custom:damage}", getDamageString());
|
|
},
|
|
AlertMatchedDifferentVehicleBody() {
|
|
return this.getCmsContent("AlertMatchedDifferentVehicleWidget", "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);
|
|
},
|
|
AlertNonServiceableZipHeader() {
|
|
return this.getCmsContent("AlertNonServiceableZipWidget", "HeadlineText").replaceAll(
|
|
"{custom:serviceZip}",
|
|
this.serviceZipCode
|
|
);
|
|
},
|
|
AlertNonServiceableZipBody() {
|
|
return this.getCmsContent("AlertNonServiceableZipWidget", "BodyText");
|
|
},
|
|
AlertPerfectMatchInsuranceNotVerifiedHeader() {
|
|
return this.getCmsContent("AlertPerfectMatchInsuranceNotVerified", "HeadlineText");
|
|
},
|
|
AlertPerfectMatchInsuranceNotVerifiedBody() {
|
|
return this.getCmsContent(
|
|
"AlertPerfectMatchInsuranceNotVerified",
|
|
"BodyText"
|
|
).replaceAll("{custom:damage}", getDamageString());
|
|
},
|
|
AlertPerfectMatchInsuranceVerifiedHeader() {
|
|
return this.getCmsContent("AlertPerfectMatchInsuranceVerified", "HeadlineText");
|
|
},
|
|
AlertPerfectMatchInsuranceVerifiedBody() {
|
|
return this.getCmsContent("AlertPerfectMatchInsuranceVerified", "BodyText").replaceAll(
|
|
"{custom:damage}",
|
|
getIsWindshieldOnly()
|
|
);
|
|
},
|
|
isInsuranceVerified() {
|
|
return (
|
|
store.getters.payment.insuranceCoverage.isVerified ||
|
|
getFunnelCookie().HasDelayedClaimRegistration
|
|
);
|
|
},
|
|
vinMask() {
|
|
if (this.vinPopulatedOnPageLoad) {
|
|
const lastSixChars = this.vin.substring(11, this.vin.length);
|
|
return `!X!X!X!X!X!X!X!X!X!X!X${lastSixChars}`;
|
|
} else {
|
|
return "XXXXXXXXXXXXXXXXX";
|
|
}
|
|
},
|
|
imageUploadMaxFileSize() {
|
|
let kiloBytes = 5140;
|
|
|
|
return kiloBytes * 1028;
|
|
},
|
|
},
|
|
watch: {
|
|
vin() {
|
|
this.displayVinNotFoundAlert = false;
|
|
this.displayVinScanFailedAlert = false;
|
|
this.$refs.navbar.updateButtonText(
|
|
this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")
|
|
);
|
|
},
|
|
serviceZipCode() {
|
|
this.displayNonServiceableZipAlert = false;
|
|
},
|
|
},
|
|
components: {
|
|
funnelHeader,
|
|
navbar,
|
|
vehicleBanner,
|
|
funnelSubHeader,
|
|
textboxQuestion,
|
|
alert,
|
|
vinInformation,
|
|
textBlock,
|
|
Form,
|
|
loadingModal,
|
|
},
|
|
};
|
|
</script>
|