- 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>
131 lines
3.9 KiB
TypeScript
131 lines
3.9 KiB
TypeScript
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 };
|
||
}
|