Calc next learning content
This commit is contained in:
parent
e5cc0aa80e
commit
cc800501c1
|
|
@ -1,6 +1,10 @@
|
||||||
import { graphqlClient } from "@/graphql/client";
|
import { graphqlClient } from "@/graphql/client";
|
||||||
import { COURSE_QUERY, COURSE_SESSION_DETAIL_QUERY } from "@/graphql/queries";
|
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 { useCompletionStore } from "@/stores/completion";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
|
|
@ -16,6 +20,7 @@ import type {
|
||||||
PerformanceCriteria,
|
PerformanceCriteria,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import { useQuery } from "@urql/vue";
|
import { useQuery } from "@urql/vue";
|
||||||
|
import orderBy from "lodash/orderBy";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import type { ComputedRef } from "vue";
|
import type { ComputedRef } from "vue";
|
||||||
import { computed, ref, watchEffect } from "vue";
|
import { computed, ref, watchEffect } from "vue";
|
||||||
|
|
@ -157,25 +162,35 @@ export function useLearningPath(courseSlug: string) {
|
||||||
// attach circle information to learning contents
|
// attach circle information to learning contents
|
||||||
if (learningPath.value) {
|
if (learningPath.value) {
|
||||||
flatCircles(learningPath.value).forEach((circle) => {
|
flatCircles(learningPath.value).forEach((circle) => {
|
||||||
circleFlatChildren(circle).forEach((lc) => {
|
circle.learning_sequences.forEach((ls, lsIndex) => {
|
||||||
lc.circle = {
|
const circleData = {
|
||||||
id: circle.id,
|
id: circle.id,
|
||||||
slug: circle.slug,
|
slug: circle.slug,
|
||||||
title: circle.title,
|
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") {
|
lu.performance_criteria.forEach((luPc) => {
|
||||||
const pc = findPerformanceCriterion(lc.id);
|
luPc.circle = Object.assign({}, circleData);
|
||||||
|
const pc = findPerformanceCriterion(luPc.id);
|
||||||
if (pc) {
|
if (pc) {
|
||||||
pc.circle = {
|
pc.circle = Object.assign({}, circleData);
|
||||||
id: circle.id,
|
|
||||||
slug: circle.slug,
|
|
||||||
title: circle.title,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -186,9 +201,9 @@ export function useLearningPath(courseSlug: string) {
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
function findCircle(slug: string) {
|
function findCircle(idOrSlug: string) {
|
||||||
return (circles.value ?? []).find((c) => {
|
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;
|
}) as PerformanceCriteria | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findLearningContent(learningContentId: string) {
|
function findLearningContent(
|
||||||
return (circles.value ?? [])
|
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) => {
|
.flatMap((c) => {
|
||||||
return circleFlatLearningContents(c);
|
return circleFlatLearningContents(c);
|
||||||
})
|
})
|
||||||
.find((lc) => {
|
.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,
|
circles,
|
||||||
findCircle,
|
findCircle,
|
||||||
findLearningContent,
|
findLearningContent,
|
||||||
|
findLearningUnit,
|
||||||
flatPerformanceCriteria,
|
flatPerformanceCriteria,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +291,7 @@ export function useLearningPathWithCompletion(
|
||||||
|
|
||||||
const courseResult = useLearningPath(courseSlug);
|
const courseResult = useLearningPath(courseSlug);
|
||||||
const completionStore = useCompletionStore();
|
const completionStore = useCompletionStore();
|
||||||
const nextLearningContent = ref<LearningContentWithCompletion | null>(null);
|
const nextLearningContent = ref<LearningContentWithCompletion | undefined>(undefined);
|
||||||
const loaded = ref(false);
|
const loaded = ref(false);
|
||||||
|
|
||||||
function updateCompletionData() {
|
function updateCompletionData() {
|
||||||
|
|
@ -287,11 +332,37 @@ export function useLearningPathWithCompletion(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME calculate nextLearningContent
|
calcNextLearningContent(completionData);
|
||||||
if (courseResult.circles.value?.length) {
|
}
|
||||||
nextLearningContent.value = circleFlatLearningContents(
|
|
||||||
courseResult.circles.value[0]
|
function calcNextLearningContent(completionData: CourseCompletion[]) {
|
||||||
)[0];
|
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,
|
...courseResult,
|
||||||
loaded,
|
loaded,
|
||||||
resultPromise,
|
resultPromise,
|
||||||
updateCompletionData,
|
|
||||||
markCompletion,
|
markCompletion,
|
||||||
nextLearningContent,
|
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 =
|
export const itCheckboxDefaultIconCheckedTailwindClass =
|
||||||
"bg-[url(/static/icons/icon-checkbox-checked.svg)] hover:bg-[url(/static/icons/icon-checkbox-checked-hover.svg)]";
|
"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"
|
:use-mobile-layout="false"
|
||||||
:hide-buttons="true"
|
:hide-buttons="true"
|
||||||
:learning-path="learningPath"
|
:learning-path="learningPath"
|
||||||
|
:next-learning-content="undefined"
|
||||||
:override-circle-url-base="`/course/${props.courseSlug}/cockpit/profile/${props.userId}`"
|
:override-circle-url-base="`/course/${props.courseSlug}/cockpit/profile/${props.userId}`"
|
||||||
></LearningPathPathView>
|
></LearningPathPathView>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCurrentCourseSession } from "@/composables";
|
import { useCurrentCourseSession, useLearningPath } from "@/composables";
|
||||||
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
|
||||||
import { useCockpitStore } from "@/stores/cockpit";
|
import { useCockpitStore } from "@/stores/cockpit";
|
||||||
import ItModal from "@/components/ui/ItModal.vue";
|
import ItModal from "@/components/ui/ItModal.vue";
|
||||||
|
|
@ -16,13 +16,11 @@ import {
|
||||||
} from "@/services/files";
|
} from "@/services/files";
|
||||||
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
import { useCourseSessionsStore } from "@/stores/courseSessions";
|
||||||
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
|
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
|
||||||
import { useCircleStore } from "@/stores/circle";
|
|
||||||
|
|
||||||
const cockpitStore = useCockpitStore();
|
const cockpitStore = useCockpitStore();
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
const circleStore = useCircleStore();
|
|
||||||
|
|
||||||
const courseSessionsStore = useCourseSessionsStore();
|
const courseSessionsStore = useCourseSessionsStore();
|
||||||
|
const courseData = useLearningPath(courseSession.value?.course.slug);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
@ -51,26 +49,18 @@ onMounted(async () => {
|
||||||
await fetchDocuments();
|
await fetchDocuments();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
const dropdownLearningSequences = computed(() => {
|
||||||
// workaround to load learning sequences when circle changes
|
if (cockpitStore.currentCircle?.slug) {
|
||||||
() => cockpitStore.currentCircle,
|
const circle = courseData.findCircle(cockpitStore.currentCircle?.slug);
|
||||||
async () => {
|
if (circle) {
|
||||||
if (cockpitStore.currentCircle) {
|
return circle.learning_sequences.map((sequence) => ({
|
||||||
await circleStore.loadCircle(
|
|
||||||
courseSession.value?.course.slug,
|
|
||||||
cockpitStore.currentCircle?.slug
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const dropdownLearningSequences = computed(() =>
|
|
||||||
circleStore.circle?.learningSequences.map((sequence) => ({
|
|
||||||
id: sequence.id,
|
id: sequence.id,
|
||||||
name: `${sequence.title}`,
|
name: `${sequence.title}`,
|
||||||
}))
|
}));
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
const circleDocuments = computed(() => {
|
const circleDocuments = computed(() => {
|
||||||
return circleDocumentsResultData.value.filter(
|
return circleDocumentsResultData.value.filter(
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ watch(
|
||||||
{{ $t("circlePage.learnMore") }}
|
{{ $t("circlePage.learnMore") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<DocumentSection v-if="!readonly" />
|
<DocumentSection v-if="!readonly" :circle="circle" />
|
||||||
<div v-if="!props.readonly" class="expert mt-8 border p-6">
|
<div v-if="!props.readonly" class="expert mt-8 border p-6">
|
||||||
<h3 class="text-blue-dark">{{ $t("circlePage.gotQuestions") }}</h3>
|
<h3 class="text-blue-dark">{{ $t("circlePage.gotQuestions") }}</h3>
|
||||||
<div class="mt-4 leading-relaxed">
|
<div class="mt-4 leading-relaxed">
|
||||||
|
|
@ -228,6 +228,7 @@ watch(
|
||||||
>
|
>
|
||||||
<LearningSequence
|
<LearningSequence
|
||||||
:course-slug="props.courseSlug"
|
:course-slug="props.courseSlug"
|
||||||
|
:circle="circle"
|
||||||
:learning-sequence="learningSequence"
|
:learning-sequence="learningSequence"
|
||||||
:readonly="props.readonly"
|
:readonly="props.readonly"
|
||||||
></LearningSequence>
|
></LearningSequence>
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,14 @@
|
||||||
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
|
import DocumentListItem from "@/components/circle/DocumentListItem.vue";
|
||||||
import { useCurrentCourseSession } from "@/composables";
|
import { useCurrentCourseSession } from "@/composables";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { useCircleStore } from "@/stores/circle";
|
import type { CircleDocument, CircleType } from "@/types";
|
||||||
import type { CircleDocument } from "@/types";
|
|
||||||
import { fetchCourseSessionDocuments } from "@/services/files";
|
import { fetchCourseSessionDocuments } from "@/services/files";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
circle: CircleType;
|
||||||
|
}>();
|
||||||
|
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
const circleStore = useCircleStore();
|
|
||||||
|
|
||||||
const circleDocumentsResultData = ref<CircleDocument[]>([]);
|
const circleDocumentsResultData = ref<CircleDocument[]>([]);
|
||||||
|
|
||||||
|
|
@ -43,7 +45,7 @@ async function fetchDocuments() {
|
||||||
|
|
||||||
const circleDocuments = computed(() => {
|
const circleDocuments = computed(() => {
|
||||||
return circleDocumentsResultData.value.filter(
|
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 { showIcon } from "@/pages/learningPath/circlePage/learningSequenceUtils";
|
||||||
import { useCircleStore } from "@/stores/circle";
|
import { useCircleStore } from "@/stores/circle";
|
||||||
import type {
|
import type {
|
||||||
|
CircleType,
|
||||||
CourseCompletionStatus,
|
CourseCompletionStatus,
|
||||||
LearningContent,
|
LearningContent,
|
||||||
LearningContentAssignment,
|
LearningContentAssignment,
|
||||||
|
|
@ -10,6 +11,7 @@ import type {
|
||||||
LearningContentWithCompletion,
|
LearningContentWithCompletion,
|
||||||
LearningSequence,
|
LearningSequence,
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
|
import type { Ref } from "vue";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import {
|
import {
|
||||||
itCheckboxDefaultIconCheckedTailwindClass,
|
itCheckboxDefaultIconCheckedTailwindClass,
|
||||||
|
|
@ -19,13 +21,16 @@ import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||||
import {
|
import {
|
||||||
allFinishedInLearningSequence,
|
allFinishedInLearningSequence,
|
||||||
calcSelfEvaluationStatus,
|
calcSelfEvaluationStatus,
|
||||||
|
circleFlatLearningContents,
|
||||||
someFinishedInLearningSequence,
|
someFinishedInLearningSequence,
|
||||||
} from "@/services/circle";
|
} from "@/services/circle";
|
||||||
import { useLearningPathWithCompletion } from "@/composables";
|
import { useLearningPathWithCompletion } from "@/composables";
|
||||||
|
import { findLastIndex } from "lodash";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
courseSlug: string;
|
courseSlug: string;
|
||||||
learningSequence: LearningSequence;
|
learningSequence: LearningSequence;
|
||||||
|
circle: CircleType;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -61,26 +66,27 @@ const allFinished = computed(() => {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const continueTranslationKeyTuple = computed(() => {
|
const continueTranslationKeyTuple: Ref<[string | undefined, boolean]> = computed(() => {
|
||||||
// if (props.learningSequence && circleStore.circle) {
|
if (props.learningSequence) {
|
||||||
// const lastFinished = findLast(
|
const flatLearningContents = circleFlatLearningContents(props.circle);
|
||||||
// circleStore.circle.flatLearningContents,
|
const lastFinishedIndex = findLastIndex(
|
||||||
// (learningContent) => {
|
circleFlatLearningContents(props.circle),
|
||||||
// return learningContent.completion_status === "SUCCESS";
|
(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];
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
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(() => {
|
const learningSequenceBorderClass = computed(() => {
|
||||||
|
|
@ -100,8 +106,8 @@ const learningSequenceBorderClass = computed(() => {
|
||||||
|
|
||||||
function belongsToCompetenceCertificate(lc: LearningContent) {
|
function belongsToCompetenceCertificate(lc: LearningContent) {
|
||||||
return (
|
return (
|
||||||
(lc.__typename === "LearningContentAssignmentObjectType" ||
|
(lc.content_type === "learnpath.LearningContentAssignment" ||
|
||||||
lc.__typename === "LearningContentEdoniqTestObjectType") &&
|
lc.content_type === "learnpath.LearningContentEdoniqTest") &&
|
||||||
lc.competence_certificate?.frontend_url
|
lc.competence_certificate?.frontend_url
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +245,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
learningContent.translation_key === continueTranslationKeyTuple[0] &&
|
learningContent.id === continueTranslationKeyTuple[0] &&
|
||||||
!props.readonly
|
!props.readonly
|
||||||
"
|
"
|
||||||
class="my-4"
|
class="my-4"
|
||||||
|
|
@ -287,7 +293,7 @@ function checkboxIconUncheckedTailwindClass(lc: LearningContent) {
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LearningContentParent from "@/pages/learningPath/learningContentPage/LearningContentParent.vue";
|
import LearningContentParent from "@/pages/learningPath/learningContentPage/LearningContentParent.vue";
|
||||||
import { useCircleStore } from "@/stores/circle";
|
|
||||||
import type { LearningContent } from "@/types";
|
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
import type { Ref } from "vue";
|
import { computed, getCurrentInstance, onUpdated } from "vue";
|
||||||
import { getCurrentInstance, onMounted, onUpdated, ref, watch } from "vue";
|
import { useLearningPathWithCompletion } from "@/composables";
|
||||||
|
import { stringifyParse } from "@/utils/utils";
|
||||||
log.debug("LearningContentView created");
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
courseSlug: string;
|
courseSlug: string;
|
||||||
|
|
@ -14,43 +11,14 @@ const props = defineProps<{
|
||||||
contentSlug: string;
|
contentSlug: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const learningContent: Ref<LearningContent | undefined> = ref(undefined);
|
log.debug("LearningContentView created", stringifyParse(props));
|
||||||
|
|
||||||
const circleStore = useCircleStore();
|
const courseData = useLearningPathWithCompletion(props.courseSlug);
|
||||||
|
const learningContent = computed(() =>
|
||||||
const loadLearningContent = async () => {
|
courseData.findLearningContent(props.contentSlug, props.circleSlug)
|
||||||
try {
|
|
||||||
learningContent.value = await circleStore.loadLearningContent(
|
|
||||||
props.courseSlug,
|
|
||||||
props.circleSlug,
|
|
||||||
props.contentSlug
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
const circle = computed(() => {
|
||||||
log.error(error);
|
return courseData.findCircle(props.circleSlug);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUpdated(() => {
|
onUpdated(() => {
|
||||||
|
|
@ -73,7 +41,11 @@ onUpdated(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LearningContentParent v-if="learningContent" :learning-content="learningContent" />
|
<LearningContentParent
|
||||||
|
v-if="learningContent && circle"
|
||||||
|
:learning-content="learningContent"
|
||||||
|
:circle="circle"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="postcss" scoped></style>
|
<style lang="postcss" scoped></style>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,12 @@
|
||||||
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
||||||
import DocumentListBlock from "@/pages/learningPath/learningContentPage/blocks/DocumentListBlock.vue";
|
import DocumentListBlock from "@/pages/learningPath/learningContentPage/blocks/DocumentListBlock.vue";
|
||||||
import EdoniqTestBlock from "@/pages/learningPath/learningContentPage/blocks/EdoniqTestBlock.vue";
|
import EdoniqTestBlock from "@/pages/learningPath/learningContentPage/blocks/EdoniqTestBlock.vue";
|
||||||
import { useCircleStore } from "@/stores/circle";
|
import type {
|
||||||
import type { LearningContent, LearningContentContentType } from "@/types";
|
CircleType,
|
||||||
|
LearningContent,
|
||||||
|
LearningContentContentType,
|
||||||
|
LearningContentWithCompletion,
|
||||||
|
} from "@/types";
|
||||||
import eventBus from "@/utils/eventBus";
|
import eventBus from "@/utils/eventBus";
|
||||||
import log from "loglevel";
|
import log from "loglevel";
|
||||||
import type { Component } from "vue";
|
import type { Component } from "vue";
|
||||||
|
|
@ -17,14 +21,19 @@ import PlaceholderBlock from "./blocks/PlaceholderBlock.vue";
|
||||||
import RichTextBlock from "./blocks/RichTextBlock.vue";
|
import RichTextBlock from "./blocks/RichTextBlock.vue";
|
||||||
import VideoBlock from "./blocks/VideoBlock.vue";
|
import VideoBlock from "./blocks/VideoBlock.vue";
|
||||||
import { getPreviousRoute } from "@/router/history";
|
import { getPreviousRoute } from "@/router/history";
|
||||||
|
import { stringifyParse } from "@/utils/utils";
|
||||||
const circleStore = useCircleStore();
|
import { useLearningPathWithCompletion } from "@/composables";
|
||||||
|
import { useCircleStore } from "@/stores/circle";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
learningContent: LearningContent;
|
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();
|
const previousRoute = getPreviousRoute();
|
||||||
|
|
||||||
|
|
@ -48,7 +57,14 @@ const component = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleFinishedLearningContent() {
|
function handleFinishedLearningContent() {
|
||||||
circleStore.continueFromLearningContent(props.learningContent, previousRoute);
|
circleStore.continueFromLearningContent(
|
||||||
|
props.learningContent,
|
||||||
|
props.circle,
|
||||||
|
previousRoute,
|
||||||
|
(lc: LearningContentWithCompletion) => {
|
||||||
|
courseCompletionData.markCompletion(lc, "SUCCESS");
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
eventBus.on("finishedLearningContent", handleFinishedLearningContent);
|
eventBus.on("finishedLearningContent", handleFinishedLearningContent);
|
||||||
|
|
@ -60,7 +76,9 @@ onUnmounted(() => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LearningContentContainer
|
<LearningContentContainer
|
||||||
@exit="circleStore.closeLearningContent(props.learningContent, previousRoute)"
|
@exit="
|
||||||
|
circleStore.closeLearningContent(props.learningContent, circle, previousRoute)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<component :is="component" :content="learningContent"></component>
|
<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 ItButton from "@/components/ui/ItButton.vue";
|
||||||
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
import ItCheckbox from "@/components/ui/ItCheckbox.vue";
|
||||||
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
import ItSuccessAlert from "@/components/ui/ItSuccessAlert.vue";
|
||||||
import { useCourseSessionDetailQuery, useCurrentCourseSession } from "@/composables";
|
import {
|
||||||
|
useCourseSessionDetailQuery,
|
||||||
|
useCurrentCourseSession,
|
||||||
|
useLearningPath,
|
||||||
|
} from "@/composables";
|
||||||
import { bustItGetCache } from "@/fetchHelpers";
|
import { bustItGetCache } from "@/fetchHelpers";
|
||||||
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
import { UPSERT_ASSIGNMENT_COMPLETION_MUTATION } from "@/graphql/mutations";
|
||||||
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
import AssignmentSubmissionResponses from "@/pages/learningPath/learningContentPage/assignment/AssignmentSubmissionResponses.vue";
|
||||||
|
|
@ -15,7 +19,6 @@ import { computed, reactive } from "vue";
|
||||||
import { useTranslation } from "i18next-vue";
|
import { useTranslation } from "i18next-vue";
|
||||||
import eventBus from "@/utils/eventBus";
|
import eventBus from "@/utils/eventBus";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useCircleStore } from "@/stores/circle";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
assignment: Assignment;
|
assignment: Assignment;
|
||||||
|
|
@ -31,7 +34,7 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
const courseSessionDetailResult = useCourseSessionDetailQuery();
|
||||||
const circleStore = useCircleStore();
|
const courseData = useLearningPath(courseSession.value.course.slug);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
@ -40,9 +43,15 @@ const state = reactive({
|
||||||
confirmPerson: false,
|
confirmPerson: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const learningContent = computed(() => {
|
||||||
|
return courseData.findLearningContent(props.learningContentId);
|
||||||
|
});
|
||||||
|
|
||||||
const circleExperts = computed(() => {
|
const circleExperts = computed(() => {
|
||||||
if (circleStore.circle) {
|
if (learningContent.value?.circle) {
|
||||||
return courseSessionDetailResult.filterCircleExperts(circleStore.circle.slug);
|
return courseSessionDetailResult.filterCircleExperts(
|
||||||
|
learningContent.value?.circle.slug
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ const currentTask = computed(() => {
|
||||||
const initUpsertAssignmentCompletion = async () => {
|
const initUpsertAssignmentCompletion = async () => {
|
||||||
try {
|
try {
|
||||||
await upsertAssignmentCompletionMutation.executeMutation({
|
await upsertAssignmentCompletionMutation.executeMutation({
|
||||||
assignmentId: props.learningContent.content_assignment_id,
|
assignmentId: props.learningContent.content_assignment.id,
|
||||||
courseSessionId: courseSession.value.id,
|
courseSessionId: courseSession.value.id,
|
||||||
learningContentId: props.learningContent.id,
|
learningContentId: props.learningContent.id,
|
||||||
completionDataString: JSON.stringify({}),
|
completionDataString: JSON.stringify({}),
|
||||||
|
|
@ -234,7 +234,7 @@ const assignmentUser = computed(() => {
|
||||||
<AssignmentTaskView
|
<AssignmentTaskView
|
||||||
v-else-if="currentTask"
|
v-else-if="currentTask"
|
||||||
:task="currentTask"
|
:task="currentTask"
|
||||||
:assignment-id="props.learningContent.content_assignment_id"
|
:assignment-id="props.learningContent.content_assignment.id"
|
||||||
:assignment-completion="assignmentCompletion"
|
:assignment-completion="assignmentCompletion"
|
||||||
:learning-content-id="props.learningContent.id"
|
:learning-content-id="props.learningContent.id"
|
||||||
></AssignmentTaskView>
|
></AssignmentTaskView>
|
||||||
|
|
|
||||||
|
|
@ -2,34 +2,44 @@
|
||||||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||||
import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue";
|
import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue";
|
||||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||||
import type { OldCircle } from "@/services/oldCircle";
|
import type { CircleType, LearningContentWithCompletion } from "@/types";
|
||||||
import type { LearningPath } from "@/services/learningPath";
|
import { computed, ref, watch } from "vue";
|
||||||
import type { Topic } from "@/types";
|
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
learningPath: LearningPath | undefined;
|
circle: CircleType;
|
||||||
circle: OldCircle;
|
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||||
topic: Topic;
|
|
||||||
isFirstCircle: boolean;
|
isFirstCircle: boolean;
|
||||||
isLastCircle: boolean;
|
isLastCircle: boolean;
|
||||||
isCurrentCircle: boolean;
|
|
||||||
overrideCircleUrl?: string;
|
overrideCircleUrl?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const circleElement = ref<HTMLElement | null>(null);
|
const circleElement = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
onMounted(() => {
|
const isCurrentCircle = computed(() => {
|
||||||
if (props.isCurrentCircle) {
|
return props.nextLearningContent?.circle?.id === props.circle.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
function scrollToCircle() {
|
||||||
|
if (isCurrentCircle.value) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
circleElement?.value?.scrollIntoView({
|
circleElement?.value?.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
inline: "center",
|
inline: "center",
|
||||||
block: "nearest",
|
block: "nearest",
|
||||||
});
|
});
|
||||||
}, 400);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isCurrentCircle.value,
|
||||||
|
(isCurrent) => {
|
||||||
|
if (isCurrent) {
|
||||||
|
scrollToCircle();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -55,10 +65,9 @@ onMounted(() => {
|
||||||
{{ props.circle.title }}
|
{{ props.circle.title }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="props.isCurrentCircle" class="whitespace-nowrap">
|
<div v-if="isCurrentCircle" class="whitespace-nowrap">
|
||||||
<LearningPathContinueButton
|
<LearningPathContinueButton
|
||||||
:has-progress="!props.learningPath?.continueData?.has_no_progress"
|
:next-learning-content="props.nextLearningContent"
|
||||||
:url="props.learningPath?.continueData?.url"
|
|
||||||
></LearningPathContinueButton>
|
></LearningPathContinueButton>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,35 @@
|
||||||
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
import LearningPathCircle from "@/pages/learningPath/learningPathPage/LearningPathCircle.vue";
|
||||||
import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue";
|
import LearningPathContinueButton from "@/pages/learningPath/learningPathPage/LearningPathContinueButton.vue";
|
||||||
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
import { calculateCircleSectorData } from "@/pages/learningPath/learningPathPage/utils";
|
||||||
import type { OldCircle } from "@/services/oldCircle";
|
import { computed, ref, watch } from "vue";
|
||||||
import type { LearningPath } from "@/services/learningPath";
|
import type { CircleType, LearningContentWithCompletion } from "@/types";
|
||||||
import { onMounted, ref } from "vue";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
learningPath: LearningPath | undefined;
|
circle: CircleType;
|
||||||
circle: OldCircle;
|
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||||
isCurrentCircle: boolean;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const circleElement = ref<HTMLElement | null>(null);
|
const circleElement = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
onMounted(() => {
|
const isCurrentCircle = computed(() => {
|
||||||
if (props.isCurrentCircle) {
|
return props.nextLearningContent?.circle?.id === props.circle.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isCurrentCircle.value,
|
||||||
|
(isCurrent) => {
|
||||||
|
if (isCurrent) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
circleElement?.value?.scrollIntoView({
|
circleElement?.value?.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
inline: "nearest",
|
inline: "center",
|
||||||
block: "center",
|
block: "nearest",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}, 400);
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -43,8 +49,7 @@ onMounted(() => {
|
||||||
|
|
||||||
<div v-if="isCurrentCircle" class="whitespace-nowrap pl-4">
|
<div v-if="isCurrentCircle" class="whitespace-nowrap pl-4">
|
||||||
<LearningPathContinueButton
|
<LearningPathContinueButton
|
||||||
:has-progress="!props.learningPath?.continueData?.has_no_progress"
|
:next-learning-content="props.nextLearningContent"
|
||||||
:url="props.learningPath?.continueData?.url"
|
|
||||||
></LearningPathContinueButton>
|
></LearningPathContinueButton>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
interface Props {
|
import type { LearningContentWithCompletion } from "@/types";
|
||||||
hasProgress?: boolean;
|
|
||||||
url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = defineProps<{
|
||||||
hasProgress: false,
|
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||||
url: "",
|
}>();
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-link
|
<router-link
|
||||||
class="btn-blue mt-2 pl-6"
|
class="btn-blue mt-2 pl-6"
|
||||||
:to="props.url"
|
:to="nextLearningContent?.continueUrl ?? '/hello'"
|
||||||
data-cy="lp-continue-button"
|
data-cy="lp-continue-button"
|
||||||
translate
|
translate
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ props.hasProgress ? $t("general.nextStep") : $t("general.start") }}
|
{{
|
||||||
|
props.nextLearningContent?.firstInCircle
|
||||||
|
? $t("general.start")
|
||||||
|
: $t("general.nextStep")
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
|
import LearningPathCircleListTile from "@/pages/learningPath/learningPathPage/LearningPathCircleListTile.vue";
|
||||||
import type { OldCircle } from "@/services/oldCircle";
|
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import type { LearningPathType } from "@/types";
|
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
learningPath: LearningPathType | undefined;
|
learningPath: LearningPathType | undefined;
|
||||||
|
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const topics = computed(() => props.learningPath?.topics ?? []);
|
const topics = computed(() => props.learningPath?.topics ?? []);
|
||||||
|
|
||||||
const isCurrentCircle = (circle: OldCircle) =>
|
|
||||||
props.learningPath?.nextLearningContent?.parentCircle === circle;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -22,9 +19,8 @@ const isCurrentCircle = (circle: OldCircle) =>
|
||||||
<LearningPathCircleListTile
|
<LearningPathCircleListTile
|
||||||
v-for="circle in topic.circles"
|
v-for="circle in topic.circles"
|
||||||
:key="circle.id"
|
:key="circle.id"
|
||||||
:learning-path="learningPath"
|
|
||||||
:circle="circle"
|
:circle="circle"
|
||||||
:is-current-circle="isCurrentCircle(circle)"
|
:next-learning-content="props.nextLearningContent"
|
||||||
></LearningPathCircleListTile>
|
></LearningPathCircleListTile>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -80,13 +80,16 @@ const changeViewType = (viewType: ViewType) => {
|
||||||
|
|
||||||
<!-- Bottom -->
|
<!-- Bottom -->
|
||||||
<div class="bg-white">
|
<div class="bg-white">
|
||||||
|
<div v-if="lpQueryResult.learningPath">
|
||||||
<div class="flex flex-col justify-between px-6 sm:flex-row sm:px-12">
|
<div class="flex flex-col justify-between px-6 sm:flex-row sm:px-12">
|
||||||
<!-- Topics -->
|
<!-- Topics -->
|
||||||
<div
|
<div
|
||||||
v-if="selectedView == 'path'"
|
v-if="selectedView == 'path'"
|
||||||
class="order-2 pb-8 sm:order-1 sm:pb-0 sm:pt-4"
|
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>
|
||||||
<div v-else class="flex-grow"></div>
|
<div v-else class="flex-grow"></div>
|
||||||
|
|
||||||
|
|
@ -103,6 +106,7 @@ const changeViewType = (viewType: ViewType) => {
|
||||||
<LearningPathPathView
|
<LearningPathPathView
|
||||||
:learning-path="learningPath"
|
:learning-path="learningPath"
|
||||||
:use-mobile-layout="useMobileLayout"
|
:use-mobile-layout="useMobileLayout"
|
||||||
|
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
||||||
></LearningPathPathView>
|
></LearningPathPathView>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -112,14 +116,16 @@ const changeViewType = (viewType: ViewType) => {
|
||||||
class="flex flex-col pl-6 sm:pl-24"
|
class="flex flex-col pl-6 sm:pl-24"
|
||||||
data-cy="lp-list-view"
|
data-cy="lp-list-view"
|
||||||
>
|
>
|
||||||
<LearningPathListView :learning-path="learningPath"></LearningPathListView>
|
<LearningPathListView
|
||||||
|
:learning-path="learningPath"
|
||||||
|
:next-learning-content="lpQueryResult.nextLearningContent.value"
|
||||||
|
></LearningPathListView>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="useMobileLayout"
|
v-if="useMobileLayout"
|
||||||
class="p-6"
|
class="p-6"
|
||||||
:class="useMobileLayout ? 'bg-gray-200' : ''"
|
:class="useMobileLayout ? 'bg-gray-200' : ''"
|
||||||
>
|
></div>
|
||||||
<!--<LearningPathAppointmentsMock></LearningPathAppointmentsMock>-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import LearningPathCircleColumn from "@/pages/learningPath/learningPathPage/LearningPathCircleColumn.vue";
|
import LearningPathCircleColumn from "@/pages/learningPath/learningPathPage/LearningPathCircleColumn.vue";
|
||||||
import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue";
|
import LearningPathScrollButton from "@/pages/learningPath/learningPathPage/LearningPathScrollButton.vue";
|
||||||
import type { OldCircle } from "@/services/oldCircle";
|
|
||||||
import { useScroll } from "@vueuse/core";
|
import { useScroll } from "@vueuse/core";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import type { LearningPathType } from "@/types";
|
import type { LearningContentWithCompletion, LearningPathType } from "@/types";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
learningPath: LearningPathType | undefined;
|
learningPath: LearningPathType | undefined;
|
||||||
|
nextLearningContent: LearningContentWithCompletion | undefined;
|
||||||
useMobileLayout: boolean;
|
useMobileLayout: boolean;
|
||||||
hideButtons?: boolean;
|
hideButtons?: boolean;
|
||||||
overrideCircleUrlBase?: string;
|
overrideCircleUrlBase?: string;
|
||||||
|
|
@ -25,9 +25,6 @@ const isLastCircle = (topicIndex: number, circleIndex: number, numCircles: numbe
|
||||||
topicIndex === (props.learningPath?.topics ?? []).length - 1 &&
|
topicIndex === (props.learningPath?.topics ?? []).length - 1 &&
|
||||||
circleIndex === numCircles - 1;
|
circleIndex === numCircles - 1;
|
||||||
|
|
||||||
const isCurrentCircle = (circle: OldCircle) =>
|
|
||||||
props.learningPath?.nextLearningContent?.parentCircle === circle;
|
|
||||||
|
|
||||||
const scrollRight = () => scrollLearnPathDiagram(scrollIncrement);
|
const scrollRight = () => scrollLearnPathDiagram(scrollIncrement);
|
||||||
|
|
||||||
const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
|
const scrollLeft = () => scrollLearnPathDiagram(-scrollIncrement);
|
||||||
|
|
@ -65,14 +62,12 @@ const scrollLearnPathDiagram = (offset: number) => {
|
||||||
<LearningPathCircleColumn
|
<LearningPathCircleColumn
|
||||||
v-for="(circle, circleIndex) in topic.circles"
|
v-for="(circle, circleIndex) in topic.circles"
|
||||||
:key="circle.id"
|
:key="circle.id"
|
||||||
:learning-path="learningPath"
|
|
||||||
:circle="circle"
|
:circle="circle"
|
||||||
:topic="topic"
|
:next-learning-content="props.nextLearningContent"
|
||||||
:is-first-circle="isFirstCircle(topicIndex, circleIndex)"
|
:is-first-circle="isFirstCircle(topicIndex, circleIndex)"
|
||||||
:is-last-circle="
|
:is-last-circle="
|
||||||
isLastCircle(topicIndex, circleIndex, topic.circles.length)
|
isLastCircle(topicIndex, circleIndex, topic.circles.length)
|
||||||
"
|
"
|
||||||
:is-current-circle="isCurrentCircle(circle) && !props.hideButtons"
|
|
||||||
:override-circle-url="
|
:override-circle-url="
|
||||||
props.overrideCircleUrlBase
|
props.overrideCircleUrlBase
|
||||||
? `${props.overrideCircleUrlBase}/${circle.slug}`
|
? `${props.overrideCircleUrlBase}/${circle.slug}`
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Topic } from "@/types";
|
import type { TopicType } from "@/types";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
topics: Topic[];
|
topics: TopicType[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const scrollToTopic = (topic: Topic) => {
|
const scrollToTopic = (topic: TopicType) => {
|
||||||
const id = `topic-${topic.slug}`;
|
const id = `topic-${topic.slug}`;
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
el?.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" });
|
el?.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" });
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCircleStore } from "@/stores/circle";
|
import { useCircleStore } from "@/stores/circle";
|
||||||
import type { LearningUnit } from "@/types";
|
import type { CircleType, LearningUnit } from "@/types";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
|
|
||||||
import { useCurrentCourseSession } from "@/composables";
|
import { useCurrentCourseSession, useLearningPathWithCompletion } from "@/composables";
|
||||||
import { COMPLETION_FAILURE, COMPLETION_SUCCESS } from "@/constants";
|
|
||||||
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
import LearningContentContainer from "@/pages/learningPath/learningContentPage/LearningContentContainer.vue";
|
||||||
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
import LearningContentMultiLayout from "@/pages/learningPath/learningContentPage/layouts/LearningContentMultiLayout.vue";
|
||||||
import eventBus from "@/utils/eventBus";
|
import eventBus from "@/utils/eventBus";
|
||||||
|
|
@ -17,16 +16,17 @@ log.debug("LearningContent.vue setup");
|
||||||
|
|
||||||
const circleStore = useCircleStore();
|
const circleStore = useCircleStore();
|
||||||
const courseSession = useCurrentCourseSession();
|
const courseSession = useCurrentCourseSession();
|
||||||
|
const courseCompletionData = useLearningPathWithCompletion();
|
||||||
|
|
||||||
const questionIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
const questionIndex = useRouteQuery("step", "0", { transform: Number, mode: "push" });
|
||||||
|
|
||||||
const previousRoute = getPreviousRoute();
|
const previousRoute = getPreviousRoute();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
learningUnit: LearningUnit;
|
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 currentQuestion = computed(() => questions.value[questionIndex.value]);
|
||||||
const showPreviousButton = computed(() => questionIndex.value != 0);
|
const showPreviousButton = computed(() => questionIndex.value != 0);
|
||||||
const showNextButton = computed(
|
const showNextButton = computed(
|
||||||
|
|
@ -44,7 +44,7 @@ function handleContinue() {
|
||||||
questionIndex.value += 1;
|
questionIndex.value += 1;
|
||||||
} else {
|
} else {
|
||||||
log.debug("continue to next learning content");
|
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() {
|
function handleFinishedLearningContent() {
|
||||||
circleStore.closeSelfEvaluation(props.learningUnit, previousRoute);
|
circleStore.closeSelfEvaluation(props.learningUnit, props.circle, previousRoute);
|
||||||
}
|
}
|
||||||
|
|
||||||
eventBus.on("finishedLearningContent", handleFinishedLearningContent);
|
eventBus.on("finishedLearningContent", handleFinishedLearningContent);
|
||||||
|
|
@ -69,7 +69,9 @@ onUnmounted(() => {
|
||||||
<template>
|
<template>
|
||||||
<div v-if="learningUnit">
|
<div v-if="learningUnit">
|
||||||
<LearningContentContainer
|
<LearningContentContainer
|
||||||
@exit="circleStore.closeSelfEvaluation(props.learningUnit, previousRoute)"
|
@exit="
|
||||||
|
circleStore.closeSelfEvaluation(props.learningUnit, props.circle, previousRoute)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<LearningContentMultiLayout
|
<LearningContentMultiLayout
|
||||||
:current-step="questionIndex"
|
:current-step="questionIndex"
|
||||||
|
|
@ -101,7 +103,7 @@ onUnmounted(() => {
|
||||||
'border-2': currentQuestion.completion_status === 'SUCCESS',
|
'border-2': currentQuestion.completion_status === 'SUCCESS',
|
||||||
}"
|
}"
|
||||||
data-cy="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>
|
<it-icon-smiley-happy class="mr-4 h-16 w-16"></it-icon-smiley-happy>
|
||||||
<span class="text-large font-bold">
|
<span class="text-large font-bold">
|
||||||
|
|
@ -111,12 +113,11 @@ onUnmounted(() => {
|
||||||
<button
|
<button
|
||||||
class="inline-flex flex-1 items-center border p-4 text-left"
|
class="inline-flex flex-1 items-center border p-4 text-left"
|
||||||
:class="{
|
:class="{
|
||||||
'border-orange-500':
|
'border-orange-500': currentQuestion.completion_status === 'FAIL',
|
||||||
currentQuestion.completion_status === COMPLETION_FAILURE,
|
'border-2': currentQuestion.completion_status === 'FAIL',
|
||||||
'border-2': currentQuestion.completion_status === COMPLETION_FAILURE,
|
|
||||||
}"
|
}"
|
||||||
data-cy="fail"
|
data-cy="fail"
|
||||||
@click="circleStore.markCompletion(currentQuestion, 'FAIL')"
|
@click="courseCompletionData.markCompletion(currentQuestion, 'FAIL')"
|
||||||
>
|
>
|
||||||
<it-icon-smiley-thinking
|
<it-icon-smiley-thinking
|
||||||
class="mr-4 h-16 w-16"
|
class="mr-4 h-16 w-16"
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
|
|
||||||
import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue";
|
import SelfEvaluation from "@/pages/learningPath/selfEvaluationPage/SelfEvaluation.vue";
|
||||||
import { useCircleStore } from "@/stores/circle";
|
import { computed } from "vue";
|
||||||
import type { LearningUnit } from "@/types";
|
import { useLearningPathWithCompletion } from "@/composables";
|
||||||
import { onMounted, reactive } from "vue";
|
|
||||||
|
|
||||||
log.debug("LearningUnitSelfEvaluationView created");
|
log.debug("LearningUnitSelfEvaluationView created");
|
||||||
|
|
||||||
|
|
@ -14,32 +13,21 @@ const props = defineProps<{
|
||||||
learningUnitSlug: string;
|
learningUnitSlug: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const circleStore = useCircleStore();
|
const courseData = useLearningPathWithCompletion(props.courseSlug);
|
||||||
|
const learningUnit = computed(() =>
|
||||||
const state: { learningUnit?: LearningUnit } = reactive({});
|
courseData.findLearningUnit(props.learningUnitSlug, props.circleSlug)
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
log.debug(
|
|
||||||
"LearningUnitSelfEvaluationView mounted",
|
|
||||||
props.courseSlug,
|
|
||||||
props.circleSlug,
|
|
||||||
props.learningUnitSlug
|
|
||||||
);
|
);
|
||||||
|
const circle = computed(() => {
|
||||||
try {
|
return courseData.findCircle(props.circleSlug);
|
||||||
state.learningUnit = await circleStore.loadSelfEvaluation(
|
|
||||||
props.courseSlug,
|
|
||||||
props.circleSlug,
|
|
||||||
props.learningUnitSlug
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
log.error(error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SelfEvaluation v-if="state.learningUnit" :learning-unit="state.learningUnit" />
|
<SelfEvaluation
|
||||||
|
v-if="learningUnit && circle"
|
||||||
|
:learning-unit="learningUnit"
|
||||||
|
:circle="circle"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="postcss" scoped></style>
|
<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 {
|
import type {
|
||||||
CourseCompletionStatus,
|
CircleType,
|
||||||
LearningContentInterface,
|
LearningContent,
|
||||||
|
LearningContentWithCompletion,
|
||||||
LearningUnit,
|
LearningUnit,
|
||||||
LearningUnitPerformanceCriteria,
|
|
||||||
PerformanceCriteria,
|
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import * as log from "loglevel";
|
import * as log from "loglevel";
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import type { RouteLocationNormalized } from "vue-router";
|
import type { RouteLocationNormalized } from "vue-router";
|
||||||
|
|
||||||
export type CircleStoreState = {
|
export type CircleStoreState = {
|
||||||
circle: OldCircle | undefined;
|
|
||||||
page: "INDEX" | "OVERVIEW";
|
page: "INDEX" | "OVERVIEW";
|
||||||
};
|
};
|
||||||
|
|
||||||
function createLearningUnitHash(learningUnit: LearningUnit | undefined) {
|
function createLearningUnitHash(
|
||||||
const luSlug = learningUnit?.slug;
|
circle: CircleType,
|
||||||
const circleSlug = learningUnit?.parentCircle?.slug;
|
learningUnitSlug: string | undefined
|
||||||
if (luSlug && circleSlug) {
|
) {
|
||||||
return "#" + luSlug.replace(`${circleSlug}-`, "");
|
if (learningUnitSlug && circle) {
|
||||||
|
return "#" + learningUnitSlug.replace(`${circle.slug}-`, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
|
|
@ -32,111 +27,30 @@ export const useCircleStore = defineStore({
|
||||||
id: "circle",
|
id: "circle",
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
circle: undefined,
|
|
||||||
page: "INDEX",
|
page: "INDEX",
|
||||||
} as CircleStoreState;
|
} as CircleStoreState;
|
||||||
},
|
},
|
||||||
getters: {},
|
getters: {},
|
||||||
actions: {
|
actions: {
|
||||||
async loadCircle(
|
openLearningContent(learningContent: LearningContent) {
|
||||||
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) {
|
|
||||||
this.router.push({
|
this.router.push({
|
||||||
path: learningContent.frontend_url,
|
path: learningContent.frontend_url,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
closeLearningContent(
|
closeLearningContent(
|
||||||
learningContent: LearningContentInterface,
|
learningContent: LearningContentWithCompletion,
|
||||||
|
circle: CircleType,
|
||||||
returnRoute?: RouteLocationNormalized
|
returnRoute?: RouteLocationNormalized
|
||||||
) {
|
) {
|
||||||
if (returnRoute) {
|
if (returnRoute) {
|
||||||
this.router.push(returnRoute);
|
this.router.push(returnRoute);
|
||||||
} else {
|
} else {
|
||||||
this.router.push({
|
this.router.push({
|
||||||
path: `${this.circle?.frontend_url}`,
|
path: `${circle.frontend_url}`,
|
||||||
hash: createLearningUnitHash(learningContent.parentLearningUnit),
|
hash: createLearningUnitHash(
|
||||||
|
circle,
|
||||||
|
learningContent.parentLearningUnit?.slug
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -147,35 +61,37 @@ export const useCircleStore = defineStore({
|
||||||
},
|
},
|
||||||
closeSelfEvaluation(
|
closeSelfEvaluation(
|
||||||
learningUnit: LearningUnit,
|
learningUnit: LearningUnit,
|
||||||
|
circle: CircleType,
|
||||||
returnRoute?: RouteLocationNormalized
|
returnRoute?: RouteLocationNormalized
|
||||||
) {
|
) {
|
||||||
if (returnRoute) {
|
if (returnRoute) {
|
||||||
this.router.push(returnRoute);
|
this.router.push(returnRoute);
|
||||||
} else {
|
} else {
|
||||||
this.router.push({
|
this.router.push({
|
||||||
path: `${this.circle?.frontend_url}`,
|
path: `${circle.frontend_url}`,
|
||||||
hash: createLearningUnitHash(learningUnit),
|
hash: createLearningUnitHash(circle, learningUnit.slug),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
continueFromLearningContent(
|
continueFromLearningContent(
|
||||||
currentLearningContent: LearningContentInterface,
|
currentLearningContent: LearningContentWithCompletion,
|
||||||
returnRoute?: RouteLocationNormalized
|
circle: CircleType,
|
||||||
|
returnRoute?: RouteLocationNormalized,
|
||||||
|
markCompletionFn?: (learningContent: LearningContentWithCompletion) => void
|
||||||
) {
|
) {
|
||||||
if (currentLearningContent) {
|
if (currentLearningContent) {
|
||||||
if (currentLearningContent.can_user_self_toggle_course_completion) {
|
if (currentLearningContent.can_user_self_toggle_course_completion) {
|
||||||
this.markCompletion(currentLearningContent, "SUCCESS");
|
if (markCompletionFn) {
|
||||||
} else {
|
markCompletionFn(currentLearningContent);
|
||||||
// reload completion data anyway
|
|
||||||
currentLearningContent.parentCircle?.parentLearningPath?.reloadCompletionData();
|
|
||||||
}
|
}
|
||||||
this.closeLearningContent(currentLearningContent, returnRoute);
|
}
|
||||||
|
this.closeLearningContent(currentLearningContent, circle, returnRoute);
|
||||||
} else {
|
} else {
|
||||||
log.error("currentLearningContent is undefined");
|
log.error("currentLearningContent is undefined");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
continueFromSelfEvaluation(learningUnit: LearningUnit) {
|
continueFromSelfEvaluation(learningUnit: LearningUnit, circle: CircleType) {
|
||||||
this.closeSelfEvaluation(learningUnit);
|
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
|
| LearningContentRichText
|
||||||
| LearningContentVideo;
|
| 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"];
|
export type LearningContentContentType = LearningContent["content_type"];
|
||||||
|
|
||||||
|
|
@ -115,6 +124,7 @@ export type LearningUnit = Omit<
|
||||||
content_type: "learnpath.LearningUnit";
|
content_type: "learnpath.LearningUnit";
|
||||||
learning_contents: LearningContentWithCompletion[];
|
learning_contents: LearningContentWithCompletion[];
|
||||||
performance_criteria: LearningUnitPerformanceCriteria[];
|
performance_criteria: LearningUnitPerformanceCriteria[];
|
||||||
|
circle?: CircleLight;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LearningSequence = Omit<
|
export type LearningSequence = Omit<
|
||||||
|
|
|
||||||
|
|
@ -211,24 +211,21 @@ class LearningUnitObjectType(DjangoObjectType):
|
||||||
interfaces = (CoursePageInterface,)
|
interfaces = (CoursePageInterface,)
|
||||||
fields = ["evaluate_url", "title_hidden"]
|
fields = ["evaluate_url", "title_hidden"]
|
||||||
|
|
||||||
@staticmethod
|
def resolve_evaluate_url(self: LearningUnit, info, **kwargs):
|
||||||
def resolve_evaluate_url(root: LearningUnit, info, **kwargs):
|
return self.get_evaluate_url()
|
||||||
return root.get_evaluate_url()
|
|
||||||
|
|
||||||
@staticmethod
|
def resolve_performance_criteria(self: LearningUnit, info, **kwargs):
|
||||||
def resolve_performance_criteria(root: LearningUnit, info, **kwargs):
|
return self.performancecriteria_set.all()
|
||||||
return root.performancecriteria_set.all()
|
|
||||||
|
|
||||||
@staticmethod
|
def resolve_learning_contents(self: LearningUnit, info, **kwargs):
|
||||||
def resolve_learning_contents(root: LearningUnit, info, **kwargs):
|
|
||||||
siblings = None
|
siblings = None
|
||||||
if hasattr(info.context, "circle_descendants"):
|
if hasattr(info.context, "circle_descendants"):
|
||||||
circle_descendants = 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 :]
|
siblings = circle_descendants[index + 1 :]
|
||||||
|
|
||||||
if not siblings:
|
if not siblings:
|
||||||
siblings = root.get_siblings().live().specific()
|
siblings = self.get_siblings().live().specific()
|
||||||
|
|
||||||
learning_contents = []
|
learning_contents = []
|
||||||
for sibling in siblings:
|
for sibling in siblings:
|
||||||
|
|
@ -286,21 +283,23 @@ class CircleObjectType(DjangoObjectType):
|
||||||
"goals",
|
"goals",
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
def resolve_learning_sequences(self: Circle, info, **kwargs):
|
||||||
def resolve_learning_sequences(root: Circle, info, **kwargs):
|
|
||||||
circle_descendants = None
|
circle_descendants = None
|
||||||
|
|
||||||
if hasattr(info.context, "learning_path_descendants"):
|
if hasattr(info.context, "learning_path_descendants"):
|
||||||
children = 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(
|
next_circle_index = find_first_index(
|
||||||
children[circle_start_index + 1 :],
|
next_children,
|
||||||
pred=lambda child: child.specific_class == Circle,
|
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:
|
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)
|
# store flattened descendents to improve performance (no need for db queries)
|
||||||
info.context.circle_descendants = list(circle_descendants)
|
info.context.circle_descendants = list(circle_descendants)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue