Merge remote-tracking branch 'origin/develop' into defect/CSR-729-3

This commit is contained in:
Katie 2022-07-13 11:24:58 -04:00
commit bcd3ad8c22
23 changed files with 396 additions and 97 deletions

View file

@ -2,7 +2,7 @@
<template> <template>
<div :class="isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'"> <div :class="isOverflowScrollable ? 'button-question button-question-overflow' : 'button-question'">
<div v-if="questionText && answers && answers.length > 0" class="question-text d-flex"> <div v-if="questionText && answers && answers.length > 0" class="question-text d-flex">
<span class="fs-5 fw-bold w-100">{{ questionText }}</span> <span class="fw-bold w-100">{{ questionText }}</span>
</div> </div>
<div class="w-100 d-flex justify-content-center"> <div class="w-100 d-flex justify-content-center">
<fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="formattedGroupName"> <fieldset class="w-100" :aria-required=isRequired :class="getFieldSetClasses" :role="isMultiSelect ? 'group' : 'radiogroup'" :aria-labelledby="formattedGroupName">
@ -214,6 +214,8 @@ export default {
.question-text { .question-text {
margin-top: 1.5rem; margin-top: 1.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 1rem;
line-height: 1.625rem;
& > span { & > span {
text-align: center; text-align: center;

View file

@ -8,8 +8,8 @@
:questionText="q.questionText" :questionText="q.questionText"
:answers="q.answers" :answers="q.answers"
:groupName="`${questionData.glassName}-${questionData.glassLocation}-${i}`" :groupName="`${questionData.glassName}-${questionData.glassLocation}-${i}`"
v-model="selectedValuesArray" v-model="selectedValue"
:isRequired=true isRequired
:validationRules="validationRules" :validationRules="validationRules"
:clearOnUnmount=false :clearOnUnmount=false
/> />
@ -25,13 +25,14 @@ export default {
data() { data() {
return { return {
currentQuestionNum: 1, currentQuestionNum: 1,
questions: [{ "BlankObject": "NOT USED... placeholder for question #0"}], questions: [{ "BlankObject": "NOT USED... placeholder for question #0 to simplify indexing"}],
}; };
}, },
props: { props: {
questionData: Object, questionData: Object,
validationRules: String, validationRules: String,
modelValue: Array, modelValue: Array,
partIndex: Number,
}, },
created() { created() {
this.questionData.partQuestions.map((q, i) => { this.questionData.partQuestions.map((q, i) => {
@ -61,12 +62,11 @@ export default {
}); });
}, },
computed: { computed: {
selectedValuesArray: { selectedValue: {
get: function() { get: function() {
return []; return "";
}, },
set: function(returnedAnswerArray) { set: function(returnedAnswer) {
const returnedAnswer = returnedAnswerArray[returnedAnswerArray.length-1];
const isQuestionChainComplete = this.handleReturnedAnswer(returnedAnswer); const isQuestionChainComplete = this.handleReturnedAnswer(returnedAnswer);
if (isQuestionChainComplete) { if (isQuestionChainComplete) {
@ -102,6 +102,7 @@ export default {
// set this question as "answered" // set this question as "answered"
this.questions[questionNum].answerSelected = questionAnswerText; this.questions[questionNum].answerSelected = questionAnswerText;
this.questions[questionNum].answerNumber = questionNum;
// update to next question index // update to next question index
this.currentQuestionNum = questionType === "nextQuestion" ? parseInt(questionAnswer) : parseInt(questionNum); // update count to display next question this.currentQuestionNum = questionType === "nextQuestion" ? parseInt(questionAnswer) : parseInt(questionNum); // update count to display next question
@ -116,16 +117,17 @@ export default {
answeredQuestions.push({ answeredQuestions.push({
questionText: q.questionText, questionText: q.questionText,
selectedAnswerText: q.answerSelected, selectedAnswerText: q.answerSelected,
questionNum: questionNum, questionNum: q.answerNumber,
}); });
} }
}); });
return { return {
answerResult: questionAnswer, answerResult: questionAnswer,
answeredQuestions: answeredQuestions, answeredQuestions: answeredQuestions,
partIndex: this.partIndex,
}; };
} }
} },
}, },
watch: { watch: {
currentQuestion: { currentQuestion: {

View file

@ -5,7 +5,7 @@ const analyticsPageEvents = {
// GA Constants // GA Constants
const GaEvents = { const GaEvents = {
GENERIC_EVENT: 'ga_Event', GENERIC_EVENT: 'event',
PAGE_VIEW_EVENT : 'logPageview' PAGE_VIEW_EVENT : 'logPageview'
}; };

View file

@ -55,6 +55,10 @@ const endpoints = {
url: "/parts/api/v1/parts/parts-or-questions", url: "/parts/api/v1/parts/parts-or-questions",
method: "POST", method: "POST",
}, },
GetParts: {
url: "/parts/api/v1/parts/parts",
method: "POST",
},
SaveOrder: { SaveOrder: {
url: "/order/api/v1/order/save", url: "/order/api/v1/order/save",
method: "POST", method: "POST",

View file

@ -20,9 +20,10 @@ const storeActions = {
LOOKUP_VIN_BY_ADDRESS: "lookupVinByAddress", LOOKUP_VIN_BY_ADDRESS: "lookupVinByAddress",
GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions", GET_PARTS_OR_QUESTIONS: "getPartsOrQuestions",
GET_PARTS: "getParts",
SAVE_ORDER: "saveOrder", SAVE_ORDER: "saveOrder",
LOAD_ORDER: "loadOrder", LOAD_ORDER: "loadOrder",
SET_REFERRAL_INFORMATION: "setReferralInformation", UPDATE_STORE_WITH_SAVE_ORDER_RESPONSE: "updateStoreWithSaveOrderResponse",
VALIDATE_ZIP: "validateZip", VALIDATE_ZIP: "validateZip",
LOG_EXPERIMENT_EXPOSURE: "logExperimentExposure", LOG_EXPERIMENT_EXPOSURE: "logExperimentExposure",
LOG_PAGE_VIEW: "logPageView", LOG_PAGE_VIEW: "logPageView",
@ -31,7 +32,6 @@ const storeActions = {
GET_EXPERIMENTS_BY_USER: "GetExperimentsByUser", GET_EXPERIMENTS_BY_USER: "GetExperimentsByUser",
CLEAR_VIN: "clearVin", CLEAR_VIN: "clearVin",
// DEPENDENCY MUTATIONS // DEPENDENCY MUTATIONS
RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies", RESET_VEHICLE_STATE_AND_DEPENDENCIES: "resetVehicleAndDependencies",
RESET_DAMAGE_STATE_AND_DEPENDENCIES: "resetDamageAndDependencies", RESET_DAMAGE_STATE_AND_DEPENDENCIES: "resetDamageAndDependencies",
@ -52,6 +52,7 @@ const storeActions = {
SAVE_VIN: "saveVin", SAVE_VIN: "saveVin",
SAVE_REGISTRATION_ADDRESS_LOOKUP: "saveRegistrationAddressLookup", SAVE_REGISTRATION_ADDRESS_LOOKUP: "saveRegistrationAddressLookup",
SAVE_GLASS_PARTS: "saveGlassParts", SAVE_GLASS_PARTS: "saveGlassParts",
SAVE_PART_QUESTION_ANSWERS: "savePartQuestionAnswers",
}; };
export { storeActions }; export { storeActions };

View file

@ -16,6 +16,7 @@ const storeMutations = {
UPDATE_IS_REPAIR: "updateIsRepair", UPDATE_IS_REPAIR: "updateIsRepair",
UPDATE_NUMBER_OF_CHIPS: "updateNumberOfChips", UPDATE_NUMBER_OF_CHIPS: "updateNumberOfChips",
UPDATE_GLASS_TO_REPLACE: "updateGlassToReplace", UPDATE_GLASS_TO_REPLACE: "updateGlassToReplace",
UPDATE_PART_QUESTION_ANSWERS: "updatePartQuestionAnswers",
UPDATE_GLASS_PARTS: "updateGlassParts", UPDATE_GLASS_PARTS: "updateGlassParts",
UPDATE_REGISTRATION_LICENSE_PLATE : "updateRegistrationLicensePlate", UPDATE_REGISTRATION_LICENSE_PLATE : "updateRegistrationLicensePlate",
@ -38,6 +39,8 @@ const storeMutations = {
UPDATE_REFERRAL_DATE: "updateReferralDate", UPDATE_REFERRAL_DATE: "updateReferralDate",
UPDATE_REFERRAL_CORRELATION_ID: "updateReferralCorrelationId", UPDATE_REFERRAL_CORRELATION_ID: "updateReferralCorrelationId",
UPDATE_PARENT_ACCT_NUMBER: "updateParentAcctNumber", UPDATE_PARENT_ACCT_NUMBER: "updateParentAcctNumber",
UPDATE_SAVE_QUOTE_ID: "updateSaveQuoteId",
UPDATE_CRM_CUSTOMER_ID: "updateCrmCustomerId",
// EVENT BUS MUTATIONS // EVENT BUS MUTATIONS
ADD_EVENT_TO_BUS: "addEventToBus", ADD_EVENT_TO_BUS: "addEventToBus",
@ -53,6 +56,8 @@ const storeMutations = {
// OTHER MUTATIONS // OTHER MUTATIONS
UPDATE_PAGE_DATA: "updatePageData", UPDATE_PAGE_DATA: "updatePageData",
UPDATE_STATE_WITH_ORDER_INFORMATION: "updateStateWithOrderInformation", UPDATE_STATE_WITH_ORDER_INFORMATION: "updateStateWithOrderInformation",
UPDATE_SAVE_ORDER_PROMISE: "updateSaveOrderPromise",
UPDATE_LAST_PAGE_VISITED: "updateLastPageVisited",
}; };
export { storeMutations }; export { storeMutations };

View file

@ -308,8 +308,11 @@ describe("navigateToHeritageFunnel", () => {
const mockReferralNumber = "2"; const mockReferralNumber = "2";
const mockCorrelationId = "55"; const mockCorrelationId = "55";
const mockReferralDate = "2022"; const mockReferralDate = "2022";
const mockAccountNumber = "167132";
const mockSaveQuoteId = "xxx-xxx-xxx";
const mockCrmCustomerId = "xxx-xxx-xxx"
const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate); const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, mockAccountNumber, mockSaveQuoteId, mockCrmCustomerId);
const mockData = { const mockData = {
actionList: [{ actionList: [{
@ -340,8 +343,11 @@ describe("navigateToHeritageFunnel", () => {
const mockReferralNumber = "2"; const mockReferralNumber = "2";
const mockCorrelationId = "55"; const mockCorrelationId = "55";
const mockReferralDate = "2022"; const mockReferralDate = "2022";
const mockAccountNumber = "167132";
const mockSaveQuoteId = "xxx-xxx-xxx";
const mockCrmCustomerId = "xxx-xxx-xxx"
const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate); const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, mockAccountNumber, mockSaveQuoteId, mockCrmCustomerId);
const mockData = { const mockData = {
actionList: [{ actionList: [{

View file

@ -1,6 +1,8 @@
import { storeActions } from "@/constants/store-actions.js"; import { storeActions } from "@/constants/store-actions.js";
import { getFunnelCookie, updateOrCreateFunnelCookie, deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js"; import { getFunnelCookie, updateOrCreateFunnelCookie, deleteFunnelCookie } from "@/helpers/heritage-integration/cookie-helper.js";
import baseMixin from "@/mixins/base-mixin"; import baseMixin from "@/mixins/base-mixin";
import store from "@/store";
import { storeMutations } from "@/constants/store-mutations";
/* /*
Will call API and hydrate state with data from API if present. If there is no order present Will call API and hydrate state with data from API if present. If there is no order present
@ -34,18 +36,20 @@ export async function loadOrderIfPresent() {
update the cookie. update the cookie.
*/ */
export async function saveOrder() { export async function saveOrder() {
const savedOrderInfo = await baseMixin.methods.dispatchStoreAction(storeActions.SAVE_ORDER); var saveOrderPromise;
if (store.getters.applicationUser.saveOrderPromise) {
// Save the referral information back from the store. // queue newest request after current saveOrderPromise resolves
await baseMixin.methods.dispatchStoreAction(storeActions.SET_REFERRAL_INFORMATION, { saveOrderPromise = store.getters.applicationUser.saveOrderPromise.then(() => {
referralNumber: savedOrderInfo.data.referralNumber.toString(), // get a new saveOrderPromise
referralCorrelationId: savedOrderInfo.data.referralCorrelationId, return saveOrderHelper();
referralDate: savedOrderInfo.data.referralDate, });
accountNumber: savedOrderInfo.data.accountNumber.toString() } else {
}, false); // create an initial saveOrderPromise
saveOrderPromise = saveOrderHelper();
// Update the cookie with the referral information when saved. }
updateOrCreateFunnelCookie(); store.commit(storeMutations.UPDATE_SAVE_ORDER_PROMISE, saveOrderPromise);
// await here to allow for a caller to await and make the function synchronous
await saveOrderPromise;
} }
@ -66,3 +70,22 @@ async function loadOrder(referralNumber, referralDate, referralCorrelationId, ac
return response; return response;
} }
/*
Encapsulates asynchronous Save Order logic inside a promise to allow for Save Order queuing
*/
async function saveOrderHelper() {
const savedOrderInfo = await baseMixin.methods.dispatchStoreAction(storeActions.SAVE_ORDER);
// Update the store with information received from the saveOrder response
await baseMixin.methods.dispatchStoreAction(storeActions.UPDATE_STORE_WITH_SAVE_ORDER_RESPONSE, {
referralNumber: savedOrderInfo.data.referralNumber.toString(),
referralCorrelationId: savedOrderInfo.data.referralCorrelationId,
referralDate: savedOrderInfo.data.referralDate,
accountNumber: savedOrderInfo.data.accountNumber.toString(),
saveQuoteId: savedOrderInfo.data.saveQuoteId,
crmCustomerId: savedOrderInfo.data.crmCustomerId.toString(),
}, false);
// Update the cookie with the referral information when saved.
updateOrCreateFunnelCookie();
}

View file

@ -111,9 +111,11 @@ describe("saveOrder", () => {
const mockReferralNumber = "2"; const mockReferralNumber = "2";
const mockCorrelationId = "55"; const mockCorrelationId = "55";
const mockReferralDate = "2022"; const mockReferralDate = "2022";
const mockAccountNumber = "5"; const mockAccountNumber = "167132";
const mockSaveQuoteId = "xxx-xxx-xxx";
const mockCrmCustomerId = "xxx-xxx-xxx"
const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, mockAccountNumber); const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, mockAccountNumber, mockSaveQuoteId, mockCrmCustomerId);
const mockData = { const mockData = {
actionList: [ actionList: [
@ -122,7 +124,7 @@ describe("saveOrder", () => {
data: mockOrderInfo, data: mockOrderInfo,
}, },
{ {
actionName: storeActions.SET_REFERRAL_INFORMATION actionName: storeActions.UPDATE_STORE_WITH_SAVE_ORDER_RESPONSE
} }
] ]
} }
@ -134,21 +136,26 @@ describe("saveOrder", () => {
// Assert // Assert
expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(storeActions.SAVE_ORDER); expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(storeActions.SAVE_ORDER);
expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(storeActions.SET_REFERRAL_INFORMATION, { expect(mocks.baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(storeActions.UPDATE_STORE_WITH_SAVE_ORDER_RESPONSE, {
referralNumber: mockReferralNumber, referralNumber: mockReferralNumber,
referralDate: mockReferralDate, referralDate: mockReferralDate,
referralCorrelationId: mockCorrelationId, referralCorrelationId: mockCorrelationId,
accountNumber: mockAccountNumber accountNumber: mockAccountNumber,
saveQuoteId: mockSaveQuoteId,
crmCustomerId: mockCrmCustomerId,
}, false); }, false);
}); });
test("saveOrder => should update DidHeritageFunnelUpdateLast cookie value to false", async () => { test("saveOrder => should update DidHeritageFunnelUpdateLast cookie value to false", async () => {
// Arrange // Arrange
const testReferralNumber = 1566818; const mockReferralNumber = 1566818;
const testReferralDate = "2022-03-15T10:56:24.597"; const mockReferralDate = "2022-03-15T10:56:24.597";
const testReferralCorrelationId = "404d2b04-f86e-45c3-b373-127b6217b060"; const mockReferralCorrelationId = "404d2b04-f86e-45c3-b373-127b6217b060";
const mockAccountNumber = "167132";
const mockSaveQuoteId = "xxx-xxx-xxx";
const mockCrmCustomerId = "xxx-xxx-xxx"
const mockOrderInfo = getMockOrderInfo(testReferralNumber, testReferralCorrelationId, testReferralDate); const mockOrderInfo = getMockOrderInfo(mockReferralNumber, mockReferralCorrelationId, mockReferralDate, mockAccountNumber, mockSaveQuoteId, mockCrmCustomerId);
const mockData = { const mockData = {
actionList: [{ actionList: [{
@ -161,9 +168,9 @@ describe("saveOrder", () => {
setupMocksForJsFiles(mockData); setupMocksForJsFiles(mockData);
const testCookieValue = { const testCookieValue = {
ReferralNumber: testReferralNumber, ReferralNumber: mockReferralNumber,
ReferralDate: testReferralDate, ReferralDate: mockReferralDate,
ReferralCorrelationId: testReferralCorrelationId, ReferralCorrelationId: mockReferralCorrelationId,
ShouldResetState: false, ShouldResetState: false,
DidHeritageFunnelUpdateLast: true DidHeritageFunnelUpdateLast: true
} }

View file

@ -90,12 +90,14 @@ export function removeAllTestCookies() {
}); });
} }
export function getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, accountNumber = "0") { export function getMockOrderInfo(mockReferralNumber, mockCorrelationId, mockReferralDate, accountNumber = "0", saveQuoteId, crmCustomerId) {
return { return {
referralNumber: mockReferralNumber, referralNumber: mockReferralNumber,
referralCorrelationId: mockCorrelationId, referralCorrelationId: mockCorrelationId,
referralDate: mockReferralDate, referralDate: mockReferralDate,
accountNumber: accountNumber accountNumber: accountNumber,
saveQuoteId: saveQuoteId,
crmCustomerId: crmCustomerId,
} }
} }

View file

@ -6,7 +6,7 @@
:questionText="questionText" :questionText="questionText"
:answers="vehicles" :answers="vehicles"
v-model="selectedVehicleVinAsArray" v-model="selectedVehicleVinAsArray"
isRequired=true isRequired
:validation-rules="validationRules" :validation-rules="validationRules"
/> />
<alert <alert

View file

@ -12,12 +12,12 @@
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" /> <funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<alert <alert
ref="alertFoundMultipleVehicles" ref="alertFoundMultipleVehicles"
class="my-5" class="my-5"
alertClass="alert-warning" alertClass="alert-warning"
:manualHeadline="AlertFoundMultipleVehiclesHeader" :manualHeadline="AlertFoundMultipleVehiclesHeader"
manualCopy="" manualCopy=""
v-bind:isDismissible="false" v-bind:isDismissible="false"
/> />
<addressVehiclesQuestion <addressVehiclesQuestion
ref="addressVehiclesQuestion" ref="addressVehiclesQuestion"
cmsWidgetName="VehicleConfirmationQuestion" cmsWidgetName="VehicleConfirmationQuestion"

View file

@ -1,33 +1,67 @@
<template> <template>
<div class="page-container-grouped-styles"> <Form
<funnelHeader cmsWidgetName="FunnelHeaderWidget" /> @submit="onSubmit"
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage=false /> @invalid-submit="onInvalidSubmit"
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" /> ref="theForm"
<div class="fade-on-route-transition sub-container make-tall"> v-slot="{ meta }"
<h1>Part Questions Page Placeholder</h1> >
<funnel-footer cmsWidgetName="FunnelFooterWidget" @back-clicked="backButtonAction" /> <div class="page-container-grouped-styles part-questions">
</div> <funnelHeader cmsWidgetName="FunnelHeaderWidget" />
</div> <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="(part, i) in partsQuestionsData" :key="i">
<questionChain
ref="questionChain"
v-model="selectedModel"
:questionData="part"
:partIndex="i"
v-if="showThisPartQuestionChain(part, i)"
validationRules="questions-required"
/>
</div>
<funnel-footer
ref="funnelFooter"
cmsWidgetName="FunnelFooterWidget"
:isForwardActionDisabled="waitingForLoad || !meta.valid"
@back-clicked="backButtonAction"
@ForwardClicked="forwardButtonAction"
/>
</div>
</div>
</Form>
</template> </template>
<script> <script>
// Components // Components
import funnelHeader from "@/common-components/funnel-header/funnel-header"; import funnelHeader from "@/common-components/funnel-header/funnel-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner"; 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 funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer"; import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
// Supporting Files // Supporting Files
import { import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
fetchCmsContentForPage import { settleAllPromises } from "@/helpers/layout-helper";
} from "@/helpers/cms-content-helper";
import {
settleAllPromises
} from "@/helpers/layout-helper";
import store from "@/store"; import store from "@/store";
import { import { storeActions } from "@/constants/store-actions";
fmgPageValues import { storeMutations } from "@/constants/store-mutations";
} from "@/router/router-constants/fmgPage-values"; 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";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
export default { export default {
name: "part-questions", name: "part-questions",
@ -36,10 +70,12 @@ export default {
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage); const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
// Settle promises and get results // Settle promises and get results
const promiseResultMap = [{ const promiseResultMap = [
resultKey: "cmsContent", {
promise: cmsContentPromise, resultKey: "cmsContent",
}, ]; promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap); const resultMap = await settleAllPromises(promiseResultMap);
@ -48,7 +84,46 @@ export default {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
}); });
}, },
data() {
return {
selectedModel: [],
partsQuestionsData: [],
waitingForLoad: true,
currentPartNum: 0,
glassNameAndParts: [],
hasMultipleParts: false,
};
},
computed: {
AlertFewMoreQuestionsHeader() {
return this.getCmsContent("AlertPartsQuestions", "HeadlineText");
},
AlertFewMoreQuestionsCopy() {
return this.getCmsContent("AlertPartsQuestions", "BodyText");
},
},
async mounted() {
const questionData = await this.$store.getters.pageData(fmgPageValues.PART_QUESTIONS);
// Map API result data, to part-questions data structure
this.partsQuestionsData = questionData.partsOrQuestions.filter((p) => {
if (Array.isArray(p.partQuestions) && p.partQuestions.length > 0) {
return {
glassName: p.glassName,
glassLocation: p.glassLocation,
partQuestions: p.partQuestions,
};
}
});
this.waitingForLoad = false;
},
methods: { methods: {
showThisPartQuestionChain(part, i) {
if (part.partQuestions?.length < 1) { return false; } // return false if only one partQuestion
if (this.currentPartNum === i || part.answerData?.answerResult.length > 0) { return true; }
return false;
},
backButtonAction() { backButtonAction() {
// route to move backwards // route to move backwards
this.$router.navigate( this.$router.navigate(
@ -56,16 +131,93 @@ export default {
this.$route this.$route
); );
}, },
async forwardButtonAction() {
const partQuestionAnswersArray = this.partsQuestionsData.map((item) => {
return {
glassLocation: item.glassLocation,
glassName: item.glassName,
result: item.answerData.answerResult,
answeredQuestions: item.answerData.answeredQuestions,
};
});
// save to vuex store as order.damage.partQuestionAnswers (array)
await this.dispatchStoreAction(this.storeActions.SAVE_PART_QUESTION_ANSWERS, partQuestionAnswersArray, false);
// call API parts method
const partsLookup = await this.dispatchStoreAction(storeActions.GET_PARTS)
.catch(() => {
this.$refs.funnelFooter.removeLoader();
});
if (!partsLookup) { return }
this.glassNameAndParts = partsLookup.data.glassNameAndParts;
// test response data for multiple parts
this.hasMultipleParts = this.glassNameAndParts.some((glass) => glass.parts?.length > 1);
// loop through all glass items
const collectedGlassParts = [];
this.glassNameAndParts.forEach((glass) => {
if (Array.isArray(glass.parts) && glass.parts.length === 1) {
const singlePart = glass.parts[0];
collectedGlassParts.push({
"partNumber": singlePart.partNumber,
"description": singlePart.description,
"color": singlePart.color,
"requiresRecalibration": singlePart.requiresRecalibration,
"childParts": singlePart.childParts,
"price": singlePart.price,
})
}
});
store.commit(storeMutations.UPDATE_GLASS_PARTS, collectedGlassParts);
this.navigateForward();
},
async navigateForward() {
if (this.hasMultipleParts) {
// if multiple parts on any glass
// go to vehicle-parts page and pass the partsData
this.$router.navigate(this.navigationScenarios.ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS,this.$route,{},{},{partsOrQuestions: this.glassNameAndParts});
} else {
// if single parts only
// go to quote page
this.$router.navigate(this.navigationScenarios.ANSWERED_QUESTIONS_WITH_SINGLE_PART,this.$route);
}
},
arePagePrerequisitesValid() { arePagePrerequisitesValid() {
return Object.keys(store.getters.pageData(fmgPageValues.PART_QUESTIONS)).length !== 0; return Object.keys(store.getters.pageData(fmgPageValues.PART_QUESTIONS)).length > 0;
},
},
watch: {
selectedModel(model) {
this.partsQuestionsData[model.partIndex].answerData = {
answerResult: model.answerResult,
answeredQuestions: model.answeredQuestions,
}
this.currentPartNum = model.partIndex + 1;
}, },
}, },
data() {},
components: { components: {
funnelHeader, funnelHeader,
vehicleBanner, vehicleBanner,
alert,
questionChain,
funnelSubHeader, funnelSubHeader,
funnelFooter, funnelFooter,
Form,
}, },
}; };
</script> </script>
<style lang="scss">
.part-questions {
.question-text {
margin-bottom: .5rem;
span {
text-align: left;
}
}
}
</style>

View file

@ -8,7 +8,7 @@
groupName="ChooseVehicleMake" groupName="ChooseVehicleMake"
textPosition="text-start" textPosition="text-start"
v-model="selectedValue" v-model="selectedValue"
isRequired=true isRequired
/> />
</template> </template>

View file

@ -8,7 +8,7 @@
groupName="ChooseVehicleModel" groupName="ChooseVehicleModel"
textPosition="text-start" textPosition="text-start"
v-model="selectedValue" v-model="selectedValue"
isRequired=true isRequired
/> />
</template> </template>

View file

@ -8,7 +8,7 @@
groupName="ChooseVehicleStyle" groupName="ChooseVehicleStyle"
textPosition="text-start" textPosition="text-start"
v-model="selectedValue" v-model="selectedValue"
isRequired=true isRequired
/> />
</template> </template>

View file

@ -8,7 +8,7 @@
groupName="ChooseVehicleYear" groupName="ChooseVehicleYear"
textPosition="text-start" textPosition="text-start"
v-model="selectedValue" v-model="selectedValue"
isRequired=true isRequired
/> />
</template> </template>

View file

@ -1,6 +1,7 @@
// Supporting files // Supporting files
import { createWebHistory, createRouter } from "vue-router"; import { createWebHistory, createRouter } from "vue-router";
import { storeActions } from "@/constants/store-actions"; import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "../constants/store-mutations";
import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js"; import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader.js";
import { routingTable } from "@/router/router-constants/routing-table.js"; import { routingTable } from "@/router/router-constants/routing-table.js";
import { globalEvents, globalEventTypes } from "@/constants/events"; import { globalEvents, globalEventTypes } from "@/constants/events";
@ -113,7 +114,9 @@ const router = createRouter({
//---------------------------------------------------------- Router Functions ---------------------------------------------------------- //---------------------------------------------------------- Router Functions ----------------------------------------------------------
router.afterEach(async (to, from) => { router.afterEach((to, from) => {
// Update lastPageVisited in the store
store.commit(storeMutations.UPDATE_LAST_PAGE_VISITED, to.name);
// Push page view to GA // Push page view to GA
analyticsMixin.methods.pushPageViewToGA(to.query[queryStrings.FMG_PAGE]); analyticsMixin.methods.pushPageViewToGA(to.query[queryStrings.FMG_PAGE]);
@ -152,9 +155,9 @@ async function navigate(scenario, currentRoute, optionalQuery = {}, optionalPara
// Append page data to the store for the NEXT page, if any. It will be an empty object if none is provided. // Append page data to the store for the NEXT page, if any. It will be an empty object if none is provided.
baseMixin.methods.savePageDataToStore(destinationFmgPageValue, optionalPageData); baseMixin.methods.savePageDataToStore(destinationFmgPageValue, optionalPageData);
// if cookie and referralNumber/Date exists // if cookie and referralNumber/Date exists OR an emailAddress has been saved
if (getFunnelCookie()?.ReferralNumber && getFunnelCookie()?.ReferralDate) { if ((getFunnelCookie()?.ReferralNumber && getFunnelCookie()?.ReferralDate) || store.getters.order.customer?.emailAddress) {
await saveOrder(); saveOrder();
} }
router.push({ router.push({

View file

@ -17,6 +17,8 @@ const navigationScenarios = {
SELECTED_MANUAL_VIN: "SELECTED_MANUAL_VIN", SELECTED_MANUAL_VIN: "SELECTED_MANUAL_VIN",
SELECTED_LICENSE_PLATE: "SELECTED_LICENSE_PLATE", SELECTED_LICENSE_PLATE: "SELECTED_LICENSE_PLATE",
SELECTED_HOME_ADDRESS: "SELECTED_HOME_ADDRESS", SELECTED_HOME_ADDRESS: "SELECTED_HOME_ADDRESS",
ANSWERED_QUESTIONS_WITH_SINGLE_PART: "ANSWERED_QUESTIONS_WITH_SINGLE_PART",
ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS: "ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS",
}; };
export { navigationScenarios }; export { navigationScenarios };

View file

@ -216,7 +216,15 @@ const routingTable = [
{ {
scenario: navigationScenarios.CLICKED_BACK, scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP destinationFmgPageValue: fmgPageValues.VIN_LOOKUP
} },
{
scenario: navigationScenarios.ANSWERED_QUESTIONS_WITH_MULTIPLE_PARTS,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
},
{
scenario: navigationScenarios.ANSWERED_QUESTIONS_WITH_SINGLE_PART,
destinationFmgPageValue: fmgPageValues.QUOTE,
},
] ]
}, },
]; ];

View file

@ -44,6 +44,7 @@ const getDefaultState = () => {
isRepair: null, isRepair: null,
numberOfChips: null, numberOfChips: null,
glassToReplace: null, glassToReplace: null,
partQuestionAnswers: null,
}, },
lineItems: { lineItems: {
glassParts: null, glassParts: null,
@ -63,7 +64,11 @@ const getDefaultState = () => {
applicationUser: { applicationUser: {
eventBus: [], eventBus: [],
pageData: {}, pageData: {},
savedSessionTimeout: getDateForSavedSessionTimeout() savedSessionTimeout: getDateForSavedSessionTimeout(),
saveOrderPromise: null,
saveQuoteId: null,
crmCustomerId: null,
lastPageVisited: null,
}, },
} }
}; };
@ -112,6 +117,9 @@ export const mutations = {
updateGlassToReplace(state, glassToReplace) { updateGlassToReplace(state, glassToReplace) {
state.order.damage.glassToReplace = glassToReplace; state.order.damage.glassToReplace = glassToReplace;
}, },
updatePartQuestionAnswers(state, answersArray) {
state.order.damage.partQuestionAnswers = answersArray;
},
updateGlassParts(state, partsData) { updateGlassParts(state, partsData) {
state.order.lineItems.glassParts = partsData; state.order.lineItems.glassParts = partsData;
}, },
@ -196,6 +204,19 @@ export const mutations = {
state.order.serviceLocation.zipCode = serviceLocationInfo.zipCode; state.order.serviceLocation.zipCode = serviceLocationInfo.zipCode;
}, },
// applicationUser MUTATIONS
updateSaveOrderPromise(state, saveOrderPromise){
state.applicationUser.saveOrderPromise = saveOrderPromise;
},
updateSaveQuoteId(state, saveQuoteId) {
state.applicationUser.saveQuoteId = saveQuoteId;
},
updateCrmCustomerId(state, crmCustomerId) {
state.applicationUser.crmCustomerId = crmCustomerId;
},
updateLastPageVisited(state, lastPageVisited) {
state.applicationUser.lastPageVisited = lastPageVisited
},
// EVENT BUS MUTATIONS // EVENT BUS MUTATIONS
addEventToBus(state, event) { addEventToBus(state, event) {
state.applicationUser.eventBus.push(event); state.applicationUser.eventBus.push(event);
@ -472,6 +493,17 @@ export const actions = {
} }
}); });
}, },
// Misc Actions
updateStoreWithSaveOrderResponse(context, { referralNumber, referralDate, referralCorrelationId, accountNumber, saveQuoteId, crmCustomerId }) {
context.commit(storeMutations.UPDATE_REFERRAL_NUMBER, referralNumber);
context.commit(storeMutations.UPDATE_REFERRAL_DATE, referralDate);
context.commit(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, referralCorrelationId);
context.commit(storeMutations.UPDATE_PARENT_ACCT_NUMBER, accountNumber);
context.commit(storeMutations.UPDATE_SAVE_QUOTE_ID, saveQuoteId);
context.commit(storeMutations.UPDATE_CRM_CUSTOMER_ID, crmCustomerId);
},
logPageView(context, { userId, sessionKey, pageName, sessionId, action, event, shouldUseSessionId }) { logPageView(context, { userId, sessionKey, pageName, sessionId, action, event, shouldUseSessionId }) {
var payload = { var payload = {
userId: userId, userId: userId,
@ -555,7 +587,7 @@ export const actions = {
}); });
}, },
// Parts API Actions // PartsOrQuestions API Actions
getPartsOrQuestions(context) { getPartsOrQuestions(context) {
const vehicle = context.getters.vehicle; const vehicle = context.getters.vehicle;
const damage = context.getters.damage; const damage = context.getters.damage;
@ -578,11 +610,37 @@ export const actions = {
}); });
}, },
// Parts API Actions
getParts(context) {
const vehicle = context.getters.vehicle;
const damage = context.getters.damage;
const order = context.state.order;
const carId = vehicle.carId;
const glassArray = damage.glassToReplace;
const resultsArray = damage.partQuestionAnswers;
const zipCode = order.serviceLocation.zipCode;
const vin = vehicle.vin;
return globalMethods.callHttpClient({
method: endpoints.GetParts.method,
endpoint: endpoints.GetParts.url,
payload: {
carId: carId,
glass: glassArray,
answerResults: resultsArray,
zip: zipCode,
vin: vin
},
});
},
// Order API Actions // Order API Actions
saveOrder(context) { saveOrder(context) {
const vehicle = context.getters.vehicle; const vehicle = context.getters.vehicle;
const damage = context.getters.damage; const damage = context.getters.damage;
const order = context.state.order; const order = context.state.order;
const applicationUser = context.getters.applicationUser;
return globalMethods.callHttpClient({ return globalMethods.callHttpClient({
method: endpoints.SaveOrder.method, method: endpoints.SaveOrder.method,
@ -621,7 +679,12 @@ export const actions = {
}, },
referralNumber: order.referralNumber?.toString(), // TODO It'd be nice to save these as strings in the first place referralNumber: order.referralNumber?.toString(), // TODO It'd be nice to save these as strings in the first place
referralDate: order.referralDate, referralDate: order.referralDate,
accountNumber: order.accountNumber?.toString() accountNumber: order.accountNumber?.toString(),
existingPromoCode: null,
lastPage: applicationUser.lastPageVisited,
crmCustomerId: applicationUser.crmCustomerId,
saveQuoteId: applicationUser.saveQuoteId,
}, },
}); });
}, },
@ -787,6 +850,10 @@ export const actions = {
context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo); context.commit(storeMutations.UPDATE_REGISTRATION, registrationInfo);
} }
}, },
savePartQuestionAnswers(context, partQuestionAnswersArray) {
//Save new values
context.commit(storeMutations.UPDATE_PART_QUESTION_ANSWERS, partQuestionAnswersArray);
},
// Misc order actions // Misc order actions
saveServiceLocation(context, serviceLocationInfo) { saveServiceLocation(context, serviceLocationInfo) {

View file

@ -571,12 +571,17 @@ describe("Actions", () => {
registration: {} registration: {}
}, },
damage: {}, damage: {},
applicationUser: {
lastPageVisited: "test-page",
crmCustomerId: "xxx-xxx-xxx",
saveQuoteId: "xxx-xxx-xxx"
}
}; };
context.state = { context.state = {
order: { order: {
serviceLocation: {}, serviceLocation: {},
customer: {} customer: {}
} },
}; };
globalMethods.callHttpClient.mockImplementation(() => { globalMethods.callHttpClient.mockImplementation(() => {
@ -611,7 +616,7 @@ describe("Actions", () => {
expect(commit).toBeCalledWith(storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, { "referralNumber": 123 }); expect(commit).toBeCalledWith(storeMutations.UPDATE_STATE_WITH_ORDER_INFORMATION, { "referralNumber": 123 });
}); });
it("setReferralInformation, should call commit three times", () => { it("updateStoreWithSaveOrderResponse, should call commit six times", () => {
// Arrange // Arrange
const context = state; const context = state;
const commit = jest.fn(); const commit = jest.fn();
@ -619,12 +624,23 @@ describe("Actions", () => {
context.commit = commit; context.commit = commit;
// Act // Act
actions.setReferralInformation(context, { referralNumber: "123", referralDate: new Date().toUTCString(), referralCorrelationId: "xxx-xxx-xxx" }); actions.updateStoreWithSaveOrderResponse(context,
{
referralNumber: "123",
referralDate: new Date().toUTCString(),
referralCorrelationId: "xxx-xxx-xxx",
accountNumber: "167132",
saveQuoteId: "xxx-xxx-xxx",
crmCustomerId: "xxx-xxx-xxx",
});
// Assert // Assert
expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_NUMBER, "123"); expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_NUMBER, "123");
expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_DATE, new Date().toUTCString()); expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_DATE, new Date().toUTCString());
expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, "xxx-xxx-xxx"); expect(commit).toBeCalledWith(storeMutations.UPDATE_REFERRAL_CORRELATION_ID, "xxx-xxx-xxx");
expect(commit).toBeCalledWith(storeMutations.UPDATE_PARENT_ACCT_NUMBER, "167132");
expect(commit).toBeCalledWith(storeMutations.UPDATE_SAVE_QUOTE_ID, "xxx-xxx-xxx");
expect(commit).toBeCalledWith(storeMutations.UPDATE_CRM_CUSTOMER_ID, "xxx-xxx-xxx");
}); });
it("logPageView action, should return nothing", async () => { it("logPageView action, should return nothing", async () => {

View file

@ -6,8 +6,7 @@
@keyup.up="handleKeyupArrow()" @keyup.up="handleKeyupArrow()"
@keyup.down="handleKeyupArrow()" @keyup.down="handleKeyupArrow()"
@keyup.left="handleKeyupArrow()" @keyup.left="handleKeyupArrow()"
@keyup.right="handleKeyupArrow()" @keyup.right="handleKeyupArrow()">
>
<input <input
:type="isMultiSelect ? 'checkbox' : 'radio'" :type="isMultiSelect ? 'checkbox' : 'radio'"
:id="buttonID" :id="buttonID"
@ -107,26 +106,25 @@ export default {
this.isLoaderDisplayed = true; this.isLoaderDisplayed = true;
}, },
handleInputChange() { handleInputChange() {
if(!this.selectingInitiatesLoad) { if(this.selectingInitiatesLoad) {
this.handleCheckChange(); this.triggerButton();
} }
else {
this.handleCheckChange();
}
}, },
handleKeyupArrow() { handleKeyupArrow() {
if (this.isMultiSelect) { if (this.isMultiSelect) {
return; // Prevent arrow keys from doing anything if element is a checkbox return; // Prevent arrow keys from doing anything if element is a checkbox
} }
if(!this.selectingInitiatesLoad) { this.handleInputChange();
this.handleCheckChange();
}
this.handleChange(this.value);
}, },
triggerButton() { triggerButton() {
if(this.selectingInitiatesLoad) { if(this.selectingInitiatesLoad) {
this.displayLoader(); this.displayLoader();
this.handleCheckChange(); this.handleCheckChange();
} }
this.handleChange(this.value);
this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.value.toString(), true); this.pushEventToGA(this.$route.query[queryStrings.FMG_PAGE], this.GaActions.CLICKED, this.value.toString(), true);
}, },
handleCheckChange() { handleCheckChange() {
@ -135,6 +133,7 @@ export default {
value: this.value.toString(), value: this.value.toString(),
buttonId: this.buttonID && this.buttonID.toString(), buttonId: this.buttonID && this.buttonID.toString(),
}; };
this.handleChange(this.value);
this.$emit("isCheckedChanged", emitEvent); this.$emit("isCheckedChanged", emitEvent);
this.$emit("update:modelValue", emitEvent); this.$emit("update:modelValue", emitEvent);
}, },