DigitalConsumer.FixMyGlass/src/digital-components/question-chain/question-chain.vue

228 lines
8.5 KiB
Vue

<template>
<div v-for="(q, i) in questions" :key="i">
<transition appear name="fade" mode="out-in">
<buttonQuestion
v-if="isQuestionVisible(q)"
class="radioQuestion"
:class="isCurrentQuestion(q) && 'current-question'"
:questionText="q.questionText"
:answers="q.answers"
:groupName="`question-${index}-${q.questionSequence}`"
v-model="q.answerSelected"
@update:modelValue="handleAnswer(q, $event)"
isRequired
:validationRules="validationRules" />
</transition>
</div>
</template>
<script>
import buttonQuestion from "@/digital-components/button-question/button-question";
import { useValidateForm } from "vee-validate";
import {
mapQuestionAnswersForChain,
normalizeAnswerSelectedValue,
} from "@/helpers/question-chain-helper";
export default {
name: "questionChain",
data() {
return {
currentQuestionNum: 0,
questions: [],
};
},
props: {
questionData: Array,
validationRules: String,
modelValue: Object,
index: Number,
answerKey: String,
hasSavedAnswer: Boolean,
hasDownstreamSavedAnswers: Boolean,
},
async created() {
// do a test validation check upon create to prevent out of sync / incorrect valid states
await useValidateForm(); // NOTE: needs to have async/await here; tested and won't work without it
this.questionData.map((q, i) => {
const answers = mapQuestionAnswersForChain(q);
const question = {
questionText: q.questionText,
questionSequence: q.questionSequence,
answers,
answerSelected: normalizeAnswerSelectedValue(q.answerSelected, answers),
};
if (!q.suppressThisQuestion) {
this.questions.push(question);
}
});
if (this.questions.length > 0) {
this.initializeCurrentQuestionNum();
this.scrollCurrentQuestionIntoView();
}
},
methods: {
getQuestionSequence(question) {
return Number(question?.questionSequence);
},
isCurrentQuestion(question) {
return this.getQuestionSequence(question) === Number(this.currentQuestionNum);
},
isQuestionVisible(question) {
return !!question.answerSelected || this.isCurrentQuestion(question);
},
initializeCurrentQuestionNum() {
const pendingNextQuestion = [...this.questions]
.reverse()
.find((question) => question.answerSelected?.includes("|nextQuestion|"));
if (pendingNextQuestion) {
const nextQuestionSequence = Number(
pendingNextQuestion.answerSelected.split("|")[2]
);
if (
this.questions.some(
(question) => this.getQuestionSequence(question) === nextQuestionSequence
)
) {
this.currentQuestionNum = nextQuestionSequence;
return;
}
}
const firstUnanswered = this.questions.find((question) => !question.answerSelected);
if (firstUnanswered) {
this.currentQuestionNum = this.getQuestionSequence(firstUnanswered);
}
},
scrollCurrentQuestionIntoView() {
this.$nextTick(() => {
document.querySelector(".current-question")?.scrollIntoView({ behavior: "smooth" });
});
},
syncAnswerSelectedToQuestionData(questionSequence, answerSelected) {
const sourceQuestion = this.questionData?.find(
(q) => this.getQuestionSequence(q) === Number(questionSequence)
);
if (sourceQuestion) {
sourceQuestion.answerSelected = answerSelected;
}
},
getProblemQuestionIdFromSelectedAnswer(question, selectedAnswer) {
if (!question?.answers || !selectedAnswer) {
return null;
}
const matchedAnswer = question.answers.find(
(answer) => answer.value === selectedAnswer
);
return matchedAnswer?.problemQuestionId ?? null;
},
handleAnswer(question, returnedAnswer) {
/*
returnedAnswer example format:
"1|answer|DD11132|Yes"
*/
question.answerSelected = returnedAnswer;
this.syncAnswerSelectedToQuestionData(question.questionSequence, returnedAnswer);
const questionChainAnswer = this.getQuestionChainAnswerIfComplete(returnedAnswer);
if (!questionChainAnswer) {
return;
}
const shouldNotifyParent =
!questionChainAnswer.incomplete ||
this.hasSavedAnswer ||
this.hasDownstreamSavedAnswers;
if (shouldNotifyParent) {
this.$emit("update:modelValue", questionChainAnswer);
}
},
getQuestionChainAnswerIfComplete(returnedAnswer) {
// this method will return either a final answer or Boolean false
if (!returnedAnswer) {
return false;
}
// returnedAnswer examples:
// "1|nextQuestion|3|No"
// "5|answer|DW02104|Yes"
const returnedAnswerArray = returnedAnswer.split("|");
const questionNum = Number(returnedAnswerArray[0]);
const questionType = returnedAnswerArray[1];
const questionAnswer = returnedAnswerArray[2];
const answeredQuestions = [];
this.questions.forEach((q) => {
const questionSequence = this.getQuestionSequence(q);
// find this question and mark it as "answered" by populating answerSelected
if (questionSequence === questionNum) {
q.answerSelected = returnedAnswer;
this.syncAnswerSelectedToQuestionData(questionSequence, returnedAnswer);
}
// remove all answers AFTER this question...
// (needed in case user is changing previously answered questions)
if (questionSequence > questionNum) {
delete q.answerSelected;
this.syncAnswerSelectedToQuestionData(questionSequence, "");
}
if (q.answerSelected) {
answeredQuestions.push({
questionText: q.questionText,
selectedAnswer: q.answerSelected,
selectedAnswerText: q.answerSelected.split("|")[3],
questionNum: q.questionSequence,
problemQuestionId: this.getProblemQuestionIdFromSelectedAnswer(
q,
q.answerSelected
),
});
}
});
// return incomplete answer if there's a nextQuestion... or return final answers (truthy)
if (questionType === "nextQuestion") {
// update to next question index
this.currentQuestionNum = Number(questionAnswer);
this.scrollCurrentQuestionIntoView();
return {
incomplete: true,
answeredQuestions,
index: this.index,
};
} else {
// reset current question index (removes .current-question class)
this.currentQuestionNum = 0; // reset count
const answeredQuestion = this.questions.find(
(q) => this.getQuestionSequence(q) === questionNum
);
// return an object with the part answer, all the answered questions, and the part index
return {
answerResult: questionAnswer,
problemQuestionId: this.getProblemQuestionIdFromSelectedAnswer(
answeredQuestion,
returnedAnswer
),
answeredQuestions: answeredQuestions,
index: this.index,
};
}
},
},
components: {
buttonQuestion,
},
};
</script>