Merge branch 'develop' into feature/Digital/SSR-136

This commit is contained in:
Jason Wheeler 2022-11-30 13:50:36 -05:00
commit c69b4a6d32
15 changed files with 1592 additions and 23 deletions

BIN
src/assets/img/loader.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,30 @@
import { shallowMount } from "@vue/test-utils";
import loadingModal from "./loading-modal";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
jest.mock("@/assets/img/loader.gif", () => "loader.gif");
jest.mock("@/assets/img/windshield.png", () => "windshield.png");
describe("loadingModal", () => {
test("showModal sets modal visible", async () => {
// Arrange
const { wrapper } = setupMocks();
wrapper.vm.isModalVisible = false;
//Act
wrapper.vm.showModal();
// Assert
expect(wrapper.vm.isModalVisible).toEqual(true);
wrapper.unmount();
});
});
function setupMocks() {
const mountOptions = getMountOptions({
});
const wrapper = shallowMount(loadingModal, mountOptions);
return { wrapper };
}

View file

@ -0,0 +1,250 @@
<template>
<div v-if="isModalVisible" class="container-fluid modal-loader">
<div class="row g-2 h-100 d-flex align-items-center">
<div class="container-fluid overflow-hidden">
<div class="row h-100">
<div class="col text-center tagbg mb-4">
<div class="spinner-border text-danger" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="text-container slide">
<p>
Finding shops near you
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Looking for dates
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Searching for times
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Nearly there
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>
Finishing up
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Modal",
data() {
return {
isModalVisible: false,
};
},
methods: {
showModal() {
//Display modal
this.isModalVisible = true;
//Force page reload on back button
window.addEventListener(
"pageshow",
function (evt) {
if (evt.persisted) {
setTimeout(function () {
window.location.reload();
}, 10);
}
},
false
);
},
},
};
</script>
<style lang="scss">
.modal-loader {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1057;
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
outline: 0;
background: $gray-100;
.tagbg {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='none' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 84 32'%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' fill='%23fff'/%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' fill='%23fff'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06.8c8.08 0 11.89.935 11.89.935 3.58.576 5.696 7.207 5.696 7.207h.727c0-2.427 1.302-2.397 2.341-2 .723.292 1.363.76 1.86 1.361 1.319 1.547.465 1.674.465 1.674h-5.067l3.846 3.01v12.016c0 2.41-2.029 2.31-2.029 2.31H22.338s-2.029.1-2.029-2.31V12.996l3.84-3.009H19.07s-.853-.127.466-1.674a4.693 4.693 0 0 1 1.86-1.361c1.032-.397 2.341-.427 2.341 2h.736s2.117-6.64 5.696-7.21c0 0 3.81-.942 11.89-.942Z' fill='%23fff' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06 8.847c7.924 0 14.685.511 14.685.511 0-2.585-2.38-6.011-2.38-6.011S50.4 2.519 42.06 2.519s-12.303.828-12.303.828-2.38 3.426-2.38 6.011c0 0 6.76-.51 14.683-.51Z' fill='%23DA291C' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M35.277 16.523a62.558 62.558 0 0 1 13.277 0m3.276-.439s2.384-2.257 8.608-2.257c0 0 1.179 2.657-2.54 3.37m-25.894-1.113s-2.387-2.257-8.611-2.257c0 0-1.16 2.579 2.543 3.37m-3.546 5.856s21.52 3.647 39.123 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: center center;
background-size: 96px;
display: flex;
justify-content: center;
align-items: center;
}
.spinner-border {
width: 6.5rem;
height: 6.5rem;
border: 0.45em solid currentColor;
border-right-color: transparent;
}
.text-container {
display: flex;
width: 350px;
overflow: hidden;
white-space: nowrap;
margin: 0 auto;
position: relative;
&:after {
content: "";
background: rgb(245, 245, 245);
background: linear-gradient(
270deg,
rgba(245, 245, 245, 1) 0%,
rgba(245, 245, 245, 0) 5%,
rgba(245, 245, 245, 0) 95%,
rgba(245, 245, 245, 1) 100%
);
position: absolute;
width: 100%;
height: 100%;
z-index: 2;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
&.slide p {
animation: slide-in 20s ease-in-out 1s forwards 1;
}
p {
margin: 0 175px;
transform: translateX(180px);
}
@keyframes slide-in {
0% {
transform: translateX(180px);
}
11% {
transform: translateX(-80px);
}
22% {
transform: translateX(-80px);
}
33% {
transform: translateX(-600px);
}
44% {
transform: translateX(-600px);
}
55% {
transform: translateX(-1110px);
}
66% {
transform: translateX(-1110px);
}
77% {
transform: translateX(-1600px);
}
88% {
transform: translateX(-1600px);
}
100% {
transform: translateX(-2050px);
}
}
.dot-1 {
animation: blinking-elipsis 0.75s linear infinite;
@keyframes blinking-elipsis {
0% {
opacity: 1;
}
65% {
opacity: 1;
}
66% {
opacity: 0;
}
100% {
opacity: 0;
}
}
}
.dot-2 {
animation: blinking-elipsis-2 0.75s linear infinite;
@keyframes blinking-elipsis-2 {
0% {
opacity: 0;
}
21% {
opacity: 0;
}
22% {
opacity: 1;
}
65% {
opacity: 1;
}
66% {
opacity: 0;
}
100% {
opacity: 0;
}
}
}
.dot-3 {
animation: blinking-elipsis-3 0.75s linear infinite;
@keyframes blinking-elipsis-3 {
0% {
opacity: 0;
}
43% {
opacity: 0;
}
44% {
opacity: 1;
}
65% {
opacity: 1;
}
66% {
opacity: 0;
}
100% {
opacity: 0;
}
}
}
}
}
</style>

View file

@ -0,0 +1,250 @@
// Components
import questionsPageLayout from "@/common-components/questions-page-layout/questions-page-layout";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import baseMixin from "../../mixins/base-mixin";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
}));
describe("questionsPageLayout.vue", () => {
describe("method showThisQuestionChain...", () => {
test("Should return true if index prop and passed index match", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setProps({ index: 0 });
const testGlassPiece = {
questions: [
{
questionSequence: 1,
questionText:
"Is your vehicle equipped with the optional Lane-Keeping System which tugs on the steering wheel and/or beeps to alert you if you drift too close to the edge of the lane?",
answers: [],
},
],
};
const testIndex = 0;
// Act
const result = wrapper.vm.showThisQuestionChain(testGlassPiece, testIndex);
//Assert
expect(result).toBe(true);
wrapper.unmount();
});
test("Should return true if answerResult exists", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setProps({ index: 0 });
const testGlassPiece = {
questions: [
{
questionSequence: 1,
questionText:
"Is your vehicle equipped with the optional Lane-Keeping System which tugs on the steering wheel and/or beeps to alert you if you drift too close to the edge of the lane?",
answers: [],
},
],
answerData: {
answerResult: "test",
},
};
const testIndex = 1;
// Act
const result = wrapper.vm.showThisQuestionChain(testGlassPiece, testIndex);
//Assert
expect(result).toBe(true);
wrapper.unmount();
});
test("Should return false if indexes don't match and answerResult is missing", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setProps({ index: 0 });
const testGlassPiece = {
questions: [
{
questionSequence: 1,
questionText:
"Is your vehicle equipped with the optional Lane-Keeping System which tugs on the steering wheel and/or beeps to alert you if you drift too close to the edge of the lane?",
answers: [],
},
],
};
const testIndex = 1;
// Act
const result = wrapper.vm.showThisQuestionChain(testGlassPiece, testIndex);
//Assert
expect(result).toBe(false);
wrapper.unmount();
});
test("Should return false if no questions", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setProps({ index: 0 });
const testGlassPiece = {};
const testIndex = 0;
// Act
const result = wrapper.vm.showThisQuestionChain(testGlassPiece, testIndex);
//Assert
expect(result).toBe(false);
wrapper.unmount();
});
test("Should return false if suppressed", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setProps({ index: 0 });
const testGlassPiece = {
isSuppressedPart: true,
questions: [
{
questionSequence: 1,
questionText:
"Is your vehicle equipped with the optional Lane-Keeping System which tugs on the steering wheel and/or beeps to alert you if you drift too close to the edge of the lane?",
answers: [],
},
],
answerData: {
answerResult: "test",
},
};
const testIndex = 0;
// Act
const result = wrapper.vm.showThisQuestionChain(testGlassPiece, testIndex);
//Assert
expect(result).toBe(false);
wrapper.unmount();
});
});
describe("method handleForwardButtonAction...", () => {
test("Should emit forwardButtonAction", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setProps({});
// Act
wrapper.vm.handleForwardButtonAction();
//Assert
expect(wrapper.emitted()).toHaveProperty("forwardButtonAction");
wrapper.unmount();
});
});
describe("method handleBackButtonAction...", () => {
test("Should emit backButtonAction", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setProps({});
// Act
wrapper.vm.handleBackButtonAction();
//Assert
expect(wrapper.emitted()).toHaveProperty("back-click");
wrapper.unmount();
});
});
});
function setupMocks() {
const baseStoreGettersPageData = () => {
return {
partsOrQuestions: [
{
parts: null,
partQuestions: [
{
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,
},
],
},
],
glassLocation: "Windshield",
glassName: "Single",
answerKey: "Windshield-Single",
answerData: null,
},
],
};
};
const baseStoreGettersDamage = () => {
return {
partsQuestionAnswers: [
{
glassLocation: "Windshield",
glassName: "Single",
result: "FW04848",
answeredQuestions: [
{
questionText:
"Is your vehicle equipped with the Panoramic Sunroof which can be identified by having a glass panel over the rear seats?",
selectedAnswerText: "Yes",
questionNum: 1,
},
{
questionText:
"Is your vehicle equipped with a heated windshield that melts snow and ice from underneath the windshield wiper blades?",
selectedAnswerText: "Yes",
questionNum: 2,
},
],
},
],
};
};
const mountOptions = getMountOptions({
mixins: [baseMixin, vehicleQuestionsMixin],
});
mountOptions["attachTo"] = document.body;
const wrapper = shallowMount(questionsPageLayout, mountOptions);
return { wrapper };
}

View file

@ -0,0 +1,107 @@
<template>
<div :class="`page-container-grouped-styles questions-page`">
<loadingModal ref="loadingModal" />
<siteHeader cmsWidgetName="SiteHeaderWidget" />
<vehicleBanner cmsWidgetName="VehicleBannerWidget" :displayGenericVehicleImage="false" />
<siteSubHeader cmsWidgetName="SiteSubHeaderWidget" />
<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>
<site-footer
ref="siteFooter"
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!isMetaValid"
@back-clicked="handleBackButtonClicked"
@ForwardClicked="handleForwardButtonAction" />
</div>
</div>
</template>
<script>
// Components
import siteHeader from "@/common-components/site-header/site-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 siteSubHeader from "@/common-components/site-sub-header/site-sub-header";
import siteFooter from "@/common-components/site-footer/site-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: Object,
index: Number,
},
computed: {
selectedAnswers: {
get() {
return this.modelValue;
},
set(newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
methods: {
showThisQuestionChain(glass, i) {
// return false if no questions or if suppressed
if (!glass.questions || glass.questions?.length < 1 || glass.isSuppressedPart) {
return false;
}
return this.index === i || glass.answerData?.answerResult?.length > 0;
},
handleForwardButtonAction() {
this.$emit("forwardButtonAction");
},
handleBackButtonAction() {
this.$emit("back-click");
},
showLoadingModal() {
this.$refs.loadingModal.showModal();
},
},
components: {
siteHeader,
vehicleBanner,
alert,
questionChain,
siteSubHeader,
siteFooter,
loadingModal,
},
};
</script>
<style lang="scss">
.questions-page {
.question-text {
margin-bottom: 0.5rem;
span {
text-align: left;
}
}
}
</style>

View file

@ -31,6 +31,14 @@ const endpoints = {
url: "/parts/api/v1/parts/damage-options",
method: "GET",
},
GetParts: {
url: "/parts/api/v1/parts/parts",
method: "POST",
},
GetCapabilityQuestions: {
url: "/parts/api/v1/parts/capability-questions",
method: "GET",
},
GetVehicle: {
url: "/vehicle/api/v1/vehicle/lookup",
method: "GET",

View file

@ -0,0 +1,146 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<questions-page-layout
ref="questionsPageLayout"
:isMetaValid="meta.valid"
:alertFewMoreQuestionsHeader="AlertFewMoreQuestionsHeader"
:alertFewMoreQuestionsCopy="AlertFewMoreQuestionsCopy"
:questionsData="questionsData"
validationRules="questions-required"
v-model="selectedAnswers"
@forwardButtonAction="forwardButtonAction"
@back-click="navigateBack"
:index="currentGlassIndex" />
</Form>
</template>
<script>
// Components
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 { useMainStore } from '@/store';
import { issPageValues } from "@/router/router-constants/issPage-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";
// DEFINE VALIDATION RULES
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.issPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
data() {
return {
questionsData: [],
selectedAnswers: {},
currentGlassIndex: 0,
};
},
computed: {
AlertFewMoreQuestionsHeader() {
return this.getCmsContent("AlertPartsQuestions", "HeadlineText");
},
AlertFewMoreQuestionsCopy() {
return this.getCmsContent("AlertPartsQuestions", "BodyText");
},
pageData() {
return useMainStore().pageData(issPageValues.PART_QUESTIONS);
},
},
mounted() {
// are there alreadyAnsweredQuestions?
const alreadyAnsweredQuestions = useMainStore().damage.partQuestionAnswers;
this.questionsData = this.pageData.partsOrQuestions
.filter((x) => x.partQuestions)
.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.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: {
arePagePrerequisitesValid() {
const partQuestionsFromPageData = useMainStore().pageData(issPageValues.PART_QUESTIONS);
return (
partQuestionsFromPageData &&
Object.keys(partQuestionsFromPageData.partsOrQuestions).length > 0
);
},
async forwardButtonAction() {
const questionAnswersArray = this.questionsData.map((glass) => {
return {
glassLocation: glass.glassLocation,
glassName: glass.glassName,
result: (glass.answerData && glass.answerData.answerResult) || "",
answeredQuestions: glass.answerData?.answeredQuestions,
isSuppressedPart: glass.isSuppressedPart,
};
});
// clear out answerData for future page loads; must occur prior to store save
this.questionsData.forEach((glass) => {
if (glass.answerData) {
glass.answerData = {};
}
});
// save to vuex store as order.damage.partQuestionAnswers (array)
await this.mainStore.savePartQuestionAnswers(questionAnswersArray);
// call API parts method
const partsLookup = await this.mainStore.getParts();
const glassPartsForStore = partsLookup.data.glassPieceParts;
// TODO KO delete for quote mvp
this.navigateForward(glassPartsForStore, null);
},
},
components: {
Form,
questionsPageLayout,
},
};
</script>

View file

@ -24,6 +24,9 @@ export default {
const footerInfoBox = document.querySelector(".footer#infoBox");
return footerInfoBox ? footerInfoBox.offsetHeight : 0;
},
savePageDataToStore(page, data) {
useMainStore().updatePageData({ page: page, data: data});
},
},
computed: {
// store will be accessible globally as its id + 'Store'

View file

@ -0,0 +1,487 @@
import { issPageValues } from "@/router/router-constants/issPage-values";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { useMainStore } from "../store";
export default {
methods: {
hasPartQuestions(partsOrQuestions) {
return partsOrQuestions?.some((pq) => pq.partQuestions?.length > 0);
},
hasGlassLocationWithMultipleParts(partsOrQuestions) {
return partsOrQuestions?.some((pq) => pq.parts?.length > 1);
},
hasChildPartQuestions(partsOrQuestions) {
return partsOrQuestions?.some((pq) => {
return pq.parts?.some((part) => part.childPartQuestions?.length > 0);
});
},
hasCapabilityQuestions(partsOrQuestions) {
return partsOrQuestions?.some((pq) => {
return pq.parts?.some((part) => part.requiresCapabilityQuestions === true);
});
},
// method to only include keys listed for lineItems.glassParts in
reducedGlassPartsArray(glassParts) {
const reducedGlassParts = [];
glassParts.forEach((glass) => {
if (Array.isArray(glass.parts) && glass.parts.length === 1) {
const singlePart = glass.parts[0];
reducedGlassParts.push({
partNumber: singlePart.partNumber,
description: singlePart.description,
color: singlePart.color,
partType: singlePart.partType,
canSafeliteRecalibrate: singlePart.canSafeliteRecalibrate,
requiresRecalibration: singlePart.requiresRecalibration,
requiresCapabilityQuestions: singlePart.requiresCapabilityQuestions,
recalibrationType: singlePart.recalibrationType,
childParts: singlePart.childParts,
price: singlePart.price,
});
}
});
return reducedGlassParts;
},
comparePageIndices(currentPage, issPage) {
const orderedVehicleQuestionPages = [
issPageValues.PART_QUESTIONS,
issPageValues.VEHICLE_PARTS,
issPageValues.MOLDING_QUESTIONS,
issPageValues.CAPABILITY_QUESTIONS,
issPageValues.QUOTE,
];
return (
orderedVehicleQuestionPages.indexOf(currentPage) -
orderedVehicleQuestionPages.indexOf(issPage)
);
},
currentPageComesBeforePage(currentPage = this.$route.query.issPage, issPage) {
return this.comparePageIndices(currentPage, issPage) < 0;
},
currentPageComesAfterPage(currentPage = this.$route.query.issPage, issPage) {
return this.comparePageIndices(currentPage, issPage) > 0;
},
setupInitialData(glass, index, alreadyAnsweredQuestions, vm) {
const self = vm ?? this;
// clear answerData if no questions are already answered
if (!alreadyAnsweredQuestions) {
glass.answerData = null;
}
alreadyAnsweredQuestions?.forEach((answeredGlass) => {
// if answeredGlass lacks any of these properties then exit
if (
!answeredGlass.glassLocation ||
!answeredGlass.glassName ||
!answeredGlass.answeredQuestions ||
(!answeredGlass.result && !answeredGlass.partNum)
) {
return;
}
// test if glass parts match
if (
glass.glassLocation === answeredGlass.glassLocation &&
glass.glassName === answeredGlass.glassName
) {
let answerString = "";
// loop through answeredQuestions for matches
answeredGlass.answeredQuestions.forEach((answeredQuestion) => {
if (!answeredQuestion.questionNum || !answeredQuestion.selectedAnswerText) {
return;
}
// determine which answer was previously chosen
const chosenAns = glass.questions[
answeredQuestion.questionNum - 1
].answers.find((a) => {
return (
a.answerText.toUpperCase() ===
answeredQuestion.selectedAnswerText.toUpperCase()
);
});
// set the answerString to use for answerSelected
if (chosenAns.nextQuestionSequence) {
answerString = `${answeredQuestion.questionNum}|nextQuestion|${chosenAns.nextQuestionSequence}|${chosenAns.answerText}`;
} else {
answerString = `${answeredQuestion.questionNum}|answer|${chosenAns.answerResult}|${chosenAns.answerText}`;
}
// mark this question as answered (question-chain will read this)
glass.questions[answeredQuestion.questionNum - 1].answerSelected =
answerString;
// mark this question as suppressed if needed (question-chain uses this)
if (answeredQuestion.suppressThisQuestion) {
glass.questions[
answeredQuestion.questionNum - 1
].suppressThisQuestion = true;
}
});
// advance the currentGlassIndex
self.currentGlassIndex = index;
const answerResult = answeredGlass.partNum
? answeredGlass.partNum
: answeredGlass.result;
// the above logic for answerResult covers all 3 ___-questions page scenarios
// add answerData to current glass
glass.answerData = {
answerResult: answerResult,
answeredQuestions: answeredGlass.answeredQuestions,
};
}
});
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;
const currentPage = self.$route.query.issPage;
const hasPartQuestions = this.hasPartQuestions(partsOrQuestions);
const hasGlassLocationWithMultipleParts =
this.hasGlassLocationWithMultipleParts(partsOrQuestions);
const hasChildPartQuestions = this.hasChildPartQuestions(partsOrQuestions);
const hasCapabilityQuestions = this.hasCapabilityQuestions(partsOrQuestions);
if (hasPartQuestions && this.currentPageComesBeforePage(currentPage, issPageValues.PART_QUESTIONS))
{
self.$router.navigate(self.navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS, self.$route,
{},
{},
{ partsOrQuestions: partsOrQuestions }
);
} else if (hasGlassLocationWithMultipleParts && this.currentPageComesBeforePage(currentPage, issPageValues.VEHICLE_PARTS))
{
// if multiple parts on any glass
// go to vehicle-parts page and pass the partsData
self.$router.navigate(
self.navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE,
self.$route,
{},
{},
{ partsOrQuestions: partsOrQuestions }
);
} else if (
hasChildPartQuestions &&
this.currentPageComesBeforePage(currentPage, issPageValues.MOLDING_QUESTIONS)
) {
// if any childpart questions
// go to molding-questions page and pass the partsData
self.$router.navigate(
self.navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS,
self.$route,
{},
{},
{ partsOrQuestions: partsOrQuestions }
);
} else if (
hasCapabilityQuestions &&
this.currentPageComesBeforePage(currentPage, issPageValues.CAPABILITY_QUESTIONS)
) {
// if has capability questions
// go to capability-questions page and pass the partsData
// mimic part-questions page data for consistency
for (let partOrQuestion of partsOrQuestions) {
if (this.hasCapabilityQuestions([partOrQuestion])) {
let capabilityQuestionsForGlassLocation = (
await self.mainStore.getCapabilityQuestions(useMainStore().vehicle.carId, partOrQuestion.parts[0].partNumber)
).data;
capabilityQuestionsForGlassLocation.forEach((question) => {
question.answers = question.answers.map((answer) => {
return {
...answer,
answerResult: answer.answerResult1,
};
});
});
partOrQuestion.capabilityQuestions = capabilityQuestionsForGlassLocation;
}
}
self.$router.navigate(
self.navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS,
self.$route,
{},
{},
{ partsOrQuestions }
);
} else {
// if single parts only
const collectedGlassParts = this.reducedGlassPartsArray(partsOrQuestions);
// save to store lineItems.glassParts
self.mainStore.updateGlassParts(collectedGlassParts);
self.$router.navigate(self.navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,self.$route);
}
},
navigateBack(vm) {
const self = vm ?? this;
const partsOrQuestions = (
self.mainStore.pageData(issPageValues.CAPABILITY_QUESTIONS) ??
self.mainStore.pageData(issPageValues.MOLDING_QUESTIONS) ??
self.mainStore.pageData(issPageValues.VEHICLE_PARTS) ??
self.mainStore.pageData(issPageValues.PART_QUESTIONS)
)?.partsOrQuestions;
const hasPartQuestions = this.hasPartQuestions(partsOrQuestions);
const hasGlassLocationWithMultipleParts =
this.hasGlassLocationWithMultipleParts(partsOrQuestions);
const hasChildPartQuestions = this.hasChildPartQuestions(partsOrQuestions);
const hasCapabilityQuestions = this.hasCapabilityQuestions(partsOrQuestions);
let backNavigationScenario = self.mainStore.vehicle.vin
? navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS
: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS;
const currentPage = self.$route.query.issPage;
if (
hasCapabilityQuestions &&
this.currentPageComesAfterPage(currentPage, issPageValues.CAPABILITY_QUESTIONS)
) {
backNavigationScenario = navigationScenarios.CLICKED_BACK_WITH_CAPABILITY_QUESTIONS;
} else if (
hasChildPartQuestions &&
this.currentPageComesAfterPage(currentPage, issPageValues.MOLDING_QUESTIONS)
) {
backNavigationScenario = navigationScenarios.CLICKED_BACK_WITH_MOLDING_QUESTIONS;
} else if (
hasGlassLocationWithMultipleParts &&
this.currentPageComesAfterPage(currentPage, issPageValues.VEHICLE_PARTS)
) {
backNavigationScenario =
navigationScenarios.CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE;
} else if (
hasPartQuestions &&
this.currentPageComesAfterPage(currentPage, issPageValues.PART_QUESTIONS)
) {
backNavigationScenario = navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS;
}
self.$router.navigate(backNavigationScenario, self.$route);
},
},
};

View file

@ -5,6 +5,7 @@ import { routingTable } from "@/router/router-constants/routing-table";
import { useMainStore } from '@/store';
import eventBus from "@/helpers/event-bus/event-bus";
import { globalEvents, globalEventTypes } from "@/constants/events";
import baseMixin from "@/mixins/base-mixin";
import {
getDeviceIdValue,
updateOrCreateISSCookie,
@ -105,14 +106,14 @@ async function GetRouteInfoFromPageName(pageName) {
return routeData;
};
router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}) => {
router.navigate = (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) => {
navigate(scenario, currentRoute, false, optionalQuery, optionalParams);
navigate(scenario, currentRoute, false, optionalQuery, optionalParams, optionalPageData);
}
// Navigate to the next route, depending on the scenario.
function navigate (scenario, currentRoute, optionalQuery = {}, optionalParams = {}) {
function navigate (scenario, currentRoute, optionalQuery = {}, optionalParams = {}, optionalPageData = {}) {
if (!scenario) {
console.error("No scenario provided. Please review the routing table.");
return;
@ -127,6 +128,16 @@ function navigate (scenario, currentRoute, optionalQuery = {}, optionalParams =
}
if (matchingScenarioMap.destinationIssPageValue) {
// Update page data to the store for next page if provided. Otherwise, keep existing page data or set to empty object
const existingPageDataForPage = useMainStore().pageData(matchingScenarioMap.destinationIssPageValue);
baseMixin.methods.savePageDataToStore(
matchingScenarioMap.destinationIssPageValue,
Object.keys(optionalPageData).length > 0
? optionalPageData
: existingPageDataForPage ?? {}
);
// We're always pushing the same path, just changing query strings. Make sure our optional query strings get combined with our issPage one.
router.push({
name: "root",

View file

@ -9,8 +9,29 @@ const navigationScenarios = {
SELECTED_MODEL: "SELECTED_MODEL",
SELECTED_STYLE: "SELECTED_STYLE",
// TESTING
CLICKED_TEST: "CLICKED_TEST"
// Vin selection
CLICKED_BACK_WITH_VIN: "CLICKED_BACK_WITH_VIN",
CLICKED_FORWARD_WITH_VIN: "CLICKED_FORWARD_WITH_VIN",
CLICKED_FORWARD_WITHOUT_VIN: "CLICKED_FORWARD_WITHOUT_VIN",
CLICKED_FORWARD_WITH_MULTIPLE_VEHICLES: "CLICKED_FORWARD_WITH_MULTIPLE_VEHICLES",
SELECTED_VIN_WITH_MISMATCHED_GLASS: "SELECTED_VIN_WITH_MISMATCHED_GLASS",
SELECTED_MANUAL_VIN: "SELECTED_MANUAL_VIN",
SELECTED_LICENSE_PLATE: "SELECTED_LICENSE_PLATE",
SELECTED_HOME_ADDRESS: "SELECTED_HOME_ADDRESS",
CLICKED_FORWARD_WITH_NO_QUESTIONS: "CLICKED_FORWARD_WITH_NO_QUESTIONS",
// Part selection
CLICKED_FORWARD_WITH_PART_QUESTIONS: "CLICKED_FORWARD_WITH_PART_QUESTIONS",
CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE: "CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE",
CLICKED_FORWARD_WITH_MOLDING_QUESTIONS: "CLICKED_FORWARD_WITH_MOLDING_QUESTIONS",
CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS: "CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS",
CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS: "CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS",
CLICKED_BACK_WITH_PART_QUESTIONS: "CLICKED_BACK_WITH_PART_QUESTIONS",
CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE: "CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE",
CLICKED_BACK_WITH_MOLDING_QUESTIONS: "CLICKED_BACK_WITH_MOLDING_QUESTIONS",
CLICKED_BACK_WITH_CAPABILITY_QUESTIONS: "CLICKED_BACK_WITH_CAPABILITY_QUESTIONS",
CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS: "CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS",
CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS: "CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS",
};
export { navigationScenarios };

View file

@ -73,6 +73,126 @@ const routingTable = function(store) {
},
],
},
{
issPageValue: issPageValues.PART_QUESTIONS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.VIN_LOOKUP,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS,
destinationIssPageValue: issPageValues.ESTIMATE,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE,
destinationIssPageValue: issPageValues.VEHICLE_PARTS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.QUOTE,
},
],
},
{
issPageValue: issPageValues.VEHICLE_PARTS,
maps: [
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationIssPageValue: issPageValues.REVEAL,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.VIN_LOOKUP,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS,
destinationIssPageValue: issPageValues.ESTIMATE,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.QUOTE,
},
],
},
{
issPageValue: issPageValues.MOLDING_QUESTIONS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.VIN_LOOKUP,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS,
destinationIssPageValue: issPageValues.ESTIMATE,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE,
destinationIssPageValue: issPageValues.VEHICLE_PARTS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.QUOTE,
},
],
},
{
issPageValue: issPageValues.CAPABILITY_QUESTIONS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.VIN_LOOKUP,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_NO_VIN_NOR_QUESTIONS,
destinationIssPageValue: issPageValues.ESTIMATE,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_MULTIPLE_PARTS_TO_CHOOSE,
destinationIssPageValue: issPageValues.VEHICLE_PARTS,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.QUOTE,
},
],
},
{
issPageValue: issPageValues.WELCOME_PAGE,
maps: [

View file

@ -1,35 +1,19 @@
import router from "@/router/";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { issPageValues } from "@/router/router-constants/issPage-values";
import { createApp } from 'vue';
import { createPinia } from "pinia";
import App from '@/App.vue';
const originalLocation = global.location
describe("Router", () => {
beforeAll(() => {
const vueApp = createApp(App);
const pinia = createPinia();
vueApp.use(pinia);
})
it("Should change the location when navigating to external site", () => {
delete global.location
global.location = { assign: jest.fn() }
let scenario = navigationScenarios.CLICKED_TEST;
let currentRoute = { query: { issPage: issPageValues.WELCOME_PAGE }};
router.navigate(scenario, currentRoute);
expect(global.location.assign).toHaveBeenCalledTimes(1)
global.location = originalLocation;
});
it("Should push next view when navigating locally", () => {
let scenario = navigationScenarios.CLICKED_FORWARD;
let currentRoute = { query: { issPage: issPageValues.WELCOME_PAGE }};
@ -37,7 +21,7 @@ describe("Router", () => {
router.push = jest.fn();
router.navigate(scenario, currentRoute);
expect(router.push.mock.calls[0][0].query.issPage).toBe(issPageValues.VEHICLE_YEAR)
expect(router.push.mock.calls[0][0].query.issPage).toBe(issPageValues.VEHICLE_YEAR);
});

View file

@ -41,8 +41,15 @@ const getDefaultState = () => {
moldingQuestionAnswers: null,
capabilityQuestionAnswers: null,
},
serviceLocation: {
address: null,
city: null,
state: null,
zipCode: null,
},
lineItems: {
glassParts: null,
otherParts: null
},
payment: {
isInsurance: true,
@ -224,6 +231,49 @@ export const useMainStore = defineStore({
payload: {},
});
},
async getParts() {
const vehicle = this.getters.vehicle;
const damage = this.getters.damage;
const order = this.order;
const carId = vehicle.carId;
const glassArray = damage.glassToReplace;
const resultsArray = damage.partQuestionAnswers;
const zipCode = order.serviceLocation.zipCode;
const vin = vehicle.vin;
// create a new array to avoid mutating state
const glassArrayForPayload = convertGlassPieceNamingForApi(glassArray);
const resultsArrayForPayload = convertResultsForApi(resultsArray);
const response = await globalMethods.callHttpClient({
method: endpoints.GetParts.method,
endpoint: endpoints.GetParts.url,
payload: {
carId: carId,
glassPieces: glassArrayForPayload,
answerResults: resultsArrayForPayload,
zip: zipCode,
vin: vin,
},
});
// Flatten location and name properties
response.data.glassPieceParts = convertGlassPieceNamingFromApi(
response.data.glassPieceParts
);
return response;
},
getCapabilityQuestions(carId, partNumber) {
return globalMethods.callHttpClient({
method: endpoints.GetCapabilityQuestions.method,
endpoint: `${endpoints.GetCapabilityQuestions.url}/${carId}/${partNumber}`,
});
},
setVehicle() {
return globalMethods
.callHttpClient({
@ -376,6 +426,55 @@ export const useMainStore = defineStore({
updateGlassToReplace(glassToReplace) {
this.order.damage.glassToReplace = glassToReplace;
},
updateGlassParts(partsData) {
this.order.lineItems.glassParts = partsData;
},
updateMoldingQuestionAnswers(answersArray) {
this.order.damage.moldingQuestionAnswers = answersArray;
},
updateCapabilityQuestionAnswers(answersArray) {
this.order.damage.capabilityQuestionAnswers = answersArray;
},
updatePartQuestionAnswers(answersArray) {
this.order.damage.partQuestionAnswers = answersArray;
},
updatePageData(pageData) {
this.applicationUser.pageData[pageData.page] = pageData.data;
},
savePartQuestionAnswers(partQuestionAnswersArray) {
// if part question answers have changed, reset subsequent question answers
const sortedPreviousResultsArray = sortArrayOfObjectsByPropertyValue(
context.getters.damage.partQuestionAnswers,
"result"
);
const sortedPartQuestionAnswersArray = sortArrayOfObjectsByPropertyValue(
partQuestionAnswersArray,
"result"
);
const havePartQuestionAnswersChanged =
sortedPreviousResultsArray?.length !== sortedPartQuestionAnswersArray.length ||
!sortedPreviousResultsArray?.every(
(x, i) => x.result === sortedPartQuestionAnswersArray[i].result
);
if (havePartQuestionAnswersChanged) {
this.updateGlassParts(null);
this.updateMoldingQuestionAnswers(null);
this.updateCapabilityQuestionAnswers(null);
this.updatePageData({ page: issPageValues.VEHICLE_PARTS, data: null});
this.updatePageData({ page: issPageValues.MOLDING_QUESTIONS, data: null});
this.updatePageData({ page: issPageValues.CAPABILITY_QUESTIONS, data: null});
}
//Save new values
this.updatePartQuestionAnswers(partQuestionAnswersArray);
},
addEventToBus (event) {
this.applicationUser.eventBus.push(event);
@ -574,3 +673,56 @@ function getHasRecalibrationPart(state) {
function getNonFalseValuesOfPropertyInArrayOfObjects(array, propertyName) {
return (array ?? []).map((x) => x[propertyName]).filter((x) => x);
}
function sortArrayOfObjectsByPropertyValue(arrayOfObjects, propertyName) {
if (!arrayOfObjects) return null;
return arrayOfObjects.sort((a, b) => {
if (a[propertyName] < b[propertyName]) return -1;
else if (a[propertyName] > b[propertyName]) return 1;
else return 0;
});
}
function convertGlassPieceNamingForApi(glassArray) {
if (!glassArray || glassArray.length === 0) 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({
location: glass.glassLocation,
name: glass.glassName,
});
});
return converted;
}
function convertResultsForApi(resultsArray) {
if (!resultsArray) return [];
const converted = [];
resultsArray.forEach((answer) => {
converted.push({
location: answer.glassLocation,
name: answer.glassName,
result: answer.result,
});
});
return converted;
}
function convertGlassPieceNamingFromApi(glassArray) {
glassArray.forEach((glass) => {
glass.glassLocation = glass.glassPiece.location;
glass.glassName = glass.glassPiece.name;
delete glass.glassPiece;
return glass;
});
return glassArray;
}