Die Abdeckung des Ausbildungsrahmenplans ist die Ansicht, die gegenueber
der IHK zaehlt. Sie unterscheidet vier Lagen statt "erledigt/offen":
vermittelt eine abgenommene Aufgabe oder ein abgeschlossener
Checklisten-Durchlauf, oder zwei gelesene Handouts
angefangen etwas erledigt, aber noch nicht genug
geplant Inhalt vorhanden, nichts davon bearbeitet
ohne Inhalt zu dieser Position gibt es im Lehrgang gar nichts
Die vierte Lage ist die wichtigste: Sie ist keine Luecke beim Azubi,
sondern eine im Lehrgang, und wird getrennt ausgewiesen. Die Regel steht
als reine Funktion in src/lib/auswertung/abdeckung.ts, mit 17 Tests.
Das Feedbackgespraech laesst beide Seiten getrennt bewerten. Wo sie um
zwei Stufen oder mehr auseinanderliegen, wird das hervorgehoben - in der
Ansicht, in der Uebersicht und im PDF, alle drei ueber dieselbe Funktion
liegtWeitAuseinander(). Nach beidseitiger Bestaetigung ist das Gespraech
unveraenderlich und wird nur noch gelesen.
Durchstich in scripts/pruefungen/baustein-9.mjs: alle fuenf Luecken
erkannt, getrennte Spalten, Sperre serverseitig, PDF nur mit Anmeldung,
ein fremder Azubi bekommt 404 - auch auf das PDF.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
174 lines
5.3 KiB
TypeScript
174 lines
5.3 KiB
TypeScript
/**
|
||
* 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<Abdeckungsstufe, string> = {
|
||
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<string, Nachweis[]>,
|
||
): 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<string, Nachweis[]>,
|
||
): Lernfeldabdeckung[] {
|
||
return lernfelder.map((lernfeld) => {
|
||
const nachweise = nachweiseJeLernfeld.get(lernfeld.id) ?? [];
|
||
return { ...lernfeld, stufe: stufeAus(nachweise), nachweise, geplant: nachweise.length };
|
||
});
|
||
}
|