203 lines
6.4 KiB
Vue
203 lines
6.4 KiB
Vue
<script setup lang="ts">
|
|
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
|
|
import type { CompetenceCertificateObjectType } from "@/gql/graphql";
|
|
import { graphqlClient } from "@/graphql/client";
|
|
import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
|
|
import { calcCompetenceCertificateGrade } from "@/pages/competence/utils";
|
|
import { percentToRoundedGrade } from "@/services/assignmentService";
|
|
import { type DashboardPersonType, fetchDashboardPersons } from "@/services/dashboard";
|
|
import type { CompetenceCertificateAssignment } from "@/types";
|
|
import _ from "lodash";
|
|
import log from "loglevel";
|
|
import { computed, onMounted, ref } from "vue";
|
|
|
|
const props = defineProps<{
|
|
agentRole: string;
|
|
courseSlug: string;
|
|
competenceCertificateId: string;
|
|
courseSessionId: string;
|
|
}>();
|
|
|
|
log.debug("AgentCompetenceGradeDetailPage created", props);
|
|
|
|
const loading = ref(true);
|
|
|
|
const participants = ref<DashboardPersonType[]>([]);
|
|
const participantUserIds = computed(() => {
|
|
return (participants.value ?? []).map((p) => p.user_id);
|
|
});
|
|
|
|
const courseSession = computed(() => {
|
|
return participants.value[0]?.course_sessions.find(
|
|
(cs) => cs.id === props.courseSessionId
|
|
);
|
|
});
|
|
|
|
const certificateData = ref<CompetenceCertificateObjectType | undefined>(undefined);
|
|
|
|
function userGrade(userId: string) {
|
|
if (certificateData.value) {
|
|
const assignmentsWithUserCompletions = _.cloneDeep(
|
|
certificateData.value.assignments
|
|
);
|
|
for (const assignment of assignmentsWithUserCompletions) {
|
|
assignment.completions = assignment.completions?.filter(
|
|
(c) => c?.assignment_user?.id === userId
|
|
);
|
|
}
|
|
|
|
return calcCompetenceCertificateGrade(
|
|
assignmentsWithUserCompletions as unknown as CompetenceCertificateAssignment[],
|
|
false
|
|
);
|
|
}
|
|
}
|
|
|
|
const totalAverageGrade = computed(() => {
|
|
if (certificateData.value) {
|
|
let divisor = 0;
|
|
const assignmentAverageGrades = certificateData.value.assignments.map(
|
|
(assignment) => {
|
|
const relevantCompletions = (assignment.completions ?? []).filter(
|
|
(c) => c?.completion_status == "EVALUATION_SUBMITTED"
|
|
);
|
|
|
|
if (relevantCompletions.length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
const averagePercent =
|
|
_.sumBy(relevantCompletions, (c) => c?.evaluation_percent ?? 0) /
|
|
relevantCompletions.length;
|
|
|
|
if (averagePercent > 0.0001) {
|
|
divisor += assignment.competence_certificate_weight ?? 1;
|
|
}
|
|
|
|
return averagePercent * (assignment.competence_certificate_weight ?? 1);
|
|
}
|
|
);
|
|
return percentToRoundedGrade(
|
|
_.sum(assignmentAverageGrades) / (divisor ?? 1),
|
|
false
|
|
);
|
|
}
|
|
return undefined;
|
|
});
|
|
|
|
onMounted(async () => {
|
|
log.debug("AgentAssignmentDetailPage mounted");
|
|
|
|
const personData = await fetchDashboardPersons("default");
|
|
participants.value = personData?.filter((p) => {
|
|
return p.course_sessions.find(
|
|
(cs) => cs.id === props.courseSessionId && cs.my_role === "BERUFSBILDNER"
|
|
);
|
|
});
|
|
|
|
const res = await graphqlClient.query(COMPETENCE_NAVI_CERTIFICATE_QUERY, {
|
|
courseSlug: props.courseSlug,
|
|
courseSessionId: props.courseSessionId,
|
|
userIds: participantUserIds.value,
|
|
});
|
|
|
|
// @ts-ignore
|
|
certificateData.value =
|
|
res.data?.competence_certificate_list?.competence_certificates.find(
|
|
// @ts-ignore
|
|
(cc) => cc.id === props.competenceCertificateId
|
|
);
|
|
|
|
loading.value = false;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="bg-gray-200">
|
|
<div v-if="loading" class="m-8 flex justify-center">
|
|
<LoadingSpinner />
|
|
</div>
|
|
<div v-else class="container-large flex flex-col space-y-4">
|
|
<router-link
|
|
class="btn-text inline-flex items-center pl-0"
|
|
:to="`/statistic/${props.agentRole}/${props.courseSlug}/competence-grade`"
|
|
data-cy="back-button"
|
|
>
|
|
<it-icon-arrow-left />
|
|
<span>{{ $t("general.back") }}</span>
|
|
</router-link>
|
|
<div>
|
|
<h2 class="mb-8">{{ certificateData?.title }}</h2>
|
|
|
|
<div class="border-b bg-white px-6 py-6">
|
|
{{ courseSession?.session_title }}
|
|
</div>
|
|
<div
|
|
class="heading-3 border-b bg-white px-6 py-6"
|
|
data-cy="total-average-grade"
|
|
>
|
|
{{ $t("a.Durchschnittsnote") }}:
|
|
{{ totalAverageGrade }}
|
|
</div>
|
|
<div class="bg-white px-4 py-2">
|
|
<div
|
|
v-for="person in participants"
|
|
:key="person.user_id"
|
|
:data-cy="`person-${person.user_id}`"
|
|
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="w-full flex-auto md:w-1/2">
|
|
<div class="flex items-center space-x-2">
|
|
<img
|
|
class="inline-block h-11 w-11 rounded-full"
|
|
:src="
|
|
person.avatar_url_small ||
|
|
'/static/avatars/myvbv-default-avatar.png'
|
|
"
|
|
:alt="`${person.first_name} ${person.last_name}`"
|
|
/>
|
|
<div>
|
|
<div class="text-bold">
|
|
{{ person.first_name }}
|
|
{{ person.last_name }}
|
|
</div>
|
|
<div class="text-gray-900">{{ person.email }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-auto items-center gap-2 md:w-1/4">
|
|
<div>{{ $t("a.Note") }}:</div>
|
|
<div class="min-w-12 text-center">
|
|
<div
|
|
class="rounded px-2 py-1 font-bold"
|
|
:class="{ 'bg-red-400': (userGrade(person.user_id) ?? 4) < 4 }"
|
|
>
|
|
{{ userGrade(person.user_id) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="w-full flex-auto items-end md:w-1/4 md:text-end">
|
|
<router-link
|
|
:to="{
|
|
name: 'profileLearningPath',
|
|
params: {
|
|
userId: person.user_id,
|
|
courseSlug: props.courseSlug,
|
|
},
|
|
query: { courseSessionId: props.courseSessionId },
|
|
}"
|
|
data-cy="person-learning-path-link"
|
|
class="link w-full lg:text-right"
|
|
>
|
|
{{ $t("a.Profil anzeigen") }}
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|