import { useCSRFFetch } from "@/fetchHelpers"; import type { CourseStatisticsType } from "@/gql/graphql"; import { graphqlClient } from "@/graphql/client"; import { COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY, COMPETENCE_NAVI_CERTIFICATE_QUERY, COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY, } from "@/graphql/queries"; import { circleFlatChildren, circleFlatLearningContents, circleFlatLearningUnits, someFinishedInLearningSequence, } from "@/services/circle"; import type { DashboardDueDate, DashboardPersonRoleType, DashboardPersonType, } from "@/services/dashboard"; import { courseIdForCourseSlug, fetchDashboardDueDates, fetchDashboardPersons, fetchStatisticData, } from "@/services/dashboard"; import { presignUpload, uploadFile } from "@/services/files"; import { useCompletionStore } from "@/stores/completion"; import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useDashboardStore } from "@/stores/dashboard"; import { useUserStore } from "@/stores/user"; import type { ActionCompetence, CircleType, Course, CourseCompletion, CourseCompletionStatus, CourseSession, CourseSessionDetail, DashboardPersonsPageMode, LearningContentWithCompletion, LearningMentor, LearningPathType, LearningUnitPerformanceCriteria, PerformanceCriteria, } from "@/types"; import { useQuery } from "@urql/vue"; import dayjs from "dayjs"; import { t } from "i18next"; import orderBy from "lodash/orderBy"; import log from "loglevel"; import type { ComputedRef, Ref } from "vue"; import { computed, onMounted, ref, watchEffect } from "vue"; export function useCurrentCourseSession() { /** * We often need the current course session in our components. * With this composable we can get it easily. */ const store = useCourseSessionsStore(); const result: ComputedRef = computed( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion () => { if (!store.currentCourseSession) { log.error( "currentCourseSession is only defined in pages with :courseSlug in the route" ); throw new Error( `currentCourseSession is not defined in the store. It is only defined in pages with :courseSlug in the route` ); } return store.currentCourseSession; } ); return result; } export function useCourseSessionDetailQuery(courSessionId?: string) { if (!courSessionId) { courSessionId = useCurrentCourseSession().value.id; } const queryResult = useQuery({ query: COURSE_SESSION_DETAIL_QUERY, variables: { courseSessionId: courSessionId, }, }); const courseSessionDetail = computed(() => { return queryResult.data.value?.course_session as CourseSessionDetail | undefined; }); function findAssignmentByAssignmentId(assignmentId: string) { return (courseSessionDetail.value?.assignments ?? []).find((a) => { return a.learning_content?.content_assignment?.id === assignmentId; }); } function findAssignment(learningContentId: string) { return (courseSessionDetail.value?.assignments ?? []).find((a) => { return a.learning_content?.id === learningContentId; }); } function findEdoniqTest(learningContentId: string) { return (courseSessionDetail.value?.edoniq_tests ?? []).find((e) => { return e.learning_content?.id === learningContentId; }); } function findAttendanceCourse(learningContentId: string) { return (courseSessionDetail.value?.attendance_courses ?? []).find((e) => { return e.learning_content?.id === learningContentId; }); } function findUser(userId: string) { return (courseSessionDetail.value?.users ?? []).find((u) => { return u.user_id === userId; }); } function findCurrentUser() { const userStore = useUserStore(); const userId = userStore.id; return findUser(userId); } function filterMembers() { return (courseSessionDetail.value?.users ?? []).filter((u) => { return u.role === "MEMBER"; }); } function filterCircleExperts(circleSlug: string) { return (courseSessionDetail.value?.users ?? []).filter((u) => { return u.role === "EXPERT" && u.circles.map((c) => c.slug).includes(circleSlug); }); } const dataLoaded = ref(false); function waitForData() { return new Promise((resolve) => { watchEffect(() => { if (queryResult.data.value) { dataLoaded.value = true; resolve(queryResult.data.value); } }); }); } return { ...queryResult, courseSessionDetail, waitForData, findAssignmentByAssignmentId, findAssignment, findEdoniqTest, findAttendanceCourse, findUser, findCurrentUser, filterMembers, filterCircleExperts, }; } export function flatCircles(learningPath: LearningPathType) { return learningPath.topics.flatMap((t) => t.circles); } export function useCourseData(courseSlug: string) { const learningPath = ref(undefined); const actionCompetences = ref([]); const course = ref(undefined); // urql.useQuery is not meant to be used programmatically, so we use graphqlClient.query instead const resultPromise = graphqlClient .query(COURSE_QUERY, { slug: `${courseSlug}` }) .toPromise(); resultPromise.then((result) => { if (result.error) { log.error(result.error); } course.value = result.data?.course as unknown as Course; actionCompetences.value = result.data?.course ?.action_competences as ActionCompetence[]; learningPath.value = result.data?.course?.learning_path as LearningPathType; // attach circle information to learning contents if (learningPath.value) { flatCircles(learningPath.value).forEach((circle) => { circle.learning_sequences.forEach((ls, lsIndex) => { const circleData = { id: circle.id, slug: circle.slug, title: circle.title, }; return ls.learning_units.forEach((lu, luIndex) => { lu.circle = Object.assign({}, circleData); lu.learning_contents.forEach((lc, lcIndex) => { lc.circle = Object.assign({}, circleData); lc.continueUrl = ls.frontend_url || circle.frontend_url; lc.firstInCircle = lcIndex === 0 && luIndex === 0 && lsIndex === 0; lc.parentLearningUnit = { id: lu.id, slug: lu.slug, title: lu.title, }; }); lu.performance_criteria.forEach((luPc) => { luPc.circle = Object.assign({}, circleData); const pc = findPerformanceCriterion(luPc.id); if (pc) { pc.circle = Object.assign({}, circleData); } }); }); }); }); } }); const circles = computed(() => { if (learningPath.value) { return flatCircles(learningPath.value); } return undefined; }); function findCircle(idOrSlug: string) { return (circles.value ?? []).find((c) => { return c.id === idOrSlug || c.slug.endsWith(idOrSlug); }); } function findPerformanceCriterion(id: string) { return (actionCompetences.value ?? []) .flatMap((ac) => { return ac.performance_criteria; }) .find((pc) => { return pc.id === id; }) as PerformanceCriteria | undefined; } function findLearningContent( learningContentIdOrSlug: string, circleIdOrSlug?: string ) { let filteredCircles = circles.value ?? []; if (circleIdOrSlug) { filteredCircles = filteredCircles.filter((c) => { return c.id === circleIdOrSlug || c.slug.endsWith(circleIdOrSlug); }); } return filteredCircles .flatMap((c) => { return circleFlatLearningContents(c); }) .find((lc) => { return ( lc.id === learningContentIdOrSlug || lc.slug.endsWith(learningContentIdOrSlug) ); }); } function findLearningUnit(learningUnitIdOrSlug: string, circleIdOrSlug?: string) { let filteredCircles = circles.value ?? []; if (circleIdOrSlug) { filteredCircles = filteredCircles.filter((c) => { return c.id === circleIdOrSlug || c.slug.endsWith(circleIdOrSlug); }); } return filteredCircles .flatMap((c) => { return circleFlatLearningUnits(c); }) .find((lu) => { return lu.id === learningUnitIdOrSlug || lu.slug.endsWith(learningUnitIdOrSlug); }); } const flatPerformanceCriteria = computed(() => { return (actionCompetences.value ?? []).flatMap((ac) => { return ac.performance_criteria; }) as PerformanceCriteria[]; }); return { resultPromise, course, learningPath, actionCompetences, circles, findCircle, findLearningContent, findLearningUnit, flatPerformanceCriteria, }; } export function useCourseDataWithCompletion( courseSlug?: string, userId?: string, courseSessionId?: string ) { if (!courseSlug) { courseSlug = useCurrentCourseSession().value.course.slug; } if (!userId) { userId = useUserStore().id; } if (!courseSessionId) { courseSessionId = useCurrentCourseSession().value.id; } const courseResult = useCourseData(courseSlug); const completionStore = useCompletionStore(); const nextLearningContent = ref(undefined); const loaded = ref(false); function updateCompletionData() { if (userId && courseSessionId) { return completionStore.loadCourseSessionCompletionData(courseSessionId, userId); } return Promise.resolve([]); } function _parseCompletionData(completionData: CourseCompletion[]) { if (courseResult.circles.value) { courseResult.circles.value.forEach((circle) => { circleFlatChildren(circle).forEach((lc) => { const pageIndex = completionData.findIndex((e) => { return e.page_id === lc.id; }); if (pageIndex >= 0) { lc.completion_status = completionData[pageIndex].completion_status; } else { lc.completion_status = "UNKNOWN"; } }); }); } if (courseResult.actionCompetences.value) { courseResult.actionCompetences.value.forEach((ac) => { ac.performance_criteria.forEach((pc) => { const pageIndex = completionData.findIndex((e) => { return e.page_id === pc.id; }); if (pageIndex >= 0) { pc.completion_status = completionData[pageIndex].completion_status; } else { pc.completion_status = "UNKNOWN"; } }); }); } calcNextLearningContent(completionData); } function calcNextLearningContent(completionData: CourseCompletion[]) { const flatLearningContents = (courseResult.circles.value ?? []).flatMap((c) => { return circleFlatLearningContents(c); }); const lastCompleted = findLastCompletedLearningContent(completionData); if (lastCompleted) { const lastCompletedIndex = flatLearningContents.findIndex((lc) => { return lc.id === lastCompleted.id; }); if (flatLearningContents[lastCompletedIndex + 1]) { nextLearningContent.value = flatLearningContents[lastCompletedIndex + 1]; } else { nextLearningContent.value = undefined; } } else { nextLearningContent.value = flatLearningContents[0]; } } function findLastCompletedLearningContent(completionData: CourseCompletion[]) { const latestCompletion = orderBy(completionData ?? [], ["updated_at"], "desc").find( (c: CourseCompletion) => { return ( c.completion_status === "SUCCESS" && c.page_type.startsWith("learnpath.LearningContent") ); } ); if (latestCompletion) { return courseResult.findLearningContent(latestCompletion.page_id); } } async function markCompletion( page: LearningContentWithCompletion | LearningUnitPerformanceCriteria, completion_status: CourseCompletionStatus = "SUCCESS" ) { if (userId && courseSessionId) { page.completion_status = completion_status; const completionData = await completionStore.markPage( page, userId, courseSessionId ); _parseCompletionData(completionData); } } async function _start() { return Promise.all([courseResult.resultPromise, updateCompletionData()]).then( // eslint-disable-next-line @typescript-eslint/no-unused-vars ([_queryResults, completionData]) => { _parseCompletionData(completionData); loaded.value = true; } ); } const resultPromise = _start(); return { ...courseResult, loaded, resultPromise, markCompletion, nextLearningContent, }; } export function useCourseStatistics() { const dashboardStore = useDashboardStore(); const statistics = computed(() => { return dashboardStore.currentDashBoardData as CourseStatisticsType; }); const courseSessionName = (courseSessionId: string) => { return statistics.value.course_session_properties.sessions.find( (session) => session.id === courseSessionId )?.name; }; const circleMeta = (circleId: string) => { return statistics.value.course_session_properties.circles.find( (circle) => circle.id === circleId ); }; return { courseSessionName, circleMeta }; } export function useFileUpload() { const error = ref(false); const loading = ref(false); const fileInfo = ref({} as { id: string; name: string; url: string }); async function upload(e: Event) { const { files } = e.target as HTMLInputElement; if (!files?.length) return; try { error.value = false; loading.value = true; const file = files[0]; const presignData = await presignUpload(file); await uploadFile(presignData.pre_sign, file); fileInfo.value = presignData.file_info; } catch (e) { console.error(e); error.value = true; } finally { loading.value = false; } } return { upload, error, loading, fileInfo }; } export function useMyLearningMentors() { const learningMentors = ref([]); const currentCourseSessionId = useCurrentCourseSession().value.id; const loading = ref(false); const fetchMentors = async () => { loading.value = true; const { data } = await useCSRFFetch( `/api/mentor/${currentCourseSessionId}/mentors` ).json(); learningMentors.value = data.value; loading.value = false; }; onMounted(fetchMentors); return { learningMentors, loading, }; } export function getVvRoleDisplay(role: DashboardPersonRoleType) { switch (role) { case "LEARNING_MENTOR": return t("a.Lernbegleitung"); case "LEARNING_MENTEE": return t("a.Teilnehmer"); case "EXPERT": return t("a.Experte"); case "MEMBER": return t("a.Teilnehmer"); case "SUPERVISOR": return t("a.Regionenleiter"); default: return role; } } export function getUkRoleDisplay(role: DashboardPersonRoleType) { switch (role) { case "LEARNING_MENTOR": return t("a.Praxisbildner"); case "LEARNING_MENTEE": return t("a.Teilnehmer"); case "EXPERT": return t("a.Trainer"); case "MEMBER": return t("a.Teilnehmer"); case "SUPERVISOR": return t("a.Regionenleiter"); default: return role; } } export function useDashboardPersonsDueDates( mode: DashboardPersonsPageMode = "default" ) { const dashboardPersons = ref([]); const dashboardDueDates = ref([]); const loading = ref(false); // due dates from today to future const currentDueDates = ref([]); const fetchData = async () => { loading.value = true; try { const [persons, dueDates] = await Promise.all([ fetchDashboardPersons(mode), fetchDashboardDueDates(), ]); dashboardPersons.value = persons; // attach role name to persons dashboardPersons.value.forEach((person) => { person.course_sessions.forEach((cs) => { if (cs.is_uk) { cs.my_role_display = getUkRoleDisplay(cs.my_role); cs.user_role_display = getUkRoleDisplay(cs.user_role); } else if (cs.is_vv) { cs.my_role_display = getVvRoleDisplay(cs.my_role); cs.user_role_display = getVvRoleDisplay(cs.user_role); } else { cs.my_role_display = ""; cs.user_role_display = ""; } }); }); dashboardDueDates.value = dueDates.map((dueDate) => { const dateType = t(dueDate.date_type_translation_key); const assignmentType = t(dueDate.assignment_type_translation_key); dueDate.translatedType = dateType; if (assignmentType) { dueDate.translatedType += " " + assignmentType; } return dueDate; }); currentDueDates.value = dashboardDueDates.value.filter((dueDate) => { let refDate = dayjs(dueDate.start); if (dueDate.end) { refDate = dayjs(dueDate.end); } return refDate >= dayjs().startOf("day"); }); // attach `LEARNING_MENTEE` to due dates for `LEARNING_MENTOR` persons currentDueDates.value.forEach((dueDate) => { if (dueDate.course_session.my_role === "LEARNING_MENTOR") { dueDate.persons = dashboardPersons.value.filter((person) => { if ( person.course_sessions .map((cs) => cs.id) .includes(dueDate.course_session.id) ) { return person.course_sessions.some( (cs) => cs.user_role === "LEARNING_MENTEE" ); } }); } }); } catch (error) { console.error("Error fetching data:", error); } finally { loading.value = false; } }; onMounted(fetchData); return { dashboardPersons, dashboardDueDates, currentDueDates, loading, }; } export function useCourseCircleProgress(circles: Ref) { const inProgressCirclesCount = computed(() => { if (circles.value?.length) { return circles.value.filter( (circle) => circle.learning_sequences.filter((ls) => someFinishedInLearningSequence(ls)) .length ).length; } return 0; }); const circlesCount = computed(() => { return circles.value?.length ?? 0; }); return { inProgressCirclesCount, circlesCount }; } export function useCourseStatisticsv2(courseSlug: string) { const dashboardStore = useDashboardStore(); const courseStatistics = ref(null); const loading = ref(false); const fetchData = async () => { loading.value = true; await dashboardStore.loadDashboardDetails(); const courseId = courseIdForCourseSlug( dashboardStore.dashboardConfigsv2, courseSlug ); try { if (courseId) { courseStatistics.value = await fetchStatisticData(courseId); } } finally { loading.value = false; } }; const courseSessionName = (courseSessionId: string) => { return courseStatistics?.value?.course_session_properties?.sessions.find( (session) => session.id === courseSessionId )?.name; }; const circleMeta = (circleId: string) => { return courseStatistics?.value?.course_session_properties.circles.find( (circle) => circle.id === circleId ); }; onMounted(fetchData); return { courseStatistics, loading, courseSessionName, circleMeta, }; } export function useCertificateQuery(userId: string | undefined, courseSlug: string) { const certificatesQuery = (() => { const courseSession = useCurrentCourseSession(); if (userId) { return useQuery({ query: COMPETENCE_NAVI_CERTIFICATE_FOR_USER_QUERY, variables: { courseSlug: courseSlug, courseSessionId: courseSession.value.id, userId: userId, }, }); } else { return useQuery({ query: COMPETENCE_NAVI_CERTIFICATE_QUERY, variables: { courseSlug: courseSlug, courseSessionId: courseSession.value.id, }, }); } })(); return { certificatesQuery }; } export function useEvaluationWithFeedback() { const currentCourseSession = useCurrentCourseSession(); const hasFeedback = computed( () => currentCourseSession.value.course.configuration.enable_learning_mentor && currentCourseSession.value.course.configuration.is_vv ); return { hasFeedback }; }