Files
lernbar/src/lib/planung/sicht.ts
T
Florian WildeandClaude Opus 5 b9d6041f3b Baustein 6: Checklisten mit Durchlaeufen und PDF-Export
- Durchlauf aus einer Vorlage, mit Datum, Ticket und Kunde
- Schritt fuer Schritt abhaken, Bemerkung je Schritt
- Fortschritt gesamt und je Gruppe (Vorbereiten/Durchfuehren/Pruefen/Abschliessen)
- Abschliessen erst, wenn alles abgehakt ist; Meldung nennt die offenen Schritte
- Abgeschlossene Durchlaeufe sind gesperrt und bleiben als Nachweis
- Titel und Schritttexte werden beim Start kopiert, nicht verwiesen
- PDF-Export mit gezeichneten Ankreuzkaestchen und Unterschriftenzeilen
- Gemeinsame PDF-Grundlage fuer alle kuenftigen Ausgaben
- Ausbilder liest mit und darf das PDF abrufen, hakt aber nicht ab
- Abgeschlossene Durchlaeufe zaehlen im Wochenfortschritt mit

Behoben beim Durchstich: Die Fusszeile wurde unterhalb des Satzspiegels
gesetzt, wodurch PDFKit je Fusszeile eine leere Seite anhaengte - aus zwei
Seiten wurden sechs. Ein Test zaehlt jetzt die Seitenobjekte im PDF.

29 neue Tests, insgesamt 186.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 21:00:30 +02:00

149 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { inhalte, type Handout, type Aufgabe } from '../inhalte';
import type { Checkliste, Kontrolle, Woche } from '../inhalte/schema';
import { gelesenSet } from '../repos/fortschritt';
import { planVon } from '../repos/wochenplanung';
import { uebersicht as kartenUebersicht } from '../repos/karten';
import { anzahlAbgenommen } from '../repos/aufgaben';
import { anzahlAbgeschlossen } from '../repos/checklisten';
import { heute } from '../zeit';
import { aktuelleWoche } from './wochen';
import { wochenFortschritt, type Fortschritt } from './fortschritt';
import type { Wochenplanung } from '../db/schema';
/**
* Setzt aus Inhalt (Datei), Planung (Datenbank) und Fortschritt (Datenbank)
* die Wochenansicht zusammen. Alles, was die Startseite braucht, kommt hier her.
*/
export type Tagessicht = {
tag: string;
theorie: Handout[];
praxis: string;
/** Der Tag, der im Kalender zu diesem Wochentag gehört. */
datum: string;
istHeute: boolean;
};
export type Wochensicht = {
woche: Woche;
planung: Wochenplanung;
fortschritt: Fortschritt;
tage: Tagessicht[];
handouts: { handout: Handout; gelesen: boolean }[];
aufgaben: Aufgabe[];
checklisten: Checkliste[];
kontrolle: Kontrolle | null;
vorherige: number | null;
naechste: number | null;
};
export function wochensicht(ausbildungId: string, azubiId: string, wochenNummer: number): Wochensicht | null {
const alle = inhalte();
const woche = alle.wocheNach.get(wochenNummer);
if (!woche) return null;
const plan = planVon(ausbildungId);
const planung = plan.find((z) => z.wochenNummer === wochenNummer);
if (!planung) return null;
const gelesen = gelesenSet(azubiId);
const heuteDatum = heute();
const handouts = woche.handouts
.map((id) => alle.handoutNach.get(id))
.filter((h) => h !== undefined)
.map((handout) => ({ handout, gelesen: gelesen.has(handout.id) }));
const tage: Tagessicht[] = woche.tage.map((tag, stelle) => {
const datum = tagVersetzt(planung.geplantVon, stelle);
return {
tag: tag.tag,
praxis: tag.praxis,
datum,
istHeute: datum === heuteDatum,
theorie: tag.theorie.map((id) => alle.handoutNach.get(id)).filter((h) => h !== undefined),
};
});
const aufgaben = woche.aufgaben.map((id) => alle.aufgabeNach.get(id)).filter((a) => a !== undefined);
const checklisten = woche.checklisten.map((id) => alle.checklisteNach.get(id)).filter((c) => c !== undefined);
const kontrolle = woche.lernzielkontrolle ? (alle.kontrolleNach.get(woche.lernzielkontrolle) ?? null) : null;
// Die Merkkarten dieser Woche sind die Karten ihrer Handouts.
const karteIds = handouts.flatMap(({ handout }) =>
alle.karten.filter((k) => k.handoutId === handout.id).map((k) => k.id),
);
const kartenStand = kartenUebersicht(azubiId, karteIds, heuteDatum);
const fortschritt = wochenFortschritt({
handouts: { erledigt: handouts.filter((h) => h.gelesen).length, gesamt: handouts.length },
karten: { erledigt: kartenStand.gelernt, gesamt: kartenStand.gesamt },
aufgaben: { erledigt: anzahlAbgenommen(azubiId, aufgaben.map((a) => a.id)), gesamt: aufgaben.length },
// Diese Bereiche werden mit ihren Bausteinen scharf geschaltet. Bis dahin
// zeigen sie ehrlich an, was die Woche vorsieht und noch offen ist.
checklisten: {
erledigt: anzahlAbgeschlossen(azubiId, checklisten.map((c) => c.id)),
gesamt: checklisten.length,
},
kontrolle: { erledigt: 0, gesamt: kontrolle ? 1 : 0 },
});
const nummern = plan.map((z) => z.wochenNummer);
const stelle = plan.findIndex((z) => z.wochenNummer === wochenNummer);
return {
woche,
planung,
fortschritt,
tage,
handouts,
aufgaben,
checklisten,
kontrolle,
vorherige: stelle > 0 ? (nummern[stelle - 1] ?? null) : null,
naechste: stelle < nummern.length - 1 ? (nummern[stelle + 1] ?? null) : null,
};
}
/** Die Woche, die gerade dran ist. */
export function aktuelleWochensicht(ausbildungId: string, azubiId: string): Wochensicht | null {
const plan = planVon(ausbildungId);
const dran = aktuelleWoche(plan, heute());
if (!dran) return null;
return wochensicht(ausbildungId, azubiId, dran.wochenNummer);
}
/** Kurzstand für die Ausbilderübersicht – ohne den ganzen Wochenaufbau. */
export type Kurzstand = {
wochenNummer: number;
titel: string;
geplantVon: string;
geplantBis: string;
status: Wochenplanung['status'];
fortschritt: Fortschritt;
handoutsGelesen: number;
handoutsGesamt: number;
};
export function kurzstand(ausbildungId: string, azubiId: string): Kurzstand | null {
const sicht = aktuelleWochensicht(ausbildungId, azubiId);
if (!sicht) return null;
return {
wochenNummer: sicht.woche.nummer,
titel: sicht.woche.titel,
geplantVon: sicht.planung.geplantVon,
geplantBis: sicht.planung.geplantBis,
status: sicht.planung.status,
fortschritt: sicht.fortschritt,
handoutsGelesen: sicht.handouts.filter((h) => h.gelesen).length,
handoutsGesamt: sicht.handouts.length,
};
}
function tagVersetzt(montag: string, tage: number): string {
const d = new Date(`${montag}T12:00:00Z`);
d.setUTCDate(d.getUTCDate() + tage);
return d.toISOString().slice(0, 10);
}