Skip to content

Commit

Permalink
Conditional Questions
Browse files Browse the repository at this point in the history
  • Loading branch information
datajohnson committed Mar 22, 2024
1 parent 15a734b commit 2a07922
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 19 deletions.
37 changes: 29 additions & 8 deletions src/api/src/routes/admin-survey-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,23 @@ adminSurveyRouter.get("/", async (req: Request, res: Response) => {
list = await db("SURVEY").withSchema(DB_SCHEMA).whereIn("SID", surveys);
} else list = await db("SURVEY").withSchema(DB_SCHEMA);

for (let item of list) {
item.questions = await db("QUESTION").withSchema(DB_SCHEMA).where({ SID: item.SID }).orderBy("ORD");
item.choices = await db("JSON_DEF").withSchema(DB_SCHEMA).where({ SID: item.SID }).orderBy("TITLE");
item.responses = await db("PARTICIPANT")
.withSchema(DB_SCHEMA)
.innerJoin("PARTICIPANT_DATA", "PARTICIPANT.TOKEN", "PARTICIPANT_DATA.TOKEN")
.where({ SID: item.SID })
.whereNotNull("RESPONSE_DATE");
let allQuestions = await db("QUESTION").withSchema(DB_SCHEMA).orderBy("SID", "ORD");
let allChoices = await db("JSON_DEF").withSchema(DB_SCHEMA).orderBy("SID", "TITLE");
let allResponses = await db("PARTICIPANT")
.withSchema(DB_SCHEMA)
.innerJoin("PARTICIPANT_DATA", "PARTICIPANT.TOKEN", "PARTICIPANT_DATA.TOKEN")
.whereNotNull("RESPONSE_DATE");
let allConditions = await db("Q_CONDITION_TBL").withSchema(DB_SCHEMA);

for (let item of list) {
item.questions = allQuestions.filter((q) => q.SID == item.SID);
item.choices = allChoices.filter((c) => c.SID == item.SID);
item.responses = allResponses.filter((r) => r.SID == item.SID);
item.choices.map((c: any) => (c.choices = JSON.parse(c.SELECTION_JSON)));

for (let q of item.questions) {
q.conditions = allConditions.filter((c) => c.QID == q.QID);
}
}

res.json({ data: list });
Expand Down Expand Up @@ -144,6 +151,20 @@ adminSurveyRouter.put("/:SID/question/:QID/order", async (req: Request, res: Res
res.json({ data: "success" });
});

adminSurveyRouter.put("/:SID/question/:QID/conditions", async (req: Request, res: Response) => {
let { QID } = req.params;
let { conditions } = req.body;

await db("Q_CONDITION_TBL").withSchema(DB_SCHEMA).where({ QID }).delete();

for (let cond of conditions) {
delete cond.COND_ID;
await db("Q_CONDITION_TBL").withSchema(DB_SCHEMA).insert(cond);
}

res.json({ data: "success" });
});

adminSurveyRouter.put("/:SID/question/:QID", async (req: Request, res: Response) => {
let { SID, QID } = req.params;
let { ASK, OPTIONAL, ORD, TYPE, RANGE, GROUP_ID, JSON_ID, SELECT_LIMIT, choices, choiceTitle } = req.body;
Expand Down
2 changes: 2 additions & 0 deletions src/api/src/routes/survey-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ surveyRouter.get(
}

for (let q of questions) {
q.conditions = await db("Q_CONDITION_TBL").withSchema(DB_SCHEMA).where({ QID: q.QID });
if (q.JSON_ID) {
q.choices = choices.find((c) => c.JSON_ID == q.JSON_ID)?.choices;
}
Expand Down Expand Up @@ -68,6 +69,7 @@ surveyRouter.get(
let questions = await db("QUESTION").withSchema(DB_SCHEMA).where({ SID: token }).orderBy("ORD");

for (let q of questions) {
q.conditions = await db("Q_CONDITION_TBL").withSchema(DB_SCHEMA).where({ QID: q.QID });
if (q.JSON_ID) {
q.choices = choices.find((c) => c.JSON_ID == q.JSON_ID)?.choices;
}
Expand Down
4 changes: 2 additions & 2 deletions src/web/src/components/QuestionRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@
</tr>

<tr v-for="sub of question.subQuestions">
<th style="text-align: left">
<th style="text-align: left" v-if="sub.isVisible">
{{ sub.ASK }}
</th>
<td v-for="ch of sub.choices">
<td v-for="ch of sub.choices" v-if="sub.isVisible">
<v-checkbox
class="my-0 mx-auto"
v-model="sub.answer"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@
color="primary"
icon="mdi-chevron-up"
size="x-small"
class="float-right ml-1"
class="float-right ml-1 my-0"
:disabled="index == 0"></v-btn>
<v-btn
@click.stop="moveDown(index)"
color="primary"
icon="mdi-chevron-down"
size="x-small"
class="float-right"
class="float-right ml-1 my-0"
:disabled="index >= survey.questions.length - 1"></v-btn>
<v-list-item-title>Question {{ index + 1 }}: ({{ question.TYPE }})</v-list-item-title>
<v-btn
@click.stop="editConditionsClick(index)"
color="yg_moss"
icon="mdi-timeline-question-outline"
size="x-small"
class="float-right ml-1 my-0"
title="Edit conditions"></v-btn>
<v-list-item-title
>Question {{ index + 1 }}: ({{ question.TYPE }}) - {{ question.conditions.length }} conditions</v-list-item-title
>
<v-list-item-subtitle>{{ question.ASK }}</v-list-item-subtitle>
</v-list-item>

Expand Down Expand Up @@ -76,6 +85,57 @@
</v-card-text>
</v-card>
</v-dialog>

<v-dialog v-model="conditionsVisible" persistent max-width="800">
<v-card v-if="question">
<v-toolbar color="primary" variant="dark" title="Condition Editor">
<v-spacer></v-spacer>
<v-btn icon @click="conditionsVisible = false" color="white"><v-icon>mdi-close</v-icon></v-btn>
</v-toolbar>
<v-card-text>
<div v-for="(condition, cidx) of question.conditions">
<v-row>
<v-col>
<v-select
v-model="condition.CQID"
label="Question"
:items="otherQuestions"
item-title="ASK"
item-value="QID" />
</v-col>

<v-col>
<v-text-field v-model="condition.TVALUE" label="Value" />
</v-col>
<v-col>
<div class="d-flex">
<v-select v-model="condition.LOGIC" label="Logic" :items="['AND', 'OR']" />
<v-btn
class="ml-2 mt-2"
size="x-small"
@click="deleteConditionClick(cidx)"
:disabled="disabled || !canSaveCondition"
color="warning"
icon="mdi-delete-outline"></v-btn>
</div>
</v-col>
</v-row>
</div>

<v-btn color="info" @click="addConditionClick" :disabled="disabled || otherQuestions.length == 0"
>Add Condition</v-btn
>

<div class="d-flex mb-2">
<v-btn color="primary" class="mt-5" @click="saveConditionClick" :disabled="disabled">Save</v-btn>
<v-spacer />
<v-btn color="warning" variant="tonal" class="mt-5" @click="conditionsVisible = false" :disabled="disabled"
>close</v-btn
>
</div>
</v-card-text>
</v-card>
</v-dialog>
</template>

<script>
Expand All @@ -86,7 +146,7 @@ import QuestionChoices from "./QuestionChoices.vue";
export default {
props: ["question", "index", "disabled"],
components: { QuestionChoices },
data: () => ({ visible: false }),
data: () => ({ visible: false, conditionsVisible: false }),
computed: {
...mapState(useAdminSurveyStore, ["survey", "questionTypes", "groupOptions", "saveQuestion", "deleteQuestion"]),
canSave() {
Expand All @@ -102,9 +162,15 @@ export default {
return true;
},
canSaveCondition() {
return true;
},
otherQuestions() {
return this.survey.questions.filter((q) => q.QID != this.question.QID && q.ORD <= this.question.ORD);
},
},
methods: {
...mapActions(useAdminSurveyStore, ["moveUp", "moveDown"]),
...mapActions(useAdminSurveyStore, ["moveUp", "moveDown", "saveQuestionConditions"]),
showEditor() {
this.visible = true;
},
Expand All @@ -119,6 +185,26 @@ export default {
this.deleteQuestion(this.question);
this.visible = false;
},
editConditionsClick(index) {
this.question.conditions = this.question.conditions || [];
this.conditionsVisible = true;
},
async saveConditionClick() {
await this.saveQuestionConditions(this.question);
this.conditionsVisible = false;
},
deleteConditionClick(cidx) {
this.question.conditions.splice(cidx, 1);
},
addConditionClick() {
this.question.conditions.push({
COND_ID: 0,
QID: this.question.QID,
CQID: this.otherQuestions[0]?.QID ?? null,
TVALUE: "",
LOGIC: "AND",
});
},
},
};
</script>
16 changes: 16 additions & 0 deletions src/web/src/modules/administration/modules/survey/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,22 @@ export const useAdminSurveyStore = defineStore("adminSurvey", {
.catch();
}
},

async saveQuestionConditions(question: { QID: number; conditions: any[] }) {
let conditions = question.conditions;

if (this.survey) {
await api
.secureCall("put", `${ADMIN_SURVEY_URL}/${this.survey.SID}/question/${question.QID}/conditions`, {
conditions,
})
.then(async (resp) => {
await this.loadSurveys();
this.selectById(this.survey?.SID || 0);
})
.catch();
}
},
},
});

Expand Down
21 changes: 20 additions & 1 deletion src/web/src/store/SurveyStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export const useSurveyStore = defineStore("survey", {
q.answer = null;
q.isValid = () => {
if (q.OPTIONAL == 1) return true;
if (q.TYPE == 'text') return true;
if (q.TYPE == "text") return true;
if (!q.isVisible) return true;

if (q.subQuestions) {
for (let sub of q.subQuestions) {
Expand All @@ -60,6 +61,24 @@ export const useSurveyStore = defineStore("survey", {
if (trimAnswer && trimAnswer.length > 0) return true;
return false;
};
q.isVisible = true;
q.checkConditions = () => {
let newVisible = true;

for (let cond of q.conditions) {
let parentQuestion = value.questions.find((q: any) => q.QID == cond.CQID);

if (parentQuestion && parentQuestion.answer) {
if (parentQuestion.answer == cond.TVALUE) {
} else {
newVisible = false;
}
} else {
newVisible = false;
}
}
return newVisible;
};
}

this.survey = value;
Expand Down
20 changes: 18 additions & 2 deletions src/web/src/views/Preview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
<div v-if="!moveOn">
<v-card class="default">
<v-card-text>
<div class="markdown"
<div
class="markdown"
v-html="renderMarkdown(survey.survey.PAGE_INTRO).output"
v-if="renderMarkdown(survey.survey.PAGE_INTRO).isMarkdown"></div>
<p v-else>{{ survey.survey.PAGE_INTRO }}</p>
Expand All @@ -30,7 +31,11 @@
<div class="row">
<div class="col-sm-12 col-md-9 col-lg-7">
<div v-for="(question, idx) of questionGroups" :key="idx">
<question-renderer :index="idx" :question="question" @answerChanged="checkAllValid"></question-renderer>
<question-renderer
:index="getQuestionNumber(question)"
:question="question"
@answerChanged="checkAllValid"
v-if="question.isVisible"></question-renderer>
</div>

<div v-if="survey.survey.CONTACT_QUESTION">
Expand Down Expand Up @@ -113,6 +118,7 @@ export default {
let v = true;
for (let q of this.survey.questions) {
q.isVisible = q.checkConditions();
let qv = q.isValid();
v = v && qv;
}
Expand Down Expand Up @@ -143,6 +149,16 @@ export default {
renderMarkdown(input) {
return RenderMarkdown(input);
},
getQuestionNumber(question) {
let index = 0;
for (let q of this.survey.questions) {
if (q.QID == question.QID) return index;
if (q.isVisible) index++;
}
return index;
},
},
};
</script>
Expand Down
13 changes: 12 additions & 1 deletion src/web/src/views/Survey.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<div class="row">
<div class="col-sm-12 col-md-9 col-lg-7">
<div v-for="(question, idx) of questionGroups" :key="idx">
<question-renderer :index="idx" :question="question" @answerChanged="checkAllValid"></question-renderer>
<question-renderer :index="getQuestionNumber(question)" :question="question" @answerChanged="checkAllValid"></question-renderer>
</div>

<div v-if="survey.survey.CONTACT_QUESTION">
Expand Down Expand Up @@ -123,6 +123,7 @@ export default {
let v = true;
for (let q of this.survey.questions) {
q.isVisible = q.checkConditions();
let qv = q.isValid();
v = v && qv;
}
Expand Down Expand Up @@ -156,6 +157,16 @@ export default {
});
}
},
getQuestionNumber(question) {
let index = 0;
for (let q of this.survey.questions) {
if (q.QID == question.QID) return index;
if (q.isVisible) index++;
}
return index;
},
},
};
</script>
Expand Down

0 comments on commit 2a07922

Please sign in to comment.