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,204 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { datenbank, schema } from '../db';
|
||||
import { jetzt } from '../zeit';
|
||||
import { begrenzePunkte, werteAus, type Ergebnis, type Fragenstand } from '../kontrollen/bewertung';
|
||||
import type { Kontrolle as Kontrollinhalt } from '../inhalte/schema';
|
||||
import type { Kontrolle, KontrollAntwort } from '../db/schema';
|
||||
|
||||
/** Bearbeitungsstand der Lernzielkontrollen. */
|
||||
|
||||
export function standVon(benutzerId: string, kontrolleId: string): Kontrolle | null {
|
||||
return (
|
||||
datenbank()
|
||||
.select()
|
||||
.from(schema.kontrolle)
|
||||
.where(and(eq(schema.kontrolle.benutzerId, benutzerId), eq(schema.kontrolle.kontrolleId, kontrolleId)))
|
||||
.get() ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function standNachId(id: string): Kontrolle | null {
|
||||
return datenbank().select().from(schema.kontrolle).where(eq(schema.kontrolle.id, id)).get() ?? null;
|
||||
}
|
||||
|
||||
export function staendeVon(benutzerId: string): Map<string, Kontrolle> {
|
||||
const zeilen = datenbank()
|
||||
.select()
|
||||
.from(schema.kontrolle)
|
||||
.where(eq(schema.kontrolle.benutzerId, benutzerId))
|
||||
.all();
|
||||
return new Map(zeilen.map((z) => [z.kontrolleId, z]));
|
||||
}
|
||||
|
||||
export function antwortenVon(standId: string): Map<string, KontrollAntwort> {
|
||||
const zeilen = datenbank()
|
||||
.select()
|
||||
.from(schema.kontrollAntwort)
|
||||
.where(eq(schema.kontrollAntwort.kontrolleId, standId))
|
||||
.all();
|
||||
return new Map(zeilen.map((z) => [z.frageId, z]));
|
||||
}
|
||||
|
||||
/** Legt bei Bedarf eine Zeile an – noch ohne Freigabe. */
|
||||
export function sorgeFuerStand(benutzerId: string, kontrolleId: string): Kontrolle {
|
||||
const vorhanden = standVon(benutzerId, kontrolleId);
|
||||
if (vorhanden) return vorhanden;
|
||||
|
||||
const datensatz: Kontrolle = {
|
||||
id: crypto.randomUUID(),
|
||||
benutzerId,
|
||||
kontrolleId,
|
||||
freigegebenAm: null,
|
||||
freigegebenVon: null,
|
||||
abgegebenAm: null,
|
||||
punkte: null,
|
||||
anmerkung: null,
|
||||
korrigiertVon: null,
|
||||
korrigiertAm: null,
|
||||
geaendertAm: jetzt(),
|
||||
};
|
||||
|
||||
datenbank().insert(schema.kontrolle).values(datensatz).run();
|
||||
return datensatz;
|
||||
}
|
||||
|
||||
export function freigeben(benutzerId: string, kontrolleId: string, ausbilderId: string): Kontrolle {
|
||||
const stand = sorgeFuerStand(benutzerId, kontrolleId);
|
||||
|
||||
datenbank()
|
||||
.update(schema.kontrolle)
|
||||
.set({ freigegebenAm: jetzt(), freigegebenVon: ausbilderId, geaendertAm: jetzt() })
|
||||
.where(eq(schema.kontrolle.id, stand.id))
|
||||
.run();
|
||||
|
||||
return { ...stand, freigegebenAm: jetzt(), freigegebenVon: ausbilderId };
|
||||
}
|
||||
|
||||
/** Freigabe zurücknehmen – nur solange nichts abgegeben wurde. */
|
||||
export function freigabeZuruecknehmen(standId: string): void {
|
||||
datenbank()
|
||||
.update(schema.kontrolle)
|
||||
.set({ freigegebenAm: null, freigegebenVon: null, geaendertAm: jetzt() })
|
||||
.where(eq(schema.kontrolle.id, standId))
|
||||
.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine einzelne Antwort speichern.
|
||||
*
|
||||
* Wird beim Tippen laufend aufgerufen – deshalb ein einzelner Schreibvorgang
|
||||
* je Frage statt eines Rundumschlags über das ganze Formular.
|
||||
*/
|
||||
export function antwortSpeichern(standId: string, frageId: string, antwort: string): void {
|
||||
datenbank()
|
||||
.insert(schema.kontrollAntwort)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
kontrolleId: standId,
|
||||
frageId,
|
||||
antwort,
|
||||
punkte: null,
|
||||
anmerkung: null,
|
||||
geaendertAm: jetzt(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [schema.kontrollAntwort.kontrolleId, schema.kontrollAntwort.frageId],
|
||||
set: { antwort, geaendertAm: jetzt() },
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
export function abgeben(standId: string): void {
|
||||
datenbank()
|
||||
.update(schema.kontrolle)
|
||||
.set({ abgegebenAm: jetzt(), geaendertAm: jetzt() })
|
||||
.where(eq(schema.kontrolle.id, standId))
|
||||
.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Punkte und Anmerkungen des Ausbilders schreiben.
|
||||
*
|
||||
* Die Gesamtpunktzahl wird aus den Einzelpunkten gerechnet und mitgeschrieben,
|
||||
* damit Listen und Auswertungen nicht jedes Mal alle Antworten laden müssen.
|
||||
*/
|
||||
export function korrigieren(
|
||||
standId: string,
|
||||
ausbilderId: string,
|
||||
inhalt: Kontrollinhalt,
|
||||
bewertungen: { frageId: string; punkte: number | null; anmerkung: string | null }[],
|
||||
anmerkung: string | null,
|
||||
): Ergebnis {
|
||||
const db = datenbank();
|
||||
const wann = jetzt();
|
||||
const moeglichNach = new Map(inhalt.fragen.map((f) => [f.id, f.punkte]));
|
||||
|
||||
for (const bewertung of bewertungen) {
|
||||
const moeglich = moeglichNach.get(bewertung.frageId);
|
||||
if (moeglich === undefined) continue; // Frage gibt es nicht (mehr)
|
||||
|
||||
const punkte = bewertung.punkte === null ? null : begrenzePunkte(bewertung.punkte, moeglich);
|
||||
|
||||
db.insert(schema.kontrollAntwort)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
kontrolleId: standId,
|
||||
frageId: bewertung.frageId,
|
||||
antwort: '',
|
||||
punkte,
|
||||
anmerkung: bewertung.anmerkung,
|
||||
geaendertAm: wann,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [schema.kontrollAntwort.kontrolleId, schema.kontrollAntwort.frageId],
|
||||
set: { punkte, anmerkung: bewertung.anmerkung, geaendertAm: wann },
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
const ergebnis = ergebnisVon(standId, inhalt);
|
||||
|
||||
db.update(schema.kontrolle)
|
||||
.set({
|
||||
punkte: ergebnis.erreicht,
|
||||
anmerkung,
|
||||
korrigiertVon: ausbilderId,
|
||||
korrigiertAm: ergebnis.vollstaendig ? wann : null,
|
||||
geaendertAm: wann,
|
||||
})
|
||||
.where(eq(schema.kontrolle.id, standId))
|
||||
.run();
|
||||
|
||||
return ergebnis;
|
||||
}
|
||||
|
||||
/** Rechnet das Ergebnis aus den gespeicherten Einzelpunkten. */
|
||||
export function ergebnisVon(standId: string, inhalt: Kontrollinhalt): Ergebnis {
|
||||
const antworten = antwortenVon(standId);
|
||||
|
||||
const fragen: Fragenstand[] = inhalt.fragen.map((frage) => ({
|
||||
frageId: frage.id,
|
||||
moeglich: frage.punkte,
|
||||
erreicht: antworten.get(frage.id)?.punkte ?? null,
|
||||
}));
|
||||
|
||||
return werteAus(fragen);
|
||||
}
|
||||
|
||||
/** Abgegebene, aber noch nicht fertig korrigierte Kontrollen – für „Was wartet auf mich". */
|
||||
export function wartetAufKorrektur(benutzerIds: string[]): Kontrolle[] {
|
||||
if (benutzerIds.length === 0) return [];
|
||||
return datenbank()
|
||||
.select()
|
||||
.from(schema.kontrolle)
|
||||
.all()
|
||||
.filter((z) => benutzerIds.includes(z.benutzerId) && z.abgegebenAm !== null && z.korrigiertAm === null);
|
||||
}
|
||||
|
||||
/** Zählt abgegebene Kontrollen einer Liste – für den Wochenfortschritt. */
|
||||
export function anzahlAbgegeben(benutzerId: string, kontrollIds: string[]): number {
|
||||
if (kontrollIds.length === 0) return 0;
|
||||
const staende = staendeVon(benutzerId);
|
||||
return kontrollIds.filter((id) => staende.get(id)?.abgegebenAm != null).length;
|
||||
}
|
||||
Reference in New Issue
Block a user