Files
lernbar/src/lib/repos/karten.ts
T
Florian WildeandClaude Opus 5 bb592ad2bd Baustein 4: Merkkarten mit Wiederholung nach Zeitabstand
- Abgespecktes SM-2 mit drei Bewertungsstufen statt sechs
- Abstaende 1, 3, dann Abstand mal Leichtigkeit; gedeckelt bei 180 Tagen
- "nochmal" bringt die Karte in derselben Sitzung wieder
- Taeglicher Stapel, gedeckelt auf 20 Karten, ueberfaellige zuerst
- Fuenf Faecher zur Anzeige, abgeleitet aus dem Abstand
- Filter nach Themenblock fuer gezieltes Ueben
- Lernmodus fuers Handy: grosse Flaechen, Antippen, Wischen, kein Hover;
  am Rechner zusaetzlich Leertaste und Tasten 1/2/3
- Tabellen karte und karten_verlauf, jede Bewertung wird mitgeschrieben
- Merkkarten zaehlen im Wochenfortschritt mit, gelernt ab Fach 4
- Kopfzeile am Handy einzeilig, Tipp-Hervorhebung abgeschaltet
- 29 neue Tests, insgesamt 133

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 11:16:40 +02:00

131 lines
3.9 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 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<string, Karte> {
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<number, number>;
};
/** 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<number, number> = { 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 };
}