Filter for list, add average grade

This commit is contained in:
Daniel Egger 2024-07-27 10:20:54 +02:00
parent b7231fb1b7
commit 29c42f3512
12 changed files with 204 additions and 71 deletions

View File

@ -4,27 +4,53 @@ import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import { useTranslation } from "i18next-vue";
import type { StatisticsCourseSessionPropertiesType } from "@/gql/graphql";
import type { StatisticsFilterItem } from "@/types";
import _ from "lodash";
const { t } = useTranslation();
const props = defineProps<{
items: StatisticsFilterItem[];
courseSessionProperties: StatisticsCourseSessionPropertiesType;
hideCircleFilter?: boolean;
}>();
defineExpose({ getFilteredItems });
const regionFilter = computed(() => {
const regions = _.uniq(
props.courseSessionProperties.sessions.map((session) => session.region)
);
const f = regions.map((region) => ({
name: `Region: ${region}`,
id: region,
}));
return [
{
name: `${t("Region")}: ${t("a.Alle")}`,
id: "_all",
},
...f,
];
});
const sessionFilter = computed(() => {
const f = props.courseSessionProperties.sessions.map((session) => ({
name: `${t("a.Durchfuehrung")}: ${session.name}`,
let values = props.courseSessionProperties.sessions.map((session) => ({
name: session.name,
region: session.region,
id: session.id,
}));
return [{ name: t("a.AlleDurchführungen"), id: "_all" }, ...f];
// filter by selected region
if (regionFilterValue.value.id !== "_all") {
values = values.filter((cs) => cs.region === regionFilterValue.value.id);
}
return [{ name: t("a.AlleDurchführungen"), id: "_all" }, ...values];
});
const generationFilter = computed(() => {
const f = props.courseSessionProperties.generations.map((generation) => ({
name: `${t("a.Generation")}: ${generation}`,
name: generation,
id: generation,
}));
return [{ name: t("a.AlleGenerationen"), id: "_all" }, ...f];
@ -32,12 +58,13 @@ const generationFilter = computed(() => {
const circleFilter = computed(() => {
const f = props.courseSessionProperties.circles.map((circle) => ({
name: `Circle: ${circle.name}`,
name: circle.name,
id: circle.id,
}));
return [{ name: t("a.AlleCircle"), id: "_all" }, ...f];
});
const regionFilterValue = ref(regionFilter.value[0]);
const sessionFilterValue = ref(sessionFilter.value[0]);
const generationFilterValue = ref(generationFilter.value[0]);
const circleFilterValue = ref(circleFilter.value[0]);
@ -48,12 +75,21 @@ watch(
sessionFilterValue.value = sessionFilter.value[0];
generationFilterValue.value = generationFilter.value[0];
circleFilterValue.value = circleFilter.value[0];
regionFilterValue.value = regionFilter.value[0];
},
{ deep: true }
);
watch(regionFilterValue, () => {
console.log("regionFilterValue", regionFilterValue.value);
sessionFilterValue.value = sessionFilter.value[0];
});
const filteredItems = computed(() => {
return props.items.filter((item) => {
const regionMatch =
regionFilterValue.value.id === "_all" ||
item.region === regionFilterValue.value.id;
const sessionMatch =
sessionFilterValue.value.id === "_all" ||
item.course_session_id === sessionFilterValue.value.id;
@ -64,7 +100,7 @@ const filteredItems = computed(() => {
circleFilterValue.value.id === "_all" ||
item.circle_id === circleFilterValue.value.id;
return sessionMatch && generationMatch && circleMatch;
return regionMatch && sessionMatch && generationMatch && circleMatch;
});
});
@ -75,30 +111,45 @@ function getFilteredItems() {
<template>
<div>
<div class="flex flex-col space-x-2 lg:flex-row">
<div class="flex flex-col space-x-2 lg:flex-row border-b">
<ItDropdownSelect
v-if="regionFilter.length > 2"
v-model="regionFilterValue"
class="min-w-[12rem]"
:items="regionFilter"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-model="sessionFilterValue"
class="min-w-[18rem]"
class="min-w-[12rem]"
:items="sessionFilter"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-model="generationFilterValue"
class="min-w-[18rem]"
class="min-w-[12rem]"
:items="generationFilter"
borderless
></ItDropdownSelect>
<ItDropdownSelect
v-if="!props.hideCircleFilter"
v-model="circleFilterValue"
class="min-w-[18rem]"
class="min-w-[12rem]"
:items="circleFilter"
borderless
></ItDropdownSelect>
</div>
<div v-for="item in filteredItems" :key="item._id" class="px-5">
<div class="border-t border-gray-500 py-4">
<slot :item="item"></slot>
<slot name="header"></slot>
<section>
<div
v-for="item in filteredItems"
:key="item._id"
class="mx-6 border-t border-gray-500 first:border-t-0"
>
<div class="py-4">
<slot :item="item"></slot>
</div>
</div>
</div>
</section>
</div>
</template>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -52,9 +52,11 @@ type AssignmentStatisticsRecordType {
course_session_assignment_id: ID!
circle_id: ID!
generation: String!
region: String!
assignment_type_translation_key: String!
assignment_title: String!
deadline: DateTime!
course_session_title: String
competence_certificate_id: ID
competence_certificate_title: String
metrics: AssignmentCompletionMetricsType!
@ -98,6 +100,7 @@ type StatisticsCourseSessionPropertiesType {
type StatisticsCourseSessionDataType {
id: ID!
name: String!
region: String!
}
type StatisticsCircleDataType {
@ -122,6 +125,7 @@ type PresenceRecordStatisticsType {
_id: ID!
course_session_id: ID!
generation: String!
region: String!
circle_id: ID!
due_date: DateTime!
participants_present: Int!
@ -145,6 +149,7 @@ type FeedbackStatisticsRecordType {
_id: ID!
course_session_id: ID!
generation: String!
region: String!
circle_id: ID!
satisfaction_average: Float!
satisfaction_max: Int!
@ -175,6 +180,7 @@ type CompetenceRecordStatisticsType {
_id: ID!
course_session_id: ID!
generation: String!
region: String!
title: String!
circle_id: ID!
success_count: Int!

View File

@ -370,6 +370,7 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
sessions {
id
name
region
}
generations
circles {
@ -390,6 +391,7 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
_id
course_session_id
generation
region
circle_id
due_date
participants_present
@ -408,6 +410,7 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
_id
course_session_id
generation
region
circle_id
experts
satisfaction_average
@ -436,6 +439,7 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
course_session_assignment_id
circle_id
generation
region
assignment_title
assignment_type_translation_key
competence_certificate_title
@ -465,6 +469,7 @@ export const DASHBOARD_COURSE_STATISTICS = graphql(`
_id
course_session_id
generation
region
circle_id
title
success_count
@ -490,6 +495,7 @@ export const DASHBOARD_MENTOR_COMPETENCE_SUMMARY = graphql(`
sessions {
id
name
region
}
generations
circles {
@ -510,8 +516,10 @@ export const DASHBOARD_MENTOR_COMPETENCE_SUMMARY = graphql(`
_id
course_session_id
course_session_assignment_id
course_session_title
circle_id
generation
region
assignment_title
assignment_type_translation_key
competence_certificate_id

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import log from "loglevel";
import { computed, onMounted, ref } from "vue";
import { computed, onMounted, type Ref, ref } from "vue";
import {
courseIdForCourseSlug,
fetchMentorCompetenceSummary,
@ -10,6 +10,8 @@ import { useDashboardStore } from "@/stores/dashboard";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import _ from "lodash";
import { percentToRoundedGrade } from "@/services/assignmentService";
import StatisticFilterList from "@/components/dashboard/StatisticFilterList.vue";
import type { StatisticsFilterItem } from "@/types";
const dashboardStore = useDashboardStore();
@ -32,6 +34,22 @@ const courseSessionName = (courseSessionId: string) => {
);
};
const statisticFilter: Ref<typeof StatisticFilterList | null> = ref(null);
const filteredItems = computed(() => {
if (!statisticFilter.value) {
return [];
}
return statisticFilter.value.getFilteredItems();
});
const totalAverageGrade = computed(() => {
return percentToRoundedGrade(
_.sumBy(filteredItems.value, (i) => {
return (i as GroupedAssignmentEntry).averageEvaluationPercent ?? 0;
}) / filteredItems.value.length,
false
);
});
onMounted(async () => {
await dashboardStore.loadDashboardDetails();
courseId.value = courseIdForCourseSlug(
@ -53,26 +71,31 @@ onMounted(async () => {
loading.value = false;
});
type GroupedAssignments = {
interface GroupedAssignmentEntry extends StatisticsFilterItem {
competenceCertificateId: string;
competenceCertificateTitle: string;
generation: string;
courseSessionId: string;
course_session_title: string;
assignments: AssignmentStatisticsRecordType[];
sumAverageEvaluationPercent: number;
averageEvaluationPercent: number | null;
averageGrade: number | null;
};
}
function isGroupedAssignmentEntry(
item: StatisticsFilterItem
): item is GroupedAssignmentEntry {
return (item as GroupedAssignmentEntry).competenceCertificateId !== undefined;
}
const courseSessionCompetenceAssignments = computed(() => {
let resultArray = [] as GroupedAssignments[];
let resultArray = [] as GroupedAssignmentEntry[];
// group assignments by competence and course session
for (const assignment of agentAssignmentData.value?.assignments.records ?? []) {
const entry = resultArray.find(
(r) =>
r.competenceCertificateId === assignment.competence_certificate_id &&
r.courseSessionId === assignment.course_session_id
r.course_session_id === assignment.course_session_id
);
if (entry) {
if (assignment.metrics.ranking_completed) {
@ -80,14 +103,18 @@ const courseSessionCompetenceAssignments = computed(() => {
}
} else {
const newEntry = {
_id: `${assignment.competence_certificate_id}-${assignment.course_session_id}`,
competenceCertificateId: assignment.competence_certificate_id ?? "",
competenceCertificateTitle: assignment.competence_certificate_title ?? "",
generation: assignment.generation ?? "",
courseSessionId: assignment.course_session_id ?? "",
region: assignment.region ?? "",
course_session_id: assignment.course_session_id ?? "",
course_session_title: assignment.course_session_title ?? "",
assignments: [] as AssignmentStatisticsRecordType[],
sumAverageEvaluationPercent: 0,
averageEvaluationPercent: null,
averageGrade: null,
circle_id: assignment.circle_id ?? "",
};
if (assignment && assignment.metrics.ranking_completed) {
newEntry.assignments.push(assignment);
@ -114,7 +141,11 @@ const courseSessionCompetenceAssignments = computed(() => {
});
entry.averageGrade = percentToRoundedGrade(entry.averageEvaluationPercent, false);
}
return resultArray;
return _.orderBy(
resultArray,
["course_session_title", "competenceCertificateTitle"],
["asc", "asc"]
);
});
</script>
@ -129,48 +160,61 @@ const courseSessionCompetenceAssignments = computed(() => {
<span>{{ $t("general.back") }}</span>
</router-link>
<div>
<h2>{{ $t("a.Kompetenznachweise") }}</h2>
<h2 class="mb-8">{{ $t("a.Kompetenznachweise") }}</h2>
<div class="bg-white px-4 py-2">
<section
class="flex flex-col space-x-0 border-b bg-white lg:flex-row lg:space-x-3"
></section>
<div
v-for="entry in courseSessionCompetenceAssignments"
:key="entry.courseSessionId"
:data-cy="`entry-${entry.courseSessionId}`"
class="flex flex-col justify-between gap-4 border-b p-2 last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
<div class="bg-white py-2">
<StatisticFilterList
v-if="
agentAssignmentData?.course_session_properties &&
courseSessionCompetenceAssignments?.length
"
ref="statisticFilter"
:course-session-properties="agentAssignmentData?.course_session_properties"
:items="courseSessionCompetenceAssignments"
:hide-circle-filter="true"
>
<div class="w-full flex-auto md:w-1/2">
{{ entry.competenceCertificateTitle }}
<br />
{{ $t("a.Durchführung") }} «{{
courseSessionName(entry.courseSessionId)
}}»
</div>
<div class="flex flex-auto items-center gap-2 md:w-1/4">
<div>{{ $t("a.Durchschnittsnote") }}:</div>
<div class="min-w-12 text-center">
<div
class="rounded px-2 py-1 font-bold"
:class="{ 'bg-red-400': (entry.averageGrade ?? 4) < 4 }"
>
{{ entry.averageGrade }}
<template #header>
<div class="heading-3 border-b px-6 py-4">
{{ $t("a.Durchschnittsnote") }}: {{ totalAverageGrade }}
</div>
</template>
<template #default="{ item: item }">
<div
v-if="isGroupedAssignmentEntry(item)"
class="flex flex-col justify-between gap-4 border-b last:border-b-0 md:flex-row md:items-center md:justify-between md:gap-16"
>
<div class="w-full flex-auto md:w-1/2">
<span class="text-bold">
{{ item.competenceCertificateTitle }}
</span>
<br />
{{ $t("a.Durchführung") }} «{{
courseSessionName(item.course_session_id)
}}»
</div>
<div class="flex flex-auto items-center gap-2 md:w-1/4">
<div>{{ $t("a.Durchschnittsnote") }}:</div>
<div class="min-w-12 text-center">
<div
class="rounded px-2 py-1 font-bold"
:class="{ 'bg-red-400': (item.averageGrade ?? 4) < 4 }"
>
{{ item.averageGrade }}
</div>
</div>
</div>
<div class="w-full flex-auto items-end md:w-1/4 md:text-end">
<router-link
class="underline"
:to="`/statistic/${props.agentRole}/${props.courseSlug}/competence-grade/${item.course_session_id}/${item.competenceCertificateId}`"
data-cy="basebox.detailsLink"
>
{{ $t("a.Details anschauen") }}
</router-link>
</div>
</div>
</div>
<div class="w-full flex-auto items-end md:w-1/4 md:text-end">
<router-link
class="underline"
:to="`/statistic/${props.agentRole}/${props.courseSlug}/competence-grade/${entry.courseSessionId}/${entry.competenceCertificateId}`"
data-cy="basebox.detailsLink"
>
{{ $t("a.Details anschauen") }}
</router-link>
</div>
</div>
</template>
</StatisticFilterList>
</div>
</div>
</div>

View File

@ -623,6 +623,7 @@ export type DashboardPersonsPageMode = "default" | "competenceMetrics";
export interface StatisticsFilterItem {
_id: string;
course_session_id: string;
region: string;
generation: string;
circle_id: string;
}

View File

@ -36,9 +36,11 @@ class AssignmentStatisticsRecordType(graphene.ObjectType):
course_session_assignment_id = graphene.ID(required=True)
circle_id = graphene.ID(required=True)
generation = graphene.String(required=True)
region = graphene.String(required=True)
assignment_type_translation_key = graphene.String(required=True)
assignment_title = graphene.String(required=True)
deadline = graphene.DateTime(required=True)
course_session_title = graphene.String()
competence_certificate_id = graphene.ID()
competence_certificate_title = graphene.String()
metrics = graphene.Field(AssignmentCompletionMetricsType, required=True)
@ -171,9 +173,11 @@ def create_record(
# make sure it's unique, across all types of assignments!
_id=f"{course_session_assignment._meta.model_name}#{course_session_assignment.id}@{urql_id_postfix}", # noqa
course_session_id=str(course_session_assignment.course_session.id), # noqa
course_session_title=course_session_assignment.course_session.title, # noqa
circle_id=learning_content.get_circle().id, # noqa
course_session_assignment_id=str(course_session_assignment.id), # noqa
generation=course_session_assignment.course_session.generation, # noqa
region=course_session_assignment.course_session.region, # noqa
assignment_type_translation_key=due_date.assignment_type_translation_key, # noqa
competence_certificate_id=str(competence_certificate.id), # noqa
competence_certificate_title=competence_certificate.title, # noqa

View File

@ -19,6 +19,7 @@ class PresenceRecordStatisticsType(graphene.ObjectType):
_id = graphene.ID(required=True)
course_session_id = graphene.ID(required=True)
generation = graphene.String(required=True)
region = graphene.String(required=True)
circle_id = graphene.ID(required=True)
due_date = graphene.DateTime(required=True)
participants_present = graphene.Int(required=True)
@ -83,6 +84,7 @@ def attendance_day_presences(
_id=f"{urql_id}:attendance_day:{attendance_day.id}", # noqa
course_session_id=course_session.id, # noqa
generation=course_session.generation, # noqa
region=course_session.region, # noqa
circle_id=circle.id, # noqa
due_date=attendance_day.due_date.end, # noqa
participants_present=participants_present, # noqa
@ -98,7 +100,9 @@ def attendance_day_presences(
)
return AttendanceDayPresencesStatisticsType(
summary=summary, records=records, _id=course_id # noqa
summary=summary,
records=records,
_id=course_id, # noqa
)

View File

@ -16,6 +16,7 @@ class CompetenceRecordStatisticsType(graphene.ObjectType):
_id = graphene.ID(required=True)
course_session_id = graphene.ID(required=True)
generation = graphene.String(required=True)
region = graphene.String(required=True)
title = graphene.String(required=True)
circle_id = graphene.ID(required=True)
success_count = graphene.Int(required=True)
@ -80,6 +81,7 @@ def competences(
title=learning_unit.title, # noqa
course_session_id=completion.course_session.id, # noqa
generation=completion.course_session.generation, # noqa
region=completion.course_session.region, # noqa
circle_id=circle.id, # noqa
success_count=0, # noqa
fail_count=0, # noqa

View File

@ -26,6 +26,7 @@ from vbv_lernwelt.learnpath.models import Circle
class StatisticsCourseSessionDataType(graphene.ObjectType):
id = graphene.ID(required=True)
name = graphene.String(required=True)
region = graphene.String(required=True)
class StatisticsCircleDataType(graphene.ObjectType):
@ -129,6 +130,7 @@ class BaseStatisticsType(graphene.ObjectType):
StatisticsCourseSessionDataType(
id=course_session.id, # noqa
name=course_session.title, # noqa
region=course_session.region, # noqa
)
)
generations.add(course_session.generation)
@ -199,7 +201,8 @@ class CourseStatisticsType(BaseStatisticsType):
records, success_total, fail_total = competences(
course_slug=str(root.course_slug),
course_session_selection_ids=[
str(cs) for cs in root.course_session_selection_ids # noqa
str(cs)
for cs in root.course_session_selection_ids # noqa
],
user_selection_ids=user_selection_ids, # noqa
circle_ids=root.get_circle_ids(info), # noqa

View File

@ -18,6 +18,7 @@ class FeedbackStatisticsRecordType(graphene.ObjectType):
_id = graphene.ID(required=True)
course_session_id = graphene.ID(required=True)
generation = graphene.String(required=True)
region = graphene.String(required=True)
circle_id = graphene.ID(required=True)
satisfaction_average = graphene.Float(required=True)
satisfaction_max = graphene.Int(required=True)
@ -68,6 +69,7 @@ def feedback_responses(
feedbacks=fbs,
course_session_id=course_session.id,
generation=course_session.generation,
region=course_session.region,
course_slug=str(course_slug),
urql_id_postfix=urql_id,
)
@ -96,6 +98,7 @@ def circle_feedback_average(
feedbacks: List[FeedbackResponse],
course_session_id,
generation: str,
region: str,
course_slug: str,
urql_id_postfix: str = "",
):
@ -128,6 +131,7 @@ def circle_feedback_average(
_id=f"circle:{circle_id}-course_session:{course_session_id}@{urql_id_postfix}", # noqa
course_session_id=course_session_id, # noqa
generation=generation, # noqa
region=region, # noqa
circle_id=circle_id, # noqa
satisfaction_average=data["total"] / data["count"], # noqa
satisfaction_max=4, # noqa