Add basic Feedback page
This commit is contained in:
parent
44ed154814
commit
ec58ca176c
|
|
@ -1,35 +1,36 @@
|
|||
<template>
|
||||
<div class="px-6 py-5 bg-white mb-4">
|
||||
<h2 class="bg-feedback bg-no-repeat pl-[68px] heading-3 bg-60 leading-[60px] mb-4">{{ $t("general.feedback", 2) }}</h2>
|
||||
<div class="mb-4 bg-white px-6 py-5">
|
||||
<h2 class="heading-3 mb-4 bg-feedback bg-60 bg-no-repeat pl-[68px] leading-[60px]">
|
||||
{{ $t("general.feedback", 2) }}
|
||||
</h2>
|
||||
<ol>
|
||||
<ItRow
|
||||
v-for="feedbacks in feedbackSummary"
|
||||
:key="feedbacks.circle_id"
|
||||
>
|
||||
<template #firstRow><span class="text-bold">{{ $t("feedback.circleFeedback") }}</span></template>
|
||||
<template #center>
|
||||
<div class="flex justify-between w-full">
|
||||
<div>Circle: {{ feedbacks.circle.title }}</div>
|
||||
<div>{{ $t("feedback.sentByUsers", feedbacks.count) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #link>
|
||||
<router-link
|
||||
:to="`${url}/cockpit/feedback/`"
|
||||
class="underline w-full text-right"
|
||||
>
|
||||
{{ $t("feedback.showDetails") }}
|
||||
</router-link>
|
||||
</template>
|
||||
</ItRow>
|
||||
<ItRow v-for="feedbacks in feedbackSummary" :key="feedbacks.circle_id">
|
||||
<template #firstRow>
|
||||
<span class="text-bold">{{ $t("feedback.circleFeedback") }}</span>
|
||||
</template>
|
||||
<template #center>
|
||||
<div class="flex w-full justify-between">
|
||||
<div>Circle: {{ feedbacks.circle.title }}</div>
|
||||
<div>{{ $t("feedback.sentByUsers", feedbacks.count) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #link>
|
||||
<router-link
|
||||
:to="`${url}/cockpit/feedback/${feedbacks.circle_id}`"
|
||||
class="w-full text-right underline"
|
||||
>
|
||||
{{ $t("feedback.showDetails") }}
|
||||
</router-link>
|
||||
</template>
|
||||
</ItRow>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import ItRow from "@/components/ui/ItRow.vue";
|
||||
import { itGet } from "@/fetchHelpers";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
|
||||
import type { Circle } from "@/services/circle";
|
||||
|
||||
|
|
@ -42,15 +43,20 @@ interface FeedbackDisplaySummary extends FeedbackSummary {
|
|||
circle: Circle;
|
||||
}
|
||||
|
||||
function makeSummary(feedbackData: FeedbackSummary[], circles: Circle[], selectedCircles: string[]) {
|
||||
const summary: FeedbackDisplaySummary[] = circles.filter(circle => selectedCircles.includes(circle.translation_key))
|
||||
function makeSummary(
|
||||
feedbackData: FeedbackSummary[],
|
||||
circles: Circle[],
|
||||
selectedCircles: string[]
|
||||
) {
|
||||
const summary: FeedbackDisplaySummary[] = circles
|
||||
.filter((circle) => selectedCircles.includes(circle.translation_key))
|
||||
.reduce((acc: FeedbackDisplaySummary[], circle) => {
|
||||
const circleFeedbacks = feedbackData
|
||||
.filter(data => data.circle_id === circle.id)
|
||||
.map(data => Object.assign({}, data, { circle }));
|
||||
const circleFeedbacks = feedbackData
|
||||
.filter((data) => data.circle_id === circle.id)
|
||||
.map((data) => Object.assign({}, data, { circle }));
|
||||
|
||||
return acc.concat(circleFeedbacks);
|
||||
}, []);
|
||||
return acc.concat(circleFeedbacks);
|
||||
}, []);
|
||||
return summary;
|
||||
}
|
||||
|
||||
|
|
@ -66,12 +72,28 @@ let feedbackData: FeedbackSummary[] = [];
|
|||
|
||||
onMounted(async () => {
|
||||
feedbackData = await itGet(`/api/core/feedback/${props.courseId}/summary`);
|
||||
feedbackSummary.value = makeSummary(feedbackData, props.circles, props.selctedCircles);
|
||||
})
|
||||
feedbackSummary.value = makeSummary(
|
||||
feedbackData,
|
||||
props.circles,
|
||||
props.selctedCircles
|
||||
);
|
||||
});
|
||||
|
||||
watch(() => props, () => {
|
||||
if (feedbackData.length > 0 && props.circles.length > 0 && props.selctedCircles.length > 0) {
|
||||
feedbackSummary.value = makeSummary(feedbackData, props.circles, props.selctedCircles);
|
||||
}
|
||||
}, { deep: true });
|
||||
watch(
|
||||
() => props,
|
||||
() => {
|
||||
if (
|
||||
feedbackData.length > 0 &&
|
||||
props.circles.length > 0 &&
|
||||
props.selctedCircles.length > 0
|
||||
) {
|
||||
feedbackSummary.value = makeSummary(
|
||||
feedbackData,
|
||||
props.circles,
|
||||
props.selctedCircles
|
||||
);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const props = defineProps<{
|
|||
<template>
|
||||
<ItRow>
|
||||
<template #firstRow>
|
||||
<img class="h-[45px] rounded-full mr-2" :src="avatarUrl" />
|
||||
<img class="mr-2 h-[45px] rounded-full" :src="avatarUrl" />
|
||||
<p class="text-bold lg:leading-[45px]">{{ name }}</p>
|
||||
</template>
|
||||
<template #center>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<li
|
||||
class="py-4 leading-[45px] border-t border-gray-500 flex flex-col lg:flex-row justify-between"
|
||||
class="flex flex-col justify-between border-t border-gray-500 py-4 leading-[45px] lg:flex-row"
|
||||
>
|
||||
<div class="flex flex-row md:w-1/4 items-center">
|
||||
<div class="flex flex-row items-center md:w-1/4">
|
||||
<slot name="firstRow"></slot>
|
||||
</div>
|
||||
<div class="flex-1 flex items-center">
|
||||
<div class="flex flex-1 items-center">
|
||||
<slot name="center"></slot>
|
||||
</div>
|
||||
<div class="flex items-center md:w-1/4">
|
||||
|
|
@ -14,7 +14,6 @@
|
|||
</li>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<QuestionSummary :title="props.title" :text="props.text">
|
||||
<div class="inline-grid grid-cols-[140px_auto] grid-rows-2 gap-x-2">
|
||||
<h4 class="text-xl font-bold">Durchschnitt:</h4>
|
||||
<div class="mb-6 inline-grid grid-cols-[140px_auto] grid-rows-2 gap-x-2">
|
||||
<h4 class="text-xl font-bold">{{ $t("feedback.average") }}:</h4>
|
||||
<span
|
||||
class="col-start-2 row-span-2 inline-flex h-9 w-11 items-center justify-center rounded text-xl"
|
||||
class="col-start-2 row-span-2 inline-flex h-9 w-11 items-center justify-center rounded text-xl font-bold"
|
||||
:style="ratingValueStyle"
|
||||
>
|
||||
{{ props.rating }}
|
||||
{{ rating }}
|
||||
</span>
|
||||
<h5 class="text-base">{{ props.answers }} Antworten</h5>
|
||||
<h5 class="text-base">{{ answers }} {{ $t("feedback.answers") }}</h5>
|
||||
</div>
|
||||
<div
|
||||
class="relative grid grid-cols-3 pt-3 grid-areas-rating-scale-slim md:grid-cols-8 md:grid-areas-rating-scale"
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-for="legend in legends"
|
||||
v-for="(legend, i) of legends"
|
||||
:key="legend.index"
|
||||
:class="{
|
||||
'items-start grid-in-fst before:left-0 md:before:left-1/2': legend.index == 1,
|
||||
|
|
@ -37,15 +37,34 @@
|
|||
}"
|
||||
class="legend relative inline-flex flex-col pt-4 before:absolute before:top-0 before:h-4 before:w-px before:bg-gray-500 md:items-center"
|
||||
>
|
||||
<div
|
||||
:class="[{ hidden: legend.index === 3 || legend.index === 2 }, 'md:block']"
|
||||
>
|
||||
{{ legend.index }}
|
||||
</div>
|
||||
<Popover class="relative">
|
||||
<PopoverButton class="focus:outline-none">
|
||||
<div
|
||||
:class="[
|
||||
{ hidden: legend.index === 3 || legend.index === 2 },
|
||||
'md:block',
|
||||
]"
|
||||
>
|
||||
{{ legend.index }}
|
||||
</div>
|
||||
|
||||
<p :class="[{ hidden: legend.index === 3 || legend.index === 2 }, 'md:block']">
|
||||
{{ legend.label }}
|
||||
</p>
|
||||
<p
|
||||
:class="[
|
||||
{ hidden: legend.index === 3 || legend.index === 2 },
|
||||
'md:block',
|
||||
]"
|
||||
>
|
||||
{{ legend.label }}
|
||||
</p>
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
class="absolute top-[-150%] z-10 w-[120px] border border-gray-500 bg-white p-1 text-left text-sm"
|
||||
>
|
||||
<p>
|
||||
{{ `"${legend.label}" ${numberOfRatings[i]} ${$t("feedback.answers")}` }}
|
||||
</p>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</QuestionSummary>
|
||||
|
|
@ -53,7 +72,11 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import QuestionSummary from "@/components/ui/QuestionSummary.vue";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/vue";
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
type RGB = [number, number, number];
|
||||
const red: RGB = [221, 103, 81]; // red-600
|
||||
|
|
@ -62,25 +85,37 @@ const lightGreen: RGB = [120, 222, 163]; // green-500
|
|||
const darkGreen: RGB = [91, 183, 130]; // green-600
|
||||
|
||||
const legends = [
|
||||
{ index: 1, label: "Sehr unzufrieden" },
|
||||
{ index: 2, label: "Unzufrieden" },
|
||||
{ index: 3, label: "Zufrieden" },
|
||||
{ index: 4, label: "Sehr zufrieden" },
|
||||
{ index: 1, label: t("feedback.veryUnhappy") },
|
||||
{ index: 2, label: t("feedback.unhappy") },
|
||||
{ index: 3, label: t("feedback.happy") },
|
||||
{ index: 4, label: t("feedback.veryHappy") },
|
||||
];
|
||||
|
||||
const props = defineProps<{
|
||||
rating: number;
|
||||
answers: number;
|
||||
ratings: number[];
|
||||
title: string;
|
||||
text: string;
|
||||
}>();
|
||||
|
||||
const rating = computed((): number => {
|
||||
const sum = props.ratings.reduce((a, b) => a + b, 0);
|
||||
return sum / props.ratings.length;
|
||||
});
|
||||
|
||||
const weight = computed(() => {
|
||||
return props.rating % 1;
|
||||
return rating.value % 1;
|
||||
});
|
||||
|
||||
const scale = computed(() => {
|
||||
return Math.floor(props.rating);
|
||||
return Math.floor(rating.value);
|
||||
});
|
||||
|
||||
const answers = computed(() => props.ratings.length);
|
||||
|
||||
const numberOfRatings = computed(() => {
|
||||
return legends.map(
|
||||
(legend) => props.ratings.filter((rating) => rating === legend.index).length
|
||||
);
|
||||
});
|
||||
|
||||
const colors = computed(() => {
|
||||
|
|
|
|||
|
|
@ -134,7 +134,16 @@
|
|||
"feedbackSent": "Dein Feedback wurde abgeschickt",
|
||||
"circleFeedback": "Feedback zum Circle",
|
||||
"showDetails": "Details anzeigen",
|
||||
"sentByUsers": "Von {count} Teilnehmern ausgefüllt"
|
||||
"sentByUsers": "Von {count} Teilnehmern ausgefüllt",
|
||||
"feedbackPageTitle": "Feedback zum Lehrgang",
|
||||
"feedbackPageInfo": "Teilnehmer haben das Feedback ausgefüllt",
|
||||
"questionTitle": "Frage",
|
||||
"veryUnhappy": "Sehr unzufrieden",
|
||||
"unhappy": "Unzufrieden",
|
||||
"happy": "Zufrieden",
|
||||
"veryHappy": "Sehr zufrieden",
|
||||
"average": "Durchschnitt",
|
||||
"answers": "Antworten"
|
||||
},
|
||||
"constants": {
|
||||
"yes": "Ja",
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ const app = createApp(App);
|
|||
|
||||
Sentry.init({
|
||||
app,
|
||||
environment: import.meta.env.VITE_SENTRY_ENV || "http://localhost:8000/server/graphql/",
|
||||
environment:
|
||||
import.meta.env.VITE_SENTRY_ENV || "http://localhost:8000/server/graphql/",
|
||||
dsn: "https://2df6096a4fd94bd6b4802124d10e4b8d@o8544.ingest.sentry.io/4504157846372352",
|
||||
tracesSampleRate: 0.0,
|
||||
enabled:
|
||||
|
|
|
|||
|
|
@ -450,64 +450,12 @@ function log(data: any) {
|
|||
/>
|
||||
<div>
|
||||
<RatingScale
|
||||
:rating="1.1"
|
||||
:answers="19"
|
||||
title="Frage 1"
|
||||
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
|
||||
/>
|
||||
<RatingScale
|
||||
:rating="2"
|
||||
:answers="17"
|
||||
title="Frage 2"
|
||||
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
|
||||
/>
|
||||
<RatingScale
|
||||
:rating="2.5"
|
||||
:ratings="[3, 4, 3, 4]"
|
||||
:answers="19"
|
||||
title="Frage 3"
|
||||
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
|
||||
/>
|
||||
<RatingScale
|
||||
:rating="3.9"
|
||||
:answers="18"
|
||||
title="Frage 4"
|
||||
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
|
||||
/>
|
||||
<RatingScale
|
||||
:rating="4"
|
||||
:answers="20"
|
||||
title="Frage 5"
|
||||
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
|
||||
/>
|
||||
<RatingScale
|
||||
:rating="1"
|
||||
:answers="20"
|
||||
title="Frage 6"
|
||||
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
|
||||
/>
|
||||
<RatingScale
|
||||
:rating="2"
|
||||
:answers="20"
|
||||
title="Frage 7"
|
||||
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
|
||||
/>
|
||||
<RatingScale
|
||||
:rating="3"
|
||||
:answers="20"
|
||||
title="Frage 8"
|
||||
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
|
||||
/>
|
||||
<RatingScale
|
||||
:rating="4"
|
||||
:answers="20"
|
||||
title="Frage 9"
|
||||
text="Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?"
|
||||
/>
|
||||
<VerticalBarChart title="Frage X" text="Fragentext" :ratio="0.5" />
|
||||
<VerticalBarChart title="Frage X" text="Fragentext" :ratio="0.8" />
|
||||
<VerticalBarChart title="Frage X" text="Fragentext" :ratio="0.2" />
|
||||
<VerticalBarChart title="Frage X" text="Fragentext" :ratio="2" />
|
||||
<VerticalBarChart title="Frage X" text="Fragentext" :ratio="0" />
|
||||
<HorizontalBarChart title="Frage X" text="Fragentext" :items="barChartItems" />
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import FeedbackSummary from "@/components/feedback/feedbackSummary.vue";
|
||||
import LearningPathDiagram from "@/components/learningPath/LearningPathDiagram.vue";
|
||||
import ItPersonRow from "@/components/ui/ItPersonRow.vue";
|
||||
import ItProgress from "@/components/ui/ItProgress.vue";
|
||||
import FeedbackSummary from "@/components/feedback/feedbackSummary.vue";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import { useCompetenceStore } from "@/stores/competence";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import log from "loglevel";
|
||||
import { computed } from "vue";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -95,8 +95,8 @@ function setActiveClasses(translationKey: string) {
|
|||
</ul>
|
||||
</div>
|
||||
<!-- Status -->
|
||||
<div class="grid gap-4 grid-rows-2 lg:grid-rows-none lg:grid-cols-2 mb-4">
|
||||
<div class="px-6 py-5 bg-white">
|
||||
<div class="mb-4 grid grid-rows-2 gap-4 lg:grid-cols-2 lg:grid-rows-none">
|
||||
<div class="bg-white px-6 py-5">
|
||||
<h1
|
||||
class="heading-3 mb-4 bg-assignment bg-60 bg-no-repeat pl-[68px] leading-[60px]"
|
||||
>
|
||||
|
|
@ -109,7 +109,7 @@ function setActiveClasses(translationKey: string) {
|
|||
</div>
|
||||
<div class="bg-white px-6 py-5">
|
||||
<h1
|
||||
class="bg-test bg-no-repeat pl-[68px] heading-3 bg-60 leading-[60px] mb-4"
|
||||
class="heading-3 mb-4 bg-test bg-60 bg-no-repeat pl-[68px] leading-[60px]"
|
||||
>
|
||||
{{ $t("general.examResult", 2) }}
|
||||
</h1>
|
||||
|
|
@ -120,12 +120,15 @@ function setActiveClasses(translationKey: string) {
|
|||
</div>
|
||||
</div>
|
||||
<!-- Feedback -->
|
||||
<FeedbackSummary
|
||||
:selcted-circles="cockpitStore.selectedCircles || []"
|
||||
:circles="learningPathStore.learningPathForUser(props.courseSlug, userStore.id)?.circles || []"
|
||||
:course-id="courseSessionStore.courseSessionForRoute?.course.id"
|
||||
:url="courseSessionStore.courseSessionForRoute.course_url"
|
||||
></FeedbackSummary>
|
||||
<FeedbackSummary
|
||||
:selcted-circles="cockpitStore.selectedCircles || []"
|
||||
:circles="
|
||||
learningPathStore.learningPathForUser(props.courseSlug, userStore.id)
|
||||
?.circles || []
|
||||
"
|
||||
:course-id="courseSessionStore.courseSessionForRoute?.course.id"
|
||||
:url="courseSessionStore.courseSessionForRoute.course_url"
|
||||
></FeedbackSummary>
|
||||
<div>
|
||||
<!-- progress -->
|
||||
<div v-if="cockpitStore.courseSessionUsers?.length" class="bg-white p-6">
|
||||
|
|
@ -139,7 +142,7 @@ function setActiveClasses(translationKey: string) {
|
|||
>
|
||||
<template #center>
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<div class="flex flex-row items-center gap-4 mr-6">
|
||||
<div class="mr-6 flex flex-row items-center gap-4">
|
||||
<div class="h-12">
|
||||
<LearningPathDiagram
|
||||
v-if="
|
||||
|
|
@ -197,7 +200,7 @@ function setActiveClasses(translationKey: string) {
|
|||
<template #link>
|
||||
<router-link
|
||||
:to="`/course/${props.courseSlug}/cockpit/profile/${csu.user_id}`"
|
||||
class="underline w-full text-right"
|
||||
class="w-full text-right underline"
|
||||
>
|
||||
{{ $t("general.profileLink") }}
|
||||
</router-link>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
<template>
|
||||
<div class="bg-gray-200">
|
||||
<div class="container-large">
|
||||
<nav class="py-4 pb-4">
|
||||
<router-link
|
||||
class="btn-text inline-flex items-center pl-0"
|
||||
:to="`/course/${props.courseSlug}/cockpit`"
|
||||
>
|
||||
<it-icon-arrow-left />
|
||||
<span>{{ $t("general.back") }}</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
<main>
|
||||
<h1 class="mb-2">{{ $t("feedback.feedbackPageTitle") }}</h1>
|
||||
<p class="mb-10">
|
||||
<span class="font-bold">{{ feedbackData.amount }}</span>
|
||||
{{ $t("feedback.feedbackPageInfo") }}
|
||||
</p>
|
||||
<ol v-if="Object.keys(feedbackData).length > 0">
|
||||
<li v-for="(question, i) in orderedQuestions" class="mb-8 bg-white p-8">
|
||||
<RatingScale
|
||||
v-if="ratingKeys.includes(question.key)"
|
||||
:ratings="feedbackData.questions[question.key]"
|
||||
:title="`${$t('feedback.questionTitle')} ${i}`"
|
||||
:text="question.question"
|
||||
/>
|
||||
<VerticalBarChart
|
||||
v-else-if="verticalChartKyes.includes(question.key)"
|
||||
:title="`${$t('feedback.questionTitle')} ${i}`"
|
||||
:text="question.question"
|
||||
:ratio="0.2"
|
||||
/>
|
||||
<!-- HorizontalBarChart
|
||||
v-else
|
||||
:title="`${$t('feedback.questionTitle')} ${i}`"
|
||||
:text="question.question"
|
||||
:items="barChartItems" /-->
|
||||
</li>
|
||||
</ol>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import HorizontalBarChart from "@/components/ui/HorizontalBarChart.vue";
|
||||
import RatingScale from "@/components/ui/RatingScale.vue";
|
||||
import VerticalBarChart from "@/components/ui/VerticalBarChart.vue";
|
||||
import { itGet } from "@/fetchHelpers";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import * as log from "loglevel";
|
||||
import { onMounted, reactive } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
circleId: string;
|
||||
}>();
|
||||
|
||||
log.debug("FeedbackPage created", props.circleId);
|
||||
|
||||
const courseSessionStore = useCourseSessionsStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const orderedQuestions = [
|
||||
{
|
||||
key: "would_recommend",
|
||||
question: t("feedback.recommendLabel"),
|
||||
},
|
||||
{
|
||||
key: "satisfaction",
|
||||
question: t("feedback.satisfactionLabel"),
|
||||
},
|
||||
{
|
||||
key: "goal_attainment",
|
||||
question: t("feedback.goalAttainmentLabel"),
|
||||
},
|
||||
{
|
||||
key: "proficieny",
|
||||
question: t("feedback.proficiencyLabel"),
|
||||
},
|
||||
{
|
||||
key: "received_materials",
|
||||
question: t("feedback.receivedMaterialsLabel"),
|
||||
},
|
||||
{
|
||||
key: "materials_rating",
|
||||
question: t("feedback.materialsRatingLabel"),
|
||||
},
|
||||
{
|
||||
key: "instructor_competence",
|
||||
question: t("feedback.instructorCompetenceLabel"),
|
||||
},
|
||||
{
|
||||
key: "instructor_respect",
|
||||
question: t("feedback.instructorRespectLabel"),
|
||||
},
|
||||
{
|
||||
key: "instructor_open_feedback",
|
||||
question: t("feedback.instructorOpenFeedbackLabel"),
|
||||
},
|
||||
{
|
||||
key: "course_negative_feedback",
|
||||
question: t("feedback.courseNegativeFeedbackLabel"),
|
||||
},
|
||||
{
|
||||
key: "course_positive_feedback",
|
||||
question: t("feedback.coursePositiveFeedbackLabel"),
|
||||
},
|
||||
];
|
||||
|
||||
const ratingKeys = [
|
||||
"satisfaction",
|
||||
"goal_attainment",
|
||||
"materials_rating",
|
||||
"instructor_competence",
|
||||
"instructor_respect",
|
||||
];
|
||||
const verticalChartKyes = ["received_materials", "would_recommend"];
|
||||
|
||||
const feedbackData = reactive({});
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug("FeedbackPage mounted");
|
||||
const data = await itGet(
|
||||
`/api/core/feedback/${courseSessionStore.courseSessionForRoute?.course.id}/${props.circleId}`
|
||||
);
|
||||
Object.assign(feedbackData, data);
|
||||
});
|
||||
|
||||
function calculateAverage(ratings: number[]): number {
|
||||
return ratings.reduce((a, b) => a + b, 0) / ratings.length;
|
||||
}
|
||||
|
||||
function getNumberOfAnswers(ratings: number[]): number {
|
||||
return ratings.filter((r) => typeof r !== "string" || r !== "").length;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -119,6 +119,11 @@ const router = createRouter({
|
|||
component: () => import("@/pages/cockpit/CockpitUserCirclePage.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: "feedback/:circleId",
|
||||
component: () => import("@/pages/cockpit/FeedbackPage.vue"),
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
from django import forms
|
||||
|
||||
from vbv_lernwelt.feedback.models import FeedbackResponse
|
||||
|
||||
|
||||
class FeedbackForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FeedbackResponse
|
||||
fields = "__all__"
|
||||
|
|
@ -135,13 +135,14 @@ class FeedbackSummaryApiTestCase(FeedbackApiBaseTestCase):
|
|||
|
||||
class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
|
||||
def test_can_receive_feedback(self):
|
||||
expected = {
|
||||
feedback_data = {
|
||||
"satisfaction": [1, 4, 2],
|
||||
"goal_attainment": [2, 4, 3],
|
||||
"proficiency": [20, 60, 80],
|
||||
"received_materials": [True, False, True],
|
||||
"materials_rating": [3, 2, 1],
|
||||
"instructor_competence": [1, 2, 3],
|
||||
"instructor_respect": [40, 80, 100],
|
||||
"instructor_open_feedback": ["super", "ok", "naja"],
|
||||
"would_recommend": [False, True, False],
|
||||
"course_positive_feedback": ["Bla", "Katze", "Hund"],
|
||||
|
|
@ -158,24 +159,30 @@ class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
|
|||
FeedbackFactory(
|
||||
circle=circle,
|
||||
course_session=csu.course_session,
|
||||
satisfaction=expected["satisfaction"][i],
|
||||
goal_attainment=expected["goal_attainment"][i],
|
||||
proficiency=expected["proficiency"][i],
|
||||
received_materials=expected["received_materials"][i],
|
||||
materials_rating=expected["materials_rating"][i],
|
||||
instructor_competence=expected["instructor_competence"][i],
|
||||
instructor_open_feedback=expected["instructor_open_feedback"][i],
|
||||
would_recommend=expected["would_recommend"][i],
|
||||
course_positive_feedback=expected["course_positive_feedback"][i],
|
||||
course_negative_feedback=expected["course_negative_feedback"][i],
|
||||
satisfaction=feedback_data["satisfaction"][i],
|
||||
goal_attainment=feedback_data["goal_attainment"][i],
|
||||
proficiency=feedback_data["proficiency"][i],
|
||||
received_materials=feedback_data["received_materials"][i],
|
||||
materials_rating=feedback_data["materials_rating"][i],
|
||||
instructor_competence=feedback_data["instructor_competence"][i],
|
||||
instructor_open_feedback=feedback_data["instructor_open_feedback"][i],
|
||||
would_recommend=feedback_data["would_recommend"][i],
|
||||
course_positive_feedback=feedback_data["course_positive_feedback"][i],
|
||||
course_negative_feedback=feedback_data["course_negative_feedback"][i],
|
||||
).save()
|
||||
|
||||
response = self.client.get(
|
||||
f"/api/core/feedback/{csu.course_session.course.id}/{circle.id}/"
|
||||
)
|
||||
self.maxDiff = None
|
||||
|
||||
expected = {
|
||||
"amount": 3,
|
||||
"questions": feedback_data,
|
||||
}
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data, expected)
|
||||
self.assertDictEqual(response.data, expected)
|
||||
|
||||
def test_cannot_receive_feedback_from_other_circle(self):
|
||||
csu = CourseSessionUser.objects.get(
|
||||
|
|
@ -192,7 +199,7 @@ class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
|
|||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data, {})
|
||||
self.assertEqual(response.data, {"amount": 0, "questions": {}})
|
||||
|
||||
def test_student_cannot_receive_feedback(self):
|
||||
self.client.login(username="student", password="test")
|
||||
|
|
@ -208,4 +215,4 @@ class FeedbackDetailApiTestCase(FeedbackApiBaseTestCase):
|
|||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.data, {})
|
||||
self.assertEqual(response.data, {"amount": 0, "questions": {}})
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ FEEDBACK_FIELDS = [
|
|||
"received_materials",
|
||||
"materials_rating",
|
||||
"instructor_competence",
|
||||
"instructor_respect",
|
||||
"instructor_open_feedback",
|
||||
"would_recommend",
|
||||
"course_positive_feedback",
|
||||
|
|
@ -51,16 +52,19 @@ def get_feedback_for_circle(request, course_id, circle_id):
|
|||
)
|
||||
|
||||
# I guess this is ok for the üK case
|
||||
feedback_data = {}
|
||||
feedback_data = {
|
||||
"amount": len(feedbacks),
|
||||
"questions": {}
|
||||
}
|
||||
|
||||
if len(feedbacks) == 0:
|
||||
if feedback_data["amount"] == 0:
|
||||
return Response(status=200, data=feedback_data)
|
||||
|
||||
for field in FEEDBACK_FIELDS:
|
||||
feedback_data[field] = []
|
||||
feedback_data["questions"][field] = []
|
||||
|
||||
for feedback in feedbacks:
|
||||
for field in FEEDBACK_FIELDS:
|
||||
feedback_data[field].append(getattr(feedback, field))
|
||||
feedback_data["questions"][field].append(getattr(feedback, field))
|
||||
|
||||
return Response(status=200, data=feedback_data)
|
||||
|
|
|
|||
Loading…
Reference in New Issue