/** * Abdeckung des Ausbildungsrahmenplans. * * Das ist die Ansicht, die im Zweifel gegenüber der IHK zählt: Welche * Berufsbildpositionen sind vermittelt, welche noch nicht? * * Die entscheidende Unterscheidung, die eine naive Zählung übersieht: * **Geplant** ist nicht **vermittelt.** Dass ein Handout zu Position B2 * existiert, heißt nicht, dass der Azubi es durchgearbeitet hat. Und eine * Position, zu der es gar keinen Inhalt gibt, ist eine Lücke im Lehrgang * selbst – die schlimmste der vier Lagen, weil sie sonst unbemerkt bleibt. */ export type Nachweisart = 'handout' | 'aufgabe' | 'checkliste'; export type Nachweis = { art: Nachweisart; id: string; titel: string; /** Hat der Azubi das nachweislich erledigt? */ erledigt: boolean; /** Wann, für die Anzeige. */ erledigtAm?: string | null; }; /** * Gewicht eines Nachweises. * * Eine abgenommene Praxisaufgabe und ein abgeschlossener Checklisten-Durchlauf * sind **belastbare** Nachweise: Da hat jemand etwas getan und ein anderer hat * es gesehen. Ein gelesenes Handout ist ein **stützender** Nachweis – es sagt * aus, dass der Stoff behandelt wurde, aber nicht, dass er angewendet wurde. */ export function istBelastbar(art: Nachweisart): boolean { return art === 'aufgabe' || art === 'checkliste'; } export type Abdeckungsstufe = 'ohne_inhalt' | 'offen' | 'angefangen' | 'abgedeckt'; export type Positionsabdeckung = { id: string; titel: string; kurz: string; gruppe: string; verordnung: string; stufe: Abdeckungsstufe; nachweise: Nachweis[]; belastbare: number; stuetzende: number; /** Was es zu dieser Position überhaupt gibt – erledigt oder nicht. */ geplant: number; }; export const stufenText: Record = { ohne_inhalt: 'kein Inhalt hinterlegt', offen: 'geplant, noch nicht bearbeitet', angefangen: 'angefangen', abgedeckt: 'vermittelt', }; /** * Die Regel, nach der eine Position als vermittelt gilt. * * **Ein belastbarer Nachweis** (abgenommene Aufgabe, abgeschlossene Checkliste) * **oder zwei stützende** (gelesene Handouts). Ein einzelnes gelesenes Handout * genügt bewusst nicht – sonst wäre die Abdeckung nach dem ersten Klick voll * und die Ansicht wertlos. * * Die Regel steht hier an einer Stelle, damit sie sich ändern lässt, ohne die * Anzeige anzufassen – und damit im Zweifel nachlesbar ist, was gezählt wurde. */ export function stufeAus(nachweise: Nachweis[]): Abdeckungsstufe { if (nachweise.length === 0) return 'ohne_inhalt'; const erledigt = nachweise.filter((n) => n.erledigt); if (erledigt.length === 0) return 'offen'; const belastbare = erledigt.filter((n) => istBelastbar(n.art)).length; const stuetzende = erledigt.length - belastbare; if (belastbare >= 1 || stuetzende >= 2) return 'abgedeckt'; return 'angefangen'; } export type Position = { id: string; titel: string; kurz: string; gruppe: string; verordnung: string; }; export function berechneAbdeckung( positionen: Position[], nachweiseJePosition: Map, ): Positionsabdeckung[] { return positionen.map((position) => { const nachweise = nachweiseJePosition.get(position.id) ?? []; const erledigt = nachweise.filter((n) => n.erledigt); return { ...position, stufe: stufeAus(nachweise), nachweise, belastbare: erledigt.filter((n) => istBelastbar(n.art)).length, stuetzende: erledigt.filter((n) => !istBelastbar(n.art)).length, geplant: nachweise.length, }; }); } export type Bilanz = { gesamt: number; abgedeckt: number; angefangen: number; offen: number; ohneInhalt: number; /** Anteil der vermittelten Positionen an allen. */ prozent: number; }; export function bilanz(abdeckung: Positionsabdeckung[]): Bilanz { const zaehle = (stufe: Abdeckungsstufe) => abdeckung.filter((a) => a.stufe === stufe).length; const gesamt = abdeckung.length; const abgedeckt = zaehle('abgedeckt'); return { gesamt, abgedeckt, angefangen: zaehle('angefangen'), offen: zaehle('offen'), ohneInhalt: zaehle('ohne_inhalt'), prozent: gesamt === 0 ? 0 : Math.round((abgedeckt / gesamt) * 100), }; } /** Nach Gruppe zusammengefasst – Abschnitt A, B und C stehen getrennt im Plan. */ export function nachGruppen( abdeckung: Positionsabdeckung[], ): { gruppe: string; positionen: Positionsabdeckung[]; bilanz: Bilanz }[] { const gruppen: string[] = []; for (const eintrag of abdeckung) { if (!gruppen.includes(eintrag.gruppe)) gruppen.push(eintrag.gruppe); } return gruppen.map((gruppe) => { const eigene = abdeckung.filter((a) => a.gruppe === gruppe); return { gruppe, positionen: eigene, bilanz: bilanz(eigene) }; }); } // --- Lernfelder der Berufsschule ------------------------------------------- export type Lernfeldabdeckung = { id: string; titel: string; jahr: number; stunden: number; stufe: Abdeckungsstufe; nachweise: Nachweis[]; geplant: number; }; export function berechneLernfelder( lernfelder: { id: string; titel: string; jahr: number; stunden: number }[], nachweiseJeLernfeld: Map, ): Lernfeldabdeckung[] { return lernfelder.map((lernfeld) => { const nachweise = nachweiseJeLernfeld.get(lernfeld.id) ?? []; return { ...lernfeld, stufe: stufeAus(nachweise), nachweise, geplant: nachweise.length }; }); }