Baustein 3: Wochenplan als Startseite, Fortschritt, Wochen verschieben

- Wochenplan ist die Startseite des Azubis: Thema, Leitfrage, fuenf Tage
  mit Theorie und Praxis, Fortschrittsbalken, was noch offen ist
- Ausbilder sieht je Azubi die laufende Woche mit Stand und offenen Punkten
- /woche/<nr> fuer jede Woche, Ausbilder waehlt ueber ?azubi= den Stand;
  Zugriff wird serverseitig bei jedem Aufruf geprueft
- Tabelle wochenplanung mit geplantem und tatsaechlichem Datum, Zustand
  und Notiz fuer den Azubi
- Verschieben per Datumsfeld, wahlweise mit allen folgenden Wochen
- Reihenfolge per Ziehen und Ablegen sowie per Tastatur (Alt + Pfeil)
- Warnung bei sich ueberlappenden Terminen
- Plan entsteht automatisch beim Anlegen eines Azubikontos
- Planungs- und Fortschrittslogik als reine Funktionen, 36 Tests dafuer
- Insgesamt 104 Tests

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-10 09:04:39 +02:00
co-authored by Claude Opus 5
parent 1af5613e9b
commit 103a603eb4
29 changed files with 2158 additions and 47 deletions
+136
View File
@@ -0,0 +1,136 @@
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 { heute } from '../zeit';
import { aktuelleWoche } from './wochen';
import { leer, 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;
const fortschritt = wochenFortschritt({
handouts: { erledigt: handouts.filter((h) => h.gelesen).length, gesamt: handouts.length },
// Diese Bereiche werden mit ihren Bausteinen scharf geschaltet. Bis dahin
// zeigen sie ehrlich an, was die Woche vorsieht und noch offen ist.
karten: leer,
aufgaben: { erledigt: 0, gesamt: aufgaben.length },
checklisten: { erledigt: 0, 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);
}