vbv/client/src/composables.ts

731 lines
20 KiB
TypeScript

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<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 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<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 useMyLearningMentors() {
const learningMentors = ref<LearningMentor[]>([]);
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<DashboardPersonType[]>([]);
const dashboardDueDates = ref<DashboardDueDate[]>([]);
const loading = ref(false);
// due dates from today to future
const currentDueDates = ref<DashboardDueDate[]>([]);
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<CircleType[] | undefined>) {
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<CourseStatisticsType | null>(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 };
}