wip: Add base feedback component
This commit is contained in:
parent
aedb8ac04a
commit
2a6b6c9658
|
|
@ -24,7 +24,8 @@ const documents = {
|
||||||
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument,
|
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n }\n }\n": types.DashboardConfigDocument,
|
||||||
"\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument,
|
"\n query dashboardProgress($courseId: ID!) {\n course_progress(course_id: $courseId) {\n _id\n course_id\n session_to_continue_id\n competence {\n _id\n total_count\n success_count\n fail_count\n }\n assignment {\n _id\n total_count\n points_max_count\n points_achieved_count\n }\n }\n }\n": types.DashboardProgressDocument,
|
||||||
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument,
|
"\n query courseStatistics($courseId: ID!) {\n course_statistics(course_id: $courseId) {\n _id\n course_id\n course_title\n course_slug\n course_session_properties {\n _id\n sessions {\n id\n name\n }\n generations\n circles {\n id\n name\n }\n }\n course_session_selection_ids\n course_session_selection_metrics {\n _id\n session_count\n participant_count\n expert_count\n }\n attendance_day_presences {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n due_date\n participants_present\n participants_total\n details_url\n }\n summary {\n _id\n days_completed\n participants_present\n }\n }\n feedback_responses {\n _id\n records {\n _id\n course_session_id\n generation\n circle_id\n experts\n satisfaction_average\n satisfaction_max\n details_url\n }\n summary {\n _id\n satisfaction_average\n satisfaction_max\n total_responses\n }\n }\n assignments {\n _id\n summary {\n _id\n completed_count\n average_passed\n }\n records {\n _id\n course_session_id\n course_session_assignment_id\n circle_id\n generation\n assignment_title\n assignment_type_translation_key\n details_url\n deadline\n metrics {\n _id\n passed_count\n failed_count\n unranked_count\n ranking_completed\n average_passed\n }\n }\n }\n competences {\n _id\n summary {\n _id\n success_total\n fail_total\n }\n records {\n _id\n course_session_id\n generation\n circle_id\n title\n success_count\n fail_count\n details_url\n }\n }\n }\n }\n": types.CourseStatisticsDocument,
|
||||||
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
|
"\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutationDocument,
|
||||||
|
"\n mutation SendFeedbackMutation2(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n": types.SendFeedbackMutation2Document,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -88,7 +89,11 @@ export function graphql(source: "\n query courseStatistics($courseId: ID!) {\n
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"];
|
export function graphql(source: "\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"];
|
||||||
|
/**
|
||||||
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
|
*/
|
||||||
|
export function graphql(source: "\n mutation SendFeedbackMutation2(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"): (typeof documents)["\n mutation SendFeedbackMutation2(\n $courseSessionId: ID!\n $learningContentId: ID!\n $learningContentType: String!\n $data: GenericScalar!\n $submitted: Boolean\n ) {\n send_feedback(\n course_session_id: $courseSessionId\n learning_content_page_id: $learningContentId\n learning_content_type: $learningContentType\n data: $data\n submitted: $submitted\n ) {\n feedback_response {\n id\n data\n submitted\n }\n errors {\n field\n messages\n }\n }\n }\n"];
|
||||||
|
|
||||||
export function graphql(source: string) {
|
export function graphql(source: string) {
|
||||||
return (documents as any)[source] ?? {};
|
return (documents as any)[source] ?? {};
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -851,7 +851,7 @@ type CompetenceCertificateListObjectType implements CoursePageInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, submitted: Boolean = false): SendFeedbackMutation
|
send_feedback(course_session_id: ID!, data: GenericScalar, learning_content_page_id: ID!, learning_content_type: String!, submitted: Boolean = false): SendFeedbackMutation
|
||||||
update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation
|
update_course_session_attendance_course_users(attendance_user_list: [AttendanceUserInputType]!, id: ID!): AttendanceCourseUserMutation
|
||||||
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
|
upsert_assignment_completion(assignment_id: ID!, assignment_user_id: UUID, completion_data_string: String, completion_status: AssignmentCompletionStatus, course_session_id: ID!, evaluation_passed: Boolean, evaluation_points: Float, initialize_completion: Boolean, learning_content_page_id: ID): AssignmentCompletionMutation
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { graphql } from "@/gql";
|
||||||
|
import FeedbackCompletition from "@/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue";
|
||||||
|
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||||
|
import type { LearningContentFeedbackUK, LearningContentFeedbackVV } 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: LearningContentFeedbackVV | LearningContentFeedbackUK;
|
||||||
|
stepLabels: string[];
|
||||||
|
questionData: any[];
|
||||||
|
introduction: string;
|
||||||
|
title: string;
|
||||||
|
completionTitle: string;
|
||||||
|
completionDescription: string;
|
||||||
|
showAvatar: boolean;
|
||||||
|
}>();
|
||||||
|
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}»: ${props.title}`);
|
||||||
|
|
||||||
|
const circleExperts = computed(() => {
|
||||||
|
if (props.content?.circle?.slug) {
|
||||||
|
return courseSessionDetailResult.filterCircleExperts(props.content.circle.slug);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const localStepLabels = ref(props.stepLabels);
|
||||||
|
const localQuestionData = ref(props.questionData);
|
||||||
|
const feedbackData: FeedbackData = reactive(feedbackDataFactory());
|
||||||
|
|
||||||
|
const numSteps = computed(() => localStepLabels.value.length);
|
||||||
|
const textQuestionKeys = computed(() =>
|
||||||
|
props.questionData.filter((item) => isTextNode(item)).map((item) => item.modelKey)
|
||||||
|
);
|
||||||
|
const avatarUrl = computed(() => circleExperts.value[0]?.avatar_url);
|
||||||
|
|
||||||
|
// noinspection GraphQLUnresolvedReference -> mute IntelliJ warning
|
||||||
|
const sendFeedbackMutation = graphql(`
|
||||||
|
mutation SendFeedbackMutation(
|
||||||
|
$courseSessionId: ID!
|
||||||
|
$learningContentId: ID!
|
||||||
|
$learningContentType: String!
|
||||||
|
$data: GenericScalar!
|
||||||
|
$submitted: Boolean
|
||||||
|
) {
|
||||||
|
send_feedback(
|
||||||
|
course_session_id: $courseSessionId
|
||||||
|
learning_content_page_id: $learningContentId
|
||||||
|
learning_content_type: $learningContentType
|
||||||
|
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 previousStep = () => {
|
||||||
|
if (stepNo.value > 0) {
|
||||||
|
stepNo.value -= 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
if (stepNo.value < numSteps.value && hasStepValidInput(stepNo.value)) {
|
||||||
|
stepNo.value += 1;
|
||||||
|
}
|
||||||
|
log.debug(`next step ${stepNo.value} of ${numSteps.value}`);
|
||||||
|
mutateFeedback(feedbackData);
|
||||||
|
};
|
||||||
|
|
||||||
|
function hasStepValidInput(stepNumber: number) {
|
||||||
|
const question = localQuestionData.value[stepNumber - 1];
|
||||||
|
if (question) {
|
||||||
|
if (textQuestionKeys.value.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,
|
||||||
|
learningContentType: props.content.content_type,
|
||||||
|
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;
|
||||||
|
textQuestionKeys.value.map((key) => {
|
||||||
|
if (!responseData[key]) {
|
||||||
|
responseData[key] = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.assign(feedbackData, responseData);
|
||||||
|
log.debug("feedback data", feedbackData);
|
||||||
|
feedbackSubmitted.value =
|
||||||
|
result.data?.send_feedback?.feedback_response?.submitted || false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => log.error(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
function feedbackDataFactory() {
|
||||||
|
const data: FeedbackData = {};
|
||||||
|
localQuestionData.value.map((item) => {
|
||||||
|
data[item.modelKey] = isTextNode(item) ? "" : null;
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTextNode(item: any) {
|
||||||
|
return item.component.props.placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
log.debug("Feedback mounted");
|
||||||
|
await mutateFeedback({});
|
||||||
|
if (feedbackSubmitted.value) {
|
||||||
|
stepNo.value = numSteps.value - 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">
|
||||||
|
{{ introduction }}
|
||||||
|
</p>
|
||||||
|
<p v-if="stepNo > 0 && stepNo + 1 < numSteps" class="pb-2">
|
||||||
|
{{ localStepLabels[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 && feedbackData != undefined"
|
||||||
|
v-model="feedbackData[question.modelKey] as any"
|
||||||
|
:items="question['items']"
|
||||||
|
:cy-key="question.modelKey"
|
||||||
|
/>
|
||||||
|
<!-- eslint-enable -->
|
||||||
|
</div>
|
||||||
|
<FeedbackCompletition
|
||||||
|
v-if="stepNo === numSteps - 1"
|
||||||
|
:avatar-url="avatarUrl"
|
||||||
|
:show-avatar="showAvatar"
|
||||||
|
:title="completionTitle"
|
||||||
|
:description="completionDescription"
|
||||||
|
:feedback-sent="feedbackSubmitted"
|
||||||
|
@send-feedback="mutateFeedback(feedbackData, true)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</LearningContentMultiLayout>
|
||||||
|
</template>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { graphql } from "@/gql";
|
||||||
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
||||||
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||||
import { graphql } from "@/gql";
|
|
||||||
import FeedbackCompletition from "@/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue";
|
import FeedbackCompletition from "@/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue";
|
||||||
import {
|
import {
|
||||||
PERCENTAGES,
|
PERCENTAGES,
|
||||||
|
|
@ -9,8 +9,8 @@ import {
|
||||||
YES_NO,
|
YES_NO,
|
||||||
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
|
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
|
||||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||||
import type { LearningContentFeedback } from "@/types";
|
|
||||||
import { useMutation } from "@urql/vue";
|
import { useMutation } from "@urql/vue";
|
||||||
|
import type { LearningContentFeedback } from "@/types";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
|
|
@ -57,15 +57,17 @@ const numSteps = stepLabels.length;
|
||||||
|
|
||||||
// noinspection GraphQLUnresolvedReference -> mute IntelliJ warning
|
// noinspection GraphQLUnresolvedReference -> mute IntelliJ warning
|
||||||
const sendFeedbackMutation = graphql(`
|
const sendFeedbackMutation = graphql(`
|
||||||
mutation SendFeedbackMutation(
|
mutation SendFeedbackMutation2(
|
||||||
$courseSessionId: ID!
|
$courseSessionId: ID!
|
||||||
$learningContentId: ID!
|
$learningContentId: ID!
|
||||||
|
$learningContentType: String!
|
||||||
$data: GenericScalar!
|
$data: GenericScalar!
|
||||||
$submitted: Boolean
|
$submitted: Boolean
|
||||||
) {
|
) {
|
||||||
send_feedback(
|
send_feedback(
|
||||||
course_session_id: $courseSessionId
|
course_session_id: $courseSessionId
|
||||||
learning_content_page_id: $learningContentId
|
learning_content_page_id: $learningContentId
|
||||||
|
learning_content_type: $learningContentType
|
||||||
data: $data
|
data: $data
|
||||||
submitted: $submitted
|
submitted: $submitted
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -1,107 +1,37 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
|
||||||
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
import ItTextarea from "@/components/ui/ItTextarea.vue";
|
||||||
import { graphql } from "@/gql";
|
|
||||||
import FeedbackCompletition from "@/pages/learningPath/learningContentPage/feedback/FeedbackCompletition.vue";
|
|
||||||
import {
|
import {
|
||||||
PERCENTAGES,
|
PERCENTAGES,
|
||||||
RATINGS,
|
RATINGS,
|
||||||
YES_NO,
|
YES_NO,
|
||||||
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
|
} from "@/pages/learningPath/learningContentPage/feedback/feedback.constants";
|
||||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
|
||||||
import type { LearningContentFeedback } from "@/types";
|
import type { LearningContentFeedback } from "@/types";
|
||||||
import { useMutation } from "@urql/vue";
|
import FeedbackBase from "@/pages/learningPath/learningContentPage/feedback/FeedbackBase.vue";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
|
||||||
import log from "loglevel";
|
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
content: LearningContentFeedback;
|
content: LearningContentFeedback;
|
||||||
}>();
|
}>();
|
||||||
const courseSession = useCurrentCourseSession();
|
|
||||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
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 = [
|
const stepLabels = [
|
||||||
t("general.introduction"),
|
t("general.introduction"),
|
||||||
t("feedback.satisfactionLabel"),
|
t("feedback.satisfactionLabel"),
|
||||||
t("feedback.goalAttainmentLabel"),
|
t("feedback.goalAttainmentLabel"),
|
||||||
t("feedback.proficiencyLabel"),
|
t("feedback.proficiencyLabelVV"),
|
||||||
t("feedback.preparationTaskClarityLabel"),
|
t("feedback.praxisAssignmentClarity"),
|
||||||
t("feedback.instructorCompetenceLabel"),
|
t("feedback.recommendLabelVV"),
|
||||||
t("feedback.instructorRespectLabel"),
|
|
||||||
t("feedback.instructorOpenFeedbackLabel"),
|
|
||||||
t("feedback.recommendLabel"),
|
|
||||||
t("feedback.coursePositiveFeedbackLabel"),
|
t("feedback.coursePositiveFeedbackLabel"),
|
||||||
t("feedback.courseNegativeFeedbackLabel"),
|
t("feedback.courseNegativeFeedbackLabel"),
|
||||||
t("general.submission"),
|
t("general.submission"),
|
||||||
];
|
];
|
||||||
|
|
||||||
const numSteps = stepLabels.length;
|
const introduction = t("a.feedback.introductionVV");
|
||||||
|
const title = t("Feedback");
|
||||||
// noinspection GraphQLUnresolvedReference -> mute IntelliJ warning
|
const completionTitle = t("feedback.completionDescriptionVV");
|
||||||
const sendFeedbackMutation = graphql(`
|
const completionDescription = t("feedback.completionDescriptionVV");
|
||||||
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 = [
|
const questionData = [
|
||||||
{
|
{
|
||||||
|
|
@ -124,20 +54,6 @@ const questionData = [
|
||||||
items: YES_NO,
|
items: YES_NO,
|
||||||
component: ItRadioGroup,
|
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",
|
modelKey: "would_recommend",
|
||||||
items: YES_NO,
|
items: YES_NO,
|
||||||
|
|
@ -152,134 +68,17 @@ const questionData = [
|
||||||
component: ItTextarea,
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LearningContentMultiLayout
|
<FeedbackBase
|
||||||
:title="title"
|
:step-labels="stepLabels"
|
||||||
sub-title="Feedback"
|
:question-data="questionData"
|
||||||
:learning-content="content"
|
:content="props.content"
|
||||||
:show-start-button="stepNo === 0"
|
:introduction="$t('a.feedback.introductionVV')"
|
||||||
:show-next-button="stepNo > 0 && stepNo + 1 < numSteps"
|
:title="$t('Feedback')"
|
||||||
:disable-next-button="!hasStepValidInput(stepNo)"
|
:completion-title="$t('feedback.completionDescriptionVV')"
|
||||||
:show-previous-button="stepNo > 0 && !feedbackSubmitted"
|
:completion-description="$t('feedback.completionDescriptionVV')"
|
||||||
:show-exit-button="stepNo + 1 === numSteps"
|
:show-avatar="false"
|
||||||
: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>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@
|
||||||
<div
|
<div
|
||||||
class="b-0 flex flex-col lg:flex-row lg:items-center lg:border lg:border-gray-400 lg:p-8"
|
class="b-0 flex flex-col lg:flex-row lg:items-center lg:border lg:border-gray-400 lg:p-8"
|
||||||
>
|
>
|
||||||
<img :src="avatarUrl" class="mb-6 h-16 w-16 rounded-full lg:mr-12" />
|
<img
|
||||||
|
v-if="showAvatar"
|
||||||
|
:src="avatarUrl"
|
||||||
|
class="mb-6 h-16 w-16 rounded-full lg:mr-12"
|
||||||
|
/>
|
||||||
<h2 class="mb-8 block lg:hidden">{{ title }}</h2>
|
<h2 class="mb-8 block lg:hidden">{{ title }}</h2>
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-6">{{ description }}</p>
|
<p class="mb-6">{{ description }}</p>
|
||||||
|
|
@ -33,6 +37,7 @@ interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
feedbackSent?: boolean;
|
feedbackSent?: boolean;
|
||||||
|
showAvatar?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
withDefaults(defineProps<Props>(), {
|
||||||
|
|
@ -40,6 +45,7 @@ withDefaults(defineProps<Props>(), {
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
feedbackSent: false,
|
feedbackSent: false,
|
||||||
|
showAvatar: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits(["sendFeedback"]);
|
defineEmits(["sendFeedback"]);
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ from vbv_lernwelt.learnpath.tests.learning_path_factories import (
|
||||||
LearningPathFactory,
|
LearningPathFactory,
|
||||||
LearningSequenceFactory,
|
LearningSequenceFactory,
|
||||||
LearningUnitFactory,
|
LearningUnitFactory,
|
||||||
TopicFactory,
|
TopicFactory, LearningContentFeedbackVVFactory,
|
||||||
)
|
)
|
||||||
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
from vbv_lernwelt.media_library.tests.media_library_factories import (
|
||||||
MediaLibraryCategoryPageFactory,
|
MediaLibraryCategoryPageFactory,
|
||||||
|
|
@ -531,6 +531,7 @@ damit du erfolgreich mit deinem Lernpfad (durch-)starten kannst.
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
LearningContentFeedbackUKFactory(
|
LearningContentFeedbackUKFactory(
|
||||||
|
title="Feedback",
|
||||||
parent=circle,
|
parent=circle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -614,7 +615,8 @@ def create_test_circle_reisen(lp):
|
||||||
title="Reflexion",
|
title="Reflexion",
|
||||||
parent=parent,
|
parent=parent,
|
||||||
)
|
)
|
||||||
LearningContentFeedbackUKFactory(
|
LearningContentFeedbackVVFactory(
|
||||||
|
title="Feedback",
|
||||||
parent=parent,
|
parent=parent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from vbv_lernwelt.feedback.graphql.types import (
|
||||||
from vbv_lernwelt.feedback.serializers import CourseFeedbackSerializer
|
from vbv_lernwelt.feedback.serializers import CourseFeedbackSerializer
|
||||||
from vbv_lernwelt.feedback.services import update_feedback_response
|
from vbv_lernwelt.feedback.services import update_feedback_response
|
||||||
from vbv_lernwelt.iam.permissions import has_course_session_access
|
from vbv_lernwelt.iam.permissions import has_course_session_access
|
||||||
from vbv_lernwelt.learnpath.models import LearningContentFeedback
|
from vbv_lernwelt.learnpath.models import LearningContentFeedbackVV, LearningContentFeedbackUK
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -25,6 +25,7 @@ class SendFeedbackMutation(graphene.Mutation):
|
||||||
class Arguments:
|
class Arguments:
|
||||||
course_session_id = graphene.ID(required=True)
|
course_session_id = graphene.ID(required=True)
|
||||||
learning_content_page_id = graphene.ID(required=True)
|
learning_content_page_id = graphene.ID(required=True)
|
||||||
|
learning_content_type = graphene.String(required=True)
|
||||||
data = GenericScalar()
|
data = GenericScalar()
|
||||||
submitted = graphene.Boolean(required=False, default_value=False)
|
submitted = graphene.Boolean(required=False, default_value=False)
|
||||||
|
|
||||||
|
|
@ -35,11 +36,18 @@ class SendFeedbackMutation(graphene.Mutation):
|
||||||
info,
|
info,
|
||||||
course_session_id,
|
course_session_id,
|
||||||
learning_content_page_id,
|
learning_content_page_id,
|
||||||
|
learning_content_type,
|
||||||
data,
|
data,
|
||||||
submitted,
|
submitted,
|
||||||
):
|
):
|
||||||
feedback_user_id = info.context.user.id
|
feedback_user_id = info.context.user.id
|
||||||
learning_content = LearningContentFeedback.objects.get(
|
|
||||||
|
if learning_content_type == "learnpath.LearningContentFeedbackVV":
|
||||||
|
learningContentFeedbackModel = LearningContentFeedbackVV
|
||||||
|
else:
|
||||||
|
learningContentFeedbackModel = LearningContentFeedbackUK
|
||||||
|
|
||||||
|
learning_content = learningContentFeedbackModel.objects.get(
|
||||||
id=learning_content_page_id
|
id=learning_content_page_id
|
||||||
)
|
)
|
||||||
circle = learning_content.get_circle()
|
circle = learning_content.get_circle()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue