diff --git a/client/src/composables.ts b/client/src/composables.ts index 31fd14b7..d22d32f3 100644 --- a/client/src/composables.ts +++ b/client/src/composables.ts @@ -1,6 +1,10 @@ import { graphqlClient } from "@/graphql/client"; import { COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries"; -import { circleFlatChildren, circleFlatLearningContents } from "@/services/circle"; +import { + circleFlatChildren, + circleFlatLearningContents, + circleFlatLearningUnits, +} from "@/services/circle"; import { useCompletionStore } from "@/stores/completion"; import { useCourseSessionsStore } from "@/stores/courseSessions"; import { useUserStore } from "@/stores/user"; @@ -16,6 +20,7 @@ import type { PerformanceCriteria, } from "@/types"; import { useQuery } from "@urql/vue"; +import orderBy from "lodash/orderBy"; import log from "loglevel"; import type { ComputedRef } from "vue"; import { computed, ref, watchEffect } from "vue"; @@ -157,23 +162,33 @@ export function useLearningPath(courseSlug: string) { // attach circle information to learning contents if (learningPath.value) { flatCircles(learningPath.value).forEach((circle) => { - circleFlatChildren(circle).forEach((lc) => { - lc.circle = { + circle.learning_sequences.forEach((ls, lsIndex) => { + const circleData = { id: circle.id, slug: circle.slug, title: circle.title, }; - - if (lc.content_type === "competence.PerformanceCriteria") { - const pc = findPerformanceCriterion(lc.id); - if (pc) { - pc.circle = { - 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); + } + }); + }); }); }); } @@ -186,9 +201,9 @@ export function useLearningPath(courseSlug: string) { return undefined; }); - function findCircle(slug: string) { + function findCircle(idOrSlug: string) { return (circles.value ?? []).find((c) => { - return c.slug.endsWith(slug); + return c.id === idOrSlug || c.slug.endsWith(idOrSlug); }); } @@ -202,13 +217,42 @@ export function useLearningPath(courseSlug: string) { }) as PerformanceCriteria | undefined; } - function findLearningContent(learningContentId: string) { - return (circles.value ?? []) + 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 === learningContentId; + 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); }); } @@ -225,6 +269,7 @@ export function useLearningPath(courseSlug: string) { circles, findCircle, findLearningContent, + findLearningUnit, flatPerformanceCriteria, }; } @@ -246,7 +291,7 @@ export function useLearningPathWithCompletion( const courseResult = useLearningPath(courseSlug); const completionStore = useCompletionStore(); - const nextLearningContent = ref(null); + const nextLearningContent = ref(undefined); const loaded = ref(false); function updateCompletionData() { @@ -287,11 +332,37 @@ export function useLearningPathWithCompletion( }); } - // FIXME calculate nextLearningContent - if (courseResult.circles.value?.length) { - nextLearningContent.value = circleFlatLearningContents( - courseResult.circles.value[0] - )[0]; + calcNextLearningContent(completionData); + } + + function calcNextLearningContent(completionData: CourseCompletion[]) { + const lastCompleted = findLastCompletedLearningContent(completionData); + if (lastCompleted) { + const flatLearningContents = (courseResult.circles.value ?? []).flatMap((c) => { + return circleFlatLearningContents(c); + }); + const lastCompletedIndex = flatLearningContents.findIndex((lc) => { + return lc.id === lastCompleted.id; + }); + if (flatLearningContents[lastCompletedIndex + 1]) { + nextLearningContent.value = flatLearningContents[lastCompletedIndex + 1]; + } else { + nextLearningContent.value = undefined; + } + } + } + + 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); } } @@ -326,7 +397,6 @@ export function useLearningPathWithCompletion( ...courseResult, loaded, resultPromise, - updateCompletionData, markCompletion, nextLearningContent, }; diff --git a/client/src/constants.ts b/client/src/constants.ts index 65dee1e9..84aaefc3 100644 --- a/client/src/constants.ts +++ b/client/src/constants.ts @@ -1,9 +1,3 @@ -import type { CourseCompletionStatus } from "@/types"; - -export const COMPLETION_SUCCESS: CourseCompletionStatus = "SUCCESS"; -export const COMPLETION_FAILURE: CourseCompletionStatus = "FAIL"; -export const COMPLETION_UNKNOWN: CourseCompletionStatus = "UNKNOWN"; - export const itCheckboxDefaultIconCheckedTailwindClass = "bg-[url(/static/icons/icon-checkbox-checked.svg)] hover:bg-[url(/static/icons/icon-checkbox-checked-hover.svg)]"; diff --git a/client/src/pages/cockpit/CockpitUserProfilePage.vue b/client/src/pages/cockpit/CockpitUserProfilePage.vue index 7ef608f8..2a51e45c 100644 --- a/client/src/pages/cockpit/CockpitUserProfilePage.vue +++ b/client/src/pages/cockpit/CockpitUserProfilePage.vue @@ -71,6 +71,7 @@ function setActiveClasses(isActive: boolean) { :use-mobile-layout="false" :hide-buttons="true" :learning-path="learningPath" + :next-learning-content="undefined" :override-circle-url-base="`/course/${props.courseSlug}/cockpit/profile/${props.userId}`" > diff --git a/client/src/pages/cockpit/documentPage/DocumentPage.vue b/client/src/pages/cockpit/documentPage/DocumentPage.vue index 694a25c1..d8e9bfa5 100644 --- a/client/src/pages/cockpit/documentPage/DocumentPage.vue +++ b/client/src/pages/cockpit/documentPage/DocumentPage.vue @@ -1,5 +1,5 @@ diff --git a/client/src/pages/learningPath/learningContentPage/LearningContentParent.vue b/client/src/pages/learningPath/learningContentPage/LearningContentParent.vue index 8bf8d964..0a9e8ea6 100644 --- a/client/src/pages/learningPath/learningContentPage/LearningContentParent.vue +++ b/client/src/pages/learningPath/learningContentPage/LearningContentParent.vue @@ -2,8 +2,12 @@ import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue"; import DocumentListBlock from "@/pages/learningPath/learningContentPage/blocks/DocumentListBlock.vue"; import EdoniqTestBlock from "@/pages/learningPath/learningContentPage/blocks/EdoniqTestBlock.vue"; -import { useCircleStore } from "@/stores/circle"; -import type { LearningContent, LearningContentContentType } from "@/types"; +import type { + CircleType, + LearningContent, + LearningContentContentType, + LearningContentWithCompletion, +} from "@/types"; import eventBus from "@/utils/eventBus"; import log from "loglevel"; import type { Component } from "vue"; @@ -17,14 +21,19 @@ import PlaceholderBlock from "./blocks/PlaceholderBlock.vue"; import RichTextBlock from "./blocks/RichTextBlock.vue"; import VideoBlock from "./blocks/VideoBlock.vue"; import { getPreviousRoute } from "@/router/history"; - -const circleStore = useCircleStore(); +import { stringifyParse } from "@/utils/utils"; +import { useLearningPathWithCompletion } from "@/composables"; +import { useCircleStore } from "@/stores/circle"; const props = defineProps<{ learningContent: LearningContent; + circle: CircleType; }>(); -log.debug("LearningContentParent setup", props.learningContent); +log.debug("LearningContentParent setup", stringifyParse(props)); + +const courseCompletionData = useLearningPathWithCompletion(); +const circleStore = useCircleStore(); const previousRoute = getPreviousRoute(); @@ -48,7 +57,14 @@ const component = computed(() => { }); function handleFinishedLearningContent() { - circleStore.continueFromLearningContent(props.learningContent, previousRoute); + circleStore.continueFromLearningContent( + props.learningContent, + props.circle, + previousRoute, + (lc: LearningContentWithCompletion) => { + courseCompletionData.markCompletion(lc, "SUCCESS"); + } + ); } eventBus.on("finishedLearningContent", handleFinishedLearningContent); @@ -60,7 +76,9 @@ onUnmounted(() => {