Make AgentCompetenceGradeDetailPage

This commit is contained in:
Daniel Egger 2024-07-26 17:08:37 +02:00
parent da348b7756
commit b24dbc84c8
10 changed files with 147 additions and 127 deletions

View File

@ -73,14 +73,14 @@ export function useCurrentCourseSession() {
return result;
}
export function useCourseSessionDetailQuery(courSessionId?: string) {
if (!courSessionId) {
courSessionId = useCurrentCourseSession().value.id;
export function useCourseSessionDetailQuery(courseSessionId?: string) {
if (!courseSessionId) {
courseSessionId = useCurrentCourseSession().value.id;
}
const queryResult = useQuery({
query: COURSE_SESSION_DETAIL_QUERY,
variables: {
courseSessionId: courSessionId,
courseSessionId: courseSessionId,
},
});

View File

@ -18,7 +18,7 @@ const documents = {
"\n fragment CoursePageFields on CoursePageInterface {\n title\n id\n slug\n content_type\n frontend_url\n }\n": types.CoursePageFieldsFragmentDoc,
"\n query attendanceCheckQuery($courseSessionId: ID!) {\n course_session_attendance_course(id: $courseSessionId) {\n id\n attendance_user_list {\n user_id\n status\n }\n }\n }\n": types.AttendanceCheckQueryDocument,
"\n query assignmentCompletionQuery(\n $assignmentId: ID!\n $courseSessionId: ID!\n $learningContentId: ID\n $assignmentUserId: UUID\n ) {\n assignment(id: $assignmentId) {\n assignment_type\n needs_expert_evaluation\n max_points\n content_type\n effort_required\n evaluation_description\n evaluation_document_url\n evaluation_tasks\n id\n intro_text\n performance_objectives\n slug\n tasks\n title\n translation_key\n solution_sample {\n id\n url\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n assignment_completion(\n assignment_id: $assignmentId\n course_session_id: $courseSessionId\n assignment_user_id: $assignmentUserId\n learning_content_page_id: $learningContentId\n ) {\n id\n completion_status\n submitted_at\n evaluation_submitted_at\n evaluation_user {\n id\n first_name\n last_name\n }\n assignment_user {\n avatar_url\n first_name\n last_name\n id\n }\n evaluation_points\n evaluation_max_points\n evaluation_points_deducted\n evaluation_points_deducted_reason\n evaluation_points_final\n\n evaluation_passed\n edoniq_extended_time_flag\n completion_data\n task_completion_data\n }\n }\n": types.AssignmentCompletionQueryDocument,
"\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userIds: [UUID!]!\n ) {\n competence_certificate_list(course_slug: $courseSlug, user_ids: $userIds) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n competence_certificate_weight\n completions(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_final\n evaluation_points_deducted\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateForUserQueryDocument,
"\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userIds: [UUID!]!\n ) {\n competence_certificate_list(course_slug: $courseSlug, user_ids: $userIds) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n competence_certificate_weight\n completions(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_final\n evaluation_points_deducted\n evaluation_max_points\n evaluation_passed\n evaluation_percent\n assignment_user {\n id\n }\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n": types.CompetenceCertificateForUserQueryDocument,
"\n query courseSessionDetail($courseSessionId: ID!) {\n course_session(id: $courseSessionId) {\n id\n title\n course {\n id\n title\n slug\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n }\n }\n users {\n id\n user_id\n first_name\n last_name\n email\n avatar_url\n role\n circles {\n id\n title\n slug\n }\n }\n attendance_courses {\n id\n location\n trainer\n due_date {\n id\n start\n end\n }\n learning_content_id\n learning_content {\n id\n title\n circle {\n id\n title\n slug\n }\n }\n }\n assignments {\n id\n submission_deadline {\n id\n start\n }\n evaluation_deadline {\n id\n start\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n edoniq_tests {\n id\n deadline {\n id\n start\n end\n }\n learning_content {\n id\n title\n content_assignment {\n id\n title\n assignment_type\n }\n }\n }\n }\n }\n": types.CourseSessionDetailDocument,
"\n query courseQuery($slug: String!) {\n course(slug: $slug) {\n id\n title\n slug\n category_name\n configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n is_uk\n }\n action_competences {\n competence_id\n ...CoursePageFields\n performance_criteria {\n competence_id\n learning_unit {\n id\n slug\n evaluate_url\n }\n ...CoursePageFields\n }\n }\n learning_path {\n ...CoursePageFields\n topics {\n is_visible\n ...CoursePageFields\n circles {\n description\n goals\n ...CoursePageFields\n learning_sequences {\n icon\n ...CoursePageFields\n learning_units {\n evaluate_url\n ...CoursePageFields\n performance_criteria {\n ...CoursePageFields\n }\n learning_contents {\n can_user_self_toggle_course_completion\n content_url\n minutes\n description\n ...CoursePageFields\n ... on LearningContentAssignmentObjectType {\n assignment_type\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentEdoniqTestObjectType {\n checkbox_text\n has_extended_time_test\n content_assignment {\n id\n assignment_type\n }\n competence_certificate {\n ...CoursePageFields\n }\n }\n ... on LearningContentRichTextObjectType {\n text\n }\n }\n }\n }\n }\n }\n }\n }\n }\n": types.CourseQueryDocument,
"\n query dashboardConfig {\n dashboard_config {\n id\n slug\n name\n dashboard_type\n course_configuration {\n id\n enable_circle_documents\n enable_learning_mentor\n enable_competence_certificates\n is_uk\n }\n }\n }\n": types.DashboardConfigDocument,
@ -66,7 +66,7 @@ export function graphql(source: "\n query assignmentCompletionQuery(\n $assi
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userIds: [UUID!]!\n ) {\n competence_certificate_list(course_slug: $courseSlug, user_ids: $userIds) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n competence_certificate_weight\n completions(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_final\n evaluation_points_deducted\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userIds: [UUID!]!\n ) {\n competence_certificate_list(course_slug: $courseSlug, user_ids: $userIds) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n competence_certificate_weight\n completions(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_final\n evaluation_points_deducted\n evaluation_max_points\n evaluation_passed\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"];
export function graphql(source: "\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userIds: [UUID!]!\n ) {\n competence_certificate_list(course_slug: $courseSlug, user_ids: $userIds) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n competence_certificate_weight\n completions(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_final\n evaluation_points_deducted\n evaluation_max_points\n evaluation_passed\n evaluation_percent\n assignment_user {\n id\n }\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query competenceCertificateForUserQuery(\n $courseSlug: String!\n $courseSessionId: ID!\n $userIds: [UUID!]!\n ) {\n competence_certificate_list(course_slug: $courseSlug, user_ids: $userIds) {\n ...CoursePageFields\n competence_certificates {\n ...CoursePageFields\n assignments {\n ...CoursePageFields\n assignment_type\n max_points\n competence_certificate_weight\n completions(course_session_id: $courseSessionId) {\n id\n completion_status\n submitted_at\n evaluation_points\n evaluation_points_final\n evaluation_points_deducted\n evaluation_max_points\n evaluation_passed\n evaluation_percent\n assignment_user {\n id\n }\n }\n learning_content {\n ...CoursePageFields\n circle {\n id\n title\n slug\n }\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

File diff suppressed because one or more lines are too long

View File

@ -581,6 +581,7 @@ type AssignmentCompletionObjectType {
evaluation_points: Float
evaluation_points_final: Float
evaluation_max_points: Float
evaluation_percent: Float
}
"""

View File

@ -113,6 +113,10 @@ export const COMPETENCE_NAVI_CERTIFICATE_QUERY = graphql(`
evaluation_points_deducted
evaluation_max_points
evaluation_passed
evaluation_percent
assignment_user {
id
}
}
learning_content {
...CoursePageFields

View File

@ -1,18 +1,17 @@
<script setup lang="ts">
import log from "loglevel";
import { computed, onMounted, ref } from "vue";
import {
courseIdForCourseSlug,
fetchMentorCompetenceSummary,
} from "@/services/dashboard";
import type { AssignmentStatisticsRecordType, BaseStatisticsType } from "@/gql/graphql";
import { useDashboardStore } from "@/stores/dashboard";
import { type DashboardPersonType, fetchDashboardPersons } from "@/services/dashboard";
import LoadingSpinner from "@/components/ui/LoadingSpinner.vue";
import { useCurrentCourseSession } from "@/composables";
import { COMPETENCE_NAVI_CERTIFICATE_QUERY } from "@/graphql/queries";
import { graphqlClient } from "@/graphql/client";
import type { CompetenceCertificateObjectType } from "@/gql/graphql";
import { calcCompetenceCertificateGrade } from "@/pages/competence/utils";
import _ from "lodash";
import type { CompetenceCertificateAssignment } from "@/types";
import { percentToRoundedGrade } from "@/services/assignmentService";
const dashboardStore = useDashboardStore();
const props = defineProps<{
agentRole: string;
courseSlug: string;
@ -22,101 +21,87 @@ const props = defineProps<{
log.debug("AgentCompetenceGradeDetailPage created", props);
const courseSession = useCurrentCourseSession();
const loading = ref(true);
const courseId = ref<string | undefined>(undefined);
const agentAssignmentData = ref<BaseStatisticsType | null>(null);
const courseSessionName = (courseSessionId: string) => {
return (
agentAssignmentData.value?.course_session_properties?.sessions.find(
(session) => session.id === courseSessionId
)?.name ?? ""
);
};
onMounted(async () => {
await dashboardStore.loadDashboardDetails();
courseId.value = courseIdForCourseSlug(
dashboardStore.dashboardConfigsv2,
props.courseSlug
);
if (!courseId.value) {
log.error("CourseId not found for courseSlug", props.courseSlug);
return;
}
log.debug("courseId", courseId.value);
agentAssignmentData.value = await fetchMentorCompetenceSummary(
courseId.value,
props.agentRole
);
loading.value = false;
const participants = ref<DashboardPersonType[]>([]);
const participantUserIds = computed(() => {
return (participants.value ?? []).map((p) => p.user_id);
});
type GroupedAssignments = {
competenceCertificateId: string;
competenceCertificateTitle: string;
generation: string;
courseSessionId: string;
assignments: AssignmentStatisticsRecordType[];
sumAverageEvaluationPercent: number;
averageEvaluationPercent: number | null;
averageGrade: number | null;
};
const certificateData = ref<CompetenceCertificateObjectType | undefined>(undefined);
const courseSessionCompetenceAssignments = computed(() => {
let resultArray = [] as GroupedAssignments[];
// 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
function userGrade(userId: string) {
if (certificateData.value) {
const assignmentsWithUserCompletions = _.cloneDeep(
certificateData.value.assignments
);
if (entry) {
if (assignment.metrics.ranking_completed) {
entry.assignments.push(assignment);
}
} else {
const newEntry = {
competenceCertificateId: assignment.competence_certificate_id ?? "",
competenceCertificateTitle: assignment.competence_certificate_title ?? "",
generation: assignment.generation ?? "",
courseSessionId: assignment.course_session_id ?? "",
assignments: [] as AssignmentStatisticsRecordType[],
sumAverageEvaluationPercent: 0,
averageEvaluationPercent: null,
averageGrade: null,
};
if (assignment && assignment.metrics.ranking_completed) {
newEntry.assignments.push(assignment);
}
resultArray.push(newEntry);
}
}
// filter out entries without assignments
resultArray = resultArray.filter((entry) => entry.assignments.length > 0);
// calculate average grade
for (const entry of resultArray) {
entry.sumAverageEvaluationPercent = _.sumBy(entry.assignments, (a) => {
return (
(a.metrics.average_evaluation_percent ?? 0) *
(a.metrics.competence_certificate_weight ?? 1)
for (const assignment of assignmentsWithUserCompletions) {
assignment.completions = assignment.completions?.filter(
(c) => c?.assignment_user.id === userId
);
});
entry.averageEvaluationPercent =
entry.sumAverageEvaluationPercent /
_.sumBy(entry.assignments, (a) => {
return a.metrics.competence_certificate_weight ?? 1;
});
entry.averageGrade = percentToRoundedGrade(entry.averageEvaluationPercent, false);
}
return calcCompetenceCertificateGrade(
assignmentsWithUserCompletions as unknown as CompetenceCertificateAssignment[],
false
);
}
return resultArray;
}
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"
);
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
);
}
});
onMounted(async () => {
log.debug("AgentAssignmentDetailPage mounted", courseSession);
const personData = await fetchDashboardPersons("default");
participants.value = personData?.filter((p) => {
return p.course_sessions.find(
(cs) => cs.id === courseSession.value.id && cs.my_role === "BERUFSBILDNER"
);
});
const res = await graphqlClient.query(COMPETENCE_NAVI_CERTIFICATE_QUERY, {
courseSlug: props.courseSlug,
courseSessionId: courseSession.value.id,
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>
@ -136,34 +121,47 @@ const courseSessionCompetenceAssignments = computed(() => {
{{ $t("a.Statistik für alle Lernenden") }}
</p>
<pre>{{ totalAverageGrade }}</pre>
<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}`"
v-for="person in participants"
:key="person.user_id"
data-cy="person"
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/3">
{{ entry.competenceCertificateTitle }}
<br />
{{ $t("a.Durchführung") }} «{{
courseSessionName(entry.courseSessionId)
}}»
</div>
<div class="flex w-full flex-auto items-start md:w-1/3">
<div class="flex">
<div>{{ $t("a.Durchschnittsnote") }}:</div>
<div class="w-16 text-center">
{{ entry.averageGrade }}
<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="w-full flex-auto items-end md:w-1/3 md:text-end">
<div class="flex w-full flex-auto items-start md:w-1/4">
<div class="flex">
<div>{{ $t("a.Note") }}:</div>
<div class="w-16 text-center">{{ userGrade(person.user_id) }}</div>
</div>
</div>
<div class="w-full flex-auto items-end md:w-1/4 md:text-end">
Details anzeigen
</div>
</div>

View File

@ -32,12 +32,6 @@ const courseSessionName = (courseSessionId: string) => {
);
};
const circleMeta = (circleId: string) => {
return agentAssignmentData.value?.course_session_properties.circles.find(
(circle) => circle.id === circleId
);
};
onMounted(async () => {
await dashboardStore.loadDashboardDetails();
courseId.value = courseIdForCourseSlug(
@ -168,7 +162,13 @@ const courseSessionCompetenceAssignments = computed(() => {
</div>
<div class="w-full flex-auto items-end md:w-1/3 md:text-end">
Details anzeigen
<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>
</div>

View File

@ -358,6 +358,12 @@ const router = createRouter({
component: () =>
import("@/pages/dashboard/agentAssignment/AgentCompetenceGradePage.vue"),
},
{
path: "/statistic/:agentRole/:courseSlug/competence-grade/:courseSessionId/:competenceCertificateId",
props: true,
component: () =>
import("@/pages/dashboard/agentAssignment/AgentCompetenceGradeDetailPage.vue"),
},
{
path: "/shop",

View File

@ -21,6 +21,7 @@ class AssignmentCompletionObjectType(DjangoObjectType):
evaluation_points = graphene.Float()
evaluation_points_final = graphene.Float()
evaluation_max_points = graphene.Float()
evaluation_percent = graphene.Float()
class Meta:
model = AssignmentCompletion
@ -61,6 +62,11 @@ class AssignmentCompletionObjectType(DjangoObjectType):
return round(self.evaluation_max_points, 1) # noqa
return None
def resolve_evaluation_percent(self, info):
if self.evaluation_points:
return self.evaluation_percent
return None
class AssignmentObjectType(DjangoObjectType):
tasks = JSONStreamField()

View File

@ -368,6 +368,10 @@ class AssignmentCompletion(models.Model):
return None
return self.evaluation_points - self.evaluation_points_deducted
@property
def evaluation_percent(self):
return (self.evaluation_points_final or 0) / (self.evaluation_max_points or 1)
assignment_user = models.ForeignKey(User, on_delete=models.CASCADE)
assignment = models.ForeignKey(Assignment, on_delete=models.CASCADE)
course_session = models.ForeignKey("course.CourseSession", on_delete=models.CASCADE)