diff --git a/src/common-components/button-question/button-question.spec.js b/src/common-components/button-question/button-question.spec.js index 9cd9307db..3002920e8 100644 --- a/src/common-components/button-question/button-question.spec.js +++ b/src/common-components/button-question/button-question.spec.js @@ -96,7 +96,7 @@ describe("buttonQuestion.vue", () => { const answer = { 'Name': 'testName', 'Text': 'testText' }; // Assert - expect(buttonQuestion.methods.getValues.call(localThis, answer)).toBe('testText'); + expect(buttonQuestion.methods.getValue.call(localThis, answer)).toBe('testText'); }); }); @@ -107,7 +107,7 @@ describe("buttonQuestion.vue", () => { const answer = { 'Name': 'testName', 'Text': 'testText' }; // Assert - expect(buttonQuestion.methods.getValues.call(localThis, answer)).toBe('testName'); + expect(buttonQuestion.methods.getValue.call(localThis, answer)).toBe('testName'); }); }); diff --git a/src/common-components/button-question/button-question.vue b/src/common-components/button-question/button-question.vue index a4ee6b330..8ba0f7e92 100644 --- a/src/common-components/button-question/button-question.vue +++ b/src/common-components/button-question/button-question.vue @@ -5,8 +5,8 @@ {{ questionText }}
-
- +
+ {{ questionText }} {{(isMultiSelect && answers && answers.length > 1) ? 'Select one or more options below.' : 'Select an option below.' }} @@ -15,13 +15,13 @@
- +
@@ -95,9 +95,6 @@ export default { valueToLogType: String, }, computed: { - formattedGroupName() { - return this.groupName.replace(" ", "-"); - }, getFieldSetClasses() { if (this.isOverflowScrollable) { return "container-fluid overflow-scroll position-absolute px-5 pt-1 py-0"; @@ -155,13 +152,23 @@ export default { }, }, methods: { - getValues(answer){ - if (this.useTextForValue){ - return answer.Text - } - + formatString(str) { + return str.replace(" ", "-"); + }, + getValue(answer){ + if (this.useTextForValue) { return answer.Text } return answer.Name ? answer.Name : answer; }, + getAnswerString(answer, prop = "Name") { + switch (typeof answer) { + case "string": + case "number": + case "boolean": + return this.formatString(answer.toString()); + default: + return answer[prop] ? this.formatString(answer[prop]) : this.formatString(answer.toString()); + } + }, handleCheckedChanged(val) { if(this.selectingInitiatesLoad) { this.selectedValues = val.value; diff --git a/src/common-components/question-chain/question-chain.vue b/src/common-components/question-chain/question-chain.vue index 3ac603eb2..dee5d1f32 100644 --- a/src/common-components/question-chain/question-chain.vue +++ b/src/common-components/question-chain/question-chain.vue @@ -9,7 +9,7 @@ :answers="q.answers" :groupName="`${keyString}-${q.questionSequence}`" v-model="q.answerSelected" - @isCheckedChanged="handleChainCompleted" + @isCheckedChanged="handleAnswer" isRequired :validationRules="validationRules" /> @@ -58,12 +58,15 @@ export default { q.questionSequence + "|nextQuestion|" + a.nextQuestionSequence + "|" + a.answerText : q.questionSequence + "|answer|" + a.answerResult + "|" + a.answerText, nextQuestionSequence: a.nextQuestionSequence, + answerResult: a.answerResult, + questionSequence: q.questionSequence, + questionType: a.nextQuestionSequence ? "nextQuestion" : "answer", } }), answerSelected: q.answerSelected || "", }; question.answerPair = answerPair; - if (!q.isDuplicateQuestion) { + if (!q.suppressQuestion) { this.questions.push(question); } }); @@ -76,75 +79,86 @@ export default { document.querySelector('.current-question').scrollIntoView({behavior: "smooth"}); }) } - }, methods: { - handleChainCompleted(returnedAnswer) { - const isQuestionChainComplete = this.handleReturnedAnswer(returnedAnswer.value); + handleAnswer(returnedAnswer) { + /* + returnedAnswer example format: + { + "checkValue": "1|answer|DD11132|Yes", + "value": "1|answer|DD11132|Yes", + "buttonId": "Driver-Front-1-1|answer|DD11132|Yes" + } + */ + const isQuestionChainComplete = this.getQuestionChainAnswerIfComplete(returnedAnswer.checkValue); - if (isQuestionChainComplete) { - this.$emit("update:modelValue", isQuestionChainComplete); - } - }, - handleReturnedAnswer(returnedAnswer) { // this method will return either a final answer or Boolean false - if (!returnedAnswer) { return false } + if (isQuestionChainComplete) { + this.$emit("update:modelValue", isQuestionChainComplete); + } + }, + getQuestionChainAnswerIfComplete(returnedAnswer) { // this method will return either a final answer or Boolean false + if (!returnedAnswer) { return false } - // Example returnedAnswers: - // "1|nextQuestion|3|No" - // "5|answer|DW02104|Yes" + // Example returnedAnswers: + // "1|nextQuestion|3|No" + // "5|answer|DW02104|Yes" - const returnedAnswerArray = returnedAnswer.split("|"); - const questionNum = parseInt(returnedAnswerArray[0]); - const questionType = returnedAnswerArray[1]; - const questionAnswer = returnedAnswerArray[2]; - const questionAnswerText = returnedAnswerArray[3]; + const returnedAnswerArray = returnedAnswer.split("|"); + const questionNum = parseInt(returnedAnswerArray[0]); + const questionType = returnedAnswerArray[1]; + const questionAnswer = returnedAnswerArray[2]; + const questionAnswerText = returnedAnswerArray[3]; + const answeredQuestions = []; - this.questions.forEach((q) => { - // mark this question as "answered" - if (q.questionSequence === questionNum) { - q.answerSelected = returnedAnswer; - q.answerNumber = questionNum; - } - // remove all previous answers after the index of this one in questions - if ((q.questionSequence > questionNum)) { - delete q.answerSelected; - } - }); - - // return false if there's a nextQuestion... or return an object with "final" answers - if (questionType === "nextQuestion") { - // update to next question index - this.currentQuestionNum = parseInt(questionAnswer); // update count to display next question - // scroll the next question into view - this.$nextTick(() => { - document.querySelector('.current-question').scrollIntoView({behavior: "smooth"}); - }) - return false; - } else { - const answeredQuestions = []; - this.questions.forEach(( q ) => { - if (q.answerSelected) { - answeredQuestions.push({ - questionText: q.questionText, - selectedAnswerText: q.answerSelected.split("|")[3], - questionNum: q.questionSequence, + this.questions.forEach((q) => { + // find this question and mark it as "answered" by populating answerSelected + if (q.questionSequence === questionNum) { + q.answerSelected = returnedAnswer; + q.answerNumber = questionNum; + q.selectedAnswerText = questionAnswerText; + } + // remove all answers AFTER this question... + // (needed in case user is changing previously answered questions) + if ((q.questionSequence > questionNum)) { + delete q.answerSelected; + } + if (q.answerSelected) { + answeredQuestions.push({ + questionText: q.questionText, + selectedAnswerText: q.answerSelected.split("|")[3], + questionNum: q.questionSequence, + }); + } }); - } - }); - // reset current question index - this.currentQuestionNum = 0; // reset count - return { - answerResult: questionAnswer, - answeredQuestions: answeredQuestions, - partIndex: this.partIndex, - }; + // return false if there's a nextQuestion... or return an object with final answers (truthy) + if (questionType === "nextQuestion") { - } + // update to next question index + this.currentQuestionNum = parseInt(questionAnswer); // update count to display next question + // scroll the next question into view + this.$nextTick(() => { + document.querySelector('.current-question').scrollIntoView({behavior: "smooth"}); + }) + return false; + + } else { + + // reset current question index (removes .current-question class) + this.currentQuestionNum = 0; // reset count + + // return an object with the part answer, all the answered questions, and the part index + return { + answerResult: questionAnswer, + answeredQuestions: answeredQuestions, + partIndex: this.partIndex, + }; + + } + }, + }, + components: { + buttonQuestion, }, - }, - components: { - buttonQuestion, - }, }; diff --git a/src/layouts/part-questions/part-questions.vue b/src/layouts/part-questions/part-questions.vue index 8082e42f7..881eaf90b 100644 --- a/src/layouts/part-questions/part-questions.vue +++ b/src/layouts/part-questions/part-questions.vue @@ -23,7 +23,6 @@
0; }, - handleAnswerUpdates(key, answer) { // only runs when all questions in a question-chain have been answered - // when selectedAnswers updates, user has completed this part's question chain and has a final answer - // (does not get run for each invididual question's answer, only when - // all relevent questions for the current part have been answered) - const glassPartWithAnswer = this.partsQuestionsData[answer.partIndex]; - const completeAnsweredQuestions = answer.answeredQuestions ? [...answer.answeredQuestions] : []; + handleAnswerUpdates(key, answer) { + // only runs when all questions in a question-chain have been answered + // when selectedAnswers updates, user has completed this part's question chain and has a final answer + // (does not get run for each invididual question's answer, only when + // all relevent 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, + "extra": { + "Text": "Yes", + "nextQuestionSequence": null, + "answerResult": "DD11132", + "questionSequence": 1, + "questionType": "answer" + } + } + ], + "partIndex": 0 + } + */ + + // collect a list of the answered questions' numbers, needed later below const answeredQuestionIndexes = []; - // since user has answered a question differently than anything that was preloaded, - // then clear out the preloaded answers + // if user has answered a question differently than anything that was preloaded, + // we need to clear out any preloaded answers this.selectedAnswers = {}; - // examine all the answers returned that were part of the user's journey through question-chain - - // loop through every answered question on currently answered glass part + // loop through every answered question on the currently answered glass part answer.answeredQuestions?.forEach((aq) => { - // gather all the question numbers of the answered questions + // keep track of this question number answeredQuestionIndexes.push(aq.questionNum); const answeredQuestionText = aq.questionText.toUpperCase(); const answeredQuestionAnswer = aq.selectedAnswerText.toUpperCase(); // HANDLE DUPLICATE QUESTIONS - // loop through all glass parts data (but only examining parts after currently being answered part) - this.partsQuestionsData.forEach((glassPart, i) => { + + // loop through all glass parts data + this.partsQuestionsData.forEach((glassPart, gpIndex) => { - // restrict duplicate logic to only parts that follow the currently being answered part - if (i > answer.partIndex) { + // only look for duplicates forward... to parts that follow after the currently being answered part + if (gpIndex > answer.partIndex) { + + let suppressUntil; + // reset this glass part, in case user is changing their previous answers + glassPart.answerData = null; + glassPart.isSuppressedPart = null; // loop through this glass part's part questions, looking for a questionText match glassPart.partQuestions.forEach((pq, pqIndex) => { + // clear out any previously set answers - //delete pq.answerSelected; pq.answerSelected = null; - // does pq.questionText match answeredQuestionText? (aka do we have a duplicate question?) + // clear or set suppressQuestion property for each question + if (suppressUntil) { + // if suppressUntil has been set, then suppress this question if before it + if (pqIndex + 1 < suppressUntil) { + pq.suppressQuestion = true; + } else { + pq.suppressQuestion = null; + } + } else { + pq.suppressQuestion = null; + } + + // if these match then we have a duplicate question if (pq.questionText.toUpperCase() === answeredQuestionText) { - - // which one of this partQuestions' answers matches our answer? + + const thisAnsweredPartQuestion = glassPart.partQuestions[pqIndex]; let matchedAnswer; - pq.answers.forEach((ans, ansIndex) => { - // delete pq.answers[ansIndex].selected; - pq.answers[ansIndex].selected = null; + let rejectedAnswers = []; // set as an array, in case we ever have questions with more than 2 answers... + + // which one of this partQuestions' answers matches our answer? + pq.answers.forEach((ans) => { if (ans.answerText.toUpperCase() === answeredQuestionAnswer) { matchedAnswer = ans; - pq.answers[ansIndex].selected = true; + ans.selected = true; + } else { + rejectedAnswers.push(ans); + ans.selected = null; } }); - if (matchedAnswer) { - const thisAnsweredPartQuestion = glassPart.partQuestions[pqIndex]; + // Update the key to re-render this part's question-chain component + this.partsQuestionsData[gpIndex].key = this.partsQuestionsData[gpIndex].glassLocation + this.partsQuestionsData[gpIndex].glassName + Date.now().toString(); - // remove answerData from this glass part - glassPart.answerData = null; - glassPart.isSuppressedPart = null; - - // Update the key to re-render this part's question-chain component - this.partsQuestionsData[i].key = this.partsQuestionsData[i].glassLocation + this.partsQuestionsData[i].glassName + Date.now().toString(); - - // handle suppressing downstream in this question chain - const rejectedAnswer = thisAnsweredPartQuestion.answers.filter((ans) => { - return !ans.selected; - }); - - if (rejectedAnswer[0].nextQuestionSequence) { - glassPart.partQuestions[rejectedAnswer[0].nextQuestionSequence - 1].suppressDuplicateQuestion = true; - } - if (matchedAnswer.nextQuestionSequence) { - // ensure that accepted answer is NOT suppressed - glassPart.partQuestions[matchedAnswer.nextQuestionSequence - 1].suppressDuplicateQuestion = null; - } - - // handle suppressing upstream in this question chain - glassPart.partQuestions.forEach((q) => { - q.answers.forEach((thisAns) => { - // restore any of the answers that formerly led to the duplicated question - if (thisAns.originalNextQuestionSequence === pq.questionSequence) { - // restore original nextQuestionSequence - thisAns.nextQuestionSequence = thisAns.originalNextQuestionSequence; - this.originalNextQuestionSequence = null; - // restore original answerResult - if (thisAns.originalAnswerResult) { - thisAns.answerResult = thisAns.originalAnswerResult; - thisAns.originalAnswerResult = null; - } - } - // search for any of the answers that lead to the duplicated question - if (thisAns.nextQuestionSequence === pq.questionSequence) { - // update either the nextQuestionSequence or the answerResult - if (matchedAnswer.nextQuestionSequence) { - thisAns.originalNextQuestionSequence = thisAns.nextQuestionSequence; - thisAns.nextQuestionSequence = matchedAnswer.nextQuestionSequence; - } else { - thisAns.originalNextQuestionSequence = thisAns.nextQuestionSequence; - thisAns.nextQuestionSequence = null; - thisAns.originalAnswerResult = thisAns.originalAnswerResult || thisAns.answerResult; - thisAns.answerResult = matchedAnswer.answerResult; - } - } - }); - }); + // handle suppressing downstream in this question chain - // suppress current question - thisAnsweredPartQuestion.suppressDuplicateQuestion = true; - - const thisGlassPart = "glassPart" + i; - if (this.foundDuplicateQuestions[thisGlassPart]) { - if (!this.foundDuplicateQuestions[thisGlassPart].includes(thisAnsweredPartQuestion.questionSequence)) { - this.foundDuplicateQuestions[thisGlassPart].push(thisAnsweredPartQuestion.questionSequence); + if (matchedAnswer.nextQuestionSequence) { + // ensure that the question that the accepted answer has set to be next is NOT suppressed + glassPart.partQuestions[matchedAnswer.nextQuestionSequence - 1].suppressQuestion = null; + // if the duplicate is the 1ST question, then set suppressUntil var to lowest nextQuestion number + if (pqIndex === 0) { + if (!suppressUntil) { suppressUntil = matchedAnswer.nextQuestionSequence } + if (matchedAnswer.nextQuestionSequence < suppressUntil) { suppressUntil = matchedAnswer.nextQuestionSequence} + } + } + + // handle suppressing upstream in this question chain + glassPart.partQuestions.forEach((q) => { + q.answers.forEach((thisAns) => { + // restore any of the answers that formerly led to the duplicated question + if (thisAns.originalNextQuestionSequence === pq.questionSequence) { + // restore original nextQuestionSequence + thisAns.nextQuestionSequence = thisAns.originalNextQuestionSequence; + this.originalNextQuestionSequence = null; + // restore original answerResult + if (thisAns.originalAnswerResult) { + thisAns.answerResult = thisAns.originalAnswerResult; + thisAns.originalAnswerResult = null; + } + } + // search for any answers that lead to the duplicated question + if (thisAns.nextQuestionSequence === pq.questionSequence) { + // update either the nextQuestionSequence or the answerResult + if (matchedAnswer.nextQuestionSequence) { + thisAns.originalNextQuestionSequence = thisAns.nextQuestionSequence; + thisAns.nextQuestionSequence = matchedAnswer.nextQuestionSequence; + } else { + thisAns.originalNextQuestionSequence = thisAns.nextQuestionSequence; + thisAns.nextQuestionSequence = null; + thisAns.originalAnswerResult = thisAns.originalAnswerResult || thisAns.answerResult; + thisAns.answerResult = matchedAnswer.answerResult; + } } - } else { - this.foundDuplicateQuestions[thisGlassPart] = [thisAnsweredPartQuestion.questionSequence]; - } - - // are there any questions left that are not suppressed? - const remainingQuestions = glassPart.partQuestions.filter((q) => { - return !q.suppressDuplicateQuestion; }); - - if (remainingQuestions.length < 1) { - // this is the final answer for this glass part - - // mark this part as completely answered by adding answerData - const answeredQuestionObj = { - questionText: pq.questionText, - selectedAnswerText: matchedAnswer.answerText, - questionNum: pq.questionSequence, - isDuplicateQuestion: pq.suppressDuplicateQuestion, - }; - // set the answerData as 'already answered' - glassPart.answerData = { - answerResult: matchedAnswer.nextQuestionSequence ? matchedAnswer.nextQuestionSequence : matchedAnswer.answerResult, - answeredQuestions: [answeredQuestionObj], - }; - - // suppress this glassPart because it has an answer - glassPart.isSuppressedPart = true; + }); + + // suppress current question + thisAnsweredPartQuestion.suppressQuestion = true; + + const thisGlassPart = "glassPart" + gpIndex; + if (this.foundDuplicateQuestions[thisGlassPart]) { + if (!this.foundDuplicateQuestions[thisGlassPart].includes(thisAnsweredPartQuestion.questionSequence)) { + this.foundDuplicateQuestions[thisGlassPart].push(thisAnsweredPartQuestion.questionSequence); } + } else { + this.foundDuplicateQuestions[thisGlassPart] = [thisAnsweredPartQuestion.questionSequence]; + } - } // END of if (matchedAnswer) + // are there any questions left that are not suppressed? + const remainingQuestions = glassPart.partQuestions.filter((q) => { + return !q.suppressQuestion; + }); + + if (remainingQuestions.length < 1) { + // this is the final answer for this glass part + + // mark this part as completely answered by adding answerData + const answeredQuestionObj = { + questionText: pq.questionText, + selectedAnswerText: matchedAnswer.answerText, + questionNum: pq.questionSequence, + suppressQuestion: pq.suppressQuestion, + }; + // set the answerData as 'already answered' + glassPart.answerData = { + answerResult: matchedAnswer.nextQuestionSequence ? matchedAnswer.nextQuestionSequence : matchedAnswer.answerResult, + answeredQuestions: [answeredQuestionObj], + }; + + // suppress this glassPart because it has an answer + glassPart.isSuppressedPart = true; + } } }); // Update the key to re-render this part's question-chain component - this.partsQuestionsData[i].key = this.partsQuestionsData[i].glassLocation + this.partsQuestionsData[i].glassName + Date.now().toString(); + this.partsQuestionsData[gpIndex].key = this.partsQuestionsData[gpIndex].glassLocation + this.partsQuestionsData[gpIndex].glassName + Date.now().toString(); } @@ -308,8 +339,10 @@ export default { }); + // DETERMINE ANSWERED QUESTIONS LIST FOR THIS GLASS PART + // look through all (this part's) part questions for any duplicates that were suppressed; - // add them to the list of answered questions if found + // add them to the list of answered questions if found // EX answeredQuestionIndexes: [1,5,11,13] @@ -319,13 +352,15 @@ export default { // }; const thisPartsDupes = this.foundDuplicateQuestions["glassPart" + answer.partIndex]; - + const completeAnsweredQuestions = answer.answeredQuestions ? [...answer.answeredQuestions] : []; + const glassPartAnswered = this.partsQuestionsData[answer.partIndex]; + thisPartsDupes?.forEach((dupe) => { // dupe is a single integer - const dupeQuestion = glassPartWithAnswer.partQuestions[dupe - 1]; + const dupeQuestion = glassPartAnswered.partQuestions[dupe - 1]; const dupeQuestionAnswer = dupeQuestion.answers.find((q) => q.selected === true); - glassPartWithAnswer.partQuestions.forEach((q) => { + glassPartAnswered.partQuestions.forEach((q) => { let includeThisDupeInAnsweredQuestions = false; // did one of the answers of this question point to the duplicated question? @@ -347,7 +382,7 @@ export default { questionNum: dupeQuestion.questionSequence, questionText: dupeQuestion.questionText, selectedAnswerText: dupeQuestionAnswer.answerText, - isDuplicateQuestion: dupeQuestion.suppressDuplicateQuestion, + suppressQuestion: dupeQuestion.suppressQuestion, }); } }); @@ -363,7 +398,7 @@ export default { filteredCompleteAnsweredQuestions = filteredCompleteAnsweredQuestions.sort((a,b) => a.questionNum - b.questionNum); // set final answer data for the current answered glass part - glassPartWithAnswer.answerData = { + glassPartAnswered.answerData = { answerResult: answer.answerResult, answeredQuestions: filteredCompleteAnsweredQuestions, } @@ -386,8 +421,12 @@ export default { this.partsQuestionsData = this.partsQuestionsFromApi.map((part, i) => { part.key = part.glassLocation + "-" + part.glassName; this.selectedAnswers[part.key] = []; + if (!alreadyAnsweredQuestions) { + part.answerData = null; + } alreadyAnsweredQuestions?.forEach((savedPart) => { + if (!savedPart.glassLocation || !savedPart.glassName || !savedPart.answeredQuestions || !savedPart.result) { return; } @@ -412,8 +451,8 @@ export default { // mark this partQuestion as answered (question-chain will read this) part.partQuestions[aq.questionNum-1].answerSelected = answerString; // mark this partQuestion as duplicate if it is (question-chain will read this) - if (aq.isDuplicateQuestion) { - part.partQuestions[aq.questionNum-1].isDuplicateQuestion = true; + if (aq.suppressQuestion) { + part.partQuestions[aq.questionNum-1].suppressQuestion = true; } }); @@ -426,6 +465,7 @@ export default { answeredQuestions: savedPart.answeredQuestions } } + }); // Set up watch for each set of part questions, which gets updated when all questions for a part have been answered diff --git a/src/ux-components/list-button-horizontal/list-button-horizontal.vue b/src/ux-components/list-button-horizontal/list-button-horizontal.vue index 83c9b0fd1..7011835b9 100644 --- a/src/ux-components/list-button-horizontal/list-button-horizontal.vue +++ b/src/ux-components/list-button-horizontal/list-button-horizontal.vue @@ -15,6 +15,7 @@ :value="value" :aria-required="isRequired" v-model="checkValue" + :checked="checkValue" @change="handleInputChange()" />