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:
Florian Wilde
2026-09-10 11:16:40 +02:00
co-authored by Claude Opus 5
parent 103a603eb4
commit bb592ad2bd
32 changed files with 1489 additions and 7 deletions
+39 -2
View File
@@ -1,4 +1,4 @@
import { index, integer, primaryKey, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
import { index, integer, primaryKey, real, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
/** Rollen – bewusst nur zwei. */
export const rollen = ['ausbilder', 'azubi'] as const;
@@ -89,11 +89,48 @@ export const wochenplanung = sqliteTable(
],
);
export const kartenBewertungen = ['sitzt', 'unsicher', 'nochmal'] as const;
export type KartenBewertung = (typeof kartenBewertungen)[number];
export const karte = sqliteTable(
'karte',
{
benutzerId: text('benutzer_id')
.notNull()
.references(() => benutzer.id, { onDelete: 'cascade' }),
karteId: text('karte_id').notNull(),
fach: integer('fach').notNull().default(1),
faelligAm: text('faellig_am').notNull(),
letzteBewertung: text('letzte_bewertung', { enum: kartenBewertungen }),
wiederholungen: integer('wiederholungen').notNull().default(0),
abstandTage: integer('abstand_tage').notNull().default(0),
leichtigkeit: real('leichtigkeit').notNull().default(2.5),
zuletztGelerntAm: text('zuletzt_gelernt_am'),
},
(t) => [primaryKey({ columns: [t.benutzerId, t.karteId] }), index('karte_faellig_idx').on(t.benutzerId, t.faelligAm)],
);
export const kartenVerlauf = sqliteTable(
'karten_verlauf',
{
id: text('id').primaryKey(),
benutzerId: text('benutzer_id')
.notNull()
.references(() => benutzer.id, { onDelete: 'cascade' }),
karteId: text('karte_id').notNull(),
bewertung: text('bewertung', { enum: kartenBewertungen }).notNull(),
bewertetAm: text('bewertet_am').notNull(),
},
(t) => [index('karten_verlauf_benutzer_idx').on(t.benutzerId, t.bewertetAm)],
);
export type Benutzer = typeof benutzer.$inferSelect;
export type Sitzung = typeof sitzung.$inferSelect;
export type Ausbildung = typeof ausbildung.$inferSelect;
export type HandoutGelesen = typeof handoutGelesen.$inferSelect;
export type Wochenplanung = typeof wochenplanung.$inferSelect;
export type Karte = typeof karte.$inferSelect;
export type KartenVerlauf = typeof kartenVerlauf.$inferSelect;
/** Wird von der Schema-Abgleichprüfung im Test verwendet. */
export const alleTabellen = { benutzer, sitzung, ausbildung, handoutGelesen, wochenplanung };
export const alleTabellen = { benutzer, sitzung, ausbildung, handoutGelesen, wochenplanung, karte, kartenVerlauf };