- 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>
212 lines
8.1 KiB
TypeScript
212 lines
8.1 KiB
TypeScript
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('.', ',');
|
||
}
|