Files
lernbar/src/app/(angemeldet)/kontrollen/page.tsx
T
Florian WildeandClaude Opus 5 d226a6bae8 Baustein 8: Lernzielkontrollen mit Freigabe, Abgabe und Korrektur
- Ausbilder gibt frei; vorher sieht der Azubi die Fragen nicht
- Freigabe laesst sich zuruecknehmen, solange nicht abgegeben wurde
- Antworten werden beim Tippen je Frage zwischengespeichert
- Musterloesungen erst nach dem Absenden: sichtbareKontrolle() entfernt sie
  serverseitig, bevor die Daten in die Ausgabe gelangen
- Korrektur mit Punkten und Anmerkung je Frage, Summe und Note laufen mit
- Notenschluessel der IHK; solange eine Frage offen ist, bleibt das Ergebnis
  vorlaeufig und die Kontrolle gilt nicht als korrigiert
- Ergebnis bei beiden sichtbar, Schnitt ueber alle Kontrollen
- Abgegebene Kontrollen unter "Was auf dich wartet" und im Wochenfortschritt
- scripts/pruefungen/musterloesung-dichtheit.mjs prueft die Sperre gegen den
  echten Wortlaut aus der Inhaltsdatei im rohen Seitenquelltext

36 neue Tests, davon 21 fuer die Sichtbarkeit der Musterloesung.
Insgesamt 277.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 08:57:00 +02:00

212 lines
8.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 type { Metadata } from 'next';
import Link from 'next/link';
import { erfordereBenutzer } from '@/lib/auth/rechte';
import { ausbildungenVonAusbilder } from '@/lib/repos/ausbildung';
import { inhalte } from '@/lib/inhalte';
import { ergebnisVon, staendeVon } from '@/lib/repos/kontrollen';
import { gesamtpunkte, zustandVon, type Zustand } from '@/lib/kontrollen/sicht';
import { verlauf } from '@/lib/kontrollen/bewertung';
import { alsZeitpunktText } from '@/lib/zeit';
import { kontrolleFreigeben, freigabeZurueck } from './aktionen';
export const metadata: Metadata = { title: 'Lernzielkontrollen' };
export const dynamic = 'force-dynamic';
const zustandsmarke: Record<Zustand, { text: string; ton: string; zeichen: string }> = {
gesperrt: { text: 'nicht freigegeben', ton: 'neutral', zeichen: '○' },
freigegeben: { text: 'freigegeben', ton: 'warnung', zeichen: '▶' },
abgegeben: { text: 'abgegeben', ton: 'warnung', zeichen: '▲' },
korrigiert: { text: 'korrigiert', ton: 'gut', zeichen: '✓' },
};
export default async function KontrollenSeite() {
const benutzer = await erfordereBenutzer();
const alle = inhalte();
if (benutzer.rolle === 'azubi') {
const staende = staendeVon(benutzer.id);
const zeilen = alle.kontrollen.map((inhalt) => {
const stand = staende.get(inhalt.id);
const zustand = zustandVon({
azubiId: benutzer.id,
freigegebenAm: stand?.freigegebenAm ?? null,
abgegebenAm: stand?.abgegebenAm ?? null,
korrigiertAm: stand?.korrigiertAm ?? null,
});
const ergebnis = stand && stand.korrigiertAm ? ergebnisVon(stand.id, inhalt) : null;
return { inhalt, zustand, ergebnis, stand };
});
const korrigierte = zeilen
.filter((z) => z.ergebnis)
.map((z) => ({ titel: z.inhalt.titel, prozent: z.ergebnis!.prozent, note: z.ergebnis!.note }));
const stand = verlauf(korrigierte);
return (
<div className="stapel">
<div>
<h1>Lernzielkontrollen</h1>
<p className="leise">
{alle.kontrollen.length} Kontrollen · {korrigierte.length} korrigiert
{stand.anzahl > 0 ? ` · Schnitt ${stand.schnittProzent} %, Note ${zahl(stand.schnittNote)}` : ''}
</p>
</div>
<ul className="kontrollliste">
{zeilen.map(({ inhalt, zustand, ergebnis }) => {
const marke = zustandsmarke[zustand];
const offen = zustand === 'gesperrt';
return (
<li key={inhalt.id}>
{offen ? (
<div className="kontrollzeile kontrollzeile-gesperrt">
<Zeileninhalt inhalt={inhalt} marke={marke} ergebnis={ergebnis} />
</div>
) : (
<Link href={`/kontrollen/${inhalt.id}`} className="kontrollzeile">
<Zeileninhalt inhalt={inhalt} marke={marke} ergebnis={ergebnis} />
</Link>
)}
</li>
);
})}
</ul>
<p className="leise">
Eine Kontrolle wird von deinem Ausbilder freigegeben. Vorher sind die Fragen nicht einsehbar – die
Musterlösungen erst nach dem Absenden.
</p>
</div>
);
}
// --- Ausbilder ---
const meine = ausbildungenVonAusbilder(benutzer.id);
return (
<div className="stapel">
<div>
<h1>Lernzielkontrollen</h1>
<p className="leise">Freigeben, korrigieren, Ergebnisse ansehen.</p>
</div>
{meine.length === 0 ? (
<p className="hinweis hinweis-warnung">
Noch kein Azubikonto. <Link href="/verwaltung">Jetzt eines anlegen</Link>.
</p>
) : (
meine.map(({ azubi }) => {
const staende = staendeVon(azubi.id);
return (
<section key={azubi.id} className="stapel" aria-labelledby={`azubi-${azubi.id}`}>
<h2 id={`azubi-${azubi.id}`} style={{ margin: 0 }}>
{azubi.name}
</h2>
<ul className="kontrollliste">
{alle.kontrollen.map((inhalt) => {
const stand = staende.get(inhalt.id);
const zustand = zustandVon({
azubiId: azubi.id,
freigegebenAm: stand?.freigegebenAm ?? null,
abgegebenAm: stand?.abgegebenAm ?? null,
korrigiertAm: stand?.korrigiertAm ?? null,
});
const marke = zustandsmarke[zustand];
const ergebnis = stand && stand.abgegebenAm ? ergebnisVon(stand.id, inhalt) : null;
return (
<li key={inhalt.id}>
<div className="kontrollzeile">
<Link href={`/kontrollen/${inhalt.id}?azubi=${azubi.id}`} className="kontrollzeile-haupt">
<span className="kontroll-woche">Woche {inhalt.woche}</span>
<span className="kontroll-titel">
{inhalt.titel}
<span className="leise" style={{ display: 'block', fontWeight: 400 }}>
{inhalt.fragen.length} Fragen · {gesamtpunkte(inhalt)} Punkte
{stand?.abgegebenAm ? ` · abgegeben ${alsZeitpunktText(stand.abgegebenAm)}` : ''}
</span>
</span>
</Link>
<div className="reihe" style={{ gap: '0.4rem' }}>
{ergebnis && stand?.korrigiertAm ? (
<span className="marke-status" data-ton="gut">
{ergebnis.prozent} % · Note {ergebnis.note}
</span>
) : null}
<span className="marke-status" data-ton={marke.ton}>
{marke.zeichen} {marke.text}
</span>
{zustand === 'gesperrt' ? (
<form action={kontrolleFreigeben}>
<input type="hidden" name="azubiId" value={azubi.id} />
<input type="hidden" name="kontrolleId" value={inhalt.id} />
<button type="submit" className="knopf knopf-akzent">
Freigeben
</button>
</form>
) : null}
{zustand === 'freigegeben' ? (
<form action={freigabeZurueck}>
<input type="hidden" name="azubiId" value={azubi.id} />
<input type="hidden" name="kontrolleId" value={inhalt.id} />
<button type="submit" className="knopf knopf-still">
Freigabe zurück
</button>
</form>
) : null}
</div>
</div>
</li>
);
})}
</ul>
</section>
);
})
)}
</div>
);
}
function Zeileninhalt({
inhalt,
marke,
ergebnis,
}: {
inhalt: { id: string; woche: number; titel: string; fragen: { punkte: number }[] };
marke: { text: string; ton: string; zeichen: string };
ergebnis: { prozent: number; note: number } | null;
}) {
return (
<>
<span className="kontroll-woche">Woche {inhalt.woche}</span>
<span className="kontroll-titel">
{inhalt.titel}
<span className="leise" style={{ display: 'block', fontWeight: 400 }}>
{inhalt.fragen.length} Fragen · {inhalt.fragen.reduce((s, f) => s + f.punkte, 0)} Punkte
</span>
</span>
{ergebnis ? (
<span className="marke-status" data-ton="gut">
{ergebnis.prozent} % · Note {ergebnis.note}
</span>
) : null}
<span className="marke-status" data-ton={marke.ton}>
{marke.zeichen} {marke.text}
</span>
</>
);
}
function zahl(wert: number): string {
return wert.toString().replace('.', ',');
}