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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
103a603eb4
commit
bb592ad2bd
@@ -0,0 +1,130 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user