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
+230
View File
@@ -0,0 +1,230 @@
import Link from 'next/link';
import { Fortschrittsbalken } from '@/komponenten/Fortschrittsbalken';
import type { Wochensicht } from '@/lib/planung/sicht';
import { alsDatumText, kalenderwocheText } from '@/lib/zeit';
const statusText: Record<string, { text: string; ton: string; zeichen: string }> = {
geplant: { text: 'geplant', ton: 'neutral', zeichen: '○' },
laufend: { text: 'läuft', ton: 'warnung', zeichen: '▶' },
abgeschlossen: { text: 'abgeschlossen', ton: 'gut', zeichen: '✓' },
};
/**
* Die Wochenansicht – für den Azubi die Startseite, für den Ausbilder dieselbe
* Woche in der Ansicht seines Azubis.
*/
export function WochenAnsicht({
sicht,
istAzubi,
wochenLink,
}: {
sicht: Wochensicht;
istAzubi: boolean;
/** Vorlage für den Verweis auf eine andere Woche, `{nr}` wird ersetzt. */
wochenLink: string;
}) {
const zurWoche = (nummer: number) => wochenLink.replace('{nr}', String(nummer));
const { woche, planung, fortschritt, tage } = sicht;
const status = statusText[planung.status] ?? statusText.geplant!;
return (
<div className="stapel">
<header className="stapel-eng">
<div className="reihe" style={{ gap: '0.6rem' }}>
<span className="wochen-nummer">Woche {woche.nummer}</span>
<span className="marke-status" data-ton={status.ton}>
{status.zeichen} {status.text}
</span>
<span className="leise">
{alsDatumText(planung.geplantVon)} – {alsDatumText(planung.geplantBis)} · {kalenderwocheText(planung.geplantVon)}
</span>
</div>
<h1 style={{ marginBottom: '0.2rem' }}>{woche.titel}</h1>
<p className="leitfrage">{woche.leitfrage}</p>
<p className="leise" style={{ marginBottom: 0 }}>{woche.ziel}</p>
</header>
{planung.tatsaechlichVon && planung.tatsaechlichVon !== planung.geplantVon ? (
<p className="hinweis hinweis-warnung" style={{ marginBottom: 0 }}>
Tatsächlich begonnen am {alsDatumText(planung.tatsaechlichVon)}, geplant war der{' '}
{alsDatumText(planung.geplantVon)}.
</p>
) : null}
{planung.notiz ? (
<p className="hinweis hinweis-warnung" style={{ marginBottom: 0 }}>
<strong>Notiz des Ausbilders:</strong> {planung.notiz}
</p>
) : null}
<section className="karte stapel">
<Fortschrittsbalken fortschritt={fortschritt} />
{fortschritt.offen.length > 0 ? (
<div>
<h2 style={{ fontSize: '1rem', marginBottom: '0.4rem' }}>
{istAzubi ? 'Das ist diese Woche noch offen' : 'Offen bei deinem Azubi'}
</h2>
<ul className="offen-liste">
{fortschritt.offen.map((bereich) => (
<li key={bereich.schluessel}>
<span>{bereich.titel}</span>
<span className="leise">
{bereich.zaehlwerk.erledigt} von {bereich.zaehlwerk.gesamt}
</span>
</li>
))}
</ul>
</div>
) : fortschritt.gesamt > 0 ? (
<p className="hinweis hinweis-gut" style={{ marginBottom: 0 }}>
✓ Diese Woche ist vollständig abgearbeitet.
</p>
) : null}
</section>
<section aria-labelledby="tage" className="stapel">
<h2 id="tage">Die Woche</h2>
<ol className="wochentage">
{tage.map((tag) => (
<li key={tag.tag} className={`wochentag${tag.istHeute ? ' wochentag-heute' : ''}`}>
<div className="wochentag-kopf">
<strong>{tag.tag}</strong>
<span className="leise">{alsDatumText(tag.datum)}</span>
{tag.istHeute ? <span className="marke-status" data-ton="warnung">● heute</span> : null}
</div>
<div className="wochentag-inhalt">
<div>
<span className="wochentag-marke">Theorie</span>
{tag.theorie.length === 0 ? (
<span className="leise">–</span>
) : (
<ul className="theorie-liste">
{tag.theorie.map((handout) => {
const gelesen = sicht.handouts.find((h) => h.handout.id === handout.id)?.gelesen ?? false;
return (
<li key={handout.id}>
<Link href={`/handouts/${handout.id}`}>
<span className="handout-kuerzel">{handout.id}</span> {handout.titel}
</Link>
{gelesen ? (
<span className="marke-status" data-ton="gut">
✓ gelesen
</span>
) : null}
</li>
);
})}
</ul>
)}
</div>
<div>
<span className="wochentag-marke">Praxis</span>
<span>{tag.praxis || '–'}</span>
</div>
</div>
</li>
))}
</ol>
</section>
<div className="raster">
<section className="karte" aria-labelledby="woche-handouts">
<h2 id="woche-handouts" style={{ fontSize: '1rem' }}>
Handouts dieser Woche
</h2>
{sicht.handouts.length === 0 ? (
<p className="leise" style={{ marginBottom: 0 }}>Keine.</p>
) : (
<ul className="stapel-eng" style={{ listStyle: 'none', margin: 0, padding: 0 }}>
{sicht.handouts.map(({ handout, gelesen }) => (
<li key={handout.id} className="reihe" style={{ gap: '0.5rem', flexWrap: 'nowrap' }}>
<span className="handout-kuerzel">{handout.id}</span>
<Link href={`/handouts/${handout.id}`} style={{ flex: 1 }}>
{handout.titel}
</Link>
<span aria-label={gelesen ? 'gelesen' : 'noch nicht gelesen'} title={gelesen ? 'gelesen' : 'offen'}>
{gelesen ? '✓' : '○'}
</span>
</li>
))}
</ul>
)}
</section>
<section className="karte" aria-labelledby="woche-aufgaben">
<h2 id="woche-aufgaben" style={{ fontSize: '1rem' }}>
Praxisaufgaben
</h2>
{sicht.aufgaben.length === 0 ? (
<p className="leise" style={{ marginBottom: 0 }}>Keine.</p>
) : (
<ul className="stapel-eng" style={{ margin: 0, paddingLeft: '1.1rem' }}>
{sicht.aufgaben.map((aufgabe) => (
<li key={aufgabe.id}>
<strong>{aufgabe.id}</strong> {aufgabe.titel}
<br />
<span className="leise">{aufgabe.zeitrahmen}</span>
</li>
))}
</ul>
)}
</section>
<section className="karte" aria-labelledby="woche-checklisten">
<h2 id="woche-checklisten" style={{ fontSize: '1rem' }}>
Checklisten
</h2>
{sicht.checklisten.length === 0 ? (
<p className="leise" style={{ marginBottom: 0 }}>Keine.</p>
) : (
<ul className="stapel-eng" style={{ margin: 0, paddingLeft: '1.1rem' }}>
{sicht.checklisten.map((liste) => (
<li key={liste.id}>{liste.titel}</li>
))}
</ul>
)}
</section>
<section className="karte" aria-labelledby="woche-kontrolle">
<h2 id="woche-kontrolle" style={{ fontSize: '1rem' }}>
Lernzielkontrolle
</h2>
{sicht.kontrolle ? (
<p style={{ marginBottom: 0 }}>
{sicht.kontrolle.titel}
<br />
<span className="leise">
{sicht.kontrolle.fragen.length} Fragen ·{' '}
{sicht.kontrolle.fragen.reduce((summe, f) => summe + f.punkte, 0)} Punkte
{sicht.kontrolle.dauer_minuten ? ` · ${sicht.kontrolle.dauer_minuten} Minuten` : ''}
</span>
</p>
) : (
<p className="leise" style={{ marginBottom: 0 }}>Diese Woche keine.</p>
)}
</section>
</div>
<nav className="reihe nicht-drucken" style={{ justifyContent: 'space-between' }} aria-label="Andere Wochen">
{sicht.vorherige ? (
<Link href={zurWoche(sicht.vorherige)} className="knopf knopf-still">
← Woche {sicht.vorherige}
</Link>
) : (
<span />
)}
{sicht.naechste ? (
<Link href={zurWoche(sicht.naechste)} className="knopf knopf-still">
Woche {sicht.naechste} →
</Link>
) : (
<span />
)}
</nav>
</div>
);
}