import { useCSRFFetch } from "@/fetchHelpers"; import type { CourseStatisticsType } from "@/gql/graphql"; import { graphqlClient } from "@/graphql/client"; import { COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries"; import { circleFlatChildren, circleFlatLearningContents, circleFlatLearningUnits, } from "@/services/circle"; 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, Course, CourseCompletion, CourseCompletionStatus, CourseSession, CourseSessionDetail, LearningContentWithCompletion, LearningMentor, LearningPathType, LearningUnitPerformanceCriteria, PerformanceCriteria, } from "@/types"; import { useQuery } from "@urql/vue"; import orderBy from "lodash/orderBy"; import log from "loglevel"; import type { ComputedRef } 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 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 useLearningMentors() { const learningMentors = ref([]); const currentCourseSessionId = useCurrentCourseSession().value.id; const fetchMentors = async () => { const { data } = await useCSRFFetch( `/api/mentor/${currentCourseSessionId}/mentors` ).json(); learningMentors.value = data.value; }; onMounted(fetchMentors); return { learningMentors, }; }