import crypto from 'node:crypto'; import { and, eq } from 'drizzle-orm'; import { datenbank, schema } from '../db'; import { heute, jetzt } from '../zeit'; import { fach, faelligAm, giltAlsGelernt, naechsterStand, NEUE_KARTE, type Bewertung, type Kartenstand } from '../lernen/sm2'; import type { Karte } from '../db/schema'; /** Lernstand der Merkkarten. Die Rechnung steckt in ../lernen/sm2. */ export function staendeVon(benutzerId: string): Map { const zeilen = datenbank().select().from(schema.karte).where(eq(schema.karte.benutzerId, benutzerId)).all(); return new Map(zeilen.map((z) => [z.karteId, z])); } export function standVon(benutzerId: string, karteId: string): Karte | null { return ( datenbank() .select() .from(schema.karte) .where(and(eq(schema.karte.benutzerId, benutzerId), eq(schema.karte.karteId, karteId))) .get() ?? null ); } export function alsKartenstand(zeile: Karte | null | undefined): Kartenstand { if (!zeile) return NEUE_KARTE; return { wiederholungen: zeile.wiederholungen, abstandTage: zeile.abstandTage, leichtigkeit: zeile.leichtigkeit, }; } /** * Nimmt eine Bewertung entgegen, rechnet den neuen Stand und schreibt ihn. * Gibt den neuen Stand zurück, damit die Oberfläche gleich anzeigen kann, * wann die Karte wiederkommt. */ export function bewerte( benutzerId: string, karteId: string, bewertung: Bewertung, heuteDatum = heute(), ): { stand: Kartenstand; faelligAm: string; fach: number } { const db = datenbank(); const vorher = alsKartenstand(standVon(benutzerId, karteId)); const nachher = naechsterStand(vorher, bewertung); const faellig = faelligAm(heuteDatum, nachher); db.insert(schema.karte) .values({ benutzerId, karteId, fach: fach(nachher), faelligAm: faellig, letzteBewertung: bewertung, wiederholungen: nachher.wiederholungen, abstandTage: nachher.abstandTage, leichtigkeit: nachher.leichtigkeit, zuletztGelerntAm: jetzt(), }) .onConflictDoUpdate({ target: [schema.karte.benutzerId, schema.karte.karteId], set: { fach: fach(nachher), faelligAm: faellig, letzteBewertung: bewertung, wiederholungen: nachher.wiederholungen, abstandTage: nachher.abstandTage, leichtigkeit: nachher.leichtigkeit, zuletztGelerntAm: jetzt(), }, }) .run(); db.insert(schema.kartenVerlauf) .values({ id: crypto.randomUUID(), benutzerId, karteId, bewertung, bewertetAm: jetzt(), }) .run(); return { stand: nachher, faelligAm: faellig, fach: fach(nachher) }; } /** Setzt den Lernstand einer Karte zurück – für den Fall, dass sich der Inhalt geändert hat. */ export function zuruecksetzen(benutzerId: string, karteId: string): void { datenbank() .delete(schema.karte) .where(and(eq(schema.karte.benutzerId, benutzerId), eq(schema.karte.karteId, karteId))) .run(); } export type Kartenuebersicht = { gesamt: number; gelernt: number; faellig: number; nieGelernt: number; jeFach: Record; }; /** Zählwerk über alle Karten – für Wochenfortschritt und Auswertung. */ export function uebersicht(benutzerId: string, karteIds: string[], heuteDatum = heute()): Kartenuebersicht { const staende = staendeVon(benutzerId); const jeFach: Record = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; let gelernt = 0; let faellig = 0; let nieGelernt = 0; for (const karteId of karteIds) { const zeile = staende.get(karteId); if (!zeile) { nieGelernt += 1; faellig += 1; jeFach[1] = (jeFach[1] ?? 0) + 1; continue; } const stand = alsKartenstand(zeile); jeFach[fach(stand)] = (jeFach[fach(stand)] ?? 0) + 1; if (giltAlsGelernt(stand)) gelernt += 1; if (zeile.faelligAm <= heuteDatum) faellig += 1; } return { gesamt: karteIds.length, gelernt, faellig, nieGelernt, jeFach }; }