Merge pull request #599 from Safelite/feature/CSR-108

Feature/CSR-108
This commit is contained in:
AdamCaouetteSafelite 2022-07-13 09:15:46 -04:00 committed by GitHub
commit e4751fd9d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 247 additions and 42 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,7 +8,7 @@
: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=true
: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

@ -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,6 +20,7 @@ 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", SET_REFERRAL_INFORMATION: "setReferralInformation",
@ -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",
@ -51,7 +51,8 @@ const storeActions = {
SAVE_REGISTRATION_LICENSE_PLATE_LOOKUP: "saveRegistrationLicensePlateLookup", SAVE_REGISTRATION_LICENSE_PLATE_LOOKUP: "saveRegistrationLicensePlateLookup",
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",

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

@ -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,
@ -112,6 +113,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;
}, },
@ -555,7 +559,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,6 +582,31 @@ 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;
@ -787,6 +816,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) {