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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7c4a051347
commit
d226a6bae8
@@ -0,0 +1,211 @@
|
||||
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('.', ',');
|
||||
}
|
||||
Reference in New Issue
Block a user