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"; function isLearningContentType(object: any): object is LearningContent { return ( object?.content_type === "learnpath.LearningContentAssignment" || object?.content_type === "learnpath.LearningContentAttendanceDay" || 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.LearningContentTest" || object?.content_type === "learnpath.LearningContentVideo" ); } export function parseLearningSequences( circle: Circle, 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 { 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 Circle implements WagtailCircle { readonly content_type = "learnpath.Circle"; readonly learningSequences: LearningSequence[]; nextCircle?: Circle; previousCircle?: Circle; constructor( public readonly id: number, 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 ): Circle { // TODO add error checking when the data does not conform to the schema return new Circle( 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 isComplete(): boolean { return this.learningSequences.every((ls) => this.allFinishedInLearningSequence(ls.translation_key) ); } public parseCompletionData(completionData: CourseCompletion[]) { this.flatChildren.forEach((page) => { const pageIndex = completionData.findIndex((e) => { return e.page_key === page.translation_key; }); if (pageIndex >= 0) { page.completion_status = completionData[pageIndex].completion_status; } else { page.completion_status = "unknown"; } }); if (this.parentLearningPath) { this.parentLearningPath.calcNextLearningContent(completionData); } } }