vbv/client/src/composables.ts

486 lines
14 KiB
TypeScript

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<CourseSession> = 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<LearningPathType | undefined>(undefined);
const actionCompetences = ref<ActionCompetence[]>([]);
const course = ref<Course | undefined>(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<LearningContentWithCompletion | undefined>(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<LearningMentor[]>([]);
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,
};
}