Calc next learning content
This commit is contained in:
parent
e5cc0aa80e
commit
cc800501c1
|
|
@ -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,25 +162,35 @@ 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,
|
||||
};
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
if (lc.content_type === "competence.PerformanceCriteria") {
|
||||
const pc = findPerformanceCriterion(lc.id);
|
||||
lu.performance_criteria.forEach((luPc) => {
|
||||
luPc.circle = Object.assign({}, circleData);
|
||||
const pc = findPerformanceCriterion(luPc.id);
|
||||
if (pc) {
|
||||
pc.circle = {
|
||||
id: circle.id,
|
||||
slug: circle.slug,
|
||||
title: circle.title,
|
||||
};
|
||||
}
|
||||
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<LearningContentWithCompletion | null>(null);
|
||||
const nextLearningContent = ref<LearningContentWithCompletion | undefined>(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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)]";
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`"
|
||||
></LearningPathPathView>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { useCurrentCourseSession, useLearningPath } from "@/composables";
|
||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||
import { useCockpitStore } from "@/stores/cockpit";
|
||||
import ItModal from "@/components/ui/ItModal.vue";
|
||||
|
|
@ -16,13 +16,11 @@ import {
|
|||
} from "@/services/files";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
|
||||
const cockpitStore = useCockpitStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseData = useLearningPath(courseSession.value?.course.slug);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -51,26 +49,18 @@ onMounted(async () => {
|
|||
await fetchDocuments();
|
||||
});
|
||||
|
||||
watch(
|
||||
// workaround to load learning sequences when circle changes
|
||||
() => cockpitStore.currentCircle,
|
||||
async () => {
|
||||
if (cockpitStore.currentCircle) {
|
||||
await circleStore.loadCircle(
|
||||
courseSession.value?.course.slug,
|
||||
cockpitStore.currentCircle?.slug
|
||||
);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const dropdownLearningSequences = computed(() =>
|
||||
circleStore.circle?.learningSequences.map((sequence) => ({
|
||||
const dropdownLearningSequences = computed(() => {
|
||||
if (cockpitStore.currentCircle?.slug) {
|
||||
const circle = courseData.findCircle(cockpitStore.currentCircle?.slug);
|
||||
if (circle) {
|
||||
return circle.learning_sequences.map((sequence) => ({
|
||||
id: sequence.id,
|
||||
name: `${sequence.title}`,
|
||||
}))
|
||||
);
|
||||
}));
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const circleDocuments = computed(() => {
|
||||
return circleDocumentsResultData.value.filter(
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ watch(
|
|||
{{ $t("circlePage.learnMore") }}
|
||||
</button>
|
||||
</div>
|
||||
<DocumentSection v-if="!readonly" />
|
||||
<DocumentSection v-if="!readonly" :circle="circle" />
|
||||
<div v-if="!props.readonly" class="expert mt-8 border p-6">
|
||||
<h3 class="text-blue-dark">{{ $t("circlePage.gotQuestions") }}</h3>
|
||||
<div class="mt-4 leading-relaxed">
|
||||
|
|
@ -228,6 +228,7 @@ watch(
|
|||
>
|
||||
<LearningSequence
|
||||
:course-slug="props.courseSlug"
|
||||
:circle="circle"
|
||||
:learning-sequence="learningSequence"
|
||||
:readonly="props.readonly"
|
||||
></LearningSequence>
|
||||
|
|
|
|||
|
|
@ -23,12 +23,14 @@
|
|||
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { CircleDocument } from "@/types";
|
||||
import type { CircleDocument, CircleType } from "@/types";
|
||||
import { fetchCourseSessionDocuments } from "@/services/files";
|
||||
|
||||
const props = defineProps<{
|
||||
circle: CircleType;
|
||||
}>();
|
||||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const circleDocumentsResultData = ref<CircleDocument[]>([]);
|
||||
|
||||
|
|
@ -43,7 +45,7 @@ async function fetchDocuments() {
|
|||
|
||||
const circleDocuments = computed(() => {
|
||||
return circleDocumentsResultData.value.filter(
|
||||
(d) => d.learning_sequence.circle.slug === circleStore.circle?.slug
|
||||
(d) => d.learning_sequence.circle.slug === props.circle?.slug
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import LearningContentBadge from "@/pages/learningPath/LearningContentTypeBadge.
|
|||
import { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type {
|
||||
CircleType,
|
||||
CourseCompletionStatus,
|
||||
LearningContent,
|
||||
LearningContentAssignment,
|
||||
|
|
@ -10,6 +11,7 @@ import type {
|
|||
LearningContentWithCompletion,
|
||||
LearningSequence,
|
||||
} from "@/types";
|
||||
import type { Ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
itCheckboxDefaultIconCheckedTailwindClass,
|
||||
|
|
@ -19,13 +21,16 @@ import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
|||
import {
|
||||
allFinishedInLearningSequence,
|
||||
calcSelfEvaluationStatus,
|
||||
circleFlatLearningContents,
|
||||
someFinishedInLearningSequence,
|
||||
} from "@/services/circle";
|
||||
import { useLearningPathWithCompletion } from "@/composables";
|
||||
import { findLastIndex } from "lodash";
|
||||
|
||||
type Props = {
|
||||
courseSlug: string;
|
||||
learningSequence: LearningSequence;
|
||||
circle: CircleType;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -61,26 +66,27 @@ const allFinished = computed(() => {
|
|||
return false;
|
||||
});
|
||||
|
||||
const continueTranslationKeyTuple = computed(() => {
|
||||
// if (props.learningSequence && circleStore.circle) {
|
||||
// const lastFinished = findLast(
|
||||
// circleStore.circle.flatLearningContents,
|
||||
// (learningContent) => {
|
||||
// return learningContent.completion_status === "SUCCESS";
|
||||
// }
|
||||
// );
|
||||
//
|
||||
// if (!lastFinished) {
|
||||
// // must be the first
|
||||
// return [circleStore.circle.flatLearningContents[0].translation_key, true];
|
||||
// }
|
||||
//
|
||||
// if (lastFinished && lastFinished.nextLearningContent) {
|
||||
// return [lastFinished.nextLearningContent.translation_key, false];
|
||||
// }
|
||||
// }
|
||||
const continueTranslationKeyTuple: Ref<[string | undefined, boolean]> = computed(() => {
|
||||
if (props.learningSequence) {
|
||||
const flatLearningContents = circleFlatLearningContents(props.circle);
|
||||
const lastFinishedIndex = findLastIndex(
|
||||
circleFlatLearningContents(props.circle),
|
||||
(learningContent) => {
|
||||
return learningContent.completion_status === "SUCCESS";
|
||||
}
|
||||
);
|
||||
|
||||
return "";
|
||||
if (lastFinishedIndex === -1) {
|
||||
// must be the first
|
||||
return [flatLearningContents[0].id, true];
|
||||
}
|
||||
|
||||
if (flatLearningContents[lastFinishedIndex + 1]) {
|
||||
return [flatLearningContents[lastFinishedIndex + 1].id, false];
|
||||
}
|
||||
}
|
||||
|
||||
return [undefined, false];
|
||||
});
|
||||
|
||||
const learningSequenceBorderClass = computed(() => {
|
||||
|
|
@ -100,8 +106,8 @@ const learningSequenceBorderClass = computed(() => {
|
|||
|
||||
function belongsToCompetenceCertificate(lc: LearningContent) {
|
||||
return (
|
||||
(lc.__typename === "LearningContentAssignmentObjectType" ||
|
||||
lc.__typename === "LearningContentEdoniqTestObjectType") &&
|
||||
(lc.content_type === "learnpath.LearningContentAssignment" ||
|
||||
lc.content_type === "learnpath.LearningContentEdoniqTest") &&
|
||||
lc.competence_certificate?.frontend_url
|
||||
);
|
||||
}
|
||||
|
|
@ -239,7 +245,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
|||
|
||||
<div
|
||||
v-if="
|
||||
learningContent.translation_key === continueTranslationKeyTuple[0] &&
|
||||
learningContent.id === continueTranslationKeyTuple[0] &&
|
||||
!props.readonly
|
||||
"
|
||||
class="my-4"
|
||||
|
|
@ -287,7 +293,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" />
|
||||
<!-- <hr v-if="!learningUnit.last" class="-mx-4 text-gray-500" />-->
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import LearningContentParent from "@/pages/learningPath/learningContentPage/LearningContentParent.vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningContent } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
import type { Ref } from "vue";
|
||||
import { getCurrentInstance, onMounted, onUpdated, ref, watch } from "vue";
|
||||
|
||||
log.debug("LearningContentView created");
|
||||
import { computed, getCurrentInstance, onUpdated } from "vue";
|
||||
import { useLearningPathWithCompletion } from "@/composables";
|
||||
import { stringifyParse } from "@/utils/utils";
|
||||
|
||||
const props = defineProps<{
|
||||
courseSlug: string;
|
||||
|
|
@ -14,43 +11,14 @@ const props = defineProps<{
|
|||
contentSlug: string;
|
||||
}>();
|
||||
|
||||
const learningContent: Ref<LearningContent | undefined> = ref(undefined);
|
||||
log.debug("LearningContentView created", stringifyParse(props));
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const loadLearningContent = async () => {
|
||||
try {
|
||||
learningContent.value = await circleStore.loadLearningContent(
|
||||
props.courseSlug,
|
||||
props.circleSlug,
|
||||
props.contentSlug
|
||||
const courseData = useLearningPathWithCompletion(props.courseSlug);
|
||||
const learningContent = computed(() =>
|
||||
courseData.findLearningContent(props.contentSlug, props.circleSlug)
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.contentSlug,
|
||||
async () => {
|
||||
log.debug(
|
||||
"LearningContentView props.contentSlug changed",
|
||||
props.courseSlug,
|
||||
props.circleSlug,
|
||||
props.contentSlug
|
||||
);
|
||||
await loadLearningContent();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug(
|
||||
"LearningContentView mounted",
|
||||
props.courseSlug,
|
||||
props.circleSlug,
|
||||
props.contentSlug
|
||||
);
|
||||
await loadLearningContent();
|
||||
const circle = computed(() => {
|
||||
return courseData.findCircle(props.circleSlug);
|
||||
});
|
||||
|
||||
onUpdated(() => {
|
||||
|
|
@ -73,7 +41,11 @@ onUpdated(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<LearningContentParent v-if="learningContent" :learning-content="learningContent" />
|
||||
<LearningContentParent
|
||||
v-if="learningContent && circle"
|
||||
:learning-content="learningContent"
|
||||
:circle="circle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
|||
|
||||
<template>
|
||||
<LearningContentContainer
|
||||
@exit="circleStore.closeLearningContent(props.learningContent, previousRoute)"
|
||||
@exit="
|
||||
circleStore.closeLearningContent(props.learningContent, circle, previousRoute)
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<component :is="component" :content="learningContent"></component>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import DateEmbedding from "@/components/dueDates/DateEmbedding.vue";
|
|||
import ItButton from "@/components/ui/ItButton.vue";
|
||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
||||
import {
|
||||
useCourseSessionDetailQuery,
|
||||
useCurrentCourseSession,
|
||||
useLearningPath,
|
||||
} from "@/composables";
|
||||
import { bustItGetCache } from "@/fetchHelpers";
|
||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||
|
|
@ -15,7 +19,6 @@ import { computed, reactive } from "vue";
|
|||
import { useTranslation } from "i18next-vue";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import dayjs from "dayjs";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
|
||||
const props = defineProps<{
|
||||
assignment: Assignment;
|
||||
|
|
@ -31,7 +34,7 @@ const emit = defineEmits<{
|
|||
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||
const circleStore = useCircleStore();
|
||||
const courseData = useLearningPath(courseSession.value.course.slug);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -40,9 +43,15 @@ const state = reactive({
|
|||
confirmPerson: false,
|
||||
});
|
||||
|
||||
const learningContent = computed(() => {
|
||||
return courseData.findLearningContent(props.learningContentId);
|
||||
});
|
||||
|
||||
const circleExperts = computed(() => {
|
||||
if (circleStore.circle) {
|
||||
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
|
||||
if (learningContent.value?.circle) {
|
||||
return courseSessionDetailResult.filterCircleExperts(
|
||||
learningContent.value?.circle.slug
|
||||
);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ const currentTask = computed(() => {
|
|||
const initUpsertAssignmentCompletion = async () => {
|
||||
try {
|
||||
await upsertAssignmentCompletionMutation.executeMutation({
|
||||
assignmentId: props.learningContent.content_assignment_id,
|
||||
assignmentId: props.learningContent.content_assignment.id,
|
||||
courseSessionId: courseSession.value.id,
|
||||
learningContentId: props.learningContent.id,
|
||||
completionDataString: JSON.stringify({}),
|
||||
|
|
@ -234,7 +234,7 @@ const assignmentUser = computed(() => {
|
|||
<AssignmentTaskView
|
||||
v-else-if="currentTask"
|
||||
:task="currentTask"
|
||||
:assignment-id="props.learningContent.content_assignment_id"
|
||||
:assignment-id="props.learningContent.content_assignment.id"
|
||||
:assignment-completion="assignmentCompletion"
|
||||
:learning-content-id="props.learningContent.id"
|
||||
></AssignmentTaskView>
|
||||
|
|
|
|||
|
|
@ -2,34 +2,44 @@
|
|||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue";
|
||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||
import type { OldCircle } from "@/services/oldCircle";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import type { Topic } from "@/types";
|
||||
import { onMounted, ref } from "vue";
|
||||
import type { CircleType, LearningContentWithCompletion } from "@/types";
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
learningPath: LearningPath | undefined;
|
||||
circle: OldCircle;
|
||||
topic: Topic;
|
||||
circle: CircleType;
|
||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||
isFirstCircle: boolean;
|
||||
isLastCircle: boolean;
|
||||
isCurrentCircle: boolean;
|
||||
overrideCircleUrl?: string;
|
||||
}>();
|
||||
|
||||
const circleElement = ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.isCurrentCircle) {
|
||||
const isCurrentCircle = computed(() => {
|
||||
return props.nextLearningContent?.circle?.id === props.circle.id;
|
||||
});
|
||||
|
||||
function scrollToCircle() {
|
||||
if (isCurrentCircle.value) {
|
||||
setTimeout(() => {
|
||||
circleElement?.value?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
inline: "center",
|
||||
block: "nearest",
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isCurrentCircle.value,
|
||||
(isCurrent) => {
|
||||
if (isCurrent) {
|
||||
scrollToCircle();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -55,10 +65,9 @@ onMounted(() => {
|
|||
{{ props.circle.title }}
|
||||
</div>
|
||||
|
||||
<div v-if="props.isCurrentCircle" class="whitespace-nowrap">
|
||||
<div v-if="isCurrentCircle" class="whitespace-nowrap">
|
||||
<LearningPathContinueButton
|
||||
:has-progress="!props.learningPath?.continueData?.has_no_progress"
|
||||
:url="props.learningPath?.continueData?.url"
|
||||
:next-learning-content="props.nextLearningContent"
|
||||
></LearningPathContinueButton>
|
||||
</div>
|
||||
</router-link>
|
||||
|
|
|
|||
|
|
@ -2,29 +2,35 @@
|
|||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||
import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue";
|
||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||
import type { OldCircle } from "@/services/oldCircle";
|
||||
import type { LearningPath } from "@/services/learningPath";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import type { CircleType, LearningContentWithCompletion } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
learningPath: LearningPath | undefined;
|
||||
circle: OldCircle;
|
||||
isCurrentCircle: boolean;
|
||||
circle: CircleType;
|
||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||
}>();
|
||||
|
||||
const circleElement = ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.isCurrentCircle) {
|
||||
const isCurrentCircle = computed(() => {
|
||||
return props.nextLearningContent?.circle?.id === props.circle.id;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => isCurrentCircle.value,
|
||||
(isCurrent) => {
|
||||
if (isCurrent) {
|
||||
setTimeout(() => {
|
||||
circleElement?.value?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
inline: "nearest",
|
||||
block: "center",
|
||||
inline: "center",
|
||||
block: "nearest",
|
||||
});
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -43,8 +49,7 @@ onMounted(() => {
|
|||
|
||||
<div v-if="isCurrentCircle" class="whitespace-nowrap pl-4">
|
||||
<LearningPathContinueButton
|
||||
:has-progress="!props.learningPath?.continueData?.has_no_progress"
|
||||
:url="props.learningPath?.continueData?.url"
|
||||
:next-learning-content="props.nextLearningContent"
|
||||
></LearningPathContinueButton>
|
||||
</div>
|
||||
</router-link>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
<script setup lang="ts">
|
||||
interface Props {
|
||||
hasProgress?: boolean;
|
||||
url?: string;
|
||||
}
|
||||
import type { LearningContentWithCompletion } from "@/types";
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
hasProgress: false,
|
||||
url: "",
|
||||
});
|
||||
const props = defineProps<{
|
||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-link
|
||||
class="btn-blue mt-2 pl-6"
|
||||
:to="props.url"
|
||||
:to="nextLearningContent?.continueUrl ?? '/hello'"
|
||||
data-cy="lp-continue-button"
|
||||
translate
|
||||
>
|
||||
<span>
|
||||
{{ props.hasProgress ? $t("general.nextStep") : $t("general.start") }}
|
||||
{{
|
||||
props.nextLearningContent?.firstInCircle
|
||||
? $t("general.start")
|
||||
: $t("general.nextStep")
|
||||
}}
|
||||
</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
|
||||
import type { OldCircle } from "@/services/oldCircle";
|
||||
import { computed } from "vue";
|
||||
import type { LearningPathType } from "@/types";
|
||||
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
learningPath: LearningPathType | undefined;
|
||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||
}>();
|
||||
|
||||
const topics = computed(() => props.learningPath?.topics ?? []);
|
||||
|
||||
const isCurrentCircle = (circle: OldCircle) =>
|
||||
props.learningPath?.nextLearningContent?.parentCircle === circle;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -22,9 +19,8 @@ const isCurrentCircle = (circle: OldCircle) =>
|
|||
<LearningPathCircleListTile
|
||||
v-for="circle in topic.circles"
|
||||
:key="circle.id"
|
||||
:learning-path="learningPath"
|
||||
:circle="circle"
|
||||
:is-current-circle="isCurrentCircle(circle)"
|
||||
:next-learning-content="props.nextLearningContent"
|
||||
></LearningPathCircleListTile>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -80,13 +80,16 @@ const changeViewType = (viewType: ViewType) => {
|
|||
|
||||
<!-- Bottom -->
|
||||
<div class="bg-white">
|
||||
<div v-if="lpQueryResult.learningPath">
|
||||
<div class="flex flex-col justify-between px-6 sm:flex-row sm:px-12">
|
||||
<!-- Topics -->
|
||||
<div
|
||||
v-if="selectedView == 'path'"
|
||||
class="order-2 pb-8 sm:order-1 sm:pb-0 sm:pt-4"
|
||||
>
|
||||
<LearningPathTopics :topics="learningPath?.topics ?? []"></LearningPathTopics>
|
||||
<LearningPathTopics
|
||||
:topics="learningPath?.topics ?? []"
|
||||
></LearningPathTopics>
|
||||
</div>
|
||||
<div v-else class="flex-grow"></div>
|
||||
|
||||
|
|
@ -103,6 +106,7 @@ const changeViewType = (viewType: ViewType) => {
|
|||
<LearningPathPathView
|
||||
:learning-path="learningPath"
|
||||
:use-mobile-layout="useMobileLayout"
|
||||
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
||||
></LearningPathPathView>
|
||||
</div>
|
||||
|
||||
|
|
@ -112,14 +116,16 @@ const changeViewType = (viewType: ViewType) => {
|
|||
class="flex flex-col pl-6 sm:pl-24"
|
||||
data-cy="lp-list-view"
|
||||
>
|
||||
<LearningPathListView :learning-path="learningPath"></LearningPathListView>
|
||||
<LearningPathListView
|
||||
:learning-path="learningPath"
|
||||
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
||||
></LearningPathListView>
|
||||
</div>
|
||||
<div
|
||||
v-if="useMobileLayout"
|
||||
class="p-6"
|
||||
:class="useMobileLayout ? 'bg-gray-200' : ''"
|
||||
>
|
||||
<!--<LearningPathAppointmentsMock></LearningPathAppointmentsMock>-->
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import LearningPathCircleColumn from "@/pages/learningPath/learningPathPage/LearningPathCircleColumn.vue";
|
||||
import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue";
|
||||
import type { OldCircle } from "@/services/oldCircle";
|
||||
import { useScroll } from "@vueuse/core";
|
||||
import { ref } from "vue";
|
||||
import type { LearningPathType } from "@/types";
|
||||
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
learningPath: LearningPathType | undefined;
|
||||
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||
useMobileLayout: boolean;
|
||||
hideButtons?: boolean;
|
||||
overrideCircleUrlBase?: string;
|
||||
|
|
@ -25,9 +25,6 @@ const isLastCircle = (topicIndex: number, circleIndex: number, numCircles: numbe
|
|||
topicIndex === (props.learningPath?.topics ?? []).length - 1 &&
|
||||
circleIndex === numCircles - 1;
|
||||
|
||||
const isCurrentCircle = (circle: OldCircle) =>
|
||||
props.learningPath?.nextLearningContent?.parentCircle === circle;
|
||||
|
||||
const scrollRight = () => scrollLearnPathDiagram(scrollIncrement);
|
||||
|
||||
const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
|
||||
|
|
@ -65,14 +62,12 @@ const scrollLearnPathDiagram = (offset: number) => {
|
|||
<LearningPathCircleColumn
|
||||
v-for="(circle, circleIndex) in topic.circles"
|
||||
:key="circle.id"
|
||||
:learning-path="learningPath"
|
||||
:circle="circle"
|
||||
:topic="topic"
|
||||
:next-learning-content="props.nextLearningContent"
|
||||
:is-first-circle="isFirstCircle(topicIndex, circleIndex)"
|
||||
:is-last-circle="
|
||||
isLastCircle(topicIndex, circleIndex, topic.circles.length)
|
||||
"
|
||||
:is-current-circle="isCurrentCircle(circle) && !props.hideButtons"
|
||||
:override-circle-url="
|
||||
props.overrideCircleUrlBase
|
||||
? `${props.overrideCircleUrlBase}/${circle.slug}`
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import type { Topic } from "@/types";
|
||||
import type { TopicType } from "@/types";
|
||||
|
||||
const props = defineProps<{
|
||||
topics: Topic[];
|
||||
topics: TopicType[];
|
||||
}>();
|
||||
|
||||
const scrollToTopic = (topic: Topic) => {
|
||||
const scrollToTopic = (topic: TopicType) => {
|
||||
const id = `topic-${topic.slug}`;
|
||||
const el = document.getElementById(id);
|
||||
el?.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" });
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningUnit } from "@/types";
|
||||
import type { CircleType, LearningUnit } from "@/types";
|
||||
import * as log from "loglevel";
|
||||
|
||||
import { useCurrentCourseSession } from "@/composables";
|
||||
import { COMPLETION_FAILURE, COMPLETION_SUCCESS } from "@/constants";
|
||||
import { useCurrentCourseSession, useLearningPathWithCompletion } from "@/composables";
|
||||
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
|
|
@ -17,16 +16,17 @@ log.debug("LearningContent.vue setup");
|
|||
|
||||
const circleStore = useCircleStore();
|
||||
const courseSession = useCurrentCourseSession();
|
||||
const courseCompletionData = useLearningPathWithCompletion();
|
||||
|
||||
const questionIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
||||
|
||||
const previousRoute = getPreviousRoute();
|
||||
|
||||
const props = defineProps<{
|
||||
learningUnit: LearningUnit;
|
||||
circle: CircleType;
|
||||
}>();
|
||||
|
||||
const questions = computed(() => props.learningUnit?.children);
|
||||
const questions = computed(() => props.learningUnit?.performance_criteria ?? []);
|
||||
const currentQuestion = computed(() => questions.value[questionIndex.value]);
|
||||
const showPreviousButton = computed(() => questionIndex.value != 0);
|
||||
const showNextButton = computed(
|
||||
|
|
@ -44,7 +44,7 @@ function handleContinue() {
|
|||
questionIndex.value += 1;
|
||||
} else {
|
||||
log.debug("continue to next learning content");
|
||||
circleStore.continueFromSelfEvaluation(props.learningUnit);
|
||||
circleStore.continueFromSelfEvaluation(props.learningUnit, props.circle);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ function handleBack() {
|
|||
}
|
||||
|
||||
function handleFinishedLearningContent() {
|
||||
circleStore.closeSelfEvaluation(props.learningUnit, previousRoute);
|
||||
circleStore.closeSelfEvaluation(props.learningUnit, props.circle, previousRoute);
|
||||
}
|
||||
|
||||
eventBus.on("finishedLearningContent", handleFinishedLearningContent);
|
||||
|
|
@ -69,7 +69,9 @@ onUnmounted(() => {
|
|||
<template>
|
||||
<div v-if="learningUnit">
|
||||
<LearningContentContainer
|
||||
@exit="circleStore.closeSelfEvaluation(props.learningUnit, previousRoute)"
|
||||
@exit="
|
||||
circleStore.closeSelfEvaluation(props.learningUnit, props.circle, previousRoute)
|
||||
"
|
||||
>
|
||||
<LearningContentMultiLayout
|
||||
:current-step="questionIndex"
|
||||
|
|
@ -101,7 +103,7 @@ onUnmounted(() => {
|
|||
'border-2': currentQuestion.completion_status === 'SUCCESS',
|
||||
}"
|
||||
data-cy="success"
|
||||
@click="circleStore.markCompletion(currentQuestion, COMPLETION_SUCCESS)"
|
||||
@click="courseCompletionData.markCompletion(currentQuestion, 'SUCCESS')"
|
||||
>
|
||||
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
|
||||
<span class="text-large font-bold">
|
||||
|
|
@ -111,12 +113,11 @@ onUnmounted(() => {
|
|||
<button
|
||||
class="inline-flex flex-1 items-center border p-4 text-left"
|
||||
:class="{
|
||||
'border-orange-500':
|
||||
currentQuestion.completion_status === COMPLETION_FAILURE,
|
||||
'border-2': currentQuestion.completion_status === COMPLETION_FAILURE,
|
||||
'border-orange-500': currentQuestion.completion_status === 'FAIL',
|
||||
'border-2': currentQuestion.completion_status === 'FAIL',
|
||||
}"
|
||||
data-cy="fail"
|
||||
@click="circleStore.markCompletion(currentQuestion, 'FAIL')"
|
||||
@click="courseCompletionData.markCompletion(currentQuestion, 'FAIL')"
|
||||
>
|
||||
<it-icon-smiley-thinking
|
||||
class="mr-4 h-16 w-16"
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
import * as log from "loglevel";
|
||||
|
||||
import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue";
|
||||
import { useCircleStore } from "@/stores/circle";
|
||||
import type { LearningUnit } from "@/types";
|
||||
import { onMounted, reactive } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useLearningPathWithCompletion } from "@/composables";
|
||||
|
||||
log.debug("LearningUnitSelfEvaluationView created");
|
||||
|
||||
|
|
@ -14,32 +13,21 @@ const props = defineProps<{
|
|||
learningUnitSlug: string;
|
||||
}>();
|
||||
|
||||
const circleStore = useCircleStore();
|
||||
|
||||
const state: { learningUnit?: LearningUnit } = reactive({});
|
||||
|
||||
onMounted(async () => {
|
||||
log.debug(
|
||||
"LearningUnitSelfEvaluationView mounted",
|
||||
props.courseSlug,
|
||||
props.circleSlug,
|
||||
props.learningUnitSlug
|
||||
const courseData = useLearningPathWithCompletion(props.courseSlug);
|
||||
const learningUnit = computed(() =>
|
||||
courseData.findLearningUnit(props.learningUnitSlug, props.circleSlug)
|
||||
);
|
||||
|
||||
try {
|
||||
state.learningUnit = await circleStore.loadSelfEvaluation(
|
||||
props.courseSlug,
|
||||
props.circleSlug,
|
||||
props.learningUnitSlug
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
const circle = computed(() => {
|
||||
return courseData.findCircle(props.circleSlug);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelfEvaluation v-if="state.learningUnit" :learning-unit="state.learningUnit" />
|
||||
<SelfEvaluation
|
||||
v-if="learningUnit && circle"
|
||||
:learning-unit="learningUnit"
|
||||
:circle="circle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped></style>
|
||||
|
|
|
|||
|
|
@ -1,177 +0,0 @@
|
|||
import orderBy from "lodash/orderBy";
|
||||
|
||||
import { OldCircle } from "@/services/oldCircle";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import type {
|
||||
Course,
|
||||
CourseCompletion,
|
||||
LearningContentInterface,
|
||||
LearningPathChild,
|
||||
Topic,
|
||||
WagtailLearningPath,
|
||||
} from "@/types";
|
||||
|
||||
// FIXME: remove
|
||||
|
||||
export interface ContinueData {
|
||||
url: string;
|
||||
has_no_progress: boolean;
|
||||
}
|
||||
|
||||
function getLastCompleted(courseSlug: string, completionData: CourseCompletion[]) {
|
||||
if (completionData.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSession = courseSessionsStore.courseSessionForCourse(courseSlug);
|
||||
return orderBy(completionData, ["updated_at"], "desc").find((c: CourseCompletion) => {
|
||||
return (
|
||||
c.completion_status === "SUCCESS" &&
|
||||
c.course_session_id === courseSession?.id &&
|
||||
c.page_type.startsWith("learnpath.LearningContent")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export class LearningPath implements WagtailLearningPath {
|
||||
readonly content_type = "learnpath.LearningPath";
|
||||
public topics: Topic[];
|
||||
public circles: OldCircle[];
|
||||
public nextLearningContent?: LearningContentInterface;
|
||||
|
||||
public static fromJson(
|
||||
json: WagtailLearningPath,
|
||||
completionData: CourseCompletion[],
|
||||
userId: string | undefined
|
||||
): LearningPath {
|
||||
return new LearningPath(
|
||||
json.id,
|
||||
json.slug,
|
||||
json.course.title,
|
||||
json.translation_key,
|
||||
json.frontend_url,
|
||||
json.course,
|
||||
json.children,
|
||||
userId,
|
||||
completionData
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly slug: string,
|
||||
public readonly title: string,
|
||||
public readonly translation_key: string,
|
||||
public readonly frontend_url: string,
|
||||
public readonly course: Course,
|
||||
public children: LearningPathChild[],
|
||||
public userId: string | undefined,
|
||||
completionData?: CourseCompletion[]
|
||||
) {
|
||||
// parse children
|
||||
this.topics = [];
|
||||
this.circles = [];
|
||||
|
||||
let topic: Topic | undefined;
|
||||
|
||||
this.children.forEach((page) => {
|
||||
if (page.content_type === "learnpath.Topic") {
|
||||
if (topic) {
|
||||
this.topics.push(topic);
|
||||
}
|
||||
topic = Object.assign(page, { circles: [] });
|
||||
}
|
||||
if (page.content_type === "learnpath.Circle") {
|
||||
const circle = OldCircle.fromJson(page, this);
|
||||
if (completionData && completionData.length > 0) {
|
||||
circle.parseCompletionData(completionData);
|
||||
}
|
||||
if (topic) {
|
||||
topic.circles.push(circle);
|
||||
}
|
||||
|
||||
circle.previousCircle = this.circles[this.circles.length - 1];
|
||||
if (circle.previousCircle) {
|
||||
circle.previousCircle.nextCircle = circle;
|
||||
}
|
||||
this.circles.push(circle);
|
||||
}
|
||||
});
|
||||
|
||||
if (topic) {
|
||||
this.topics.push(topic);
|
||||
}
|
||||
|
||||
if (completionData) {
|
||||
this.calcNextLearningContent(completionData);
|
||||
}
|
||||
}
|
||||
|
||||
public async reloadCompletionData() {
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const completionData = await learningPathStore.loadCourseSessionCompletionData(
|
||||
this.course.slug,
|
||||
this.userId
|
||||
);
|
||||
for (const circle of this.circles) {
|
||||
circle.parseCompletionData(completionData);
|
||||
}
|
||||
}
|
||||
|
||||
public calcNextLearningContent(completionData: CourseCompletion[]): void {
|
||||
this.nextLearningContent = undefined;
|
||||
|
||||
const lastCompletedLearningContent = getLastCompleted(
|
||||
this.course.slug,
|
||||
completionData
|
||||
);
|
||||
|
||||
if (lastCompletedLearningContent) {
|
||||
const lastCircle = this.circles.find((circle) => {
|
||||
return circle.flatLearningContents.find(
|
||||
(learningContent) =>
|
||||
learningContent.id === lastCompletedLearningContent.page_id
|
||||
);
|
||||
});
|
||||
if (lastCircle) {
|
||||
const lastLearningContent = lastCircle.flatLearningContents.find(
|
||||
(learningContent) =>
|
||||
learningContent.id === lastCompletedLearningContent.page_id
|
||||
);
|
||||
if (lastLearningContent && lastLearningContent.nextLearningContent) {
|
||||
this.nextLearningContent = lastLearningContent.nextLearningContent;
|
||||
} else {
|
||||
if (lastCircle.nextCircle) {
|
||||
this.nextLearningContent = lastCircle.nextCircle.flatLearningContents[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.circles[0]) {
|
||||
this.nextLearningContent = this.circles[0].flatLearningContents[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get continueData(): ContinueData {
|
||||
if (this.nextLearningContent) {
|
||||
const circle = this.nextLearningContent.parentCircle;
|
||||
const url =
|
||||
this.nextLearningContent.parentLearningSequence?.frontend_url ||
|
||||
circle.frontend_url;
|
||||
const isFirst =
|
||||
this.nextLearningContent.translation_key ===
|
||||
this.circles[0].flatLearningContents[0].translation_key;
|
||||
return {
|
||||
url,
|
||||
has_no_progress: isFirst,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
url: "",
|
||||
has_no_progress: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,264 +0,0 @@
|
|||
import type { LearningPath } from "@/services/learningPath";
|
||||
import type {
|
||||
CircleChild,
|
||||
CircleGoal,
|
||||
CircleJobSituation,
|
||||
CourseCompletion,
|
||||
LearningContent,
|
||||
LearningContentInterface,
|
||||
LearningSequence,
|
||||
LearningUnit,
|
||||
LearningUnitPerformanceCriteria,
|
||||
WagtailCircle,
|
||||
} from "@/types";
|
||||
import groupBy from "lodash/groupBy";
|
||||
import partition from "lodash/partition";
|
||||
import values from "lodash/values";
|
||||
import log from "loglevel";
|
||||
|
||||
// FIXME: remove
|
||||
|
||||
function isLearningContentType(object: any): object is LearningContent {
|
||||
return (
|
||||
object?.content_type === "learnpath.LearningContentAssignment" ||
|
||||
object?.content_type === "learnpath.LearningContentAttendanceCourse" ||
|
||||
object?.content_type === "learnpath.LearningContentDocumentList" ||
|
||||
object?.content_type === "learnpath.LearningContentFeedback" ||
|
||||
object?.content_type === "learnpath.LearningContentLearningModule" ||
|
||||
object?.content_type === "learnpath.LearningContentMediaLibrary" ||
|
||||
object?.content_type === "learnpath.LearningContentPlaceholder" ||
|
||||
object?.content_type === "learnpath.LearningContentRichText" ||
|
||||
object?.content_type === "learnpath.LearningContentEdoniqTest" ||
|
||||
object?.content_type === "learnpath.LearningContentVideo"
|
||||
);
|
||||
}
|
||||
|
||||
export function parseLearningSequences(
|
||||
circle: OldCircle,
|
||||
children: CircleChild[]
|
||||
): LearningSequence[] {
|
||||
let learningSequence: LearningSequence | undefined;
|
||||
let learningUnit: LearningUnit | undefined;
|
||||
let learningContent: LearningContent | undefined;
|
||||
let previousLearningContent: LearningContent | undefined;
|
||||
const result: LearningSequence[] = [];
|
||||
|
||||
children.forEach((child) => {
|
||||
if (child.content_type === "learnpath.LearningSequence") {
|
||||
if (learningSequence) {
|
||||
if (learningUnit) {
|
||||
learningUnit.last = true;
|
||||
}
|
||||
}
|
||||
learningSequence = Object.assign(child, { learningUnits: [] });
|
||||
result.push(learningSequence);
|
||||
} else if (child.content_type === "learnpath.LearningUnit") {
|
||||
if (!learningSequence) {
|
||||
throw new Error("LearningUnit found before LearningSequence");
|
||||
}
|
||||
|
||||
learningUnit = Object.assign(child, {
|
||||
learningContents: [],
|
||||
parentLearningSequence: learningSequence,
|
||||
parentCircle: circle,
|
||||
children: child.children.map((c) => {
|
||||
c.parentLearningUnit = learningUnit;
|
||||
c.parentLearningSequence = learningSequence;
|
||||
return c;
|
||||
}),
|
||||
});
|
||||
learningSequence.learningUnits.push(learningUnit);
|
||||
} else if (isLearningContentType(child)) {
|
||||
if (!learningUnit) {
|
||||
throw new Error(`LearningContent found before LearningUnit ${child.slug}`);
|
||||
}
|
||||
previousLearningContent = learningContent;
|
||||
|
||||
learningContent = Object.assign(child, {
|
||||
parentCircle: circle,
|
||||
parentLearningSequence: learningSequence,
|
||||
parentLearningUnit: learningUnit,
|
||||
previousLearningContent: previousLearningContent,
|
||||
});
|
||||
|
||||
if (previousLearningContent) {
|
||||
previousLearningContent.nextLearningContent = learningContent;
|
||||
}
|
||||
|
||||
learningUnit.learningContents.push(child);
|
||||
} else {
|
||||
log.error("Unknown CircleChild found...", child);
|
||||
throw new Error("Unknown CircleChild found...");
|
||||
}
|
||||
});
|
||||
|
||||
if (learningUnit) {
|
||||
learningUnit.last = true;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Finished with LearningContent but there is no LearningSequence and LearningUnit"
|
||||
);
|
||||
}
|
||||
|
||||
// sum minutes
|
||||
result.forEach((learningSequence) => {
|
||||
learningSequence.minutes = 0;
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
learningUnit.minutes = 0;
|
||||
learningUnit.learningContents.forEach((learningContent) => {
|
||||
learningUnit.minutes += learningContent.minutes;
|
||||
});
|
||||
learningSequence.minutes += learningUnit.minutes;
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export class OldCircle implements WagtailCircle {
|
||||
readonly content_type = "learnpath.Circle";
|
||||
readonly learningSequences: LearningSequence[];
|
||||
|
||||
nextCircle?: OldCircle;
|
||||
previousCircle?: OldCircle;
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly slug: string,
|
||||
public readonly title: string,
|
||||
public readonly translation_key: string,
|
||||
public readonly frontend_url: string,
|
||||
public readonly description: string,
|
||||
public readonly children: CircleChild[],
|
||||
public readonly goal_description: string,
|
||||
public readonly goals: CircleGoal[],
|
||||
public readonly job_situation_description: string,
|
||||
public readonly job_situations: CircleJobSituation[],
|
||||
public readonly parentLearningPath?: LearningPath
|
||||
) {
|
||||
this.learningSequences = parseLearningSequences(this, this.children);
|
||||
}
|
||||
|
||||
public static fromJson(
|
||||
wagtailCircle: WagtailCircle,
|
||||
learningPath?: LearningPath
|
||||
): OldCircle {
|
||||
// TODO add error checking when the data does not conform to the schema
|
||||
return new OldCircle(
|
||||
wagtailCircle.id,
|
||||
wagtailCircle.slug,
|
||||
wagtailCircle.title,
|
||||
wagtailCircle.translation_key,
|
||||
wagtailCircle.frontend_url,
|
||||
wagtailCircle.description,
|
||||
wagtailCircle.children,
|
||||
wagtailCircle.goal_description,
|
||||
wagtailCircle.goals,
|
||||
wagtailCircle.job_situation_description,
|
||||
wagtailCircle.job_situations,
|
||||
learningPath
|
||||
);
|
||||
}
|
||||
|
||||
public get flatChildren(): (
|
||||
| LearningContentInterface
|
||||
| LearningUnitPerformanceCriteria
|
||||
)[] {
|
||||
const result: (LearningContentInterface | LearningUnitPerformanceCriteria)[] = [];
|
||||
this.learningSequences.forEach((learningSequence) => {
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
learningUnit.children.forEach((performanceCriteria) => {
|
||||
result.push(performanceCriteria);
|
||||
});
|
||||
learningUnit.learningContents.forEach((learningContent) => {
|
||||
result.push(learningContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public get flatLearningContents(): LearningContent[] {
|
||||
const result: LearningContent[] = [];
|
||||
this.learningSequences.forEach((learningSequence) => {
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
learningUnit.learningContents.forEach((learningContent) => {
|
||||
result.push(learningContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public get flatLearningUnits(): LearningUnit[] {
|
||||
const result: LearningUnit[] = [];
|
||||
this.learningSequences.forEach((learningSequence) => {
|
||||
learningSequence.learningUnits.forEach((learningUnit) => {
|
||||
result.push(learningUnit);
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public someFinishedInLearningSequence(translationKey: string): boolean {
|
||||
if (translationKey) {
|
||||
return (
|
||||
this.flatChildren.filter((lc) => {
|
||||
return (
|
||||
lc.completion_status === "SUCCESS" &&
|
||||
lc.parentLearningSequence?.translation_key === translationKey
|
||||
);
|
||||
}).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public allFinishedInLearningSequence(translationKey: string): boolean {
|
||||
if (translationKey) {
|
||||
const [performanceCriteria, learningContents] = partition(
|
||||
this.flatChildren.filter(
|
||||
(lc) => lc.parentLearningSequence?.translation_key === translationKey
|
||||
),
|
||||
function (child) {
|
||||
return child.content_type === "competence.PerformanceCriteria";
|
||||
}
|
||||
);
|
||||
|
||||
const groupedPerformanceCriteria = values(
|
||||
groupBy(performanceCriteria, (pc) => pc.parentLearningUnit?.id)
|
||||
);
|
||||
|
||||
return (
|
||||
learningContents.every((lc) => lc.completion_status === "SUCCESS") &&
|
||||
(groupedPerformanceCriteria.length === 0 ||
|
||||
groupedPerformanceCriteria.every((group) =>
|
||||
group.every(
|
||||
(pc) =>
|
||||
pc.completion_status === "SUCCESS" || pc.completion_status === "FAIL"
|
||||
)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public parseCompletionData(completionData: CourseCompletion[]) {
|
||||
this.flatChildren.forEach((page) => {
|
||||
const pageIndex = completionData.findIndex((e) => {
|
||||
return e.page_id === page.id;
|
||||
});
|
||||
if (pageIndex >= 0) {
|
||||
page.completion_status = completionData[pageIndex].completion_status;
|
||||
} else {
|
||||
page.completion_status = "UNKNOWN";
|
||||
}
|
||||
});
|
||||
|
||||
if (this.parentLearningPath) {
|
||||
this.parentLearningPath.calcNextLearningContent(completionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,23 @@
|
|||
import type { OldCircle } from "@/services/oldCircle";
|
||||
import { useCompletionStore } from "@/stores/completion";
|
||||
import { useLearningPathStore } from "@/stores/learningPath";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type {
|
||||
CourseCompletionStatus,
|
||||
LearningContentInterface,
|
||||
CircleType,
|
||||
LearningContent,
|
||||
LearningContentWithCompletion,
|
||||
LearningUnit,
|
||||
LearningUnitPerformanceCriteria,
|
||||
PerformanceCriteria,
|
||||
} from "@/types";
|
||||
import * as log from "loglevel";
|
||||
import { defineStore } from "pinia";
|
||||
import type { RouteLocationNormalized } from "vue-router";
|
||||
|
||||
export type CircleStoreState = {
|
||||
circle: OldCircle | undefined;
|
||||
page: "INDEX" | "OVERVIEW";
|
||||
};
|
||||
|
||||
function createLearningUnitHash(learningUnit: LearningUnit | undefined) {
|
||||
const luSlug = learningUnit?.slug;
|
||||
const circleSlug = learningUnit?.parentCircle?.slug;
|
||||
if (luSlug && circleSlug) {
|
||||
return "#" + luSlug.replace(`${circleSlug}-`, "");
|
||||
function createLearningUnitHash(
|
||||
circle: CircleType,
|
||||
learningUnitSlug: string | undefined
|
||||
) {
|
||||
if (learningUnitSlug && circle) {
|
||||
return "#" + learningUnitSlug.replace(`${circle.slug}-`, "");
|
||||
}
|
||||
|
||||
return "";
|
||||
|
|
@ -32,111 +27,30 @@ export const useCircleStore = defineStore({
|
|||
id: "circle",
|
||||
state: () => {
|
||||
return {
|
||||
circle: undefined,
|
||||
page: "INDEX",
|
||||
} as CircleStoreState;
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
async loadCircle(
|
||||
courseSlug: string,
|
||||
circleSlug: string,
|
||||
userId: string | undefined = undefined
|
||||
): Promise<OldCircle> {
|
||||
if (!userId) {
|
||||
const userStore = useUserStore();
|
||||
userId = userStore.id;
|
||||
}
|
||||
|
||||
this.circle = undefined;
|
||||
const learningPathSlug = courseSlug + "-lp";
|
||||
const learningPathStore = useLearningPathStore();
|
||||
const learningPath = await learningPathStore.loadLearningPath(
|
||||
learningPathSlug,
|
||||
userId
|
||||
);
|
||||
if (learningPath) {
|
||||
this.circle = learningPath.circles.find((circle) => {
|
||||
return circle.slug.endsWith(circleSlug);
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.circle) {
|
||||
throw `No circle found with slug: ${circleSlug}`;
|
||||
}
|
||||
|
||||
return this.circle;
|
||||
},
|
||||
async loadLearningContent(
|
||||
courseSlug: string,
|
||||
circleSlug: string,
|
||||
learningContentSlug: string
|
||||
) {
|
||||
const circle = await this.loadCircle(courseSlug, circleSlug);
|
||||
const result = circle.flatLearningContents.find((learningContent) => {
|
||||
return learningContent.slug.endsWith(learningContentSlug);
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw `No learning content found with slug: ${learningContentSlug}`;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
async loadSelfEvaluation(
|
||||
courseSlug: string,
|
||||
circleSlug: string,
|
||||
learningUnitSlug: string
|
||||
) {
|
||||
const circle = await this.loadCircle(courseSlug, circleSlug);
|
||||
const learningUnit = circle.flatLearningUnits.find((child) => {
|
||||
return child.slug.endsWith(learningUnitSlug);
|
||||
});
|
||||
|
||||
if (!learningUnit) {
|
||||
throw `No self evaluation found with slug: ${learningUnitSlug}`;
|
||||
}
|
||||
|
||||
return learningUnit;
|
||||
},
|
||||
async markCompletion(
|
||||
page:
|
||||
| LearningContentInterface
|
||||
| LearningUnitPerformanceCriteria
|
||||
| PerformanceCriteria
|
||||
| undefined,
|
||||
completion_status: CourseCompletionStatus = "SUCCESS"
|
||||
) {
|
||||
const completionStore = useCompletionStore();
|
||||
|
||||
try {
|
||||
if (page) {
|
||||
page.completion_status = completion_status;
|
||||
const completionData = await completionStore.markPage(page);
|
||||
if (this.circle) {
|
||||
this.circle.parseCompletionData(completionData);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
return error;
|
||||
}
|
||||
},
|
||||
openLearningContent(learningContent: LearningContentInterface) {
|
||||
openLearningContent(learningContent: LearningContent) {
|
||||
this.router.push({
|
||||
path: learningContent.frontend_url,
|
||||
});
|
||||
},
|
||||
closeLearningContent(
|
||||
learningContent: LearningContentInterface,
|
||||
learningContent: LearningContentWithCompletion,
|
||||
circle: CircleType,
|
||||
returnRoute?: RouteLocationNormalized
|
||||
) {
|
||||
if (returnRoute) {
|
||||
this.router.push(returnRoute);
|
||||
} else {
|
||||
this.router.push({
|
||||
path: `${this.circle?.frontend_url}`,
|
||||
hash: createLearningUnitHash(learningContent.parentLearningUnit),
|
||||
path: `${circle.frontend_url}`,
|
||||
hash: createLearningUnitHash(
|
||||
circle,
|
||||
learningContent.parentLearningUnit?.slug
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
@ -147,35 +61,37 @@ export const useCircleStore = defineStore({
|
|||
},
|
||||
closeSelfEvaluation(
|
||||
learningUnit: LearningUnit,
|
||||
circle: CircleType,
|
||||
returnRoute?: RouteLocationNormalized
|
||||
) {
|
||||
if (returnRoute) {
|
||||
this.router.push(returnRoute);
|
||||
} else {
|
||||
this.router.push({
|
||||
path: `${this.circle?.frontend_url}`,
|
||||
hash: createLearningUnitHash(learningUnit),
|
||||
path: `${circle.frontend_url}`,
|
||||
hash: createLearningUnitHash(circle, learningUnit.slug),
|
||||
});
|
||||
}
|
||||
},
|
||||
continueFromLearningContent(
|
||||
currentLearningContent: LearningContentInterface,
|
||||
returnRoute?: RouteLocationNormalized
|
||||
currentLearningContent: LearningContentWithCompletion,
|
||||
circle: CircleType,
|
||||
returnRoute?: RouteLocationNormalized,
|
||||
markCompletionFn?: (learningContent: LearningContentWithCompletion) => void
|
||||
) {
|
||||
if (currentLearningContent) {
|
||||
if (currentLearningContent.can_user_self_toggle_course_completion) {
|
||||
this.markCompletion(currentLearningContent, "SUCCESS");
|
||||
} else {
|
||||
// reload completion data anyway
|
||||
currentLearningContent.parentCircle?.parentLearningPath?.reloadCompletionData();
|
||||
if (markCompletionFn) {
|
||||
markCompletionFn(currentLearningContent);
|
||||
}
|
||||
this.closeLearningContent(currentLearningContent, returnRoute);
|
||||
}
|
||||
this.closeLearningContent(currentLearningContent, circle, returnRoute);
|
||||
} else {
|
||||
log.error("currentLearningContent is undefined");
|
||||
}
|
||||
},
|
||||
continueFromSelfEvaluation(learningUnit: LearningUnit) {
|
||||
this.closeSelfEvaluation(learningUnit);
|
||||
continueFromSelfEvaluation(learningUnit: LearningUnit, circle: CircleType) {
|
||||
this.closeSelfEvaluation(learningUnit, circle);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
import { itGetCached } from "@/fetchHelpers";
|
||||
import { LearningPath } from "@/services/learningPath";
|
||||
import { useCompletionStore } from "@/stores/completion";
|
||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { CourseCompletion } from "@/types";
|
||||
import eventBus from "@/utils/eventBus";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import log from "loglevel";
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, reactive } from "vue";
|
||||
|
||||
// FIXME: remove
|
||||
export type LearningPathStoreState = {
|
||||
learningPaths: Map<string, LearningPath>;
|
||||
page: "INDEX" | "OVERVIEW";
|
||||
};
|
||||
|
||||
type LearningPathKey = string;
|
||||
|
||||
function getLearningPathKey(
|
||||
slug: string,
|
||||
userId: string | number | undefined
|
||||
): LearningPathKey {
|
||||
return `${slug}-${userId}`;
|
||||
}
|
||||
|
||||
export const useLearningPathStore = defineStore("learningPath", () => {
|
||||
const state: LearningPathStoreState = reactive({
|
||||
learningPaths: new Map<LearningPathKey, LearningPath>(),
|
||||
page: "INDEX",
|
||||
});
|
||||
|
||||
const learningPathForUser = computed(() => {
|
||||
return (courseSlug: string, userId: string | number | undefined) => {
|
||||
if (state.learningPaths.size > 0) {
|
||||
const learningPathKey = getLearningPathKey(`${courseSlug}-lp`, userId);
|
||||
return state.learningPaths.get(learningPathKey);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
});
|
||||
|
||||
async function loadCourseSessionCompletionData(
|
||||
courseSlug: string,
|
||||
userId: string | undefined = undefined
|
||||
) {
|
||||
// FIXME: should not be here anymore with VBV-305
|
||||
const completionStore = useCompletionStore();
|
||||
|
||||
let completionData: CourseCompletion[] = [];
|
||||
if (userId) {
|
||||
const courseSessionsStore = useCourseSessionsStore();
|
||||
const courseSession = courseSessionsStore.courseSessionForCourse(courseSlug);
|
||||
if (courseSession) {
|
||||
completionData = await completionStore.loadCourseSessionCompletionData(
|
||||
courseSession.id,
|
||||
userId
|
||||
);
|
||||
return completionData;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function loadLearningPath(
|
||||
slug: string,
|
||||
userId: string | undefined = undefined,
|
||||
reload = false,
|
||||
fail = true
|
||||
) {
|
||||
if (!userId) {
|
||||
const userStore = useUserStore();
|
||||
userId = userStore.id;
|
||||
}
|
||||
|
||||
const key = getLearningPathKey(slug, userId);
|
||||
|
||||
if (state.learningPaths.has(key) && !reload) {
|
||||
return state.learningPaths.get(key);
|
||||
}
|
||||
|
||||
const learningPathData = await itGetCached(`/api/course/page/${slug}/`);
|
||||
if (!learningPathData && fail) {
|
||||
throw `No learning path found with: ${slug}`;
|
||||
}
|
||||
|
||||
const completionData = await loadCourseSessionCompletionData(
|
||||
learningPathData.course.slug,
|
||||
userId
|
||||
);
|
||||
|
||||
const learningPath = LearningPath.fromJson(
|
||||
cloneDeep(learningPathData),
|
||||
completionData,
|
||||
userId
|
||||
);
|
||||
state.learningPaths.set(key, learningPath);
|
||||
return learningPath;
|
||||
}
|
||||
|
||||
eventBus.on("switchedCourseSession", (courseSession) => {
|
||||
log.debug("handle switchedCourseSession", courseSession);
|
||||
// FIXME: clean up with VBV-305
|
||||
// Die Completion Daten werden nur für die aktuelle Durchführung geladen.
|
||||
// Deshalb müssen die hier neu geladen werden...
|
||||
state.learningPaths.forEach((lp) => {
|
||||
if (lp.userId) {
|
||||
lp.reloadCompletionData();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
state,
|
||||
learningPathForUser,
|
||||
loadCourseSessionCompletionData,
|
||||
loadLearningPath,
|
||||
};
|
||||
});
|
||||
|
|
@ -104,7 +104,16 @@ export type LearningContent =
|
|||
| LearningContentRichText
|
||||
| LearningContentVideo;
|
||||
|
||||
export type LearningContentWithCompletion = LearningContent & Completable;
|
||||
export type LearningContentWithCompletion = LearningContent &
|
||||
Completable & {
|
||||
continueUrl?: string;
|
||||
firstInCircle?: boolean;
|
||||
parentLearningUnit?: {
|
||||
id: string;
|
||||
slug: string;
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type LearningContentContentType = LearningContent["content_type"];
|
||||
|
||||
|
|
@ -115,6 +124,7 @@ export type LearningUnit = Omit<
|
|||
content_type: "learnpath.LearningUnit";
|
||||
learning_contents: LearningContentWithCompletion[];
|
||||
performance_criteria: LearningUnitPerformanceCriteria[];
|
||||
circle?: CircleLight;
|
||||
};
|
||||
|
||||
export type LearningSequence = Omit<
|
||||
|
|
|
|||
|
|
@ -211,24 +211,21 @@ class LearningUnitObjectType(DjangoObjectType):
|
|||
interfaces = (CoursePageInterface,)
|
||||
fields = ["evaluate_url", "title_hidden"]
|
||||
|
||||
@staticmethod
|
||||
def resolve_evaluate_url(root: LearningUnit, info, **kwargs):
|
||||
return root.get_evaluate_url()
|
||||
def resolve_evaluate_url(self: LearningUnit, info, **kwargs):
|
||||
return self.get_evaluate_url()
|
||||
|
||||
@staticmethod
|
||||
def resolve_performance_criteria(root: LearningUnit, info, **kwargs):
|
||||
return root.performancecriteria_set.all()
|
||||
def resolve_performance_criteria(self: LearningUnit, info, **kwargs):
|
||||
return self.performancecriteria_set.all()
|
||||
|
||||
@staticmethod
|
||||
def resolve_learning_contents(root: LearningUnit, info, **kwargs):
|
||||
def resolve_learning_contents(self: LearningUnit, info, **kwargs):
|
||||
siblings = None
|
||||
if hasattr(info.context, "circle_descendants"):
|
||||
circle_descendants = info.context.circle_descendants
|
||||
index = circle_descendants.index(root)
|
||||
index = circle_descendants.index(self)
|
||||
siblings = circle_descendants[index + 1 :]
|
||||
|
||||
if not siblings:
|
||||
siblings = root.get_siblings().live().specific()
|
||||
siblings = self.get_siblings().live().specific()
|
||||
|
||||
learning_contents = []
|
||||
for sibling in siblings:
|
||||
|
|
@ -286,21 +283,23 @@ class CircleObjectType(DjangoObjectType):
|
|||
"goals",
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def resolve_learning_sequences(root: Circle, info, **kwargs):
|
||||
def resolve_learning_sequences(self: Circle, info, **kwargs):
|
||||
circle_descendants = None
|
||||
|
||||
if hasattr(info.context, "learning_path_descendants"):
|
||||
children = info.context.learning_path_descendants
|
||||
circle_start_index = children.index(root)
|
||||
circle_start_index = children.index(self)
|
||||
next_children = children[circle_start_index + 1 :]
|
||||
next_circle_index = find_first_index(
|
||||
children[circle_start_index + 1 :],
|
||||
next_children,
|
||||
pred=lambda child: child.specific_class == Circle,
|
||||
)
|
||||
circle_descendants = children[circle_start_index + 1 : next_circle_index]
|
||||
circle_descendants = next_children[0:next_circle_index]
|
||||
if circle_descendants[-1].specific_class == Topic:
|
||||
circle_descendants = circle_descendants[:-1]
|
||||
|
||||
if not circle_descendants:
|
||||
circle_descendants = list(root.get_descendants().live().specific())
|
||||
circle_descendants = list(self.get_descendants().live().specific())
|
||||
|
||||
# store flattened descendents to improve performance (no need for db queries)
|
||||
info.context.circle_descendants = list(circle_descendants)
|
||||
|
|
|
|||
Loading…
Reference in New Issue