Merge remote-tracking branch 'origin/develop' into feature/CSR-731

This commit is contained in:
Scott Kiener 2022-11-14 16:11:22 -05:00
commit c114c75e02
31 changed files with 1638 additions and 1435 deletions

View file

@ -23,4 +23,5 @@ export default {
@import "@/styles/common-typography-styles.scss";
@import "@/styles/common-error-styles.scss";
@import "@/styles/common-animations.scss";
@import "@/styles/shared-input-button-styles.scss";
</style>

View file

@ -228,7 +228,7 @@ describe("baseInputButton.vue", () => {
expect(wrapper.emitted()).not.toHaveProperty("update:modelValue");
});
test("focus and click enter on a checkbox => update:modelValue is emitted with correct value", async () => {
test("focus and click enter on a checkbox => nothing should happen", async () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
@ -243,9 +243,10 @@ describe("baseInputButton.vue", () => {
// Act
await input.trigger("keypress", { key: "enter" });
console.log(wrapper.emitted()["update:modelValue"]);
// Assert
expect(wrapper.emitted()["update:modelValue"][0][0]).toEqual(["Hi"]);
expect(wrapper.emitted()["update:modelValue"]).toBe(undefined);
});
});
@ -319,7 +320,7 @@ describe("baseInputButton.vue", () => {
expect(wrapper.vm.handlePushClickEventToGACheck).not.toHaveBeenCalled();
});
test("eventType === eventTypes.ENTER => call handleClick and handlePushClickEventToGACheck", () => {
test("eventType === eventTypes.ENTER => do nothing", () => {
// Arrange
const { wrapper } = setupMocks({
mockData: {
@ -336,11 +337,9 @@ describe("baseInputButton.vue", () => {
wrapper.vm.handleEventAction("enter", { myEvent: "test" });
// Assert
expect(wrapper.vm.handleClick).toHaveBeenCalledWith({
myEvent: "test",
});
expect(wrapper.vm.handlePushClickEventToGACheck).toHaveBeenCalledWith("click");
expect(wrapper.vm.handleClick).not.toHaveBeenCalled();
expect(wrapper.vm.handleSelectionChange).not.toHaveBeenCalled();
expect(wrapper.vm.handlePushClickEventToGACheck).not.toHaveBeenCalled();
});
test("eventType === eventTypes.CHANGE => call handleClick and handlePushClickEventToGACheck", () => {

View file

@ -1,6 +1,6 @@
<template>
<label
:class="[buttonWrapperClasses, { 'has-error': errors.length > 0 }]"
:class="[buttonWrapperClasses, { 'has-error': errors.length > 0, selected: isChecked }]"
:for="buttonId"
@focusin="handleFocus"
@focusout="handleBlur"
@ -53,7 +53,6 @@ export default {
handleEventAction(eventType, e) {
if (this.isMultiSelect) {
switch (eventType) {
case this.eventTypes.ENTER:
case this.eventTypes.CHANGE:
this.handleClick(e);
this.handlePushClickEventToGACheck(this.eventTypes.CLICK);

View file

@ -1,7 +1,5 @@
<template>
<div
class="dropdown-question"
:class="(errors && errors.length) || hasError ? 'has-error' : ''">
<div class="dropdown-question" :class="errorMessage || hasError ? 'has-error' : ''">
<label
:for="inputId"
:aria-label="questionText"
@ -43,6 +41,7 @@ export default {
disableAutoFill: Boolean,
validationRules: String,
cmsWidgetName: String,
hasError: Boolean,
},
setup(props) {
const propsClone = Object.assign({}, props);

View file

@ -13,19 +13,19 @@
<div class="col">
<div class="text-container slide">
<p>
Assessing your damage
Finding shops near you
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Finding your glass
Looking for dates
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Generating your quote
Searching for times
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>

View file

@ -33,14 +33,6 @@ describe("modal.vue", () => {
});
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["BodyText"]));
});
it("Should display footer text when FooterText is defined in the CMS", async () => {
// Act
const wrapper = shallowMount(Modal, {
mixins: [mockMixin],
});
expect(wrapper.html()).toEqual(expect.stringContaining(mockCmsContent["FooterText"]));
});
});
const mockMixin = {

View file

@ -16,16 +16,19 @@
aria-label="Close"></button>
</div>
<div class="modal-body ps-4 pe-4 pt-5 pb-4">
<img :src="this.ModalImage" class="w-100 mb-4" alt="" />
<img :src="this.ModalImage" class="mw-100 d-flex mx-auto mb-4" alt="" />
<h5 class="mb-4" v-html="this.ModalHeadline"></h5>
<p class="fw-bold mb-2 subheader-text" v-html="this.ModalSubheadertext"></p>
<p class="mb-0" v-html="this.ModalBodyText"></p>
</div>
<div class="modal-footer px-5 py-4">
<button
class="btn btn-primary w-100"
data-bs-dismiss="modal"
v-html="this.ModalCloseButtonText"></button>
<buttonMain
class="w-100"
ref="buttonMain"
isPrimary
:buttonText="ModalCloseButtonText"
@click-event="buttonClick"
data-bs-dismiss="modal" />
</div>
</div>
</div>
@ -33,6 +36,8 @@
</template>
<script>
import buttonMain from "@/ux-components/button-main/button-main";
export default {
name: "modal",
props: {
@ -55,6 +60,9 @@ export default {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
},
components: {
buttonMain,
},
};
</script>
@ -104,7 +112,6 @@ export default {
box-shadow: 0px -1px 0px rgba(179, 180, 181, 0.3);
button {
margin: 0;
height: 3rem;
}
}
}

View file

@ -7,7 +7,7 @@
:class="q.questionSequence === currentQuestionNum && 'current-question'"
:questionText="q.questionText"
:answers="q.answers"
:groupName="`question-${glassIndex}-${q.questionSequence}`"
:groupName="`question-${index}-${q.questionSequence}`"
:modelValue="q.answerSelected"
@update:modelValue="handleAnswer(q, $event)"
isRequired
@ -31,8 +31,9 @@ export default {
props: {
questionData: Object,
validationRules: String,
modelValue: Array,
glassIndex: Number,
modelValue: Object,
index: Number,
answerKey: String,
},
async created() {
// do a test validation check upon create to prevent out of sync / incorrect valid states
@ -65,7 +66,7 @@ export default {
}),
answerSelected: q.answerSelected || "",
};
if (!q.suppressQuestion) {
if (!q.suppressThisQuestion) {
this.questions.push(question);
}
});
@ -81,7 +82,6 @@ export default {
},
methods: {
handleAnswer(question, returnedAnswer) {
question.answerSelected = returnedAnswer;
/*
returnedAnswer example format:
{
@ -90,6 +90,8 @@ export default {
"buttonId": "Driver-Front-1-1|answer|DD11132|Yes"
}
*/
question.answerSelected = returnedAnswer;
const isQuestionChainComplete = this.getQuestionChainAnswerIfComplete(returnedAnswer);
if (isQuestionChainComplete) {
@ -153,7 +155,7 @@ export default {
return {
answerResult: questionAnswer,
answeredQuestions: answeredQuestions,
glassIndex: this.glassIndex,
index: this.index,
};
}
},

View file

@ -0,0 +1,106 @@
<template>
<div :class="`page-container-grouped-styles questions-page`">
<loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<alert
ref="alertFewMoreQuestions"
cmsWidgetName="alertWidget"
class="my-5"
alertClass="alert-warning"
:manualHeadline="alertFewMoreQuestionsHeader"
:manualCopy="alertFewMoreQuestionsCopy"
v-bind:isDismissible="false" />
<div v-for="(questionsDatum, i) in questionsData" :key="questionsDatum.key">
<questionChain
ref="questionChain"
v-model="selectedAnswers[questionsDatum.answerKey]"
:questionData="questionsDatum.questions"
:index="i"
v-if="showThisQuestionChain(questionsDatum, i)"
:answerKey="questionsDatum.answerKey"
:validationRules="validationRules" />
</div>
<funnel-footer
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!isMetaValid"
@back-clicked="backButtonAction"
@ForwardClicked="handleForwardButtonAction" />
</div>
</div>
</template>
<script>
// Components
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import alert from "@/ux-components/alert/alert";
import questionChain from "@/common-components/question-chain/question-chain";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import loadingModal from "@/common-components/loading-modal/loading-modal.vue";
export default {
name: "questions-page",
props: {
isMetaValid: Boolean,
alertFewMoreQuestionsHeader: String,
alertFewMoreQuestionsCopy: String,
questionsData: Array,
validationRules: String,
modelValue: Array,
index: Number,
},
computed: {
selectedAnswers: {
get() {
return this.modelValue;
},
set(newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
methods: {
showThisQuestionChain(glass, i) {
if (!glass.questions || glass.questions?.length < 1 || glass.isSuppressedPart) {
return false;
} // return false if no questions or if suppressed
return this.index === i || glass.answerData?.answerResult?.length > 0;
},
handleForwardButtonAction() {
this.$emit("forwardButtonAction");
},
handleBackButtonAction() {
this.$emit("backButtonAction");
},
showLoadingModal() {
this.$refs.loadingModal.showModal();
},
},
components: {
funnelHeader,
vehicleBanner,
alert,
questionChain,
funnelSubHeader,
funnelFooter,
loadingModal,
},
};
</script>
<style lang="scss">
.questions-page {
.question-text {
margin-bottom: 0.5rem;
span {
text-align: left;
}
}
}
</style>

View file

@ -33,6 +33,46 @@ describe("textboxQuestion.vue", () => {
expect(input.exists()).toBe(true);
});
it("Should return rounded-pill class", async () => {
// Act
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
},
},
mixins: [mockMixin],
propsData: {
cornerStyle: "rounded",
},
});
// Assert
const paragraph = wrapper.find("input");
expect(paragraph.attributes("class")).toContain("rounded-pill");
});
it.only("Should return form-control class", async () => {
// Act
const wrapper = shallowMount(textboxQuestion, {
global: {
directives: {
maska: maska,
},
},
mixins: [mockMixin],
propsData: {
questionAlignment: "text",
},
});
// Assert
const paragraph = wrapper.find("input");
expect(paragraph.attributes("class")).toContain("form-control");
});
it("Should render the 'questionText' data value as the label text when disableAutoFill is false.", async () => {
// Arrange
const wrapper = shallowMount(textboxQuestion, {

View file

@ -5,25 +5,33 @@
:aria-label="questionText"
class="form-label"
v-html="labelText"></label>
<input
v-model.trim="value"
v-maska="mask"
:type="type"
class="form-control"
:ref="inputId"
:id="inputId"
:name="inputId"
:placeholder="placeholderText"
:aria-disabled="isDisabled"
:disabled="isDisabled"
:aria-required="isRequired"
:aria-label="questionText"
:class="[hasIcon ? 'has-icon' : '', iconRight ? 'icon-right' : '']"
:validationRules="validationRules"
@change="handleChange"
@blur="handleChange"
:maxlength="maxLength ? maxLength : '999'"
@focus="$emit('focus', $event.target.value)" />
<div class="input-wrapper" :class="[includeSearchIcon ? 'has-search-icon' : '']">
<input
class="form-control"
v-model.trim="value"
v-maska="mask"
:type="type"
:ref="inputId"
:id="inputId"
:name="inputId"
:placeholder="placeholderText"
:aria-disabled="isDisabled"
:disabled="isDisabled"
:aria-required="isRequired"
:aria-label="questionText"
:class="[
hasIcon ? 'has-icon' : '',
iconRight ? 'icon-right' : '',
questionAlignment === 'center' ? 'text-center' : '',
cornerStyle === 'rounded' ? 'rounded-pill' : '',
]"
:validationRules="validationRules"
@change="handleChange"
@blur="handleChange"
:maxlength="maxLength ? maxLength : '999'"
@focus="$emit('focus', $event.target.value)" />
<button v-if="includeSearchIcon" type="submit" aria-label="Search button" />
</div>
<div v-show="errorMessage" class="row my-2 form-test-error">
<span class="d-inline-flex mt-0" role="alert">{{ errorMessage }}</span>
</div>
@ -58,6 +66,9 @@ export default {
validationRules: String,
cmsWidgetName: String,
maxLength: String,
questionAlignment: String, // Left or center. Left is default.
cornerStyle: String, // Rounded or square. Square is default.
includeSearchIcon: Boolean,
},
setup(props) {
const propsClone = Object.assign({}, props);
@ -148,6 +159,30 @@ export default {
color: $black;
font-weight: 500;
}
.input-wrapper {
position: relative;
&.has-search-icon {
input[type="text"] {
border-radius: 50rem;
}
button[type="submit"] {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M15.7817 14.7328L11.8252 10.7762C12.8833 9.45005 13.3936 7.76911 13.2513 6.07849C13.1091 4.38788 12.325 2.81587 11.0601 1.6852C9.79515 0.554524 8.14538 -0.0490261 6.44946 -0.00154744C4.75353 0.0459312 3.14012 0.740836 1.94045 1.94051C0.740775 3.14018 0.0458701 4.75359 -0.00160848 6.44952C-0.0490871 8.14545 0.554463 9.79521 1.68514 11.0601C2.81581 12.325 4.38782 13.1091 6.07843 13.2514C7.76905 13.3937 9.44999 12.8834 10.7762 11.8252L14.7349 15.7839C14.8044 15.8527 14.8869 15.907 14.9774 15.9439C15.068 15.9808 15.165 15.9995 15.2628 15.9989C15.3606 15.9983 15.4573 15.9784 15.5475 15.9405C15.6376 15.9025 15.7194 15.8471 15.7881 15.7776C15.8568 15.708 15.9112 15.6256 15.9481 15.535C15.985 15.4444 16.0036 15.3474 16.0031 15.2496C16.0025 15.1518 15.9826 15.0551 15.9446 14.965C15.9067 14.8748 15.8513 14.7931 15.7817 14.7243V14.7328ZM6.63737 11.7913C5.61803 11.7913 4.62157 11.4891 3.77402 10.9228C2.92646 10.3564 2.26587 9.5515 1.87578 8.60975C1.4857 7.668 1.38363 6.63172 1.5825 5.63196C1.78136 4.6322 2.27222 3.71386 2.99301 2.99307C3.7138 2.27229 4.63214 1.78142 5.6319 1.58256C6.63166 1.38369 7.66793 1.48576 8.60969 1.87585C9.55144 2.26593 10.3564 2.92652 10.9227 3.77408C11.489 4.62163 11.7913 5.61809 11.7913 6.63743C11.7896 8.00382 11.2461 9.31376 10.2799 10.2799C9.3137 11.2461 8.00376 11.7897 6.63737 11.7913Z' fill='%231574A1'/%3E%3C/svg%3E%0A");
background-repeat: no-repeat;
background-position: center;
border-radius: 0 50rem 50rem 0;
background-color: $blue-100;
width: 2.75rem;
height: 100%;
border: 1px solid $gray-500;
border-left: none;
display: flex;
}
}
}
input {
&.has-icon {
background-image: url(~@/assets/img/icons/location-pin.svg);

View file

@ -76,7 +76,7 @@ const endpoints = {
},
LoadSession: {
url: "/order/api/v1/order/load-session",
method: "POST",
method: "GET",
},
ValidateZip: {
url: "/location/api/v1/location/zip",

View file

@ -21,6 +21,7 @@ export function updateOrCreateFunnelCookie() {
ReferralParentAccountNumber: store.getters.order.accountNumber,
HasDelayedClaimRegistration: wasClaimRegistrationDelayed,
SuppressConceptFunnel: shouldSuppressConceptFunnel,
SavedSessionId: store.getters.applicationUser.savedSessionId,
});
}

View file

@ -17,12 +17,8 @@ import { storeMutations } from "@/constants/store-mutations";
export async function loadSessionIfPresent() {
const funnelCookie = getFunnelCookie();
// Do nothing if there is no cookie, correlation id, or referral number.
if (
funnelCookie == null ||
funnelCookie.ReferralCorrelationId == null ||
!funnelCookie.ReferralNumber
) {
// Do nothing if there is no cookie or session to use for loading.
if (funnelCookie == null || funnelCookie.SavedSessionId == null) {
return null;
}
@ -33,15 +29,8 @@ export async function loadSessionIfPresent() {
return null;
}
// Load referral if there is a cookie, and it doesn't indicate it needs a state reset.
return (
await loadSession(
funnelCookie.ReferralNumber,
funnelCookie.ReferralDate,
funnelCookie.ReferralCorrelationId,
funnelCookie.ReferralParentAccountNumber
)
).data;
// Loads session including referral if there is a cookie, and it doesn't indicate it needs a state reset.
return (await loadSession(funnelCookie.SavedSessionId)).data;
}
/*
@ -72,16 +61,13 @@ export async function saveSession() {
Calls API to load session given the referral number, referralDate, and referralCorrelationId
and returns the response.
*/
async function loadSession(referralNumber, referralDate, referralCorrelationId, accountNumber) {
async function loadSession(savedSessionId) {
// await the saveSessionPromise in the store to make sure we're loading up to date information
await store.getters.applicationUser.saveSessionPromise;
const response = await baseMixin.methods.dispatchStoreAction(
storeActions.LOAD_SESSION,
{
referralNumber: referralNumber.toString(),
referralDate: referralDate,
referralCorrelationId: referralCorrelationId,
accountNumber: accountNumber?.toString(),
savedSessionId: savedSessionId?.toString(),
},
false
);

View file

@ -24,6 +24,7 @@ describe("loadSessionIfPresent", () => {
ShouldResetState: testShouldResetState,
ReferralCorrelationId: "xxx",
ReferralNumber: "12345",
SavedSessionId: "aa115a05-e268-4eb4-a095-1d54b44f4a99",
};
document.cookie = `${cookieNames.FUNNEL_SESSION_INFO}=${JSON.stringify(
@ -46,6 +47,7 @@ describe("loadSessionIfPresent", () => {
ShouldResetState: true,
ReferralCorrelationId: "xxx-xxx-xxx",
ReferralNumber: "12345",
SavedSessionId: "aa115a05-e268-4eb4-a095-1d54b44f4a99",
});
const mockData = {
@ -103,6 +105,7 @@ describe("loadSessionIfPresent", () => {
ReferralNumber: 123456,
ReferralCorrelationId: "yyy-yyy-yyyy",
ReferralDate: new Date(),
SavedSessionId: "aa115a05-e268-4eb4-a095-1d54b44f4a99",
});
const mockData = {

View file

@ -63,9 +63,7 @@ export default {
},
selectedVehicle() {
// this computed is only needed for the computed differentVehicleAlertBody text above
return this.vehicles.find(
({ vin }) => vin === this.selectedVehicleVin[this.selectedVehicleVin.length - 1]
);
return this.vehicles.find(({ vin }) => vin === this.selectedVehicleVin);
},
},
components: {

View file

@ -1,61 +1,32 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles capability-questions">
<loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<alert
ref="alertFewMoreQuestions"
class="my-5"
alertClass="alert-warning"
:manualHeadline="AlertFewMoreQuestionsHeader"
:manualCopy="AlertFewMoreQuestionsCopy"
v-bind:isDismissible="false" />
<div v-for="(glass, i) in capabilityQuestionsData" :key="glass.key">
<questionChain
ref="questionChain"
v-model="selectedAnswers[glass.glassLocation + '-' + glass.glassName]"
:questionData="glass.capabilityQuestions"
:glassIndex="i"
v-if="showThisQuestionChain(glass, i)"
validationRules="questions-required" />
</div>
<funnel-footer
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
<questions-page-layout
ref="questionsPageLayout"
:isMetaValid="meta.valid"
:alertFewMoreQuestionsHeader="AlertFewMoreQuestionsHeader"
:alertFewMoreQuestionsCopy="AlertFewMoreQuestionsCopy"
:questionsData="questionsData"
:validationRules="questions - required"
v-model="selectedAnswers"
@forwardButtonAction="forwardButtonAction"
@backButtonAction="backButtonAction"
:index="currentGlassIndex" />
</Form>
</template>
<script>
// Components
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import alert from "@/ux-components/alert/alert";
import questionChain from "@/common-components/question-chain/question-chain";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import loadingModal from "@/common-components/loading-modal/loading-modal.vue";
import questionsPageLayout from "@/common-components/questions-page-layout/questions-page-layout";
// Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
import { storeActions } from "@/constants/store-actions";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
@ -84,10 +55,9 @@ export default {
},
data() {
return {
capabilityQuestionsData: store.getters.pageData(fmgPageValues.CAPABILITY_QUESTIONS)
.partsOrQuestions,
questionsData: [],
selectedAnswers: {},
currentQuestionChainIndex: 0,
currentGlassIndex: 0,
};
},
computed: {
@ -97,14 +67,6 @@ export default {
AlertFewMoreQuestionsCopy() {
return this.getCmsContent("AdditionalPartsQuestionsAlert", "BodyText");
},
windshieldPart() {
return this.pageData.partsOrQuestions.find(
(x) => x.glassLocation === damageLocationsSelected.WINDSHIELD
);
},
windshieldPartInfo() {
return this.windshieldPart.parts[0];
},
pageData() {
return this.$store.getters.pageData(fmgPageValues.CAPABILITY_QUESTIONS);
},
@ -113,30 +75,32 @@ export default {
// are there alreadyAnsweredQuestions?
const alreadyAnsweredQuestions = store.getters.damage.capabilityQuestionAnswers;
this.capabilityQuestionsData = this.pageData.partsOrQuestions
this.questionsData = this.pageData.partsOrQuestions
.filter((x) => x.capabilityQuestions)
.map((glass, i) => {
.map((glass, index) => {
// NOTE: questions for property "questions" can differ between layouts
glass.questions = glass.capabilityQuestions;
glass.answerKey = glass.glassLocation + "-" + glass.glassName;
// reset selectedAnswers for this glass
this.selectedAnswers[glass.key] = [];
// pass in: glass, i, alreadyAnsweredQuestions
return this.setupInitialData(glass, i, alreadyAnsweredQuestions);
this.selectedAnswers[glass.answerKey] = [];
const updatedGlass = this.setupInitialData(glass, index, alreadyAnsweredQuestions);
// Set up watch for each set of glass questions
this.$watch(
"selectedAnswers." + glass.answerKey,
(newValue) => {
if (newValue && Object.keys(newValue).length > 0) {
this.handleAnswerUpdates(newValue, glass.answerKey);
}
},
{ deep: true }
);
return updatedGlass;
});
},
methods: {
showThisQuestionChain(glass, i) {
if (
!glass.capabilityQuestions ||
glass.capabilityQuestions?.length < 1 ||
glass.isSuppressedPart
) {
return false;
} // return false if no capabilityQuestions or if suppressed
return (
this.currentQuestionChainIndex === i || glass.answerData?.answerResult?.length > 0
);
},
arePagePrerequisitesValid() {
const capabilityQuestionsPageData = store.getters.pageData(
fmgPageValues.CAPABILITY_QUESTIONS
@ -146,10 +110,17 @@ export default {
);
},
async forwardButtonAction() {
const capabilityQuestionsAnswersArray = this.capabilityQuestionsData.map((glass) => {
const selectedAnswerResult2 = this.getCorrespondingAnswerResult2(
glass.answerData.answerResult
);
const questionAnswersArray = this.questionsData.map((glass) => {
// get answerResult2 of returned answer
let selectedAnswerResult2;
glass.questions.forEach((q) => {
const idx = q.answers.findIndex(
(a) => a.answerResult === glass.answerData.answerResult
);
if (idx !== -1) {
selectedAnswerResult2 = q.answers[idx].answerResult2;
}
});
return {
glassLocation: glass.glassLocation,
@ -163,361 +134,39 @@ export default {
});
// clear out answerData for future page loads; must occur prior to store save
this.capabilityQuestionsData.forEach((glass) => {
this.questionsData.forEach((glass) => {
glass.answerData = {};
});
// save to vuex store as order.damage.capabilityQuestionAnswers (array)
await this.dispatchStoreAction(
this.storeActions.SAVE_CAPABILITY_QUESTION_ANSWERS,
capabilityQuestionsAnswersArray,
questionAnswersArray,
false
);
// get parts from the capabilityQuestionAnswers
let partsOrQuestions = this.pageData.partsOrQuestions;
for (let answer of capabilityQuestionsAnswersArray) {
const correspondingPart = partsOrQuestions.find(
(partOrQuestion) => partOrQuestion.glassLocation === answer.glassLocation
);
const partsOrQuestions = this.pageData.partsOrQuestions;
for (let answer of questionAnswersArray) {
const partFromCapabilityQuestionAnswer = (
await this.dispatchStoreAction(
storeActions.GET_PART_FROM_CAPABILITY_QUESTION_ANSWER,
this.storeActions.GET_PART_FROM_CAPABILITY_QUESTION_ANSWER,
answer.glassLocation,
false
)
).data;
partsOrQuestions.find(
(partOrQuestion) => partOrQuestion.location === answer.location
(partOrQuestion) => partOrQuestion.glassLocation === answer.glassLocation
).parts = partFromCapabilityQuestionAnswer;
}
this.navigateForward(partsOrQuestions);
},
handleAnswerUpdates(answer) {
// only runs when all questions in a question-chain have been answered
// when selectedAnswers updates, user has completed this part's question chain and has a final answer
// (does not get run for each invididual question's answer, only when
// all relevant questions for the current part have been answered)
/*
answer example format:
{
"answerResult": "DD11132",
"answeredQuestions": [
{
"questionText": "Is your Grand Cherokee the Laredo model?",
"selectedAnswerText": "Yes",
"questionNum": 1,
}
],
"glassIndex": 0
}
*/
// collect a list of the answered questions' numbers, needed later below
const answeredQuestionIndexes = [];
const foundDuplicateQuestions = [];
// if user has answered a question differently than anything that was preloaded,
// we need to clear out any preloaded answers
this.selectedAnswers = {};
// loop through every answered question on the currently answered glass part
answer.answeredQuestions?.forEach((aq) => {
// keep track of this question number
answeredQuestionIndexes.push(aq.questionNum);
const answeredQuestionText = aq.questionText.toUpperCase();
const answeredQuestionAnswer = aq.selectedAnswerText.toUpperCase();
// HANDLE DUPLICATE QUESTIONS
// loop through all glass parts data
this.capabilityQuestionsData.forEach((glass, gpIndex) => {
// only look for duplicates forward... to parts that follow after the currently being answered part
if (gpIndex > answer.glass) {
let suppressUntil;
// reset this glass part, in case user is changing their previous answers
glass.answerData = null;
glass.isSuppressedPart = null;
// loop through this glass part's part questions, looking for a questionText match
glass.capabilityQuestions.forEach((pq, pqIndex) => {
// clear out any previously set answers
pq.answerSelected = null;
// clear or set suppressQuestion property for each question
if (suppressUntil) {
// if suppressUntil has been set, then suppress this question if before it
if (pqIndex + 1 < suppressUntil) {
pq.suppressQuestion = true;
} else {
pq.suppressQuestion = null;
}
} else {
pq.suppressQuestion = null;
}
// if these match then we have a duplicate question
if (pq.questionText.toUpperCase() === answeredQuestionText) {
const thisAnsweredCapabilityQuestion =
glass.capabilityQuestions[pqIndex];
let matchedAnswer;
let rejectedAnswers = []; // set as an array, in case we ever have questions with more than 2 answers...
// which one of this capabilityQuestions' answers matches our answer?
pq.answers.forEach((ans) => {
if (ans.answerText.toUpperCase() === answeredQuestionAnswer) {
matchedAnswer = ans;
ans.selected = true;
} else {
rejectedAnswers.push(ans);
ans.selected = null;
}
});
// Update the key to re-render this part's question-chain component
this.capabilityQuestionsData[gpIndex].key =
this.capabilityQuestionsData[gpIndex].glassLocation +
this.capabilityQuestionsData[gpIndex].glassName +
Date.now().toString();
// handle suppressing downstream in this question chain
if (matchedAnswer.nextQuestionSequence) {
// ensure that the question that the accepted answer has set to be next is NOT suppressed
glass.capabilityQuestions[
matchedAnswer.nextQuestionSequence - 1
].suppressQuestion = null;
// if the duplicate is the 1ST question, then set suppressUntil var to lowest nextQuestion number
if (pqIndex === 0) {
if (!suppressUntil) {
suppressUntil = matchedAnswer.nextQuestionSequence;
}
if (matchedAnswer.nextQuestionSequence < suppressUntil) {
suppressUntil = matchedAnswer.nextQuestionSequence;
}
}
}
// handle suppressing upstream in this question chain
glass.capabilityQuestions.forEach((q) => {
q.answers.forEach((thisAns) => {
// restore any of the answers that formerly led to the duplicated question
if (
thisAns.originalNextQuestionSequence ===
pq.questionSequence
) {
// restore original nextQuestionSequence
thisAns.nextQuestionSequence =
thisAns.originalNextQuestionSequence;
this.originalNextQuestionSequence = null;
// restore original answerResult
if (thisAns.originalAnswerResult) {
thisAns.answerResult = thisAns.originalAnswerResult;
thisAns.originalAnswerResult = null;
}
}
// search for any answers that lead to the duplicated question
if (thisAns.nextQuestionSequence === pq.questionSequence) {
// update either the nextQuestionSequence or the answerResult
if (matchedAnswer.nextQuestionSequence) {
thisAns.originalNextQuestionSequence =
thisAns.nextQuestionSequence;
thisAns.nextQuestionSequence =
matchedAnswer.nextQuestionSequence;
} else {
thisAns.originalNextQuestionSequence =
thisAns.nextQuestionSequence;
thisAns.nextQuestionSequence = null;
thisAns.originalAnswerResult =
thisAns.originalAnswerResult ||
thisAns.answerResult;
thisAns.answerResult = matchedAnswer.answerResult;
}
}
});
});
// suppress current question
thisAnsweredCapabilityQuestion.suppressQuestion = true;
const thisGlassPart = "glass" + gpIndex;
if (foundDuplicateQuestions[thisGlassPart]) {
if (
!foundDuplicateQuestions[thisGlassPart].includes(
thisAnsweredCapabilityQuestion.questionSequence
)
) {
foundDuplicateQuestions[thisGlassPart].push(
thisAnsweredCapabilityQuestion.questionSequence
);
}
} else {
foundDuplicateQuestions[thisGlassPart] = [
thisAnsweredCapabilityQuestion.questionSequence,
];
}
// are there any questions left that are not suppressed?
const remainingQuestions = glass.capabilityQuestions.filter((q) => {
return !q.suppressQuestion;
});
if (remainingQuestions.length < 1) {
// this is the final answer for this glass part
// mark this part as completely answered by adding answerData
const answeredQuestionObj = {
questionText: pq.questionText,
selectedAnswerText: matchedAnswer.answerText,
questionNum: pq.questionSequence,
suppressQuestion: pq.suppressQuestion,
};
// set the answerData as 'already answered'
glass.answerData = {
answerResult: matchedAnswer.nextQuestionSequence
? matchedAnswer.nextQuestionSequence
: matchedAnswer.answerResult,
answeredQuestions: [answeredQuestionObj],
};
// suppress this glass because it has an answer
glass.isSuppressedPart = true;
}
}
});
// Update the key to re-render this part's question-chain component
this.capabilityQuestionsData[gpIndex].key =
this.capabilityQuestionsData[gpIndex].glassLocation +
this.capabilityQuestionsData[gpIndex].glassName +
Date.now().toString();
}
});
});
// DETERMINE ANSWERED QUESTIONS LIST FOR THIS GLASS PART
// look through all (this part's) part questions for any duplicates that were suppressed;
// add them to the list of answered questions if found
// EX answeredQuestionIndexes: [1,5,11,13]
// EX foundDuplicateQuestions = {
// "glassPart1": [1],
// "glassPart2": [7, 10]
// };
const thisPartsDupes = foundDuplicateQuestions["glass" + answer.glassIndex];
const completeAnsweredQuestions = answer.answeredQuestions
? [...answer.answeredQuestions]
: [];
const glassPartAnswered = this.capabilityQuestionsData[answer.glassIndex];
thisPartsDupes?.forEach((dupe) => {
// dupe is a single integer
const dupeQuestion = glassPartAnswered.capabilityQuestions[dupe - 1];
const dupeQuestionAnswer = dupeQuestion.answers.find((q) => q.selected === true);
glassPartAnswered.capabilityQuestions.forEach((q) => {
let includeThisDupeInAnsweredQuestions = false;
// did one of the answers of this question point to the duplicated question?
q.answers.forEach((a) => {
if (
dupe === a.originalNextQuestionSequence &&
answeredQuestionIndexes.includes(q.questionSequence) &&
a.answerText.toUpperCase() ===
dupeQuestionAnswer.answerText.toUpperCase()
) {
includeThisDupeInAnsweredQuestions = true;
}
});
// is this q.questionSequence listed as the duplicated question's nextQuestionSequence?
if (
q.questionSequence === dupeQuestionAnswer.nextQuestionSequence &&
answeredQuestionIndexes.includes(q.questionSequence)
) {
includeThisDupeInAnsweredQuestions = true;
}
if (includeThisDupeInAnsweredQuestions) {
completeAnsweredQuestions.push({
questionNum: dupeQuestion.questionSequence,
questionText: dupeQuestion.questionText,
selectedAnswerText: dupeQuestionAnswer.answerText,
suppressQuestion: dupeQuestion.suppressQuestion,
});
}
});
});
// make sure there are no duplicated dupes in the list...
const foundInCompleteAnsweredQuestions = new Set();
let filteredCompleteAnsweredQuestions = completeAnsweredQuestions.filter((el) => {
const duplicate = foundInCompleteAnsweredQuestions.has(el.questionText);
foundInCompleteAnsweredQuestions.add(el.questionText);
return !duplicate;
});
filteredCompleteAnsweredQuestions = filteredCompleteAnsweredQuestions.sort(
(a, b) => a.questionNum - b.questionNum
);
// set final answer data for the current answered glass part
glassPartAnswered.answerData = {
answerResult: answer.answerResult,
answeredQuestions: filteredCompleteAnsweredQuestions,
};
// this part has been fully answered, so advance to next part's question chain
for (let i = answer.glassIndex + 1; i < this.capabilityQuestionsData.length; i++) {
// if this part has not yet been fully answered, then make it the current part
if (!this.capabilityQuestionsData[i].answerData?.answerResult) {
this.currentQuestionChainIndex = i;
break;
}
}
},
// TODO I'm sure there's a better/simplier way to do this that I'm missing but my brain's fried
// Find the part that has some answer that has an answerResult matching the selected answerResult, then get the corresponding answerResult2
getCorrespondingAnswerResult2(answerResult1) {
return this.capabilityQuestionsData
.find((part) =>
part.capabilityQuestions.some((question) =>
question.answers.some((answer) => answer.answerResult == answerResult1)
)
) // found part
.capabilityQuestions.find((question) =>
question.answers.some((answer) => answer.answerResult == answerResult1)
) // found question
.answers.find((answer) => answer.answerResult == answerResult1).answerResult2;
},
},
components: {
funnelHeader,
vehicleBanner,
alert,
questionChain,
funnelSubHeader,
funnelFooter,
loadingModal,
Form,
questionsPageLayout,
},
};
</script>
<style lang="scss">
.capability-questions {
.question-text {
margin-bottom: 0.5rem;
span {
text-align: left;
}
}
}
</style>

View file

@ -1,57 +1,27 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles molding-questions">
<loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<alert
ref="alertFewMoreQuestions"
cmsWidgetName="AlertFewMoreQuestionsWidget"
class="my-5"
alertClass="alert-warning"
:manualHeadline="AlertFewMoreQuestionsHeader"
:manualCopy="AlertFewMoreQuestionsCopy"
v-bind:isDismissible="false" />
<div v-for="(glass, i) in moldingQuestionsData" :key="glass.key">
<questionChain
ref="questionChain"
v-model="selectedAnswers[glass.glassLocation + '-' + glass.glassName]"
:questionData="glass.questions"
:glassIndex="i"
v-if="showThisQuestionChain(glass, i)"
validationRules="questions-required" />
</div>
<funnel-footer
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
<questions-page-layout
ref="questionsPageLayout"
:isMetaValid="meta.valid"
:alertFewMoreQuestionsHeader="AlertFewMoreQuestionsHeader"
:alertFewMoreQuestionsCopy="AlertFewMoreQuestionsCopy"
:questionsData="questionsData"
:validationRules="questions - required"
v-model="selectedAnswers"
@forwardButtonAction="forwardButtonAction"
@backButtonAction="backButtonAction"
:index="currentGlassIndex" />
</Form>
</template>
<script>
// Components
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import alert from "@/ux-components/alert/alert";
import questionChain from "@/common-components/question-chain/question-chain";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import loadingModal from "@/common-components/loading-modal/loading-modal.vue";
import questionsPageLayout from "@/common-components/questions-page-layout/questions-page-layout";
// Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
@ -85,8 +55,7 @@ export default {
},
data() {
return {
moldingQuestionsData: store.getters.pageData(fmgPageValues.MOLDING_QUESTIONS)
.partsOrQuestions,
questionsData: [],
selectedAnswers: {},
currentGlassIndex: 0,
};
@ -106,15 +75,28 @@ export default {
// are there alreadyAnsweredQuestions?
const alreadyAnsweredQuestions = store.getters.damage.moldingQuestionAnswers;
this.moldingQuestionsData = this.pageData.partsOrQuestions
this.questionsData = this.pageData.partsOrQuestions
.filter((x) => x.parts[0].childPartQuestions.length)
.map((glass, i) => {
.map((glass, index) => {
// NOTE: questions for property "questions" can differ between layouts
glass.questions = glass.parts[0].childPartQuestions;
glass.answerKey = glass.glassLocation + "-" + glass.glassName;
// reset selectedAnswers for this glass
this.selectedAnswers[glass.key] = [];
// pass in: glass, i, alreadyAnsweredQuestions
return this.setupInitialData(glass, i, alreadyAnsweredQuestions);
this.selectedAnswers[glass.answerKey] = [];
const updatedGlass = this.setupInitialData(glass, index, alreadyAnsweredQuestions);
this.$watch(
"selectedAnswers." + glass.answerKey,
(newValue) => {
if (newValue && Object.keys(newValue).length > 0) {
this.handleAnswerUpdates(newValue, glass.answerKey);
}
},
{ deep: true }
);
return updatedGlass;
});
},
methods: {
@ -126,14 +108,8 @@ export default {
moldingQuestionsFromPageData && Object.keys(moldingQuestionsFromPageData).length > 0
);
},
showThisQuestionChain(glass, i) {
if (!glass.questions || glass.questions?.length < 1 || glass.isSuppressedPart) {
return false;
} // return false if no questions or if suppressed
return this.currentGlassIndex === i || glass.answerData?.answerResult?.length > 0;
},
async forwardButtonAction() {
const questionAnswersArray = this.moldingQuestionsData.map((glass) => {
const questionAnswersArray = this.questionsData.map((glass) => {
return {
glassLocation: glass.glassLocation,
glassName: glass.glassName,
@ -144,7 +120,7 @@ export default {
});
// clear out answerData for future page loads; must occur prior to store save
this.moldingQuestionsData.forEach((glass) => {
this.questionsData.forEach((glass) => {
glass.answerData = {};
});
@ -172,313 +148,10 @@ export default {
this.navigateForward(partsOrQuestions);
},
handleAnswerUpdates(answer) {
// only runs when all questions in a question-chain have been answered
// when selectedAnswers updates, user has completed this part's question chain and has a final answer
// (does not get run for each invididual question's answer, only when
// all relevant questions for the current part have been answered)
/*
answer example format:
{
"answerResult": "DD11132",
"answeredQuestions": [
{
"questionText": "Is your Grand Cherokee the Laredo model?",
"selectedAnswerText": "Yes",
"questionNum": 1,
}
],
"glassIndex": 0
}
*/
// collect a list of the answered questions' numbers, needed later below
const answeredQuestionIndexes = [];
const foundDuplicateQuestions = [];
// if user has answered a question differently than anything that was preloaded,
// we need to clear out any preloaded answers
this.selectedAnswers = {};
// loop through every answered question on the currently answered glass part
answer.answeredQuestions?.forEach((aq) => {
// keep track of this question number
answeredQuestionIndexes.push(aq.questionNum);
const answeredQuestionText = aq.questionText.toUpperCase();
const answeredQuestionAnswer = aq.selectedAnswerText.toUpperCase();
// HANDLE DUPLICATE QUESTIONS
// loop through all glass parts data
this.moldingQuestionsData.forEach((glass, gpIndex) => {
// only look for duplicates forward... to parts that follow after the currently being answered part
if (gpIndex > answer.glassIndex) {
let suppressUntil;
// reset this glass part, in case user is changing their previous answers
glass.answerData = null;
glass.isSuppressedPart = null;
// loop through this glass part's part questions, looking for a questionText match
glass.questions.forEach((pq, pqIndex) => {
// clear out any previously set answers
pq.answerSelected = null;
// clear or set suppressQuestion property for each question
if (suppressUntil) {
// if suppressUntil has been set, then suppress this question if before it
if (pqIndex + 1 < suppressUntil) {
pq.suppressQuestion = true;
} else {
pq.suppressQuestion = null;
}
} else {
pq.suppressQuestion = null;
}
// if these match then we have a duplicate question
if (pq.questionText.toUpperCase() === answeredQuestionText) {
const thisAnsweredPartQuestion = glass.questions[pqIndex];
let matchedAnswer;
let rejectedAnswers = []; // set as an array, in case we ever have questions with more than 2 answers...
// which one of this questions' answers matches our answer?
pq.answers.forEach((ans) => {
if (ans.answerText.toUpperCase() === answeredQuestionAnswer) {
matchedAnswer = ans;
ans.selected = true;
} else {
rejectedAnswers.push(ans);
ans.selected = null;
}
});
// Update the key to re-render this part's question-chain component
this.moldingQuestionsData[gpIndex].key =
this.moldingQuestionsData[gpIndex].glassLocation +
this.moldingQuestionsData[gpIndex].glassName +
Date.now().toString();
// handle suppressing downstream in this question chain
if (matchedAnswer.nextQuestionSequence) {
// ensure that the question that the accepted answer has set to be next is NOT suppressed
glass.questions[
matchedAnswer.nextQuestionSequence - 1
].suppressQuestion = null;
// if the duplicate is the 1ST question, then set suppressUntil var to lowest nextQuestion number
if (pqIndex === 0) {
if (!suppressUntil) {
suppressUntil = matchedAnswer.nextQuestionSequence;
}
if (matchedAnswer.nextQuestionSequence < suppressUntil) {
suppressUntil = matchedAnswer.nextQuestionSequence;
}
}
}
// handle suppressing upstream in this question chain
glass.questions.forEach((q) => {
q.answers.forEach((thisAns) => {
// restore any of the answers that formerly led to the duplicated question
if (
thisAns.originalNextQuestionSequence ===
pq.questionSequence
) {
// restore original nextQuestionSequence
thisAns.nextQuestionSequence =
thisAns.originalNextQuestionSequence;
this.originalNextQuestionSequence = null;
// restore original answerResult
if (thisAns.originalAnswerResult) {
thisAns.answerResult = thisAns.originalAnswerResult;
thisAns.originalAnswerResult = null;
}
}
// search for any answers that lead to the duplicated question
if (thisAns.nextQuestionSequence === pq.questionSequence) {
// update either the nextQuestionSequence or the answerResult
if (matchedAnswer.nextQuestionSequence) {
thisAns.originalNextQuestionSequence =
thisAns.nextQuestionSequence;
thisAns.nextQuestionSequence =
matchedAnswer.nextQuestionSequence;
} else {
thisAns.originalNextQuestionSequence =
thisAns.nextQuestionSequence;
thisAns.nextQuestionSequence = null;
thisAns.originalAnswerResult =
thisAns.originalAnswerResult ||
thisAns.answerResult;
thisAns.answerResult = matchedAnswer.answerResult;
}
}
});
});
// suppress current question
thisAnsweredPartQuestion.suppressQuestion = true;
const thisGlassPart = "glass" + gpIndex;
if (foundDuplicateQuestions[thisGlassPart]) {
if (
!foundDuplicateQuestions[thisGlassPart].includes(
thisAnsweredPartQuestion.questionSequence
)
) {
foundDuplicateQuestions[thisGlassPart].push(
thisAnsweredPartQuestion.questionSequence
);
}
} else {
foundDuplicateQuestions[thisGlassPart] = [
thisAnsweredPartQuestion.questionSequence,
];
}
// are there any questions left that are not suppressed?
const remainingQuestions = glass.questions.filter((q) => {
return !q.suppressQuestion;
});
if (remainingQuestions.length < 1) {
// this is the final answer for this glass part
// mark this part as completely answered by adding answerData
const answeredQuestionObj = {
questionText: pq.questionText,
selectedAnswerText: matchedAnswer.answerText,
questionNum: pq.questionSequence,
suppressQuestion: pq.suppressQuestion,
};
// set the answerData as 'already answered'
glass.answerData = {
answerResult: matchedAnswer.nextQuestionSequence
? matchedAnswer.nextQuestionSequence
: matchedAnswer.answerResult,
answeredQuestions: [answeredQuestionObj],
};
// suppress this glass because it has an answer
glass.isSuppressedPart = true;
}
}
});
// Update the key to re-render this part's question-chain component
this.moldingQuestionsData[gpIndex].key =
this.moldingQuestionsData[gpIndex].glassLocation +
this.moldingQuestionsData[gpIndex].glassName +
Date.now().toString();
}
});
});
// DETERMINE ANSWERED QUESTIONS LIST FOR THIS GLASS PART
// look through all (this part's) part questions for any duplicates that were suppressed;
// add them to the list of answered questions if found
// EX answeredQuestionIndexes: [1,5,11,13]
// EX foundDuplicateQuestions = {
// "glassPart1": [1],
// "glassPart2": [7, 10]
// };
const thisPartsDupes = foundDuplicateQuestions["glass" + answer.glassIndex];
const completeAnsweredQuestions = answer.answeredQuestions
? [...answer.answeredQuestions]
: [];
const glassPartAnswered = this.moldingQuestionsData[answer.glassIndex];
thisPartsDupes?.forEach((dupe) => {
// dupe is a single integer
const dupeQuestion = glassPartAnswered.questions[dupe - 1];
const dupeQuestionAnswer = dupeQuestion.answers.find((q) => q.selected === true);
glassPartAnswered.questions.forEach((q) => {
let includeThisDupeInAnsweredQuestions = false;
// did one of the answers of this question point to the duplicated question?
q.answers.forEach((a) => {
if (
dupe === a.originalNextQuestionSequence &&
answeredQuestionIndexes.includes(q.questionSequence) &&
a.answerText.toUpperCase() ===
dupeQuestionAnswer.answerText.toUpperCase()
) {
includeThisDupeInAnsweredQuestions = true;
}
});
// is this q.questionSequence listed as the duplicated question's nextQuestionSequence?
if (
q.questionSequence === dupeQuestionAnswer.nextQuestionSequence &&
answeredQuestionIndexes.includes(q.questionSequence)
) {
includeThisDupeInAnsweredQuestions = true;
}
if (includeThisDupeInAnsweredQuestions) {
completeAnsweredQuestions.push({
questionNum: dupeQuestion.questionSequence,
questionText: dupeQuestion.questionText,
selectedAnswerText: dupeQuestionAnswer.answerText,
suppressQuestion: dupeQuestion.suppressQuestion,
});
}
});
});
// make sure there are no duplicated dupes in the list...
const foundInCompleteAnsweredQuestions = new Set();
let filteredCompleteAnsweredQuestions = completeAnsweredQuestions.filter((el) => {
const duplicate = foundInCompleteAnsweredQuestions.has(el.questionText);
foundInCompleteAnsweredQuestions.add(el.questionText);
return !duplicate;
});
filteredCompleteAnsweredQuestions = filteredCompleteAnsweredQuestions.sort(
(a, b) => a.questionNum - b.questionNum
);
// set final answer data for the current answered glass part
glassPartAnswered.answerData = {
answerResult: answer.answerResult,
answeredQuestions: filteredCompleteAnsweredQuestions,
};
// this part has been fully answered, so advance to next molding question chain
for (let i = answer.glassIndex + 1; i < this.moldingQuestionsData.length; i++) {
// if this part has not yet been fully answered, then make it the current part
if (!this.moldingQuestionsData[i].answerData?.answerResult) {
this.currentGlassIndex = i;
break;
}
}
},
},
components: {
funnelHeader,
vehicleBanner,
alert,
questionChain,
funnelSubHeader,
funnelFooter,
loadingModal,
Form,
questionsPageLayout,
},
};
</script>
<style lang="scss">
.molding-questions {
.question-text {
margin-bottom: 0.5rem;
span {
text-align: left;
}
}
}
</style>

View file

@ -1,57 +1,27 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles part-questions">
<loadingModal ref="loadingModal" />
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false" />
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<alert
ref="alertFewMoreQuestions"
cmsWidgetName="AlertFewMoreQuestionsWidget"
class="my-5"
alertClass="alert-warning"
:manualHeadline="AlertFewMoreQuestionsHeader"
:manualCopy="AlertFewMoreQuestionsCopy"
v-bind:isDismissible="false" />
<div v-for="(glass, i) in partsQuestionsData" :key="glass.key">
<questionChain
ref="questionChain"
v-model="selectedAnswers[glass.glassLocation + '-' + glass.glassName]"
:questionData="glass.questions"
:glassIndex="i"
v-if="showThisQuestionChain(glass, i)"
validationRules="questions-required" />
</div>
<funnel-footer
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="!meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
<questions-page-layout
ref="questionsPageLayout"
:isMetaValid="meta.valid"
:alertFewMoreQuestionsHeader="AlertFewMoreQuestionsHeader"
:alertFewMoreQuestionsCopy="AlertFewMoreQuestionsCopy"
:questionsData="questionsData"
:validationRules="questions - required"
v-model="selectedAnswers"
@forwardButtonAction="forwardButtonAction"
@backButtonAction="backButtonAction"
:index="currentGlassIndex" />
</Form>
</template>
<script>
// Components
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import alert from "@/ux-components/alert/alert";
import questionChain from "@/common-components/question-chain/question-chain";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import loadingModal from "@/common-components/loading-modal/loading-modal.vue";
import questionsPageLayout from "@/common-components/questions-page-layout/questions-page-layout";
// Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
@ -63,6 +33,7 @@ defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
export default {
name: "part-questions",
mixins: [vehicleQuestionsMixin],
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
@ -84,7 +55,7 @@ export default {
},
data() {
return {
partsQuestionsData: [],
questionsData: [],
selectedAnswers: {},
currentGlassIndex: 0,
};
@ -100,20 +71,33 @@ export default {
return this.$store.getters.pageData(fmgPageValues.PART_QUESTIONS);
},
},
mixins: [vehicleQuestionsMixin],
mounted() {
// are there alreadyAnsweredQuestions?
const alreadyAnsweredQuestions = store.getters.damage.partQuestionAnswers;
this.partsQuestionsData = this.pageData.partsOrQuestions
this.questionsData = this.pageData.partsOrQuestions
.filter((x) => x.partQuestions)
.map((glass, i) => {
.map((glass, index) => {
// NOTE: questions for property "questions" can differ between layouts
glass.questions = glass.partQuestions;
glass.answerKey = glass.glassLocation + "-" + glass.glassName;
// reset selectedAnswers for this glass
this.selectedAnswers[glass.key] = [];
// pass in: glass, i, alreadyAnsweredQuestions
return this.setupInitialData(glass, i, alreadyAnsweredQuestions);
this.selectedAnswers[glass.answerKey] = [];
const updatedGlass = this.setupInitialData(glass, index, alreadyAnsweredQuestions);
// Set up watch for each set of glass questions
this.$watch(
"selectedAnswers." + glass.answerKey,
(newValue) => {
if (newValue && Object.keys(newValue).length > 0) {
this.handleAnswerUpdates(newValue, glass.answerKey);
}
},
{ deep: true }
);
return updatedGlass;
});
},
methods: {
@ -121,14 +105,8 @@ export default {
const partQuestionsFromPageData = store.getters.pageData(fmgPageValues.PART_QUESTIONS);
return partQuestionsFromPageData && Object.keys(partQuestionsFromPageData).length > 0;
},
showThisQuestionChain(glass, i) {
if (!glass.questions || glass.questions?.length < 1 || glass.isSuppressedPart) {
return false;
} // return false if no questions or if suppressed
return this.currentGlassIndex === i || glass.answerData?.answerResult?.length > 0;
},
async forwardButtonAction() {
const questionAnswersArray = this.partsQuestionsData.map((glass) => {
const questionAnswersArray = this.questionsData.map((glass) => {
return {
glassLocation: glass.glassLocation,
glassName: glass.glassName,
@ -139,7 +117,7 @@ export default {
});
// clear out answerData for future page loads; must occur prior to store save
this.partsQuestionsData.forEach((glass) => {
this.questionsData.forEach((glass) => {
glass.answerData = {};
});
@ -151,321 +129,22 @@ export default {
);
// call API parts method
const partsLookup = await this.dispatchStoreAction(storeActions.GET_PARTS).catch(() => {
return this.$refs.funnelFooter.removeLoader();
});
const glassPiecePartsForStore = partsLookup.data.glassPieceParts;
this.navigateForward(glassPiecePartsForStore);
},
handleAnswerUpdates(answer) {
// only runs when all questions in a question-chain have been answered
// when selectedAnswers updates, user has completed this part's question chain and has a final answer
// (does not get run for each invididual question's answer, only when
// all relevant questions for the current part have been answered)
/*
answer example format:
{
"answerResult": "DD11132",
"answeredQuestions": [
{
"questionText": "Is your Grand Cherokee the Laredo model?",
"selectedAnswerText": "Yes",
"questionNum": 1,
}
],
"glassIndex": 0
const partsLookup = await this.dispatchStoreAction(this.storeActions.GET_PARTS).catch(
() => {
return this.$refs.funnelFooter.removeLoader();
}
*/
// collect a list of the answered questions' numbers, needed later below
const answeredQuestionIndexes = [];
const foundDuplicateQuestions = [];
// if user has answered a question differently than anything that was preloaded,
// we need to clear out any preloaded answers
this.selectedAnswers = {};
// loop through every answered question on the currently answered glass part
answer.answeredQuestions?.forEach((aq) => {
// keep track of this question number
answeredQuestionIndexes.push(aq.questionNum);
const answeredQuestionText = aq.questionText.toUpperCase();
const answeredQuestionAnswer = aq.selectedAnswerText.toUpperCase();
// HANDLE DUPLICATE QUESTIONS
// loop through all glass parts data
this.partsQuestionsData.forEach((glass, gpIndex) => {
// only look for duplicates forward... to parts that follow after the currently being answered part
if (gpIndex > answer.glassIndex) {
let suppressUntil;
// reset this glass part, in case user is changing their previous answers
glass.answerData = null;
glass.isSuppressedPart = null;
// loop through this glass part's questions, looking for a questionText match
glass.questions.forEach((pq, pqIndex) => {
// clear out any previously set answers
pq.answerSelected = null;
// clear or set suppressQuestion property for each question
if (suppressUntil) {
// if suppressUntil has been set, then suppress this question if before it
if (pqIndex + 1 < suppressUntil) {
pq.suppressQuestion = true;
} else {
pq.suppressQuestion = null;
}
} else {
pq.suppressQuestion = null;
}
// if these match then we have a duplicate question
if (pq.questionText.toUpperCase() === answeredQuestionText) {
const thisAnsweredQuestion = glass.questions[pqIndex];
let matchedAnswer;
let rejectedAnswers = []; // set as an array, in case we ever have questions with more than 2 answers...
// which one of this questions' answers matches our answer?
pq.answers.forEach((ans) => {
if (ans.answerText.toUpperCase() === answeredQuestionAnswer) {
matchedAnswer = ans;
ans.selected = true;
} else {
rejectedAnswers.push(ans);
ans.selected = null;
}
});
// Update the key to re-render this part's question-chain component
this.partsQuestionsData[gpIndex].key =
this.partsQuestionsData[gpIndex].glassLocation +
this.partsQuestionsData[gpIndex].glassName +
Date.now().toString();
// handle suppressing downstream in this question chain
if (matchedAnswer.nextQuestionSequence) {
// ensure that the question that the accepted answer has set to be next is NOT suppressed
glass.questions[
matchedAnswer.nextQuestionSequence - 1
].suppressQuestion = null;
// if the duplicate is the 1ST question, then set suppressUntil var to lowest nextQuestion number
if (pqIndex === 0) {
if (!suppressUntil) {
suppressUntil = matchedAnswer.nextQuestionSequence;
}
if (matchedAnswer.nextQuestionSequence < suppressUntil) {
suppressUntil = matchedAnswer.nextQuestionSequence;
}
}
}
// handle suppressing upstream in this question chain
glass.questions.forEach((q) => {
q.answers.forEach((thisAns) => {
// restore any of the answers that formerly led to the duplicated question
if (
thisAns.originalNextQuestionSequence ===
pq.questionSequence
) {
// restore original nextQuestionSequence
thisAns.nextQuestionSequence =
thisAns.originalNextQuestionSequence;
this.originalNextQuestionSequence = null;
// restore original answerResult
if (thisAns.originalAnswerResult) {
thisAns.answerResult = thisAns.originalAnswerResult;
thisAns.originalAnswerResult = null;
}
}
// search for any answers that lead to the duplicated question
if (thisAns.nextQuestionSequence === pq.questionSequence) {
// update either the nextQuestionSequence or the answerResult
if (matchedAnswer.nextQuestionSequence) {
thisAns.originalNextQuestionSequence =
thisAns.nextQuestionSequence;
thisAns.nextQuestionSequence =
matchedAnswer.nextQuestionSequence;
} else {
thisAns.originalNextQuestionSequence =
thisAns.nextQuestionSequence;
thisAns.nextQuestionSequence = null;
thisAns.originalAnswerResult =
thisAns.originalAnswerResult ||
thisAns.answerResult;
thisAns.answerResult = matchedAnswer.answerResult;
}
}
});
});
// suppress current question
thisAnsweredQuestion.suppressQuestion = true;
const thisGlassPart = "glass" + gpIndex;
if (foundDuplicateQuestions[thisGlassPart]) {
if (
!foundDuplicateQuestions[thisGlassPart].includes(
thisAnsweredQuestion.questionSequence
)
) {
foundDuplicateQuestions[thisGlassPart].push(
thisAnsweredQuestion.questionSequence
);
}
} else {
foundDuplicateQuestions[thisGlassPart] = [
thisAnsweredQuestion.questionSequence,
];
}
// are there any questions left that are not suppressed?
const remainingQuestions = glass.questions.filter((q) => {
return !q.suppressQuestion;
});
if (remainingQuestions.length < 1) {
// this is the final answer for this glass part
// mark this part as completely answered by adding answerData
const answeredQuestionObj = {
questionText: pq.questionText,
selectedAnswerText: matchedAnswer.answerText,
questionNum: pq.questionSequence,
suppressQuestion: pq.suppressQuestion,
};
// set the answerData as 'already answered'
glass.answerData = {
answerResult: matchedAnswer.nextQuestionSequence
? matchedAnswer.nextQuestionSequence
: matchedAnswer.answerResult,
answeredQuestions: [answeredQuestionObj],
};
// suppress this glass because it has an answer
glass.isSuppressedPart = true;
}
}
});
// Update the key to re-render this part's question-chain component
this.partsQuestionsData[gpIndex].key =
this.partsQuestionsData[gpIndex].glassLocation +
this.partsQuestionsData[gpIndex].glassName +
Date.now().toString();
}
});
});
// DETERMINE ANSWERED QUESTIONS LIST FOR THIS GLASS PART
// look through all (this part's) part questions for any duplicates that were suppressed;
// add them to the list of answered questions if found
// EX answeredQuestionIndexes: [1,5,11,13]
// EX foundDuplicateQuestions = {
// "glassPart1": [1],
// "glassPart2": [7, 10]
// };
const thisPartsDupes = foundDuplicateQuestions["glass" + answer.glassIndex];
const completeAnsweredQuestions = answer.answeredQuestions
? [...answer.answeredQuestions]
: [];
const glassPartAnswered = this.partsQuestionsData[answer.glassIndex];
thisPartsDupes?.forEach((dupe) => {
// dupe is a single integer
const dupeQuestion = glassPartAnswered.questions[dupe - 1];
const dupeQuestionAnswer = dupeQuestion.answers.find((q) => q.selected === true);
glassPartAnswered.questions.forEach((q) => {
let includeThisDupeInAnsweredQuestions = false;
// did one of the answers of this question point to the duplicated question?
q.answers.forEach((a) => {
if (
dupe === a.originalNextQuestionSequence &&
answeredQuestionIndexes.includes(q.questionSequence) &&
a.answerText.toUpperCase() ===
dupeQuestionAnswer.answerText.toUpperCase()
) {
includeThisDupeInAnsweredQuestions = true;
}
});
// is this q.questionSequence listed as the duplicated question's nextQuestionSequence?
if (
q.questionSequence === dupeQuestionAnswer.nextQuestionSequence &&
answeredQuestionIndexes.includes(q.questionSequence)
) {
includeThisDupeInAnsweredQuestions = true;
}
if (includeThisDupeInAnsweredQuestions) {
completeAnsweredQuestions.push({
questionNum: dupeQuestion.questionSequence,
questionText: dupeQuestion.questionText,
selectedAnswerText: dupeQuestionAnswer.answerText,
suppressQuestion: dupeQuestion.suppressQuestion,
});
}
});
});
// make sure there are no duplicated dupes in the list...
const foundInCompleteAnsweredQuestions = new Set();
let filteredCompleteAnsweredQuestions = completeAnsweredQuestions.filter((el) => {
const duplicate = foundInCompleteAnsweredQuestions.has(el.questionText);
foundInCompleteAnsweredQuestions.add(el.questionText);
return !duplicate;
});
filteredCompleteAnsweredQuestions = filteredCompleteAnsweredQuestions.sort(
(a, b) => a.questionNum - b.questionNum
);
// set final answer data for the current answered glass part
glassPartAnswered.answerData = {
answerResult: answer.answerResult,
answeredQuestions: filteredCompleteAnsweredQuestions,
};
const glassPartsForStore = partsLookup.data.glassPieceParts;
// this part has been fully answered, so advance to next part's question chain
for (let i = answer.glassIndex + 1; i < this.partsQuestionsData.length; i++) {
// if this part has not yet been fully answered, then make it the current part
if (!this.partsQuestionsData[i].answerData?.answerResult) {
this.currentGlassIndex = i;
break;
}
}
debugger; // eslint-disable-line no-debugger
this.navigateForward(glassPartsForStore);
},
},
components: {
funnelHeader,
vehicleBanner,
alert,
questionChain,
funnelSubHeader,
funnelFooter,
loadingModal,
Form,
questionsPageLayout,
},
};
</script>
<style lang="scss">
.part-questions {
.question-text {
margin-bottom: 0.5rem;
span {
text-align: left;
}
}
}
</style>

View file

@ -10,7 +10,11 @@
<div class="fade-on-route-transition sub-container make-tall">
<div class="row mt-2">
<div class="col">
<!-- includeSearchIcon and questionAlignment, placeholderText for testing only. Remove after testing. -->
<textboxQuestion
includeSearchIcon
questionAlignment="center"
placeholderText="Styled for testing only"
cmsWidgetName="VinNumberQuestionWidget"
v-model="vin"
inputId="vin"

View file

@ -1,6 +1,5 @@
import { fmgPageValues } from "@/router/router-constants/fmgPage-values";
import { storeMutations } from "@/constants/store-mutations.js";
import { damageLocationsSelected } from "@/constants/damage-locations-selected";
import { navigationScenarios } from "../router/router-constants/navigation-scenarios";
import { storeActions } from "@/constants/store-actions";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
@ -67,9 +66,9 @@ export default {
currentPageComesAfterPage(currentPage = this.$route.query.fmgPage, fmgPage) {
return this.comparePageIndices(currentPage, fmgPage) > 0;
},
setupInitialData(glass, i, alreadyAnsweredQuestions, vm) {
glass.key = glass.glassLocation + "-" + glass.glassName;
setupInitialData(glass, index, alreadyAnsweredQuestions, vm) {
const self = vm ?? this;
// clear answerData if no questions are already answered
if (!alreadyAnsweredQuestions) {
glass.answerData = null;
@ -94,33 +93,39 @@ export default {
let answerString = "";
// loop through answeredQuestions for matches
answeredGlass.answeredQuestions.forEach((aq) => {
if (!aq.questionNum || !aq.selectedAnswerText) {
answeredGlass.answeredQuestions.forEach((answeredQuestion) => {
if (!answeredQuestion.questionNum || !answeredQuestion.selectedAnswerText) {
return;
}
// determine which answer was previously chosen
const chosenAns = glass.questions[aq.questionNum - 1].answers.find((a) => {
const chosenAns = glass.questions[
answeredQuestion.questionNum - 1
].answers.find((a) => {
return (
a.answerText.toUpperCase() === aq.selectedAnswerText.toUpperCase()
a.answerText.toUpperCase() ===
answeredQuestion.selectedAnswerText.toUpperCase()
);
});
// set the answerString to use for answerSelected
if (chosenAns.nextQuestionSequence) {
answerString = `${aq.questionNum}|nextQuestion|${chosenAns.nextQuestionSequence}|${chosenAns.answerText}`;
answerString = `${answeredQuestion.questionNum}|nextQuestion|${chosenAns.nextQuestionSequence}|${chosenAns.answerText}`;
} else {
answerString = `${aq.questionNum}|answer|${chosenAns.answerResult}|${chosenAns.answerText}`;
answerString = `${answeredQuestion.questionNum}|answer|${chosenAns.answerResult}|${chosenAns.answerText}`;
}
// mark this question as answered (question-chain will read this)
glass.questions[aq.questionNum - 1].answerSelected = answerString;
glass.questions[answeredQuestion.questionNum - 1].answerSelected =
answerString;
// mark this question as suppressed if needed (question-chain uses this)
if (aq.suppressQuestion) {
glass.questions[aq.questionNum - 1].suppressQuestion = true;
if (answeredQuestion.suppressThisQuestion) {
glass.questions[
answeredQuestion.questionNum - 1
].suppressThisQuestion = true;
}
});
// advance the currentGlassIndex
self.currentGlassIndex = i;
self.currentGlassIndex = index;
const answerResult = answeredGlass.partNum
? answeredGlass.partNum
@ -135,20 +140,221 @@ export default {
}
});
// Set up watch for each set of glass questions
// (updated when all questions for a glass have been answered in question-chain)
self.$watch(
"selectedAnswers." + glass.glassLocation + "-" + glass.glassName,
(newValue) => {
if (newValue) {
self.handleAnswerUpdates(newValue);
}
},
{ deep: true }
);
return glass;
},
handleAnswerUpdates(answer, glassKey, vm) {
// only runs when all questions in a question-chain have been answered
// when selectedAnswers updates, user has completed this part's question chain and has a final answer
// (does not get run for each invididual question's answer, only when
// all relevant questions for the current part have been answered)
/* answer example format:
{
"answerResult": "DD11132",
"answeredQuestions": [
{
"questionText": "Is your Grand Cherokee the Laredo model?",
"selectedAnswerText": "Yes",
"questionNum": 1,
}
],
"index": 0
}
*/
const self = vm ?? this;
// clear out any preloaded answers
self.selectedAnswers = {};
// loop through every answered question on the currently answered glass part
answer.answeredQuestions?.forEach((answeredQuestion) => {
/* answeredQuestion example format:
{
"questionText": "Is your Grand Cherokee the Laredo model?",
"selectedAnswerText": "Yes",
"questionNum": 1,
}
*/
const answeredQuestionText = answeredQuestion.questionText.toUpperCase();
const answeredQuestionAnswer = answeredQuestion.selectedAnswerText.toUpperCase();
// HANDLE DUPLICATE QUESTIONS
// loop through all glass data
self.questionsData.forEach((glass, glassIndex) => {
/* glass example format:
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": null,
"questions": [
{
"questionSequence": 1,
"questionText": "Is your vehicle equipped with a black dotted pattern behind the rear view mirror, known as a third visor frit?",
"answers": [
{
"answerResult": "DW01537",
"answerText": "Yes",
"nextQuestionSequence": null
},
{
"answerResult": "DW01537b",
"answerText": "No",
"nextQuestionSequence": null
}
]
}
],
"answerKey": "Windshield-Single",
"answerData": null
}
*/
// limit duplicate search to glass pieces that follow after the currently being answered glass piece
if (glassIndex > answer.index) {
let indexToSuppressTo;
// reset this glass piece, in case user is changing their previous answers
glass.answerData = null;
glass.isSuppressedPart = null;
// loop through this glass piece's questions, looking for a questionText match
glass.questions.forEach((question, questionIndex) => {
// clear out any previously set answers
question.answerSelected = null;
// clear or set suppressThisQuestion property for each question
if (indexToSuppressTo && questionIndex + 1 < indexToSuppressTo) {
question.suppressThisQuestion = true;
} else {
question.suppressThisQuestion = null;
}
// handle duplicate questions
if (question.questionText.toUpperCase() === answeredQuestionText) {
let matchedAnswer;
// handle matching answer in duplicated question
question.answers.forEach((ans) => {
if (ans.answerText.toUpperCase() === answeredQuestionAnswer) {
matchedAnswer = ans;
ans.selected = true;
} else {
ans.selected = null;
}
});
// handle duplicate's nextQuestion logic on other related questions
if (matchedAnswer.nextQuestionSequence) {
// clear any suppression on the nextQuestion
glass.questions[
matchedAnswer.nextQuestionSequence - 1
].suppressThisQuestion = null;
// if the duplicate is 1ST question in array, set indexToSuppressTo
if (questionIndex === 0) {
if (
!indexToSuppressTo ||
matchedAnswer.nextQuestionSequence < indexToSuppressTo
) {
indexToSuppressTo = matchedAnswer.nextQuestionSequence;
}
}
}
// update answers in this glass piece's questions with duplication logic modifications
glass.questions.forEach((q) => {
q.answers.forEach((a) => {
// revert any previously set nextQuestion logic modifications
if (
a.originalNextQuestionSequence ===
question.questionSequence
) {
// restore original nextQuestionSequence
a.nextQuestionSequence = a.originalNextQuestionSequence;
self.originalNextQuestionSequence = null;
// restore original answerResult
if (a.originalAnswerResult) {
a.answerResult = a.originalAnswerResult;
a.originalAnswerResult = null;
}
}
// modify logic on any related questions
if (a.nextQuestionSequence === question.questionSequence) {
// update either nextQuestionSequence or answerResult
// update questions that lead to duplicated question
if (matchedAnswer.nextQuestionSequence) {
a.originalNextQuestionSequence =
a.nextQuestionSequence;
a.nextQuestionSequence =
matchedAnswer.nextQuestionSequence;
} else {
a.originalNextQuestionSequence =
a.nextQuestionSequence;
a.nextQuestionSequence = null;
a.originalAnswerResult =
a.originalAnswerResult || a.answerResult;
a.answerResult = matchedAnswer.answerResult;
}
}
});
});
// suppress current question
question.suppressThisQuestion = true;
// are there any questions left that are not suppressed?
const remainingQuestions = glass.questions.filter((q) => {
return !q.suppressThisQuestion;
});
if (remainingQuestions.length < 1) {
// this is the final answer for this glass piece
// mark this glass piece as completely answered by adding answerData
const answeredQuestionObj = {
questionText: question.questionText,
selectedAnswerText: matchedAnswer.answerText,
questionNum: question.questionSequence,
suppressThisQuestion: question.suppressThisQuestion,
};
// set the answerData (used as indicator that it has been already answered)
glass.answerData = {
answerResult: matchedAnswer.nextQuestionSequence
? matchedAnswer.nextQuestionSequence
: matchedAnswer.answerResult,
answeredQuestions: [answeredQuestionObj],
};
// suppress this glass piece because it has an answer
glass.isSuppressedPart = true;
}
}
});
// Update key to force re-render of glass piece with duplicate question in case user changes previous related answer in the chain
self.questionsData[glassIndex].key =
self.questionsData[glassIndex].key + Date.now().toString();
}
});
});
// set final answer data for the current answered glass part
self.questionsData[answer.index].answerData = {
answerResult: answer.answerResult,
answeredQuestions: answer.answeredQuestions,
};
// this part has been fully answered, so advance to next part's question chain
for (let i = answer.index + 1; i < this.questionsData.length; i++) {
// if this part has not yet been fully answered, then make it the current part
if (!this.questionsData[i].answerData?.answerResult) {
this.currentGlassIndex = i;
break;
}
}
},
// Can't use `this` because navigateForward is also called from vin-pages-mixin
async navigateForward(partsOrQuestions, vm) {
const self = vm ?? this;
@ -240,10 +446,16 @@ export default {
} else {
// if single parts only
const collectedGlassParts = this.reducedGlassPartsArray(partsOrQuestions);
// save to store lineItems.glassParts
self.$store.commit(storeMutations.UPDATE_GLASS_PARTS, collectedGlassParts);
self.$refs.loadingModal.showModal();
if (self.$refs.questionsPage) {
self.$refs.questionsPage.showLoadingModal();
} else if (self.$refs.loadingModal) {
self.$refs.loadingModal.showModal();
}
navigateToHeritageFunnel();
}
},

View file

@ -337,24 +337,6 @@ describe("vehicle-questions-mixin", () => {
});
describe("setupInitialData", () => {
describe("should always", () => {
test("return glass with new key attribute", async () => {
// Arrange
const { wrapper } = setupMocks({});
const glass = {
glassName: "Single",
glassLocation: "Windshield",
};
const i = 0;
// Act
const returnedGlass = await wrapper.vm.setupInitialData(glass, i);
// Assert
expect(returnedGlass).toMatchObject({ key: "Windshield-Single" });
});
});
describe("if no alreadyAnsweredQuestions", () => {
test("should return glass with answerData of null", async () => {
// Arrange
@ -430,6 +412,816 @@ describe("vehicle-questions-mixin", () => {
});
});
describe("handleAnswerUpdates", () => {
describe("selectedAnswers", () => {
test("should be cleared to be empty", () => {
// Arrange
const answer = {
answerResult: "DD11132",
answeredQuestions: [
{
questionText: "Test duplicate question 1?",
selectedAnswerText: "Yes",
questionNum: 1,
},
],
index: 0,
};
const { wrapper } = setupMocks({});
wrapper.vm.selectedAnswers = { test: "mockSelectedAnswers" };
wrapper.vm.questionsData = [
{
glassLocation: "Windshield",
glassName: "Single",
questions: [],
answerData: "testAnswerData1",
},
];
// Act
wrapper.vm.handleAnswerUpdates(answer, "", wrapper.vm);
// Assert
expect(wrapper.vm.selectedAnswers).toMatchObject({});
});
});
describe("questions in glass parts that are after the answered glass", () => {
test("should have answerData cleared", () => {
// Arrange
const answer = {
answerResult: "DD11132",
answeredQuestions: [
{
questionText: "Test duplicate question 1?",
selectedAnswerText: "Yes",
questionNum: 1,
},
],
index: 0,
};
const { wrapper } = setupMocks({});
wrapper.vm.questionsData = [
{
glassLocation: "Windshield",
glassName: "Single",
questions: [],
answerData: "testAnswerData1",
},
{
glassLocation: "Driver",
glassName: "Front",
questions: [],
answerData: "testAnswerData2",
},
];
// Act
wrapper.vm.handleAnswerUpdates(answer, "", wrapper.vm);
// Assert
expect(wrapper.vm.questionsData[1].answerData).toEqual(null);
});
test("should have isSuppressedPart cleared", () => {
// Arrange
const answer = {
answerResult: "DD11132",
answeredQuestions: [
{
questionText: "Test duplicate question 1?",
selectedAnswerText: "Yes",
questionNum: 1,
},
],
index: 0,
};
const { wrapper } = setupMocks({});
wrapper.vm.questionsData = [
{
glassLocation: "Windshield",
glassName: "Single",
questions: [],
answerData: "testAnswerData1",
},
{
glassLocation: "Driver",
glassName: "Front",
questions: [],
answerData: "testAnswerData2",
isSuppressedPart: true,
},
];
// Act
wrapper.vm.handleAnswerUpdates(answer, "", wrapper.vm);
// Assert
expect(wrapper.vm.questionsData[1].isSuppressedPart).toEqual(null);
});
test("should set answerSelected for each glass part question to null ", () => {
// Arrange
const answer = {
answerResult: "DD11132",
answeredQuestions: [
{
questionText: "Test duplicate question 1?",
selectedAnswerText: "Yes",
questionNum: 1,
},
],
index: 0,
};
const { wrapper } = setupMocks({});
wrapper.vm.questionsData = [
{
glassLocation: "Windshield",
glassName: "Single",
questions: [],
answerData: "testAnswerData1",
},
{
glassLocation: "Driver",
glassName: "Front",
questions: [
{
questionSequence: 1,
questionText:
"Is your vehicle equipped with the Panoramic Sunroof which can be identified by having a glass panel over the rear seats?",
answers: [
{
answerResult: "",
answerText: "Yes",
nextQuestionSequence: 2,
},
{
answerResult: "",
answerText: "No",
nextQuestionSequence: 3,
},
],
answerSelected: "456",
},
{
questionSequence: 2,
questionText:
"Is your vehicle equipped with a heated windshield that melts snow and ice from underneath the windshield wiper blades?",
answers: [
{
answerResult: "FW04848",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "FW04846",
answerText: "No",
nextQuestionSequence: null,
},
],
answerSelected: "789",
},
],
answerData: "testAnswerData2",
},
];
// Act
wrapper.vm.handleAnswerUpdates(answer, "", wrapper.vm);
// Assert
expect(wrapper.vm.questionsData[1].questions[0].answerSelected).toEqual(null);
expect(wrapper.vm.questionsData[1].questions[1].answerSelected).toEqual(null);
});
});
describe("if there is a duplicate question", () => {
test("then the glass piece's key should be updated", () => {
// Arrange
const answerNo = {
answerResult: "456",
answeredQuestions: [
{
questionText: "Test duplicate question 1?",
selectedAnswerText: "No",
questionNum: 1,
},
],
index: 0,
};
const { wrapper } = setupMocks({});
wrapper.vm.questionsData = [
{
glassLocation: "Windshield",
glassName: "Single",
key: "testkey1",
questions: [
{
questionSequence: 1,
questionText: "Test duplicate question 1?",
answers: [
{
answerResult: "123",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "456",
answerText: "No",
nextQuestionSequence: null,
},
],
},
],
},
{
glassLocation: "Driver",
glassName: "Front",
key: "testkey2",
questions: [
{
questionSequence: 1,
questionText: "Test duplicate question 1?",
answers: [
{
answerResult: "123",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "234",
answerText: "No",
nextQuestionSequence: null,
},
],
},
],
},
];
// Act
wrapper.vm.handleAnswerUpdates(answerNo, "", wrapper.vm);
const glassWithDuplicate = wrapper.vm.questionsData[1];
// Assert
expect(glassWithDuplicate.key).not.toBe("testkey2");
});
test("then that question should be supressed", () => {
// Arrange
const answerNo = {
answerResult: "456",
answeredQuestions: [
{
questionText: "Test duplicate question 1?",
selectedAnswerText: "No",
questionNum: 1,
},
],
index: 0,
};
const { wrapper } = setupMocks({});
wrapper.vm.questionsData = [
{
glassLocation: "Windshield",
glassName: "Single",
questions: [
{
questionSequence: 1,
questionText: "Test duplicate question 1?",
answers: [
{
answerResult: "123",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "456",
answerText: "No",
nextQuestionSequence: null,
},
],
},
],
},
{
glassLocation: "Driver",
glassName: "Front",
questions: [
{
questionSequence: 1,
questionText: "Test duplicate question 1?",
answers: [
{
answerResult: "123",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "234",
answerText: "No",
nextQuestionSequence: null,
},
],
},
],
},
];
// Act
wrapper.vm.handleAnswerUpdates(answerNo, "", wrapper.vm);
const duplicateQuestion = wrapper.vm.questionsData[1].questions[0];
// Assert
expect(duplicateQuestion.suppressThisQuestion).toBeTruthy();
});
describe("and the duplicate has a nextQuestionSequence value", () => {
test("then the question set as nextQuestionSequence should not be suppressed", () => {
// Arrange
const answerNo = {
answerResult: "456",
answeredQuestions: [
{
questionText: "Test duplicate question 1?",
selectedAnswerText: "No",
questionNum: 1,
},
],
index: 0,
};
const { wrapper } = setupMocks({});
wrapper.vm.questionsData = [
{
glassLocation: "Windshield",
glassName: "Single",
questions: [
{
questionSequence: 1,
questionText: "Test duplicate question 1?",
answers: [
{
answerResult: "123",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "456",
answerText: "No",
nextQuestionSequence: null,
},
],
},
],
},
{
glassLocation: "Driver",
glassName: "Front",
questions: [
{
questionSequence: 1,
questionText: "ZZZTest duplicate question 1?",
answers: [
{
answerResult: "",
answerText: "Yes",
nextQuestionSequence: 2,
},
{
answerResult: "",
answerText: "No",
nextQuestionSequence: 3,
},
],
},
{
questionSequence: 2,
questionText: "Test question 2?",
answers: [
{
answerResult: "123",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "234",
answerText: "No",
nextQuestionSequence: null,
},
],
},
{
questionSequence: 3,
questionText: "Test question 3?",
answers: [
{
answerResult: "345",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "456",
answerText: "No",
nextQuestionSequence: null,
},
],
suppressThisQuestion: true,
},
],
},
];
// Act
wrapper.vm.handleAnswerUpdates(answerNo, "", wrapper.vm);
const nextQuestionAfterDuplicate = wrapper.vm.questionsData[1].questions[2];
// Assert
expect(nextQuestionAfterDuplicate.suppressThisQuestion).not.toBeTruthy();
});
test("then the nextQuestionSequence should be updated and the original value saved", () => {
// Arrange
const answerNo = {
answerResult: "456",
answeredQuestions: [
{
questionText: "Test question 3?",
selectedAnswerText: "No",
questionNum: 1,
},
],
index: 0,
};
const { wrapper } = setupMocks({});
wrapper.vm.questionsData = [
{
glassLocation: "Windshield",
glassName: "Single",
questions: [
{
questionSequence: 1,
questionText: "Test question 3?",
answers: [
{
answerResult: "123",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "456",
answerText: "No",
nextQuestionSequence: null,
},
],
},
],
},
{
glassLocation: "Driver",
glassName: "Front",
questions: [
{
questionSequence: 1,
questionText: "Test question 1?",
answers: [
{
answerResult: "",
answerText: "Yes",
nextQuestionSequence: 2,
},
{
answerResult: "",
answerText: "No",
nextQuestionSequence: 3,
},
],
},
{
questionSequence: 2,
questionText: "Test question 2?",
answers: [
{
answerResult: "123",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "234",
answerText: "No",
nextQuestionSequence: null,
},
],
},
{
questionSequence: 3,
questionText: "Test question 3?",
answers: [
{
answerResult: "",
answerText: "Yes",
nextQuestionSequence: 4,
},
{
answerResult: "",
answerText: "No",
nextQuestionSequence: 5,
},
],
},
{
questionSequence: 4,
questionText: "Test question 4?",
answers: [
{
answerResult: "567",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "678",
answerText: "No",
nextQuestionSequence: null,
},
],
},
{
questionSequence: 5,
questionText: "Test question 5?",
answers: [
{
answerResult: "789",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "890",
answerText: "No",
nextQuestionSequence: null,
},
],
},
],
},
];
// Act
wrapper.vm.handleAnswerUpdates(answerNo, "", wrapper.vm);
const duplicatedQuestion = wrapper.vm.questionsData[1].questions[2];
const duplicatedQuestionAnswer = duplicatedQuestion.answers.filter((a) => {
return a.selected;
});
const answerToTest = wrapper.vm.questionsData[1].questions[0].answers.filter(
(a) => {
return a.originalNextQuestionSequence;
}
);
// Assert
expect(answerToTest[0].originalNextQuestionSequence).toEqual(
duplicatedQuestion.questionSequence
);
expect(answerToTest[0].nextQuestionSequence).toEqual(
duplicatedQuestionAnswer[0].nextQuestionSequence
);
});
describe("and the duplicate was the first question for that part", () => {
test("then any questions up to the nextQuestionSequence value should be marked as suppressed", () => {
// Arrange
const answerNo = {
answerResult: "456",
answeredQuestions: [
{
questionText: "Test duplicate question 1?",
selectedAnswerText: "No",
questionNum: 1,
},
],
index: 0,
};
const { wrapper } = setupMocks({});
wrapper.vm.questionsData = [
{
glassLocation: "Windshield",
glassName: "Single",
questions: [
{
questionSequence: 1,
questionText: "Test duplicate question 1?",
answers: [
{
answerResult: "123",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "456",
answerText: "No",
nextQuestionSequence: null,
},
],
},
],
},
{
glassLocation: "Driver",
glassName: "Front",
questions: [
{
questionSequence: 1,
questionText: "Test duplicate question 1?",
answers: [
{
answerResult: "",
answerText: "Yes",
nextQuestionSequence: 2,
},
{
answerResult: "",
answerText: "No",
nextQuestionSequence: 3,
},
],
},
{
questionSequence: 2,
questionText: "Test question 2?",
answers: [
{
answerResult: "123",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "234",
answerText: "No",
nextQuestionSequence: null,
},
],
},
{
questionSequence: 3,
questionText: "Test question 3?",
answers: [
{
answerResult: "345",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "456",
answerText: "No",
nextQuestionSequence: null,
},
],
},
],
},
];
// Act
wrapper.vm.handleAnswerUpdates(answerNo, "", wrapper.vm);
// Assert
expect(
wrapper.vm.questionsData[1].questions[0].suppressThisQuestion
).toBeTruthy();
expect(
wrapper.vm.questionsData[1].questions[1].suppressThisQuestion
).toBeTruthy();
expect(
wrapper.vm.questionsData[1].questions[2].suppressThisQuestion
).toBeFalsy();
});
});
});
describe("and the duplicate has an answerResult", () => {
test("then any questions in the same glass piece that lead to the duplicate should be modified to just provide the duplicate's answerResult", async () => {
// Arrange
const answerNo = {
answerResult: "456",
answeredQuestions: [
{
questionText: "Test question 3?",
selectedAnswerText: "No",
questionNum: 1,
},
],
index: 0,
};
const { wrapper } = setupMocks({});
wrapper.vm.questionsData = [
{
glassLocation: "Windshield",
glassName: "Single",
questions: [
{
questionSequence: 1,
questionText: "Test question 3?",
answers: [
{
answerResult: "123",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "456",
answerText: "No",
nextQuestionSequence: null,
},
],
},
],
},
{
glassLocation: "Driver",
glassName: "Front",
questions: [
{
questionSequence: 1,
questionText: "Test question 1?",
answers: [
{
answerResult: "",
answerText: "Yes",
nextQuestionSequence: 2,
},
{
answerResult: "",
answerText: "No",
nextQuestionSequence: 3,
},
],
},
{
questionSequence: 2,
questionText: "Test question 2?",
answers: [
{
answerResult: "123",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "234",
answerText: "No",
nextQuestionSequence: null,
},
],
},
{
questionSequence: 3,
questionText: "Test question 3?",
answers: [
{
answerResult: "345",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "456",
answerText: "No",
nextQuestionSequence: null,
},
],
},
],
},
];
// Act
await wrapper.vm.handleAnswerUpdates(answerNo, "", wrapper.vm);
const questionsToTest = wrapper.vm.questionsData[1].questions;
const answerLeadingToDuplicate = questionsToTest[0].answers[1];
const duplicateQuestion = questionsToTest[2];
const answerInDuplicateQuestion = duplicateQuestion.answers.filter((a) => {
return a.selected;
});
// Assert
expect(answerLeadingToDuplicate.answerResult).toEqual(
answerInDuplicateQuestion[0].answerResult
);
expect(answerLeadingToDuplicate.originalAnswerResult).toBeFalsy();
expect(answerLeadingToDuplicate.nextQuestionSequence).toEqual(null);
expect(answerLeadingToDuplicate.originalNextQuestionSequence).toEqual(
duplicateQuestion.questionSequence
);
});
});
});
});
describe("navigateForward", () => {
describe("should go to parts-questions", () => {
test("single glass location has part question => go to parts-questions", async () => {

View file

@ -303,13 +303,13 @@ export const mutations = {
state.applicationUser.saveSessionPromise = null;
},
// Misc Mutations
updateStateWithOrderInformation(state, orderInformation) {
state.order.referralNumber = orderInformation.referralNumber;
state.order.referralDate = orderInformation.referralDate;
state.order.referralCorrelationId = orderInformation.referralCorrelationId;
state.order.eon = orderInformation.eon;
updateStateWithOrderInformation(state, sessionInformation) {
state.order.referralNumber = sessionInformation.order.referralNumber;
state.order.referralDate = sessionInformation.order.referralDate;
state.order.referralCorrelationId = sessionInformation.order.referralCorrelationId;
state.order.eon = sessionInformation.order.eon;
if (state.order.vehicle.vin !== orderInformation.vehicle?.vin) {
if (state.order.vehicle.vin !== sessionInformation.order.vehicle?.vin) {
state.applicationUser.pageData[fmgPageValues.PART_QUESTIONS] = null;
state.applicationUser.pageData[fmgPageValues.VEHICLE_PARTS] = null;
state.applicationUser.pageData[fmgPageValues.MOLDING_QUESTIONS] = null;
@ -317,44 +317,51 @@ export const mutations = {
}
state.order.vehicle = Object.assign(state.order.vehicle, {
year: orderInformation.vehicle?.year,
make: orderInformation.vehicle?.make,
model: orderInformation.vehicle?.model,
style: orderInformation.vehicle?.style,
vin: orderInformation.vehicle?.vin,
carId: orderInformation.vehicle?.carId,
category: orderInformation.vehicle?.category,
imageUrl: orderInformation.vehicle?.imageUrl,
imageVifNumber: orderInformation.vehicle?.imageVifNumber,
imageColor: orderInformation.vehicle?.imageVifColor,
year: sessionInformation.order.vehicle?.year,
make: sessionInformation.order.vehicle?.make,
model: sessionInformation.order.vehicle?.model,
style: sessionInformation.order.vehicle?.style,
vin: sessionInformation.order.vehicle?.vin,
carId: sessionInformation.order.vehicle?.carId,
category: sessionInformation.order.vehicle?.category,
imageUrl: sessionInformation.order.vehicle?.imageUrl,
imageVifNumber: sessionInformation.order.vehicle?.imageVifNumber,
imageColor: sessionInformation.order.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,
firstName: sessionInformation.order.vehicle.registration.firstName,
lastName: sessionInformation.order.vehicle.registration.lastName,
address: sessionInformation.order.vehicle.registration.streetAddress,
city: sessionInformation.order.vehicle.registration.city,
state: sessionInformation.order.vehicle.registration.state,
zipCode: sessionInformation.order.vehicle.registration.zipCode,
licensePlate: sessionInformation.order.vehicle.registration.licensePlateNumber,
},
});
state.order.damage.glassToReplace = orderInformation.damage.glassToReplace;
state.order.damage.isRepair = orderInformation.damage.isRepair;
state.order.damage.numberOfChips = orderInformation.damage.numberOfChips;
state.order.damage.glassToReplace = sessionInformation.order.damage.glassToReplace;
state.order.damage.isRepair = sessionInformation.order.damage.isRepair;
state.order.damage.numberOfChips = sessionInformation.order.damage.numberOfChips;
state.order.lineItems.glassParts = orderInformation.parts;
state.order.accountNumber = orderInformation.accountNumber;
(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.lineItems.glassParts = sessionInformation.order.lineItems.parts;
state.order.accountNumber = sessionInformation.order.accountNumber;
state.order.providerNumber = sessionInformation.order.providerNumber;
(state.order.serviceLocation.address =
sessionInformation.order.serviceLocation.streetAddress),
(state.order.serviceLocation.city = sessionInformation.order.serviceLocation.city),
(state.order.serviceLocation.state = sessionInformation.order.serviceLocation.state),
(state.order.serviceLocation.zipCode =
sessionInformation.order.serviceLocation.zipCode);
state.order.payment.isInsurance = orderInformation.IsInsuranceOrder;
state.order.payment.isInsurance = sessionInformation.order.payment.isInsurance;
state.order.payment.insuranceCoverage.isVerified =
orderInformation?.insuranceInfo.coverageVerified;
sessionInformation?.order.payment.insuranceCoverage.isVerified;
state.order.customer.emailAddress = orderInformation.customer.emailAddress;
state.applicationUser.experiments = orderInformation.experiments;
state.order.customer.emailAddress = sessionInformation.order.customer.emailAddress;
state.order.existingPromoCode = sessionInformation.order.existingPromoCode;
state.applicationUser.experiments = sessionInformation.applicationUser.experiments;
state.applicationUser.crmCustomerId = sessionInformation.applicationUser.crmCustomerId;
state.applicationUser.pageData = sessionInformation.applicationUser.pageData;
state.applicationUser.lastPage = sessionInformation.applicationUser.lastPage;
},
updateExperiments(state, experiments) {
state.applicationUser.experiments = experiments;
@ -984,17 +991,12 @@ export const actions = {
},
});
},
loadSession(context, { referralNumber, referralDate, referralCorrelationId, accountNumber }) {
loadSession(context, { savedSessionId }) {
return globalMethods
.callHttpClient({
method: endpoints.LoadSession.method,
endpoint: endpoints.LoadSession.url,
payload: {
referralNumber: referralNumber?.toString(),
referralDate: referralDate,
referralCorrelationId: referralCorrelationId,
accountNumber: accountNumber?.toString(),
},
endpoint:
endpoints.LoadSession.url + "?savedSessionId=" + savedSessionId?.toString(),
})
.then((response) => {
// Flatten location and name properties
@ -1409,6 +1411,12 @@ function sortArrayOfObjectsByPropertyValue(arrayOfObjects, propertyName) {
function convertGlassPieceNamingForApi(glassArray) {
if (!glassArray) return [];
// check if array already converted. (likely when a session has been saved previously and then reloaded)
if (glassArray[0].location !== undefined) {
return glassArray;
}
const converted = [];
glassArray.forEach((glass) => {
converted.push({

View file

@ -203,34 +203,65 @@ describe("Mutations", () => {
expect(storeState.applicationUser.pageData["vehicle-year"]).toEqual({});
});
it("updateStateWithOrderInformation, should set order information in state", () => {
it("updateStateWithSessonInformation, should set session information in state", () => {
// Arrange
const storeState = state;
// Act
mutations.updateStateWithOrderInformation(storeState, {
referralNumber: 123,
referralDate: new Date().toUTCString(),
referralCorrelationId: "xxx-xxx-xxx",
vehicle: {
year: "2019",
make: "Acura",
model: "ILX",
style: "4 DOOR SEDAN",
carId: "C0000001",
category: "CAR",
registration: {},
order: {
referralNumber: 123,
referralDate: new Date().toUTCString(),
referralCorrelationId: "xxx-xxx-xxx",
vehicle: {
year: "2019",
make: "Acura",
model: "ILX",
style: "4 DOOR SEDAN",
carId: "C0000001",
category: "CAR",
registration: {},
},
damage: {
glassToReplace: ["Windshield"],
isRepair: false,
numberOfChips: 0,
},
lineItems: {
glassParts: null,
},
payment: {
insuranceCoverage: {
isVerified: false,
},
isInsurance: false,
},
parts: [],
accountNumber: "123456789",
insuranceInfo: {},
serviceLocation: {},
customer: {},
},
damage: {
glassToReplace: ["Windshield"],
isRepair: false,
numberOfChips: 0,
applicationUser: {
experiments: [
{
universeName: "Concept Funnel Test With Rules",
universeId: 463,
testName: "Concept Dev Test",
testId: 392,
variationName: "Concept Test Variation",
variationId: 1133,
isActive: false,
isExposed: true,
userPartitionNumber: 84,
assignmentId: 12211739,
settings: {
someKey: "false",
sampleSetting: "Hi, my name is Vidya",
},
},
],
},
parts: [],
accountNumber: "123456789",
insuranceInfo: {},
serviceLocation: {},
customer: {},
});
// Assert
@ -640,9 +671,7 @@ describe("Actions", () => {
// Act
const response = await actions.loadSession(context, {
referralNumber: "123",
referralDate: new Date().toUTCString(),
referralCorrelationId: "xxx-xxx-xxx",
savedSessionId: "",
});
// Assert
@ -667,9 +696,7 @@ describe("Actions", () => {
// Act
const response = await actions.loadSession(context, {
referralNumber: "123",
referralDate: new Date().toUTCString(),
referralCorrelationId: "xxx-xxx-xxx",
savedSessionId: "",
});
// Assert
@ -694,9 +721,7 @@ describe("Actions", () => {
// Act
const response = await actions.loadSession(context, {
referralNumber: "123",
referralDate: new Date().toUTCString(),
referralCorrelationId: "xxx-xxx-xxx",
savedSessionId: "",
});
// Assert

View file

@ -1,15 +1,42 @@
html {
.has-error {
// START HOVER
&.list-button-horizontal,
&.list-button,
&.list-card,
&.list-button-horizontal {
&.list-button.list-group,
&.list-card {
border: 1px solid $red;
position: relative;
z-index: 4;
.button-content {
border: none;
}
&:hover {
@include box-shadow-hover($red-200);
z-index: 5;
}
input[type="radio"]:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $red;
}
}
&.ui-radio {
&:hover {
input[type="radio"],
input[type="checkbox"] {
@include box-shadow-hover($red-200);
}
}
}
.form-check-input {
&:focus {
box-shadow: 0 0 0 2.5px $red;
}
}
// END HOVER
&.list-button,
&.list-card {
color: $red;
@ -22,27 +49,11 @@ html {
box-shadow: 0 0 0 1px $red;
}
&:hover {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 0.5rem;
}
label {
border: 1px solid $red;
border-radius: 0.5rem;
}
label:hover {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 10px;
border: 1px solid $red;
}
}
&.list-button-horizontal {
color: $red;
label {
border: 1px solid $red;
&:hover {
box-shadow: 0px 0px 0px 4px $red-200;
}
}
input[type="checkbox"]:focus + label,
input[type="radio"]:focus + label {
box-shadow: 0 0 1px $red;
@ -75,23 +86,21 @@ html {
}
&.textbox-question,
&.dropdown-question {
input:hover {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 0.5rem;
border: 1px solid $red;
}
input:focus {
box-shadow: 0 0 0 2.5px $red;
}
p {
color: $red;
}
input,
select {
border: 1px solid transparent;
box-shadow: 0 0 0 1px $red;
border: 1px solid $red;
&:focus {
border: 1px solid transparent;
box-shadow: 0 0 0 2.5px $red;
}
&:hover {
box-shadow: 0px 0px 0px 4px $red-200;
border-radius: 0.5rem;
border: 1px solid $red;
}
}
select {

View file

@ -4,3 +4,7 @@
@mixin blue-gradient {
background: linear-gradient(270deg, $blue 0%, $blue-800 100%);
}
@mixin box-shadow-hover($color) {
box-shadow: 0 0 0 4px $color;
}

View file

@ -0,0 +1,15 @@
.base-input-button {
&:not(.has-error):hover {
cursor: pointer;
&.list-button,
&.list-button-horizontal,
&.list-card {
&:not(.selected) {
position: relative;
z-index: 4;
@include box-shadow-hover($blue-300);
}
}
}
}

View file

@ -2,7 +2,7 @@
<baseInputButton
v-bind="$props"
:buttonWrapperClasses="[
'list-group list-button-horizontal d-flex flex-column w-100',
'list-group list-button-horizontal d-flex flex-column w-100 base-input-button',
{ strong: isStrongStyling },
]"
v-model="selectedValue">
@ -88,18 +88,6 @@ export default {
width: 100%;
color: $gray-600;
&:hover {
@include media-breakpoint-up(sm) {
box-shadow: 0 0 0 4px $blue-300;
cursor: pointer;
z-index: 4 !important;
}
}
+ p {
display: none;
}
span {
font-size: 0.875rem;
}

View file

@ -1,7 +1,7 @@
<template>
<baseInputButton
v-bind="$props"
buttonWrapperClasses="list-group list-button rounded-3 d-flex flex-column w-100 mb-2"
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2"
v-model="selectedValue">
<div
:aria-label="buttonLabel"
@ -60,66 +60,54 @@ export default {
</script>
<style lang="scss" scoped>
.list-group {
.loader {
position: absolute;
}
&.list-button {
outline: none;
input[type="radio"],
input[type="checkbox"] {
position: static; //override bootstrap
.loader {
position: absolute;
}
.list-button {
outline: none;
input[type="radio"],
input[type="checkbox"] {
position: static; //override bootstrap
&:focus-visible + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content {
color: $black;
font-weight: 500;
background: $blue-100;
box-shadow: 0 0 0 1px $blue;
}
&:checked:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content p,
&:checked + .list-button-content span {
font-weight: 500;
}
&:checked + .list-button-content span:nth-child(2) {
font-weight: 400;
color: $gray-600;
}
&:focus-visible + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content {
color: $black;
font-weight: 500;
background: $blue-100;
box-shadow: 0 0 0 1px $blue;
}
&:checked:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content p,
&:checked + .list-button-content span {
font-weight: 500;
}
&:checked + .list-button-content span:nth-child(2) {
font-weight: 400;
color: $gray-600;
}
}
.list-button-content {
color: $gray-600;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $border-radius-lg;
border: 1px solid $gray-500;
width: 100%;
outline: none;
}
.list-button-content {
color: $gray-600;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $border-radius-lg;
border: 1px solid $gray-500;
width: 100%;
outline: none;
span {
&.small {
font-size: 0.75rem;
color: $gray-550;
}
}
&:hover {
@include media-breakpoint-up(sm) {
box-shadow: 0 0 0 4px $blue-300;
}
cursor: pointer;
}
+ p {
display: none;
span {
&.small {
font-size: 0.75rem;
color: $gray-550;
}
}
}

View file

@ -2,7 +2,7 @@
<baseInputButton
v-bind="$props"
:buttonWrapperClasses="[
'list-card w-100 rounded-3 d-flex align-items-center h-100',
'list-card w-100 rounded-3 d-flex align-items-center h-100 base-input-button',
{ horizontal: isWide },
]"
v-model="selectedValue">
@ -88,13 +88,6 @@ export default {
max-width: 100%;
}
&:hover {
@include media-breakpoint-up(sm) {
box-shadow: 0px 0px 0px 4px $blue-300;
border: 1px solid transparent;
}
}
input[type="checkbox"],
input[type="radio"] {
position: absolute;
@ -104,10 +97,6 @@ export default {
display: block;
position: relative;
&:hover {
cursor: pointer;
}
p {
color: $gray-600;
text-align: center;

View file

@ -1,7 +1,7 @@
<template>
<baseInputButton
v-bind="$props"
buttonWrapperClasses="ui-radio form-check"
buttonWrapperClasses="ui-radio form-check base-input-button"
inputClasses="form-check-input"
v-model="selectedValue">
<div class="d-flex align-items-start form-check-label">
@ -64,7 +64,7 @@ export default {
}
&:hover {
.form-check-input {
.form-check-input:not(:checked) {
box-shadow: 0 0 0 4px $blue-300;
}
}