286 lines
7.9 KiB
Vue
286 lines
7.9 KiB
Vue
<script setup lang="ts">
|
|
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
|
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
|
import { graphql } from "@/gql";
|
|
import FeedbackCompletition from "@/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue";
|
|
import {
|
|
PERCENTAGES,
|
|
RATINGS,
|
|
YES_NO,
|
|
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
|
|
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
|
import type { LearningContentFeedback } from "@/types";
|
|
import { useMutation } from "@urql/vue";
|
|
import { useRouteQuery } from "@vueuse/router";
|
|
import log from "loglevel";
|
|
import { computed, onMounted, reactive, ref } from "vue";
|
|
import { useTranslation } from "i18next-vue";
|
|
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
|
|
|
const props = defineProps<{
|
|
content: LearningContentFeedback;
|
|
}>();
|
|
const courseSession = useCurrentCourseSession();
|
|
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
|
|
|
const { t } = useTranslation();
|
|
|
|
const stepNo = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
|
|
|
const title = computed(
|
|
() => `«${props.content.circle?.title}»: ${t("feedback.areYouSatisfied")}`
|
|
);
|
|
|
|
const circleExperts = computed(() => {
|
|
if (props.content?.circle?.slug) {
|
|
return courseSessionDetailResult.filterCircleExperts(props.content.circle.slug);
|
|
}
|
|
return [];
|
|
});
|
|
|
|
const stepLabels = [
|
|
t("general.introduction"),
|
|
t("feedback.satisfactionLabel"),
|
|
t("feedback.goalAttainmentLabel"),
|
|
t("feedback.proficiencyLabel"),
|
|
t("feedback.preparationTaskClarityLabel"),
|
|
t("feedback.instructorCompetenceLabel"),
|
|
t("feedback.instructorRespectLabel"),
|
|
t("feedback.instructorOpenFeedbackLabel"),
|
|
t("feedback.recommendLabel"),
|
|
t("feedback.coursePositiveFeedbackLabel"),
|
|
t("feedback.courseNegativeFeedbackLabel"),
|
|
t("general.submission"),
|
|
];
|
|
|
|
const numSteps = stepLabels.length;
|
|
|
|
// noinspection GraphQLUnresolvedReference -> mute IntelliJ warning
|
|
const sendFeedbackMutation = graphql(`
|
|
mutation SendFeedbackMutation(
|
|
$courseSessionId: ID!
|
|
$learningContentId: ID!
|
|
$data: GenericScalar!
|
|
$submitted: Boolean
|
|
) {
|
|
send_feedback(
|
|
course_session_id: $courseSessionId
|
|
learning_content_page_id: $learningContentId
|
|
data: $data
|
|
submitted: $submitted
|
|
) {
|
|
feedback_response {
|
|
id
|
|
data
|
|
submitted
|
|
}
|
|
errors {
|
|
field
|
|
messages
|
|
}
|
|
}
|
|
}
|
|
`);
|
|
|
|
const feedbackSubmitted = ref(false);
|
|
|
|
const { executeMutation } = useMutation(sendFeedbackMutation);
|
|
|
|
interface FeedbackData {
|
|
[key: string]: number | string | null;
|
|
}
|
|
|
|
const feedbackData: FeedbackData = reactive({
|
|
satisfaction: null,
|
|
goal_attainment: null,
|
|
proficiency: null,
|
|
preparation_task_clarity: null,
|
|
instructor_competence: null,
|
|
instructor_respect: null,
|
|
instructor_open_feedback: "",
|
|
would_recommend: null,
|
|
course_positive_feedback: "",
|
|
course_negative_feedback: "",
|
|
});
|
|
|
|
const questionData = [
|
|
{
|
|
modelKey: "satisfaction",
|
|
items: RATINGS,
|
|
component: ItRadioGroup,
|
|
},
|
|
{
|
|
modelKey: "goal_attainment",
|
|
items: RATINGS,
|
|
component: ItRadioGroup,
|
|
},
|
|
{
|
|
modelKey: "proficiency",
|
|
items: PERCENTAGES,
|
|
component: ItRadioGroup,
|
|
},
|
|
{
|
|
modelKey: "preparation_task_clarity",
|
|
items: YES_NO,
|
|
component: ItRadioGroup,
|
|
},
|
|
{
|
|
modelKey: "instructor_competence",
|
|
items: RATINGS,
|
|
component: ItRadioGroup,
|
|
},
|
|
{
|
|
modelKey: "instructor_respect",
|
|
items: RATINGS,
|
|
component: ItRadioGroup,
|
|
},
|
|
{
|
|
modelKey: "instructor_open_feedback",
|
|
component: ItTextarea,
|
|
},
|
|
{
|
|
modelKey: "would_recommend",
|
|
items: YES_NO,
|
|
component: ItRadioGroup,
|
|
},
|
|
{
|
|
modelKey: "course_positive_feedback",
|
|
component: ItTextarea,
|
|
},
|
|
{
|
|
modelKey: "course_negative_feedback",
|
|
component: ItTextarea,
|
|
},
|
|
];
|
|
|
|
const previousStep = () => {
|
|
if (stepNo.value > 0) {
|
|
stepNo.value -= 1;
|
|
}
|
|
};
|
|
|
|
const nextStep = () => {
|
|
if (stepNo.value < numSteps && hasStepValidInput(stepNo.value)) {
|
|
stepNo.value += 1;
|
|
}
|
|
log.debug(`next step ${stepNo.value} of ${numSteps}`);
|
|
mutateFeedback(feedbackData);
|
|
};
|
|
|
|
function hasStepValidInput(stepNumber: number) {
|
|
const question = questionData[stepNumber - 1];
|
|
if (question) {
|
|
if (
|
|
[
|
|
"instructor_open_feedback",
|
|
"course_negative_feedback",
|
|
"course_positive_feedback",
|
|
].includes(question.modelKey)
|
|
) {
|
|
// text response questions need to have a "truthy" value (not "" or null)
|
|
return feedbackData[question.modelKey];
|
|
} else {
|
|
// other responses need to have data, can be `0` or `false`
|
|
return feedbackData[question.modelKey] !== null;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function mutateFeedback(data: FeedbackData, submit = false) {
|
|
log.debug("mutate feedback", feedbackData);
|
|
return executeMutation({
|
|
courseSessionId: courseSession.value.id,
|
|
learningContentId: props.content.id,
|
|
data: data,
|
|
submitted: submit,
|
|
})
|
|
.then((result) => {
|
|
log.debug("feedback mutation result", result);
|
|
if (result.data?.send_feedback?.feedback_response?.data) {
|
|
const responseData = result.data.send_feedback.feedback_response.data;
|
|
if (!responseData.instructor_open_feedback) {
|
|
responseData.instructor_open_feedback = "";
|
|
}
|
|
if (!responseData.course_negative_feedback) {
|
|
responseData.course_negative_feedback = "";
|
|
}
|
|
if (!responseData.course_positive_feedback) {
|
|
responseData.course_positive_feedback = "";
|
|
}
|
|
Object.assign(feedbackData, responseData);
|
|
log.debug("feedback data", feedbackData);
|
|
feedbackSubmitted.value =
|
|
result.data?.send_feedback?.feedback_response?.submitted || false;
|
|
}
|
|
})
|
|
.catch((e) => log.error(e));
|
|
}
|
|
|
|
onMounted(async () => {
|
|
log.debug("Feedback mounted");
|
|
await mutateFeedback({});
|
|
if (feedbackSubmitted.value) {
|
|
stepNo.value = numSteps - 1;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<LearningContentMultiLayout
|
|
:title="title"
|
|
sub-title="Feedback"
|
|
:learning-content="content"
|
|
:show-start-button="stepNo === 0"
|
|
:show-next-button="stepNo > 0 && stepNo + 1 < numSteps"
|
|
:disable-next-button="!hasStepValidInput(stepNo)"
|
|
:show-previous-button="stepNo > 0 && !feedbackSubmitted"
|
|
:show-exit-button="stepNo + 1 === numSteps"
|
|
:current-step="stepNo"
|
|
:steps-count="numSteps"
|
|
:start-badge-text="$t('general.introduction')"
|
|
:end-badge-text="$t('general.submission')"
|
|
:base-url="props.content.frontend_url"
|
|
close-button-variant="close"
|
|
@previous="previousStep()"
|
|
@next="nextStep()"
|
|
>
|
|
<div>
|
|
<p v-if="stepNo === 0" class="mt-10">
|
|
{{
|
|
$t("feedback.intro", {
|
|
name: `${circleExperts[0]?.first_name} ${circleExperts[0]?.last_name}`,
|
|
})
|
|
}}
|
|
</p>
|
|
<p v-if="stepNo > 0 && stepNo + 1 < numSteps" class="pb-2">
|
|
{{ stepLabels[stepNo] }}
|
|
</p>
|
|
<div v-for="(question, index) in questionData" :key="index">
|
|
<!-- eslint-disable -->
|
|
<!-- eslint does not like the dynamic v-model... -->
|
|
<component
|
|
:is="question.component"
|
|
v-if="index + 1 === stepNo"
|
|
v-model="feedbackData[question.modelKey] as any"
|
|
:items="question['items']"
|
|
:cy-key="question.modelKey"
|
|
/>
|
|
<!-- eslint-enable -->
|
|
</div>
|
|
<FeedbackCompletition
|
|
v-if="stepNo === 11"
|
|
:avatar-url="circleExperts[0].avatar_url"
|
|
:title="
|
|
$t('feedback.completionTitle', {
|
|
name: `${circleExperts[0].first_name} ${circleExperts[0].last_name}`,
|
|
})
|
|
"
|
|
:description="$t('feedback.completionDescription')"
|
|
:feedback-sent="feedbackSubmitted"
|
|
@send-feedback="mutateFeedback(feedbackData, true)"
|
|
/>
|
|
</div>
|
|
</LearningContentMultiLayout>
|
|
</template>
|