ga event defect
This commit is contained in:
commit
7fd053e043
39 changed files with 1157 additions and 273 deletions
|
|
@ -127,4 +127,45 @@ stages:
|
|||
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)
|
||||
|
||||
# Prod Build/Deploy
|
||||
- stage: Prod
|
||||
condition: eq(variables['Build.SourceBranch'], variables['prod-branch'] )
|
||||
variables:
|
||||
- group: FixMyGlassProd
|
||||
jobs:
|
||||
- deployment: prodBuildDeployment
|
||||
displayName: Build and Deploy FMG - Prod
|
||||
environment: digitalCloud-prod
|
||||
container: node
|
||||
workspace:
|
||||
clean: all
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
- template: templates/digital/step-build-vue.yml@AzureDevOps
|
||||
parameters:
|
||||
buildOutputDir: dist
|
||||
- template: templates/digital/step-deploy-vue.yml@AzureDevOps
|
||||
parameters:
|
||||
artifactName: vueDist
|
||||
awsProfile: $(prodDeploymentProfile)
|
||||
outputPath: /fmg/
|
||||
deployBuckets:
|
||||
safelite-prod-fmg-us-east-1:
|
||||
clearFolder: true
|
||||
deployFolder: ''
|
||||
region: us-east-1
|
||||
appDeployVariables:
|
||||
__VUE_APP_CONSUMER_CF_DISTRO__: $(__VUE_APP_CONSUMER_CF_DISTRO__)
|
||||
__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)
|
||||
|
|
@ -19,15 +19,18 @@ module.exports = {
|
|||
"!src/layouts/part-questions/**/*.vue",
|
||||
"!src/layouts/reveal/**/*.vue",
|
||||
"!src/layouts/estimate/**/*.vue",
|
||||
// REMOVE THESE AFTER WRITING UNIT TESTS
|
||||
// TODO REMOVE THESE AFTER WRITING UNIT TESTS
|
||||
"!src/layouts/address-lookup/address-lookup.vue",
|
||||
"!src/layouts/address-lookup/customer-questions/customer-questions.vue",
|
||||
"!src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue",
|
||||
"!src/layouts/address-vehicles/address-vehicles.vue",
|
||||
"!src/layouts/address-vehicles/address-vehicles-question/address-vehicles-question.vue",
|
||||
"!src/common-components/dropdown-question/dropdown-question.vue",
|
||||
"!src/common-components/textbox-question/textbox-question.vue",
|
||||
"!src/ux-components/alert\alert.vue",
|
||||
"!src/helpers/validation-rules.js",
|
||||
// END
|
||||
], //! means exclude from coverage.
|
||||
], // ! means exclude from coverage.
|
||||
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
|
|
|
|||
1
src/assets/img/FMGTransitionAnimation.svg
Normal file
1
src/assets/img/FMGTransitionAnimation.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 30 KiB |
|
|
@ -1,12 +1,12 @@
|
|||
<!-- Documented in confluence https://safelite.atlassian.net/wiki/spaces/DC/pages/76644418/Button+Question+Component -->
|
||||
<template>
|
||||
<div :class="isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'">
|
||||
<div v-if="questionText" :class="[this.buttonType === 'radio' ? ['mt-2', 'mb-0'] : ['mt-5', 'mb-4'], 'd-flex']">
|
||||
<span class="fs-5 fw-bold w-100" :class="this.buttonType === 'radio' ? 'text-start' : 'text-center'">{{ questionText }}</span>
|
||||
<div v-if="questionText" class="question-text d-flex">
|
||||
<span class="fs-5 fw-bold w-100">{{ questionText }}</span>
|
||||
</div>
|
||||
<div class="w-100 d-flex justify-content-center">
|
||||
<fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName ? groupName + '-radio-group' : ''">
|
||||
<legend class="sr-only" :data-focus-target="groupName" tabindex="-1">
|
||||
<fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="groupName">
|
||||
<legend class="sr-only" :data-focus-target="groupName" :id="groupName" tabindex="-1">
|
||||
{{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }}
|
||||
</legend>
|
||||
<div :class="getComponentWrapperClasses">
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="row mt-2 form-test-error">
|
||||
<div class="row form-test-error">
|
||||
<error-message :name="groupName" v-if="!suppressError"></error-message>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -160,7 +160,7 @@ export default {
|
|||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss">
|
||||
.button-question-overflow {
|
||||
height: calc(100vh - 274px);
|
||||
|
||||
|
|
@ -174,4 +174,12 @@ export default {
|
|||
.button-question {
|
||||
color: $black;
|
||||
}
|
||||
.question-text {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
& > span {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="dropdown-question">
|
||||
<div class="dropdown-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
|
||||
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
|
||||
<select v-model="selectedOption"
|
||||
class="form-select"
|
||||
|
|
@ -9,8 +9,7 @@
|
|||
:disabled="isDisabled"
|
||||
:aria-required="isRequired"
|
||||
:validationRules="validationRules"
|
||||
@input="handleChange"
|
||||
@blur="handleBlur" >
|
||||
>
|
||||
<option v-for="(value, name, index) in options" :value="name" :key="index">
|
||||
{{ value }}
|
||||
</option>
|
||||
|
|
@ -40,10 +39,23 @@ export default {
|
|||
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",
|
||||
type: "select",
|
||||
value: props.modelValue,
|
||||
initialValue: props.modelValue,
|
||||
initialValue: initialValue,
|
||||
};
|
||||
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ describe("funnel-footer.vue", () => {
|
|||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn()
|
||||
getCmsContent: jest.fn(),
|
||||
getFooterInfoBoxHeight: jest.fn(()=>80)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,10 +30,10 @@
|
|||
</div>
|
||||
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">
|
||||
<textLink
|
||||
linkType="navigation"
|
||||
:text="backLink"
|
||||
@click-event="linkClick"
|
||||
href="javascript:void(0)"
|
||||
linkType="navigation"
|
||||
:text="backLink"
|
||||
@click-event="linkClick"
|
||||
href="javascript:void(0)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -51,7 +51,6 @@ export default {
|
|||
isForwardActionDisabled: Boolean,
|
||||
isBackButtonHidden: {type: Boolean, default: false},
|
||||
cmsWidgetName: String,
|
||||
|
||||
},
|
||||
components: {
|
||||
textLink,
|
||||
|
|
@ -64,7 +63,7 @@ export default {
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
this.paddingHeight = document.querySelector(".footer #infoBox").offsetHeight + 24;
|
||||
this.paddingHeight = this.getFooterInfoBoxHeight() + 24;
|
||||
this.$nextTick(() => {
|
||||
window.addEventListener('resize', this.onResize);
|
||||
})
|
||||
|
|
@ -106,7 +105,7 @@ export default {
|
|||
linkClick() {
|
||||
this.$emit("BackClicked");
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,9 @@
|
|||
<template>
|
||||
<div v-show="isModalVisible" class="loading-modal-backdrop">
|
||||
<div v-if="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.
|
||||
<object type="image/svg+xml" :data="fmgAnimatedTransition"/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
|
@ -18,11 +11,15 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
// load the path like this - <object> data attribute doesn't load the relative path correctly
|
||||
const FMGAnimatedTransition = require("@/assets/img/FMGTransitionAnimation.svg");
|
||||
|
||||
export default {
|
||||
name: 'Modal',
|
||||
data() {
|
||||
return {
|
||||
isModalVisible: false,
|
||||
fmgAnimatedTransition: FMGAnimatedTransition,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -60,23 +57,15 @@
|
|||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
top: 48px;
|
||||
height: 246px;
|
||||
height: 186px;
|
||||
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;
|
||||
}
|
||||
|
|
@ -85,6 +74,8 @@
|
|||
position: absolute;
|
||||
vertical-align: middle;
|
||||
border: 0;
|
||||
width: 327px;
|
||||
height: 186px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
<div class="textbox-question" :class="(errors && errors.length) || hasError ? 'has-error' : ''">
|
||||
<label :for="inputId" :aria-label="questionText" class="form-label" v-html="labelText"></label>
|
||||
<!-- See https://stackoverflow.com/a/30976223 for information about "do-not-autofill" -->
|
||||
<input
|
||||
v-model="value"
|
||||
<input
|
||||
v-model.trim="value"
|
||||
v-maska="mask"
|
||||
:type="type"
|
||||
class="form-control"
|
||||
|
|
@ -14,45 +14,47 @@
|
|||
:aria-disabled="isDisabled"
|
||||
:disabled="isDisabled"
|
||||
:aria-required="isRequired"
|
||||
autocomplete="do-not-autofill"
|
||||
autocomplete="do-not-autofill"
|
||||
:class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']"
|
||||
:validationRules="validationRules"
|
||||
@change="handleChange"
|
||||
@blur="handleChange"
|
||||
/>
|
||||
<div v-show="errorMessage" class="row mt-2 form-test-error">
|
||||
<span role="alert">{{ errorMessage }}</span>
|
||||
<div v-show="errorMessage" class="row my-2 form-test-error">
|
||||
<span class="d-inline-flex mt-0" role="alert">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { useField } from "vee-validate";
|
||||
import { useField, validate } from "vee-validate";
|
||||
|
||||
export default {
|
||||
name: "textbox-question",
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
default: "text",
|
||||
},
|
||||
placeholderText: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
modelValue: String,
|
||||
inputId: String,
|
||||
isDisabled: Boolean,
|
||||
isRequired: Boolean,
|
||||
disableAutoFill: Boolean,
|
||||
hasIcon: Boolean, // If input has an icon
|
||||
iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used
|
||||
hasIcon: Boolean, // If input has an icon
|
||||
iconRight: Boolean, // Place icon on right side of text input, otherwise default is left if hasIcon prop is used
|
||||
hasError: Boolean,
|
||||
mask: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: "",
|
||||
},
|
||||
validationRules: String,
|
||||
cmsWidgetName: String
|
||||
semiAggressiveValidation: Boolean,
|
||||
cmsWidgetName: String,
|
||||
},
|
||||
setup(props) {
|
||||
const propsClone = Object.assign({}, props);
|
||||
|
|
@ -60,7 +62,7 @@ export default {
|
|||
let initialValue;
|
||||
|
||||
switch (typeof modelValue) {
|
||||
case "number":
|
||||
case "number":
|
||||
initialValue = modelValue;
|
||||
break;
|
||||
default:
|
||||
|
|
@ -71,17 +73,11 @@ export default {
|
|||
const fieldOptions = {
|
||||
type: "text",
|
||||
value: modelValue,
|
||||
initialValue: initialValue
|
||||
initialValue: initialValue,
|
||||
};
|
||||
|
||||
const {
|
||||
errorMessage,
|
||||
handleBlur,
|
||||
handleChange,
|
||||
meta,
|
||||
validate,
|
||||
errors,
|
||||
} = useField(props.inputId, props.validationRules, fieldOptions);
|
||||
const { errorMessage, handleBlur, handleChange, meta, validate, errors } =
|
||||
useField(props.inputId, props.validationRules, fieldOptions);
|
||||
|
||||
return {
|
||||
errorMessage,
|
||||
|
|
@ -93,16 +89,16 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
questionText(){
|
||||
return this.getCmsContent(this.cmsWidgetName, 'QuestionText');
|
||||
questionText() {
|
||||
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
|
||||
},
|
||||
value: {
|
||||
get: function() {
|
||||
get: function () {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function(newValue) {
|
||||
set: function (newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
}
|
||||
},
|
||||
},
|
||||
labelText: {
|
||||
get: function () {
|
||||
|
|
@ -112,7 +108,11 @@ export default {
|
|||
var words = this.questionText.toString().split(/[ ]+/);
|
||||
words.forEach(function (word) {
|
||||
const position = 1;
|
||||
word = [word.toString().slice(0, position), noBreakChar, word.toString().slice(position)].join('');
|
||||
word = [
|
||||
word.toString().slice(0, position),
|
||||
noBreakChar,
|
||||
word.toString().slice(position),
|
||||
].join("");
|
||||
questionText += `${word} `;
|
||||
});
|
||||
|
||||
|
|
@ -122,14 +122,19 @@ export default {
|
|||
}
|
||||
|
||||
return questionText;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(newValue) {
|
||||
this.handleChange(newValue);
|
||||
}
|
||||
}
|
||||
async value(newValue) {
|
||||
if (this.semiAggressiveValidation) {
|
||||
const result = await validate(newValue, this.validationRules); // do a test validation check, without triggering full validation
|
||||
if (result.valid) {
|
||||
this.handleChange(newValue); // trigger full validation on this field only
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -159,6 +164,7 @@ export default {
|
|||
border: 1px solid $gray-500;
|
||||
border-radius: .5rem;
|
||||
min-height: 3rem;
|
||||
padding: 12px 16px;
|
||||
&::placeholder {
|
||||
color: $gray-500;
|
||||
}
|
||||
|
|
@ -182,4 +188,4 @@ export default {
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -18,11 +18,12 @@ const errorMessages = {
|
|||
LAST_NAME_REQUIRED: "Please enter your last name",
|
||||
EMAIL_ADDRESS_REQUIRED: "Please enter your email address",
|
||||
EMAIL_ADDRESS_FORMAT: "Please enter a valid email address",
|
||||
SERVICE_ZIP_REQUIRED: "Please enter your Service ZIP",
|
||||
SERVICE_ZIP_FORMAT: "Please enter a valid Service ZIP",
|
||||
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: "Invalid VIN. Please make sure that you entered the correct 17-digit, alpha-numeric number. VINs do not contain the letters I, O, or Q",
|
||||
OPTION_REQUIRED: "Please select an option",
|
||||
VEHICLE_REQUIRED: "Please select a vehicle",
|
||||
};
|
||||
|
||||
export { errorMessages };
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const storeActions = {
|
|||
LOG_PAGE_VIEW: "logPageView",
|
||||
LOG_CUSTOM_EVENT: "logCustomEvent",
|
||||
GET_EXPERIMENTS_BY_USER: "GetExperimentsByUser",
|
||||
UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION: "updateServiceLocationWithVehicleRegistration",
|
||||
|
||||
// DEPENDENCY MUTATIONS
|
||||
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies",
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ const storeMutations = {
|
|||
|
||||
// OTHER MUTATIONS
|
||||
UPDATE_PAGE_DATA: "updatePageData",
|
||||
UPDATE_STATE_WITH_ORDER_INFORMATION: "updateStateWithOrderInformation"
|
||||
|
||||
UPDATE_STATE_WITH_ORDER_INFORMATION: "updateStateWithOrderInformation",
|
||||
UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION: "updateServiceLocationWithVehicleRegistration"
|
||||
};
|
||||
|
||||
export { storeMutations };
|
||||
|
|
|
|||
|
|
@ -4,8 +4,18 @@ import { storeActions } from "@/constants/store-actions";
|
|||
|
||||
export function getDamageString() {
|
||||
const damageLocations = store.getters.damage.glassToReplace;
|
||||
const isRepair = store.getters.damage.isRepair;
|
||||
|
||||
let returnString;
|
||||
if(damageLocations.length > 1){
|
||||
if (!damageLocations) {
|
||||
return;
|
||||
}
|
||||
// If it's a repair it's always a windshield.
|
||||
if(isRepair){
|
||||
return "windshield"
|
||||
}
|
||||
|
||||
if (damageLocations.length > 1) {
|
||||
returnString = "match"
|
||||
} else {
|
||||
switch(damageLocations[0]?.location) {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import { externalUrls } from "@/router/router-constants/externalUrl-values";
|
|||
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
|
||||
import { saveOrder } from "@/helpers/heritage-integration/order-helper.js";
|
||||
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import store from "@/store";
|
||||
import router from "@/router";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
|
||||
/*
|
||||
If the user has visited the funnel before this method will determine the bets place to
|
||||
|
|
@ -12,7 +14,6 @@ import router from "@/router";
|
|||
the user has an existing order and they come back in from the Safelite.com CTA.
|
||||
*/
|
||||
export async function getPageToRouteExistingOrderTo(toRoute = {}, existingHeritageOrder = false) {
|
||||
|
||||
// If the user is coming in via the Safelite.Com CTA
|
||||
if (toRoute.query[queryStrings.START_TYPE] === 'fmg') {
|
||||
// If they have an existing order, return 'heritage' for the page name.
|
||||
|
|
@ -56,6 +57,7 @@ export async function navigateToHeritageFunnel() {
|
|||
export async function navigateAfterSaveToHeritageFunnel(currentRoute) {
|
||||
const currentComponent = currentRoute.matched[0].components;
|
||||
currentComponent.default.methods.resetDependentState();
|
||||
|
||||
// Create the order (or save existing order) when navigating to Heritage Funnel.
|
||||
await saveOrder();
|
||||
|
||||
|
|
|
|||
|
|
@ -12,28 +12,28 @@
|
|||
<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"
|
||||
<alert ref="alertVinNotFound" v-if="displayVinNotFoundAlert"
|
||||
class="mb-4"
|
||||
cmsWidgetName="AlertVinNotFoundWidget"
|
||||
alertClass="alert-danger"
|
||||
v-bind:isDismissible="false"
|
||||
/>
|
||||
<alert ref="alertMatchedDifferentVehicle" v-show="displayMatchedDifferentVehicleAlert"
|
||||
class="my-3"
|
||||
<alert ref="alertMatchedDifferentVehicle" v-if="displayMatchedDifferentVehicleAlert"
|
||||
class="mb-4"
|
||||
:manualHeadline="AlertMatchedDifferentVehicleHeader"
|
||||
:manualCopy="AlertMatchedDifferentVehicleBody"
|
||||
alertClass="alert-warning"
|
||||
v-bind:isDismissible="false"
|
||||
/>
|
||||
<alert ref="alertNonServiceableZip" v-show="displayNonServiceableZipAlert"
|
||||
class="my-3"
|
||||
<alert ref="alertNonServiceableZip" v-if="displayNonServiceableZipAlert"
|
||||
class="mb-4"
|
||||
alertClass="alert-danger"
|
||||
:manualHeadline="AlertNonServiceableZipHeader"
|
||||
:manualCopy="AlertNonServiceableZipBody"
|
||||
v-bind:isDismissible="false"
|
||||
/>
|
||||
<alert ref="alertVinLookupsByHomeAddressNotAllowed" v-show="displayVinLookupByHomeAddressNotAllowedAlert"
|
||||
class="my-3"
|
||||
<alert ref="alertVinLookupsByHomeAddressNotAllowed" v-if="displayVinLookupByHomeAddressNotAllowedAlert"
|
||||
class="mb-4"
|
||||
cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget"
|
||||
alertClass="alert-danger"
|
||||
v-bind:isDismissible="false"
|
||||
|
|
@ -72,10 +72,8 @@ 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";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
import { regex } from "@/helpers/validation-rules";
|
||||
import { Form, defineRule } from "vee-validate";
|
||||
import { required, regex } from "@/helpers/validation-rules";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
|
||||
// Supporting files
|
||||
|
|
@ -262,6 +260,15 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
// if multiple cars were found
|
||||
let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId);
|
||||
if (matchingCars.length === 1) {
|
||||
// and one and only one of them matches the carId entered, save the vehicle info
|
||||
// so we can go to the Heritage Funnel directly
|
||||
const matchingCar = matchingCars[0];
|
||||
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
|
||||
}
|
||||
|
||||
// update data if the zip or service zip is servicable
|
||||
this.updateCustomerInfo();
|
||||
}
|
||||
|
|
@ -275,6 +282,8 @@ export default {
|
|||
this.displayVinLookupByHomeAddressNotAllowedAlert = false;
|
||||
},
|
||||
navigateForward(carEntered, carsFound) {
|
||||
this.updateServiceLocationIfNecessary();
|
||||
|
||||
if (carsFound.length == 1) {
|
||||
// if a different vehicle is found than the one entered and the selected glass
|
||||
// is not available for that vehicle
|
||||
|
|
@ -293,16 +302,18 @@ export default {
|
|||
navigateAfterSaveToHeritageFunnel(this.$route);
|
||||
}
|
||||
} else if (carsFound.length > 1) {
|
||||
// if multiple cars were found
|
||||
if (carsFound.find(car => car.vehicle.carId === carEntered.carId)) {
|
||||
// and one of them matches the car id entered
|
||||
// if multiple cars were found
|
||||
let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId);
|
||||
if (matchingCars.length === 1) {
|
||||
this.$refs.loadingModal.showModal();
|
||||
|
||||
// and one and only of them matches the car id entered
|
||||
const matchingCar = matchingCars[0];
|
||||
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
|
||||
navigateAfterSaveToHeritageFunnel(this.$route);
|
||||
} else {
|
||||
// and there is no match, navigate to "address-vehicles" page
|
||||
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES,
|
||||
this.$route, {}, {},
|
||||
carsFound);
|
||||
// if there are no matches or there are multiple matches, navigate to "address-vehicles" page
|
||||
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, this.$route, {}, {}, carsFound);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -344,8 +355,16 @@ export default {
|
|||
store.commit(storeMutations.UPDATE_REGISTRATION_LAST_NAME, this.customerQuestions.lastName);
|
||||
store.commit(storeMutations.UPDATE_SERVICE_LOCATION_ZIP_CODE, this.serviceZipCode);
|
||||
store.commit(storeMutations.UPDATE_CUSTOMER_EMAIL_ADDRESS, this.customerQuestions.emailAddress);
|
||||
},
|
||||
|
||||
},
|
||||
updateServiceLocationIfNecessary() {
|
||||
const serviceLocation = store.getters.order.serviceLocation;
|
||||
|
||||
if (!serviceLocation.address && serviceLocation.zipCode && serviceLocation.zipCode == store.getters.vehicle.registration.zipCode) {
|
||||
baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.attachCustomEvents();
|
||||
|
|
@ -364,13 +383,14 @@ export default {
|
|||
return text;
|
||||
},
|
||||
AlertMatchedDifferentVehicleBody(){
|
||||
let content = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText");
|
||||
content = content.replaceAll("{custom:glassText}", getDamageString());
|
||||
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);
|
||||
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}`;
|
||||
|
||||
const content = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText")
|
||||
.replaceAll("{custom:glassText}", getDamageString())
|
||||
.replaceAll("{custom:vinYmmFound}", vinYmmFound)
|
||||
.replaceAll("{custom:vinYmmExpected}", vinYmmExpected);
|
||||
|
||||
return content;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,32 +1,70 @@
|
|||
<template>
|
||||
<div class="row mt-2 mb-4">
|
||||
<div class="col">
|
||||
<textboxQuestion cmsWidgetName="StreetAddressQuestionWidget" v-model="addressModel.streetAddress" ref="autocomplete" inputId="autocomplete" placeholderText="Search" aria-haspopup="" hasIcon disableAutoFill validationRules="street-address-required" />
|
||||
<textboxQuestion
|
||||
id="streetAddressField"
|
||||
cmsWidgetName="StreetAddressQuestionWidget"
|
||||
v-model="addressModel.streetAddress"
|
||||
ref="autocomplete"
|
||||
inputId="autocomplete"
|
||||
placeholderText="Search"
|
||||
aria-haspopup=""
|
||||
hasIcon
|
||||
disableAutoFill
|
||||
validationRules="street-address-required"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="row mb-4" v-show="showAddressFields" aria-live="polite">
|
||||
<div class="col">
|
||||
<textboxQuestion cmsWidgetName="CityQuestionWidget" v-model="addressModel.city" ref="city" inputId="cbf28188fdf2436688fd735915f7ee56" disableAutoFill validationRules="city-required"/>
|
||||
<textboxQuestion
|
||||
cmsWidgetName="CityQuestionWidget"
|
||||
v-model="addressModel.city"
|
||||
ref="city"
|
||||
inputId="cbf28188fdf2436688fd735915f7ee56"
|
||||
disableAutoFill
|
||||
validationRules="city-required"
|
||||
semiAggressiveValidation
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="row mb-4" v-show="showAddressFields" aria-live="polite">
|
||||
<div class="col">
|
||||
<dropdownQuestion cmsWidgetName="StateQuestionWidget" v-model="addressModel.state" ref="state" inputId="8fdf9dc2e13e430eb57529499dceb3eb" :options="stateOptions" disableAutoFill validationRules="state-required" />
|
||||
<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.zipCode" ref="zipCode" inputId="01a9a1c2de0b4c9da8e023c9ae3be498" mask="#####" disableAutoFill validationRules="zip-code-required|zip-code-format"/>
|
||||
<textboxQuestion
|
||||
cmsWidgetName="ZipQuestionWidget"
|
||||
v-model="addressModel.zipCode"
|
||||
ref="zipCode"
|
||||
inputId="01a9a1c2de0b4c9da8e023c9ae3be498"
|
||||
mask="#####"
|
||||
disableAutoFill
|
||||
validationRules="zip-code-required|zip-code-format"
|
||||
semiAggressiveValidation
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<alert ref="alertVerificationWarning" v-show="displayVerificationWarning"
|
||||
<alert ref="alertVerificationWarning" v-if="displayVerificationWarning"
|
||||
class="mb-4"
|
||||
cmsWidgetName="AlertVerificationWarningWidget"
|
||||
alertClass="alert-warning"
|
||||
v-bind:isDismissible="false"
|
||||
/>
|
||||
<alert ref="alertNoMatchWarning" v-show="displayNoMatchWarning"
|
||||
<alert ref="alertNoMatchWarning" v-if="displayNoMatchWarning"
|
||||
class="mb-4"
|
||||
cmsWidgetName="AlertNoMatchWarningWidget"
|
||||
alertClass="alert-warning"
|
||||
v-bind:isDismissible="false"
|
||||
|
|
@ -34,18 +72,19 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import textboxQuestion from "@/common-components/textbox-question/textbox-question";
|
||||
import dropdownQuestion from "@/common-components/dropdown-question/dropdown-question";
|
||||
import alert from "@/ux-components/alert/alert";
|
||||
import { applicationConfig } from "@/constants/application-config.js";
|
||||
import { defineRule } from "vee-validate";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
import { regex } from "@/helpers/validation-rules";
|
||||
import { required, regex } from "@/helpers/validation-rules";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("street-address-required", required(errorMessages.STREET_ADDRESS_REQUIRED));
|
||||
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-code-required", required(errorMessages.ZIP_REQUIRED));
|
||||
|
|
@ -152,7 +191,7 @@ export default ({
|
|||
this.addressModel.state !== null &
|
||||
this.addressModel.zipCode !== null) {
|
||||
|
||||
this.showAddressFields = true;
|
||||
this.showAddressFields = true;
|
||||
}
|
||||
|
||||
const addressField1 = document.getElementById("autocomplete");
|
||||
|
|
@ -173,44 +212,52 @@ export default ({
|
|||
);
|
||||
|
||||
// Standard place_changed event handling
|
||||
autocomplete.addListener('place_changed', fillInAddress);
|
||||
const autocompleteListener = window.google.maps.event.addListener(autocomplete, 'place_changed', fillInAddress);
|
||||
|
||||
// Wrapping the addressField1 element in the Google Address Autocomplete object
|
||||
// will cause "autocomplete='off'" which Chrome completely ignores. This event
|
||||
// handler will set the value to something arbitrary so autofill doesn't work.
|
||||
// https://stackoverflow.com/a/30976223
|
||||
addressField1.addEventListener("focus", () => {
|
||||
addressField1.setAttribute("autocomplete", "do-not-autofill");
|
||||
// Wrapping the addressField1 element in the Google Address Autocomplete object
|
||||
// will cause "autocomplete='off'" which Chrome completely ignores. This event
|
||||
// handler will set the value to something arbitrary so autofill doesn't work.
|
||||
// https://stackoverflow.com/a/30976223
|
||||
addressField1.addEventListener("focus", () => {
|
||||
addressField1.setAttribute("autocomplete", "do-not-autofill");
|
||||
|
||||
// Make place results box stick to the input on scroll
|
||||
const streetAddressField = document.getElementById("streetAddressField");
|
||||
const autocompleteResultsContainer = document.getElementsByClassName("pac-container")[0];
|
||||
if (autocompleteResultsContainer) {
|
||||
streetAddressField.appendChild(autocompleteResultsContainer);
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
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) {
|
||||
const item = document.querySelector(".pac-container .pac-item");
|
||||
if (item != null) {
|
||||
const firstResult = item.textContent;
|
||||
const geocoder = new window.google.maps.Geocoder();
|
||||
geocoder.geocode({
|
||||
address: firstResult
|
||||
}, function (results, status) {
|
||||
if (status === window.google.maps.GeocoderStatus.OK) {
|
||||
fillInAddress(results[0]);
|
||||
self.displayVerificationWarning = true;
|
||||
self.displayNoMatchWarning = false;
|
||||
}
|
||||
});
|
||||
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) {
|
||||
const item = document.querySelector(".pac-container .pac-item");
|
||||
if (item != null) {
|
||||
const firstResult = item.textContent;
|
||||
const geocoder = new window.google.maps.Geocoder();
|
||||
geocoder.geocode({
|
||||
address: firstResult
|
||||
}, function (results, status) {
|
||||
if (status === window.google.maps.GeocoderStatus.OK) {
|
||||
fillInAddress(results[0]);
|
||||
self.displayVerificationWarning = true;
|
||||
self.displayNoMatchWarning = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
self.addressModel.city = "";
|
||||
self.addressModel.state = "";
|
||||
self.addressModel.zipCode = "";
|
||||
self.showAddressFields = true;
|
||||
self.displayVerificationWarning = false;
|
||||
self.displayNoMatchWarning = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.addressModel.city = "";
|
||||
self.addressModel.state = "";
|
||||
self.addressModel.zipCode = "";
|
||||
self.showAddressFields = true;
|
||||
self.displayVerificationWarning = false;
|
||||
self.displayNoMatchWarning = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function fillInAddress(place) {
|
||||
if (!place) {
|
||||
|
|
@ -218,19 +265,19 @@ export default ({
|
|||
}
|
||||
|
||||
if (place && place.address_components) {
|
||||
self.addressModel.streetAddress= "";
|
||||
self.showAddressFields = true;
|
||||
|
||||
for (const component of place.address_components) {
|
||||
const componentType = component.types[0];
|
||||
|
||||
switch (componentType) {
|
||||
switch (componentType) {
|
||||
case "street_number": {
|
||||
self.addressModel.streetAddress = component.long_name;
|
||||
break;
|
||||
}
|
||||
case "route": {
|
||||
self.addressModel.streetAddress += ' ' + component.short_name;
|
||||
self.addressModel.streetAddress +=
|
||||
" " + component.short_name;
|
||||
break;
|
||||
}
|
||||
case "locality": {
|
||||
|
|
@ -239,15 +286,15 @@ export default ({
|
|||
}
|
||||
case "administrative_area_level_1": {
|
||||
self.addressModel.state = component.short_name;
|
||||
// self.addressModel.state = "";
|
||||
break;
|
||||
}
|
||||
case "postal_code": {
|
||||
self.addressModel.zipCode = component.long_name;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.displayVerificationWarning = false;
|
||||
self.displayNoMatchWarning = false;
|
||||
|
|
@ -255,13 +302,21 @@ export default ({
|
|||
else {
|
||||
self.displayVerificationWarning = true;
|
||||
self.displayNoMatchWarning = false;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Failed to fetch script
|
||||
console.log("Unable to load Google Places API script");
|
||||
});
|
||||
}
|
||||
|
||||
// after showing the address fields, disable the address autocomplete
|
||||
window.google.maps.event.removeListener(autocompleteListener);
|
||||
window.google.maps.event.clearInstanceListeners(autocomplete);
|
||||
addressField1.onchange = null;
|
||||
const pacContainer = document.querySelector(".pac-container");
|
||||
pacContainer.remove();
|
||||
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Failed to fetch script
|
||||
console.log("Unable to load Google Places API script");
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
@ -269,8 +324,17 @@ export default ({
|
|||
},
|
||||
watch: {
|
||||
addressModel: {
|
||||
handler(newValue){
|
||||
this.displayNoMatchWarning = false;
|
||||
handler(newValue) {
|
||||
// The first time the address model changes is w
|
||||
if (!newValue.city &&
|
||||
!newValue.state &&
|
||||
!newValue.zipCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.displayNoMatchWarning === true) {
|
||||
this.displayNoMatchWarning = false;
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
|
|
@ -281,4 +345,15 @@ export default ({
|
|||
alert,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#streetAddressField {
|
||||
position: relative;
|
||||
|
||||
.pac-container {
|
||||
top: 76px !important; // Height of #streetAddressField
|
||||
left: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -2,17 +2,39 @@
|
|||
<addressQuestions ref="addressQuestions" v-model="customerModel.addressQuestions" :alertNotifications="alertNotifications" />
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<textboxQuestion cmsWidgetName="FirstNameQuestionWidget" v-model="customerModel.firstName" ref="firstName" inputId="08497a2efd9a4a73a70360ab47b4838d" disableAutoFill validationRules="first-name-required" />
|
||||
<textboxQuestion
|
||||
cmsWidgetName="FirstNameQuestionWidget"
|
||||
v-model="customerModel.firstName"
|
||||
ref="firstName"
|
||||
inputId="08497a2efd9a4a73a70360ab47b4838d"
|
||||
disableAutoFill
|
||||
validationRules="first-name-required"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<textboxQuestion cmsWidgetName="LastNameQuestionWidget" v-model="customerModel.lastName" ref="lastName" inputId="0030e56a57e74a4ab92de7fb8e97fec5" disableAutoFill validationRules="last-name-required" />
|
||||
<textboxQuestion
|
||||
cmsWidgetName="LastNameQuestionWidget"
|
||||
v-model="customerModel.lastName"
|
||||
ref="lastName"
|
||||
inputId="0030e56a57e74a4ab92de7fb8e97fec5"
|
||||
disableAutoFill
|
||||
validationRules="last-name-required"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="row mb-5">
|
||||
<div class="col">
|
||||
<textboxQuestion cmsWidgetName="EmailAddressQuestionWidget" v-model="customerModel.emailAddress" ref="emailAddress" inputId="00450a91b8964a768ce3992e6feb890f" disableAutoFill validationRules="email-address-required|email-address-format"/>
|
||||
<textboxQuestion
|
||||
cmsWidgetName="EmailAddressQuestionWidget"
|
||||
v-model="customerModel.emailAddress"
|
||||
ref="emailAddress"
|
||||
inputId="00450a91b8964a768ce3992e6feb890f"
|
||||
disableAutoFill
|
||||
validationRules="email-address-required|email-address-format"
|
||||
semiAggressiveValidation
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -29,7 +51,7 @@ import { errorMessages } from "@/constants/error-messages";
|
|||
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
|
||||
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
|
||||
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("email-address-format", regex(/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/, errorMessages.EMAIL_ADDRESS_FORMAT));
|
||||
|
||||
export default ({
|
||||
name: "customer-questions",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
// import store from "@/store";
|
||||
|
||||
// jest.mock("@/store", () => { return {}; }, { virtual: true });
|
||||
|
||||
describe("addressVehiclesQuestion.vue", () => {
|
||||
|
||||
it("Should include button-question component", () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(addressVehiclesQuestion, {
|
||||
propsData: {
|
||||
// vehicles: [
|
||||
// {
|
||||
// "Name": "testname",
|
||||
// "Text": "testtext",
|
||||
// }
|
||||
// ]
|
||||
}
|
||||
});
|
||||
|
||||
// console.log('wrapper.html: ', wrapper.html());
|
||||
|
||||
// Assert
|
||||
const buttonQuestion = wrapper.find('button-question-stub');
|
||||
expect(buttonQuestion).toBe;
|
||||
});
|
||||
|
||||
it("on initialize should pass in questionText", () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(addressVehiclesQuestion, {
|
||||
propsData: {},
|
||||
});
|
||||
|
||||
//Act
|
||||
addressVehiclesQuestion.methods.initializeComponent.call(wrapper.vm, {QuestionText: 'Testing question text'});
|
||||
|
||||
// console.log('wrapper.html: ', wrapper.html());
|
||||
// console.log('wrapper.vm.questionText: ', wrapper.vm.questionText);
|
||||
|
||||
// Assert
|
||||
expect(wrapper.vm.questionText).toEqual('Testing question text');
|
||||
});
|
||||
|
||||
// it("Alert should show if prop isCarIdDifferent is true", () => {
|
||||
// // Arrange
|
||||
// const wrapper = shallowMount(addressVehiclesQuestion, setupMountOptions({
|
||||
// propsData: {
|
||||
// isCarIdDifferent: true,
|
||||
// }
|
||||
// }));
|
||||
|
||||
// //Act
|
||||
// const alert = wrapper.find('alert');
|
||||
// console.log('wrapper.html: ', wrapper.html());
|
||||
// console.log('wrapper.vm.questionText: ', wrapper.vm.questionText);
|
||||
|
||||
// // Assert
|
||||
// // expect(wrapper.vm.questionText).toEqual('Testing question text');
|
||||
// });
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
function setupMountOptions(mountOptionsMockData = {}) {
|
||||
// //Mock store
|
||||
// store.dispatch = jest.fn(() => {});
|
||||
// store.getters = {};
|
||||
|
||||
// const mockMixin = {
|
||||
// methods: {
|
||||
// getCmsContent: jest.fn().mockImplementation(() => {
|
||||
// return '';
|
||||
// }),
|
||||
// getDamageString: jest.fn().mockImplementation(() => {
|
||||
// return '';
|
||||
// })
|
||||
// },
|
||||
// store: {
|
||||
// dispatch: store.dispatch,
|
||||
// getters: store.getters,
|
||||
// },
|
||||
// }
|
||||
|
||||
const mockGetCmsContent = jest.fn();
|
||||
mockGetCmsContent((cmsWidget, field) => {
|
||||
return field
|
||||
});
|
||||
const defaultMountOptions = {
|
||||
// route: { query: { fmgPage: 'page-name' } },
|
||||
mixins: {
|
||||
methods: {
|
||||
getCmsContent: mockGetCmsContent,
|
||||
},
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
store: {
|
||||
dispatch: store.dispatch,
|
||||
getters: store.getters,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const baseMountOptions = getMountOptions(Object.assign(defaultMountOptions, mountOptionsMockData));
|
||||
const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions);
|
||||
|
||||
console.log('what are allMountOptions??? ', allMountOptions);
|
||||
|
||||
return allMountOptions;
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<buttonQuestion
|
||||
class="address-vehicles-question"
|
||||
buttonType="listButton"
|
||||
groupName="ChooseAddressVehicle"
|
||||
:questionText="questionText"
|
||||
:answers="vehicles"
|
||||
v-model="selectedVehicleVin"
|
||||
isRequired=true
|
||||
:validation-rules="validationRules"
|
||||
/>
|
||||
<alert
|
||||
ref="differentVehicleAlert"
|
||||
v-if="isCarIdDifferent"
|
||||
class="my-3"
|
||||
alertClass="alert-warning"
|
||||
:manualHeadline="differentVehicleAlertHeader"
|
||||
:manualCopy="differentVehicleAlertBody"
|
||||
v-bind:isDismissible="false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import buttonQuestion from "@/common-components/button-question/button-question";
|
||||
import alert from "@/ux-components/alert/alert";
|
||||
|
||||
// Supporting files
|
||||
import { getDamageString } from "@/helpers/damage-helper";
|
||||
|
||||
export default {
|
||||
name: "address-vehicles-question",
|
||||
props: {
|
||||
vehicles: Array,
|
||||
modelValue: String,
|
||||
cmsWidgetName: String,
|
||||
validationRules: String,
|
||||
isCarIdDifferent: Boolean,
|
||||
},
|
||||
computed: {
|
||||
differentVehicleAlertHeader() {
|
||||
return this.getCmsContent("FoundWindshield", "HeadlineText").replaceAll("{custom:damage}", getDamageString());
|
||||
},
|
||||
differentVehicleAlertBody() {
|
||||
return this.getCmsContent(
|
||||
"FoundWindshield",
|
||||
"BodyText"
|
||||
)
|
||||
.replaceAll("{custom:damage}", getDamageString())
|
||||
.replaceAll(
|
||||
"{custom:vinlookupYear}",
|
||||
this.selectedVehicle?.vehicle.year
|
||||
)
|
||||
.replaceAll(
|
||||
"{custom:vinlookupMake}",
|
||||
this.selectedVehicle?.vehicle.make
|
||||
)
|
||||
.replaceAll(
|
||||
"{custom:vinlookupModel}",
|
||||
this.selectedVehicle?.vehicle.model
|
||||
);
|
||||
},
|
||||
questionText() {
|
||||
return this.getCmsContent("VehicleConfirmationQuestion", "QuestionText");
|
||||
},
|
||||
selectedVehicleVin: {
|
||||
get: function() {
|
||||
return this.modelValue;
|
||||
},
|
||||
set: function(newValue) {
|
||||
this.$emit("update:modelValue", newValue);
|
||||
}
|
||||
},
|
||||
selectedVehicle() {
|
||||
return this.vehicles.find( ({ vin }) => vin === this.selectedVehicleVin[0] );
|
||||
},
|
||||
},
|
||||
components: {
|
||||
buttonQuestion,
|
||||
alert,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.address-vehicles-question {
|
||||
.question-text {
|
||||
margin-bottom: .5rem;
|
||||
|
||||
& > span {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
26
src/layouts/address-vehicles/address-vehicles.spec.js1
Normal file
26
src/layouts/address-vehicles/address-vehicles.spec.js1
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import addressVehicles from "@/layouts/address-vehicles/address-vehicles";
|
||||
|
||||
describe("addressVehicles.vue", () => {
|
||||
|
||||
it("Should include address-vehicles-question component", () => {
|
||||
// Arrange
|
||||
const wrapper = shallowMount(addressVehicles, {
|
||||
propsData: {
|
||||
vehicles: [
|
||||
{
|
||||
"Name": "testname",
|
||||
"Text": "testtext",
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// console.log('wrapper.html: ', wrapper.html());
|
||||
|
||||
// Assert
|
||||
const addressVehiclesQuestion = wrapper.find('address-vehicles-question-stub');
|
||||
expect(addressVehiclesQuestion).toBe;
|
||||
});
|
||||
|
||||
});
|
||||
243
src/layouts/address-vehicles/address-vehicles.vue
Normal file
243
src/layouts/address-vehicles/address-vehicles.vue
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
<template>
|
||||
<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" />
|
||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
|
||||
<alert
|
||||
ref="alertFoundMultipleVehicles"
|
||||
class="my-5"
|
||||
alertClass="alert-warning"
|
||||
:manualHeadline="AlertFoundMultipleVehiclesHeader"
|
||||
manualCopy=""
|
||||
v-bind:isDismissible="false"
|
||||
/>
|
||||
<addressVehiclesQuestion
|
||||
ref="addressVehiclesQuestion"
|
||||
cmsWidgetName="VehicleConfirmationQuestion"
|
||||
:vehicles="VehiclesForQuestions"
|
||||
validationRules="vehicle-required"
|
||||
v-model="selectedVehicleVin"
|
||||
:isCarIdDifferent="isCarIdDifferent"
|
||||
/>
|
||||
<div class="alert-provide-vin my-3" v-if="splitAlertProvideVinBodyForLink.length">
|
||||
<span v-for="copy in splitAlertProvideVinBodyForLink" :key="copy">
|
||||
<span v-if="copy.includes('routerLink:')" class="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" v-html="copy"></span>
|
||||
</span>
|
||||
</div>
|
||||
<funnelFooter
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
ref="funnelFooter"
|
||||
:isForwardActionDisabled="!meta.valid"
|
||||
@back-clicked="backButtonAction"
|
||||
@ForwardClicked="forwardButtonAction"
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Components
|
||||
import funnelHeader from "@/common-components/funnel-header/funnel-header";
|
||||
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 alert from "@/ux-components/alert/alert";
|
||||
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-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 store from "@/store";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import { storeActions } from "@/constants/store-actions";
|
||||
import { storeMutations } from "@/constants/store-mutations";
|
||||
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
|
||||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { required } from "@/helpers/validation-rules";
|
||||
import { Form, defineRule } from "vee-validate";
|
||||
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||
import { isGlassAvailableForCarId } from "@/helpers/damage-helper";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("vehicle-required", required(errorMessages.VEHICLE_REQUIRED));
|
||||
|
||||
export default {
|
||||
name: "address-vehicles",
|
||||
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);
|
||||
});
|
||||
},
|
||||
props: {
|
||||
validationRules: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedVehicleVin: null,
|
||||
isCarIdDifferent: false,
|
||||
isSelectedGlassAvailableForVehicle: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
vehicleCount() {
|
||||
return this.VehiclesForQuestions.length;
|
||||
},
|
||||
AlertFoundMultipleVehiclesHeader() {
|
||||
let text = this.getCmsContent("FoundMultipleVehicles", "HeadlineText").replaceAll("{custom:vehicleCount}", this.vehicleCount);
|
||||
return text;
|
||||
},
|
||||
AlertProvideVinBody() {
|
||||
return this.getCmsContent("ProvideVinAlert", "BodyText");
|
||||
},
|
||||
splitAlertProvideVinBodyForLink() {
|
||||
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
|
||||
return this.AlertProvideVinBody.split(/{(.*?)}/g);
|
||||
},
|
||||
VehiclesForQuestions() {
|
||||
const vehiclesData = this.VehiclesFromApi;
|
||||
|
||||
// Map API result data, to address-vehicles data structure
|
||||
const mappedData = vehiclesData.map((v) => {
|
||||
const maskSymbol = "X";
|
||||
const vinStart = maskSymbol.repeat(v.vin.length-4);
|
||||
const vinEnd = v.vin.substring(v.vin.length-4);
|
||||
return {
|
||||
vin: v.vin,
|
||||
vehicle: v.vehicle,
|
||||
Text: v.vehicle.year + " " + v.vehicle.make + " " + v.vehicle.model,
|
||||
Name: v.vin,
|
||||
SubText: "VIN " + vinStart + vinEnd,
|
||||
};
|
||||
});
|
||||
|
||||
return mappedData;
|
||||
},
|
||||
VehiclesFromApi() {
|
||||
return store.getters.pageData(fmgPageValues.ADDRESS_VEHICLES);
|
||||
},
|
||||
selectedVehicle() {
|
||||
return this.VehiclesForQuestions.find( ({ vin }) => vin === this.selectedVehicleVin[0] );
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
arePagePrerequisitesValid() {
|
||||
if (
|
||||
store.getters.order.vehicle.carId
|
||||
&& store.getters.order.serviceLocation.zipCode
|
||||
&& store.getters.order.customer.emailAddress
|
||||
&& store.getters.pageData(fmgPageValues.ADDRESS_VEHICLES)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
backButtonAction() {
|
||||
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
const vinLookup = await this.lookupVin(this.selectedVehicle.vin).catch(() => {
|
||||
this.$refs.funnelFooter.removeLoader();
|
||||
return;
|
||||
});
|
||||
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(vinLookup.data.carId);
|
||||
this.updateCustomerInfo(this.selectedVehicle.vin, this.selectedVehicle.vehicle);
|
||||
this.navigateForward();
|
||||
},
|
||||
navigateForward() {
|
||||
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
|
||||
this.$router.navigateAfterSave(
|
||||
this.navigationScenarios.CLICKED_FORWARD,
|
||||
this.$route,
|
||||
{},
|
||||
{ displayVehicleChangeAlert: true },
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
this.$refs.loadingModal.showModal();
|
||||
navigateAfterSaveToHeritageFunnel(this.$route);
|
||||
return;
|
||||
}
|
||||
},
|
||||
lookupVin(vin) {
|
||||
return baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.LOOKUP_VEHICLE_BY_VIN,
|
||||
{ vin }
|
||||
);
|
||||
},
|
||||
resetDependentState() { // needed because navigateAfterSaveToHeritageFunnel calls it
|
||||
store.dispatch(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
|
||||
},
|
||||
updateCustomerInfo(vin, vehicle) {
|
||||
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
|
||||
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
|
||||
}
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_VIN, vin);
|
||||
store.commit(storeMutations.UPDATE_YEAR, vehicle.year);
|
||||
store.commit(storeMutations.UPDATE_MAKE, vehicle.make);
|
||||
store.commit(storeMutations.UPDATE_MODEL, vehicle.model);
|
||||
store.commit(storeMutations.UPDATE_STYLE, vehicle.style);
|
||||
store.commit(storeMutations.UPDATE_CAR_ID, vehicle.carId);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, vehicle.category);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, vehicle.imageUrl);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, vehicle.imageVifNumber);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, vehicle.imageColor);
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
selectedVehicleVin(vehicleVin) {
|
||||
// does this vehicle match the previously selected carId?
|
||||
this.isCarIdDifferent = this.selectedVehicle.vehicle.carId !== store.getters.vehicle.carId;
|
||||
this.$refs.funnelFooter.updateButtonText(`Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}`);
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
Form,
|
||||
funnelHeader,
|
||||
vehicleBanner,
|
||||
funnelSubHeader,
|
||||
alert,
|
||||
funnelFooter,
|
||||
addressVehiclesQuestion,
|
||||
loadingModal,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.alert-provide-vin {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
|
||||
a {
|
||||
text-underline-offset: .2em;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -8,28 +8,69 @@
|
|||
<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" />
|
||||
<textboxQuestion
|
||||
cmsWidgetName="LicensePlateNumber"
|
||||
v-model="licensePlate"
|
||||
isRequired
|
||||
inputId="license_plate"
|
||||
validationRules="license-plate-required"
|
||||
/>
|
||||
</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"
|
||||
inputId="zip"
|
||||
mask="#####"
|
||||
validationRules="zip-required"
|
||||
/>
|
||||
</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"
|
||||
inputId="email"
|
||||
validationRules="email-address-required|email-address-format"
|
||||
semiAggressiveValidation
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<alert class="my-3" :manualHeadline="NoServiceZipHeader" :manualCopy="NoServiceZipBody" v-if="!isRegistrationZipServicable && isVinValid && !isCarIdDifferent" alertClass="alert-danger" />
|
||||
<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" />
|
||||
<textboxQuestion
|
||||
v-if="!isRegistrationZipServicable"
|
||||
cmsWidgetName="ServiceZip"
|
||||
v-model="serviceZip"
|
||||
inputId="serviceZip"
|
||||
validationRules="zip-required|zip-format"
|
||||
/>
|
||||
</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" />
|
||||
<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>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
@ -46,25 +87,25 @@ 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
|
||||
import {
|
||||
fetchCmsContentForPage
|
||||
} from "@/helpers/cms-content-helper";
|
||||
import {
|
||||
settleAllPromises
|
||||
settleAllPromises
|
||||
} from "@/helpers/layout-helper";
|
||||
import store from "@/store";
|
||||
import baseMixin from "@/mixins/base-mixin.js";
|
||||
import {
|
||||
storeActions
|
||||
storeActions
|
||||
} from "@/constants/store-actions";
|
||||
import {
|
||||
storeMutations
|
||||
storeMutations
|
||||
} from "@/constants/store-mutations";
|
||||
import {
|
||||
errorMessages
|
||||
errorMessages
|
||||
} from "@/constants/error-messages";
|
||||
import {
|
||||
navigateAfterSaveToHeritageFunnel
|
||||
import {
|
||||
navigateAfterSaveToHeritageFunnel
|
||||
} from "@/helpers/heritage-integration/navigation-helper";
|
||||
import {
|
||||
getDamageString,
|
||||
|
|
@ -92,7 +133,7 @@ defineRule(
|
|||
defineRule(
|
||||
"email-address-format",
|
||||
regex(
|
||||
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,})$/,
|
||||
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/,
|
||||
errorMessages.EMAIL_ADDRESS_FORMAT
|
||||
)
|
||||
);
|
||||
|
|
@ -256,7 +297,7 @@ export default {
|
|||
vinLookup.data.vehicle,
|
||||
zipValidation.data.state
|
||||
);
|
||||
|
||||
|
||||
this.navigateForward();
|
||||
},
|
||||
navigateForward() {
|
||||
|
|
@ -281,10 +322,7 @@ export default {
|
|||
});
|
||||
},
|
||||
lookupVin(plate, state) {
|
||||
return baseMixin.methods.dispatchStoreAction(
|
||||
storeActions.LOOKUP_VIN_BY_PLATE,
|
||||
{ licensePlate: plate, licenseState: state }
|
||||
);
|
||||
return baseMixin.methods.dispatchStoreAction(storeActions.LOOKUP_VIN_BY_PLATE,{ licensePlate: plate, licenseState: state }, false);
|
||||
},
|
||||
updateCustomerInfo(vin, vehicleInfo, registrationState) {
|
||||
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
|
||||
|
|
@ -335,4 +373,4 @@ export default {
|
|||
loadingModal,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -87,8 +87,6 @@ import { required } from "@/helpers/validation-rules";
|
|||
import { errorMessages } from "@/constants/error-messages";
|
||||
import { damageLocationsCms } from "@/constants/damage-locations-cms.js";
|
||||
import { damageLocationsSelected } from "@/constants/damage-locations-selected.js";
|
||||
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||
import { queryStrings } from "@/constants/query-strings";
|
||||
|
||||
import store from "@/store";
|
||||
import baseMixin from "@/mixins/base-mixin";
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<alert
|
||||
class="my-3"
|
||||
cmsWidgetName="NoReplacementAvailableError"
|
||||
v-show="showNoReplacementAvailableError"
|
||||
v-if="showNoReplacementAvailableError"
|
||||
alertClass="alert-danger"
|
||||
:isDismissible="false"
|
||||
/>
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
<alert
|
||||
class="my-3"
|
||||
cmsWidgetName="SplitSingleConflict"
|
||||
v-show="hasSplitSingleConflict"
|
||||
v-if="hasSplitSingleConflict"
|
||||
alertClass="alert-danger"
|
||||
:isDismissible="false"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -88,6 +88,10 @@ export default {
|
|||
store.commit(storeMutations.UPDATE_STYLE, null);
|
||||
store.commit(storeMutations.UPDATE_CAR_ID, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
|
||||
|
||||
// Invokes
|
||||
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
|
||||
|
|
|
|||
|
|
@ -88,6 +88,10 @@ export default {
|
|||
store.commit(storeMutations.UPDATE_STYLE, null);
|
||||
store.commit(storeMutations.UPDATE_CAR_ID, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
|
||||
|
||||
// Invokes
|
||||
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export default {
|
|||
store.commit(storeMutations.UPDATE_IS_REPAIR, null);
|
||||
store.commit(storeMutations.UPDATE_NUMBER_OF_CHIPS, null);
|
||||
store.commit(storeMutations.UPDATE_GLASS_TO_REPLACE, []);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -107,8 +107,11 @@ export default {
|
|||
store.commit(storeMutations.UPDATE_STYLE, null);
|
||||
store.commit(storeMutations.UPDATE_CAR_ID, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_CATEGORY, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_VIN, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_URL, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_VIF_NUMBER, null);
|
||||
store.commit(storeMutations.UPDATE_VEHICLE_IMAGE_COLOR, null);
|
||||
|
||||
|
||||
// Invokes
|
||||
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
|
||||
store.dispatch(storeActions.RESET_REGISTRATION_STATE_AND_DEPENDENCIES);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ export default {
|
|||
font-size: .875rem;
|
||||
color: $gray-600;
|
||||
}
|
||||
li {
|
||||
line-height: 26px;
|
||||
}
|
||||
a {
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
|
@ -70,6 +73,7 @@ export default {
|
|||
overflow: hidden;
|
||||
opacity: 0;
|
||||
img {
|
||||
max-width: 420px;
|
||||
width: 117%;
|
||||
height: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
/>
|
||||
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
|
||||
<div class="fade-on-route-transition sub-container make-tall">
|
||||
<div class="row my-2">
|
||||
<div class="row mt-2">
|
||||
<div class="col">
|
||||
<textboxQuestion
|
||||
cmsWidgetName="VinNumber"
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-2">
|
||||
<div class="row mb-2">
|
||||
<div class="col">
|
||||
<vinInformation />
|
||||
</div>
|
||||
|
|
@ -54,18 +54,19 @@
|
|||
isRequired
|
||||
disableAutoFill
|
||||
validationRules="email-address-required|email-address-format"
|
||||
semiAggressiveValidation
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<alert
|
||||
class="my-3"
|
||||
class="my-4"
|
||||
v-model="customAlertData"
|
||||
v-if="noMatchAlert"
|
||||
alertClass="alert-warning"
|
||||
cmsWidgetName="NoMatchAlertWidget"
|
||||
/>
|
||||
<alert
|
||||
class="my-3"
|
||||
class="my-4"
|
||||
:manualHeadline="PerfectMatchNewVinAlertReadOnlyHeader"
|
||||
:manualCopy="PerfectMatchNewVinAlertReadOnlyBody"
|
||||
v-model="customAlertData"
|
||||
|
|
@ -73,15 +74,15 @@
|
|||
alertClass="alert-success"
|
||||
/>
|
||||
<alert
|
||||
class="my-3"
|
||||
class="my-4"
|
||||
:manualHeadline="MatchedDifferentVehicleAlertHeader"
|
||||
:manualCopy="MatchedDifferentVehicleAlertBody"
|
||||
v-model="customAlertData"
|
||||
v-if="isCarIdDifferent"
|
||||
v-if="isCarIdDifferent && !vinNotFound && !perfectMatchNewVinAlert"
|
||||
alertClass="alert-warning"
|
||||
/>
|
||||
<alert
|
||||
class="my-3"
|
||||
class="my-4"
|
||||
:manualHeadline="NoServiceZipHeader"
|
||||
:manualCopy="NoServiceZipBody"
|
||||
v-model="customAlertData"
|
||||
|
|
@ -89,18 +90,19 @@
|
|||
alertClass="alert-danger"
|
||||
/>
|
||||
<alert
|
||||
class="my-3"
|
||||
class="my-4"
|
||||
v-model="customAlertData"
|
||||
v-if="vinNotFound"
|
||||
alertClass="alert-warning"
|
||||
alertClass="alert-danger"
|
||||
cmsWidgetName="VinNotFound"
|
||||
/>
|
||||
<alert
|
||||
class="my-3"
|
||||
class="my-4"
|
||||
:manualHeadline="PerfectMatchNewVinAlertHeader"
|
||||
:manualCopy="PerfectMatchNewVinAlertBody"
|
||||
v-model="customAlertData"
|
||||
v-if="perfectMatchNewVinAlert"
|
||||
v-if="perfectMatchNewVinAlert && !noServiceZip && !vinNotFound && meta.valid"
|
||||
alertClass="alert-success"
|
||||
cmsWidgetName="PerfectMatchNewVinAlert"
|
||||
/>
|
||||
<funnelFooter
|
||||
cmsWidgetName="FunnelFooterWidget"
|
||||
|
|
@ -139,7 +141,10 @@ import { Form, defineRule } from "vee-validate";
|
|||
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
|
||||
|
||||
// DEFINE VALIDATION RULES
|
||||
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
|
||||
defineRule(
|
||||
"zip-required",
|
||||
required(errorMessages.SERVICE_ZIP_REQUIRED)
|
||||
);
|
||||
defineRule(
|
||||
"email-address-required",
|
||||
required(errorMessages.EMAIL_ADDRESS_REQUIRED)
|
||||
|
|
@ -147,11 +152,14 @@ defineRule(
|
|||
defineRule(
|
||||
"email-address-format",
|
||||
regex(
|
||||
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+).([a-zA-Z]{2,})$/,
|
||||
/^([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-required",
|
||||
required(errorMessages.VIN_REQUIRED)
|
||||
);
|
||||
defineRule(
|
||||
"vin-format",
|
||||
regex(/^[A-HJ-NPR-Z0-9]{17}$/, errorMessages.VIN_FORMAT)
|
||||
|
|
@ -195,9 +203,18 @@ export default {
|
|||
mounted() {
|
||||
this.attachCustomEvents();
|
||||
},
|
||||
watch: {
|
||||
vin() {
|
||||
this.$refs.funnelFooter.updateButtonText(
|
||||
this.getCmsContent("FunnelFooterWidget", "ForwardButtonText")
|
||||
);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
perfectMatchNewVinAlert() {
|
||||
return this.vinPopulatedOnPageLoad && this.vin === this.getVinFromStore();
|
||||
const isVinPerfectMatch = this.vinPopulatedOnPageLoad && this.vin === this.getVinFromStore();
|
||||
this.updateIsCarIdDifferent(isVinPerfectMatch);
|
||||
return isVinPerfectMatch;
|
||||
},
|
||||
MatchedDifferentVehicleAlertHeader(){
|
||||
const text = this.getCmsContent("MatchedDifferentVehicle",
|
||||
|
|
@ -268,8 +285,18 @@ export default {
|
|||
);
|
||||
});
|
||||
},
|
||||
updateIsCarIdDifferent(isVinPerfectMatch){
|
||||
if (isVinPerfectMatch) {
|
||||
this.isCarIdDifferent = false;
|
||||
}
|
||||
},
|
||||
backButtonAction() {
|
||||
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
if (store.getters.vehicle.vin) {
|
||||
this.$router.navigate(this.navigationScenarios.CLICKED_BACK_WITH_VIN, this.$route);
|
||||
}
|
||||
else {
|
||||
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
|
||||
}
|
||||
},
|
||||
async forwardButtonAction() {
|
||||
const zipValidation = this.validateZip(this.zip);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,9 @@ export default {
|
|||
el && el.focus();
|
||||
}
|
||||
},
|
||||
getFooterInfoBoxHeight() {
|
||||
return document.querySelector(".footer #infoBox").offsetHeight;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
storeActions() {
|
||||
|
|
|
|||
|
|
@ -214,13 +214,6 @@ function navigateToUrl(url, optionalQuery = {}) {
|
|||
externalUrl.searchParams.append(queryKey, optionalQuery[queryKey]);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
// TEMP CODE FOR TESTING WITH SPECIFIC EXPERIMENTS //
|
||||
/////////////////////////////////////////////////////
|
||||
if (externalUrl.search.indexOf("corid=") != -1)
|
||||
externalUrl.search = externalUrl.search + '&experiments=CollectEmailOnQuote=CollectEmailOnQuote_V1=YesCollectEmail_TEST1=true,RemoveServiceAreaPage=ServAreaRemoval_V7=ServAreaNoRemove_V7_TEST=true,VINeducationV2=VINeducation_V2=NoShowVINmodalV2_CONTROL=true,ServicePackages=ServicePackages_V1=NoShowPackages_CONTROL=true,PhotoUploadRedesign=PhotoUploadRedesign_V1=CurrentPhotoUpload_CONTROL=true,ScheduleDetailsServiceType=ScheduleBeforeServiceType_V1=ServTypeThenSched_CONTROL=true';
|
||||
///////////// END TEMP CODE /////////////////////////
|
||||
|
||||
window.location.assign(externalUrl);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,10 @@ const routingTable = [
|
|||
{
|
||||
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN,
|
||||
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
|
||||
}
|
||||
],
|
||||
},
|
||||
|
|
@ -139,8 +143,22 @@ const routingTable = [
|
|||
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
|
||||
},
|
||||
],
|
||||
},
|
||||
{fmgPageValue: fmgPageValues.ESTIMATE,
|
||||
},
|
||||
{
|
||||
fmgPageValue: fmgPageValues.ADDRESS_VEHICLES,
|
||||
maps: [
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_BACK,
|
||||
destinationFmgPageValue: fmgPageValues.ADDRESS_LOOKUP,
|
||||
},
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_FORWARD,
|
||||
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
fmgPageValue: fmgPageValues.ESTIMATE,
|
||||
maps: [
|
||||
{
|
||||
scenario: navigationScenarios.CLICKED_BACK,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const getDefaultState = () => {
|
|||
model: null,
|
||||
style: null,
|
||||
carId: null,
|
||||
category: null,
|
||||
category: null,
|
||||
vin: null,
|
||||
imageUrl: null,
|
||||
imageVifNumber: null,
|
||||
|
|
@ -31,6 +31,9 @@ const getDefaultState = () => {
|
|||
},
|
||||
},
|
||||
serviceLocation: {
|
||||
address: null,
|
||||
city: null,
|
||||
state: null,
|
||||
zipCode: null,
|
||||
},
|
||||
customer: {
|
||||
|
|
@ -187,6 +190,10 @@ export const mutations = {
|
|||
state.order.vehicle.style = null;
|
||||
state.order.vehicle.carId = null;
|
||||
state.order.vehicle.category = null;
|
||||
state.order.vehicle.vin = null;
|
||||
state.order.vehicle.imageUrl = null;
|
||||
state.order.vehicle.imageVifNumber = null;
|
||||
state.order.vehicle.imageColor = null;
|
||||
},
|
||||
resetDamageState(state) {
|
||||
state.order.damage.isRepair = null;
|
||||
|
|
@ -227,18 +234,39 @@ export const mutations = {
|
|||
imageUrl: orderInformation.vehicle?.imageUrl,
|
||||
imageVifNumber: orderInformation.vehicle?.imageVifNumber,
|
||||
imageColor: orderInformation.vehicle?.imageVifColor,
|
||||
registration: {
|
||||
firstName: orderInformation.vehicle.registration.firstName,
|
||||
lastName: orderInformation.vehicle.registration.lastName,
|
||||
address: orderInformation.vehicle.registration.streetAddress,
|
||||
city: orderInformation.vehicle.registration.city,
|
||||
state: orderInformation.vehicle.registration.state,
|
||||
zipCode: orderInformation.vehicle.registration.zipCode,
|
||||
licensePlate: orderInformation.vehicle.registration.licensePlateNumber,
|
||||
}
|
||||
});
|
||||
|
||||
state.order.damage.glassToReplace = orderInformation.glassToReplace;
|
||||
state.order.damage.isRepair = orderInformation.isRepair;
|
||||
state.order.damage.numberOfChips = orderInformation.numberOfChips;
|
||||
state.order.damage.glassToReplace = orderInformation.damage.glassToReplace;
|
||||
state.order.damage.isRepair = orderInformation.damage.isRepair;
|
||||
state.order.damage.numberOfChips = orderInformation.damage.numberOfChips;
|
||||
|
||||
state.order.lineItems.glassParts = orderInformation.parts;
|
||||
state.order.accountNumber = orderInformation.accountNumber;
|
||||
state.order.serviceLocation.zipCode = orderInformation.zipCode;
|
||||
state.order.serviceLocation.address = orderInformation.serviceLocation.streetAddress,
|
||||
state.order.serviceLocation.city = orderInformation.serviceLocation.city,
|
||||
state.order.serviceLocation.state = orderInformation.serviceLocation.state,
|
||||
state.order.serviceLocation.zipCode = orderInformation.serviceLocation.zipCode;
|
||||
|
||||
state.order.payment.isInsurance = orderInformation.IsInsuranceOrder;
|
||||
state.order.payment.insuranceCoverage.isVerified = orderInformation?.insuranceInfo.coverageVerified;
|
||||
|
||||
state.order.customer.emailAddress = orderInformation.customer.emailAddress;
|
||||
},
|
||||
|
||||
updateServiceLocationWithVehicleRegistration(state) {
|
||||
state.order.serviceLocation.address = state.order.vehicle.registration.address;
|
||||
state.order.serviceLocation.city = state.order.vehicle.registration.city;
|
||||
state.order.serviceLocation.state = state.order.vehicle.registration.state;
|
||||
state.order.serviceLocation.zipCode = state.order.vehicle.registration.zipCode;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -419,6 +447,9 @@ export const actions = {
|
|||
context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
|
||||
context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId);
|
||||
},
|
||||
updateServiceLocationWithVehicleRegistration(context) {
|
||||
context.commit(storeMutations.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION);
|
||||
},
|
||||
|
||||
logExperimentExposure(context, { userId, sessionKey, pageName, universeName }) {
|
||||
return globalMethods.callHttpClient({
|
||||
|
|
@ -502,6 +533,7 @@ export const actions = {
|
|||
saveOrder(context) {
|
||||
const vehicle = context.getters.vehicle;
|
||||
const damage = context.getters.damage;
|
||||
const order = context.state.order;
|
||||
|
||||
return globalMethods.callHttpClient({
|
||||
method: endpoints.SaveOrder.method,
|
||||
|
|
@ -513,14 +545,34 @@ export const actions = {
|
|||
make: vehicle.make,
|
||||
model: vehicle.model,
|
||||
style: vehicle.style,
|
||||
vin: vehicle.vin
|
||||
vin: vehicle.vin,
|
||||
registration: {
|
||||
firstName: vehicle.registration.firstName,
|
||||
lastName: vehicle.registration.lastName,
|
||||
streetAddress: vehicle.registration.address,
|
||||
city: vehicle.registration.city,
|
||||
state: vehicle.registration.state,
|
||||
zipCode: vehicle.registration.zipCode,
|
||||
licensePlateNumber: vehicle.registration.licensePlate,
|
||||
},
|
||||
},
|
||||
numberOfChips: damage.numberOfChips,
|
||||
zipCode: 43215, // TODO CSR-416, should not be hardcoded (state.order.serviceLocation.zipCode)
|
||||
glassToReplace: damage.glassToReplace,
|
||||
referralNumber: context.state.order.referralNumber,
|
||||
referralDate: context.state.order.referralDate,
|
||||
accountNumber: context.state.order.accountNumber
|
||||
damage: {
|
||||
numberOfChips: damage.numberOfChips,
|
||||
glassToReplace: damage.glassToReplace,
|
||||
isRepair: damage.isRepair
|
||||
},
|
||||
customer: {
|
||||
emailAddress: order.customer.emailAddress,
|
||||
},
|
||||
serviceLocation: {
|
||||
streetAddress: order.serviceLocation.address,
|
||||
city: order.serviceLocation.city,
|
||||
state: order.serviceLocation.state,
|
||||
zipCode: order.serviceLocation.zipCode
|
||||
},
|
||||
referralNumber: order.referralNumber,
|
||||
referralDate: order.referralDate,
|
||||
accountNumber: order.accountNumber
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -215,14 +215,19 @@ describe("Mutations", () => {
|
|||
model: "ILX",
|
||||
style: "4 DOOR SEDAN",
|
||||
carId: "C0000001",
|
||||
category: "CAR"
|
||||
category: "CAR",
|
||||
registration: {}
|
||||
},
|
||||
damage: {
|
||||
glassToReplace: ["Windshield"],
|
||||
isRepair: false,
|
||||
numberOfChips: 0,
|
||||
},
|
||||
glassToReplace: ["Windshield"],
|
||||
isRepair: false,
|
||||
numberOfChips: 0,
|
||||
parts: [],
|
||||
accountNumber: "123456789",
|
||||
insuranceInfo: {}
|
||||
insuranceInfo: {},
|
||||
serviceLocation: {},
|
||||
customer: {}
|
||||
});
|
||||
|
||||
// Assert
|
||||
|
|
@ -562,11 +567,16 @@ describe("Actions", () => {
|
|||
const context = state;
|
||||
|
||||
context.getters = {
|
||||
vehicle: {},
|
||||
vehicle: {
|
||||
registration: {}
|
||||
},
|
||||
damage: {},
|
||||
};
|
||||
context.state = {
|
||||
order: {}
|
||||
order: {
|
||||
serviceLocation: {},
|
||||
customer: {}
|
||||
}
|
||||
};
|
||||
|
||||
globalMethods.callHttpClient.mockImplementation(() => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
html {
|
||||
.has-error {
|
||||
&.list-button,
|
||||
&.list-card,
|
||||
&.list-card,
|
||||
&.list-card.list-button {
|
||||
border: none;
|
||||
color: $red;
|
||||
input[type=checkbox]:focus + label,
|
||||
input[type=radio]:focus + label {
|
||||
|
|
@ -13,15 +14,16 @@ html {
|
|||
}
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 0px 4px $red-200;
|
||||
border-radius: 10px;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
label {
|
||||
border: 1px solid $red;
|
||||
border: 1px solid $red;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
label:hover {
|
||||
box-shadow: 0px 0px 0px 4px $red-200;
|
||||
border-radius: 10px;
|
||||
border: 1px solid $red;
|
||||
border: 1px solid $red;
|
||||
}
|
||||
}
|
||||
&.list-button-horizontal {
|
||||
|
|
@ -51,12 +53,21 @@ html {
|
|||
}
|
||||
&.textbox-question,
|
||||
&.dropdown-question {
|
||||
input:hover {
|
||||
box-shadow: 0px 0px 0px 4px $red-200;
|
||||
border-radius: .5rem;
|
||||
border: 1px solid $red;
|
||||
}
|
||||
input:focus {
|
||||
box-shadow: 0 0 0 2.5px $red;
|
||||
}
|
||||
p {
|
||||
color: $red;
|
||||
}
|
||||
input,
|
||||
select {
|
||||
border: 1px solid $red;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 0 0 1px $red;
|
||||
&:focus {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
|
@ -114,8 +125,6 @@ html {
|
|||
color: $red;
|
||||
font-size: .875rem;
|
||||
font-weight: 500;
|
||||
height: 1.5rem;
|
||||
margin-top: .25rem !important;
|
||||
}
|
||||
|
||||
.form-test-invalid {
|
||||
|
|
@ -129,7 +138,6 @@ html {
|
|||
&.btn.btn-primary:hover,
|
||||
&.btn.btn-primary:focus,
|
||||
&.btn.btn-primary:focus-visible {
|
||||
color: $gray !important;
|
||||
background: $gray-200;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ describe("alert.vue", () => {
|
|||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn()
|
||||
getCmsContent: jest.fn(),
|
||||
getFooterInfoBoxHeight: jest.fn(()=> 80),
|
||||
}
|
||||
}
|
||||
|
|
@ -5,15 +5,17 @@
|
|||
:class="[isDismissible ? 'alert-dismissible' : '', this.alertClass]"
|
||||
>
|
||||
<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">
|
||||
<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" v-html="copy"></span>
|
||||
</template>
|
||||
</p>
|
||||
<p v-else class="m-0 text-body" v-html="alertCopy"></p>
|
||||
<template v-for="paragraph in splitAlertCopyForParagraphTag" :key="paragraph">
|
||||
<p class="m-0 text-body small" v-if="!doesCopyContainRouterLink(paragraph)" v-html="paragraph"></p>
|
||||
<p class="m-0 text-body small" v-else>
|
||||
<template v-for="copy in splitCopyForRouterLink(paragraph)" :key="copy">
|
||||
<span v-if="!doesCopyContainRouterLink(copy)" v-html="copy"></span>
|
||||
<span v-else>
|
||||
<router-link :to="{query: {fmgPage: `${getRouterLinkRouteFromCopy(copy)}`}, name: 'root'}">{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link>
|
||||
</span>
|
||||
</template>
|
||||
</p>
|
||||
</template>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close p-2"
|
||||
|
|
@ -41,7 +43,7 @@ export default {
|
|||
isDismissible: Boolean,
|
||||
/*
|
||||
alertClass class names:
|
||||
alert-sucess (green)
|
||||
alert-success (green)
|
||||
alert-danger (red)
|
||||
alert-warning (yellow)
|
||||
alert-info (blue)
|
||||
|
|
@ -50,6 +52,10 @@ export default {
|
|||
cmsWidgetName: String,
|
||||
manualHeadline: String,
|
||||
manualCopy: String,
|
||||
shouldScrollToOnMount: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
alertHeadline(){
|
||||
|
|
@ -58,11 +64,60 @@ export default {
|
|||
alertCopy(){
|
||||
return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'BodyText') : this.manualCopy;
|
||||
},
|
||||
splitAlertCopyForLink(){
|
||||
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
|
||||
return this.alertCopy.split(/{(.*?)}/g);
|
||||
}
|
||||
splitAlertCopyForParagraphTag(){
|
||||
// splits the alertCopy on <p ... > (with or without attributes) and </p>
|
||||
// filter removes empty strings that are a result of string.split with regex
|
||||
return this.alertCopy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter(paragraph => paragraph !== "");
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
doesCopyContainRouterLink(copy) {
|
||||
return copy.includes('routerLink:');
|
||||
},
|
||||
splitCopyForRouterLink(copy){
|
||||
// splits copy on { ... } such as {routerlink: ...}
|
||||
return copy.split(/{(.*?)}/g);
|
||||
},
|
||||
getRouterLinkRouteFromCopy(copy){
|
||||
// sample input: {routerLink:estimate,provide your VIN}
|
||||
// first split would return 'estimate,provide your VIN'
|
||||
// second split would return 'estimate'
|
||||
return copy.split(':')[1].split(',')[0];
|
||||
},
|
||||
getRouterLinkDisplayTextFromCopy(copy){
|
||||
// sample input: {routerLink:estimate,provide your VIN}
|
||||
// first split would return 'estimate,provide your VIN'
|
||||
// second split would return 'provide your VIN'
|
||||
return copy.split(':')[1].split(',')[1];
|
||||
},
|
||||
ensureAlertIsInViewPort() {
|
||||
if (this.shouldScrollToOnMount && this.$el.style.display != 'none') {
|
||||
var footerHeight = this.getFooterInfoBoxHeight();
|
||||
if (!this.isAlertInViewport(footerHeight)) {
|
||||
this.scrollContainerToAlert(footerHeight);
|
||||
}
|
||||
}
|
||||
},
|
||||
isAlertInViewport(footerHeight) {
|
||||
const rect = this.$el.getBoundingClientRect();
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
// remove footerHeight from window height to avoid items being hidden behind footer
|
||||
rect.bottom <= (window.innerHeight - footerHeight || document.documentElement.clientHeight - footerHeight)
|
||||
);
|
||||
},
|
||||
scrollContainerToAlert(footerHeight) {
|
||||
// alert position on page + height of alert + footer height
|
||||
var scrollToHeight = this.$el.scrollHeight + this.$el.offsetHeight + footerHeight;
|
||||
// find the div wrapped by the form element - this is the scrollable container
|
||||
// should be a more future-proof selector in case of CSS class changes
|
||||
var pageContainerScrollable = document.querySelector('form > div');
|
||||
pageContainerScrollable.scrollTo(0, scrollToHeight);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.ensureAlertIsInViewPort();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
@ -131,10 +186,6 @@ export default {
|
|||
& p {
|
||||
font-size: .875rem;
|
||||
margin-bottom: 0.25rem !important;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
class="list-group list-button-horizontal d-flex flex-column w-100 mb-2"
|
||||
class="list-group list-button-horizontal d-flex flex-column w-100"
|
||||
:class="[(errors.length > 0 || hasError) ? 'has-error' : '']"
|
||||
@keyup.space="triggerButton()"
|
||||
@keyup.up="handleKeyupArrow()"
|
||||
|
|
|
|||
Loading…
Reference in a new issue