Files
lernbar/src/lib/db/schema.ts
T
Florian WildeandClaude Opus 5 b9d6041f3b Baustein 6: Checklisten mit Durchlaeufen und PDF-Export
- Durchlauf aus einer Vorlage, mit Datum, Ticket und Kunde
- Schritt fuer Schritt abhaken, Bemerkung je Schritt
- Fortschritt gesamt und je Gruppe (Vorbereiten/Durchfuehren/Pruefen/Abschliessen)
- Abschliessen erst, wenn alles abgehakt ist; Meldung nennt die offenen Schritte
- Abgeschlossene Durchlaeufe sind gesperrt und bleiben als Nachweis
- Titel und Schritttexte werden beim Start kopiert, nicht verwiesen
- PDF-Export mit gezeichneten Ankreuzkaestchen und Unterschriftenzeilen
- Gemeinsame PDF-Grundlage fuer alle kuenftigen Ausgaben
- Ausbilder liest mit und darf das PDF abrufen, hakt aber nicht ab
- Abgeschlossene Durchlaeufe zaehlen im Wochenfortschritt mit

Behoben beim Durchstich: Die Fusszeile wurde unterhalb des Satzspiegels
gesetzt, wodurch PDFKit je Fusszeile eine leere Seite anhaengte - aus zwei
Seiten wurden sechs. Ein Test zaehlt jetzt die Seitenobjekte im PDF.

29 neue Tests, insgesamt 186.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 21:00:30 +02:00

246 lines
8.5 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 { index, integer, primaryKey, real, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
/** Rollen – bewusst nur zwei. */
export const rollen = ['ausbilder', 'azubi'] as const;
export type Rolle = (typeof rollen)[number];
export const benutzer = sqliteTable(
'benutzer',
{
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull(),
passwortHash: text('passwort_hash').notNull(),
rolle: text('rolle', { enum: rollen }).notNull(),
aktiv: integer('aktiv').notNull().default(1),
erstelltAm: text('erstellt_am').notNull(),
letzteAnmeldungAm: text('letzte_anmeldung_am'),
},
(t) => [uniqueIndex('benutzer_email_idx').on(t.email)],
);
export const sitzung = sqliteTable(
'sitzung',
{
id: text('id').primaryKey(),
benutzerId: text('benutzer_id')
.notNull()
.references(() => benutzer.id, { onDelete: 'cascade' }),
erstelltAm: text('erstellt_am').notNull(),
laeuftAbAm: text('laeuft_ab_am').notNull(),
},
(t) => [index('sitzung_benutzer_idx').on(t.benutzerId), index('sitzung_ablauf_idx').on(t.laeuftAbAm)],
);
export const ausbildung = sqliteTable(
'ausbildung',
{
id: text('id').primaryKey(),
azubiId: text('azubi_id')
.notNull()
.references(() => benutzer.id, { onDelete: 'cascade' }),
ausbilderId: text('ausbilder_id')
.notNull()
.references(() => benutzer.id),
beruf: text('beruf').notNull().default('Fachinformatiker/-in für Systemintegration'),
beginn: text('beginn').notNull(),
ende: text('ende'),
probezeitBis: text('probezeit_bis'),
erstelltAm: text('erstellt_am').notNull(),
},
(t) => [uniqueIndex('ausbildung_azubi_idx').on(t.azubiId), index('ausbildung_ausbilder_idx').on(t.ausbilderId)],
);
export const handoutGelesen = sqliteTable(
'handout_gelesen',
{
benutzerId: text('benutzer_id')
.notNull()
.references(() => benutzer.id, { onDelete: 'cascade' }),
handoutId: text('handout_id').notNull(),
gelesenAm: text('gelesen_am').notNull(),
},
(t) => [primaryKey({ columns: [t.benutzerId, t.handoutId] }), index('handout_gelesen_benutzer_idx').on(t.benutzerId)],
);
/** Zustände einer eingeplanten Woche – als Aufzählung, nicht als freier Text. */
export const wochenStatus = ['geplant', 'laufend', 'abgeschlossen'] as const;
export type WochenStatus = (typeof wochenStatus)[number];
export const wochenplanung = sqliteTable(
'wochenplanung',
{
id: text('id').primaryKey(),
ausbildungId: text('ausbildung_id')
.notNull()
.references(() => ausbildung.id, { onDelete: 'cascade' }),
wochenNummer: integer('wochen_nummer').notNull(),
geplantVon: text('geplant_von').notNull(),
geplantBis: text('geplant_bis').notNull(),
tatsaechlichVon: text('tatsaechlich_von'),
status: text('status', { enum: wochenStatus }).notNull().default('geplant'),
notiz: text('notiz'),
geaendertAm: text('geaendert_am').notNull(),
},
(t) => [
uniqueIndex('wochenplanung_woche_idx').on(t.ausbildungId, t.wochenNummer),
index('wochenplanung_termin_idx').on(t.ausbildungId, t.geplantVon),
index('wochenplanung_status_idx').on(t.ausbildungId, t.status),
],
);
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 const aufgabenStatus = ['offen', 'in_arbeit', 'zur_abnahme', 'abgenommen'] as const;
export type AufgabenStatus = (typeof aufgabenStatus)[number];
export const aufgabeStatus = sqliteTable(
'aufgabe_status',
{
id: text('id').primaryKey(),
benutzerId: text('benutzer_id')
.notNull()
.references(() => benutzer.id, { onDelete: 'cascade' }),
aufgabeId: text('aufgabe_id').notNull(),
status: text('status', { enum: aufgabenStatus }).notNull().default('offen'),
notiz: text('notiz').notNull().default(''),
kriterienErfuellt: text('kriterien_erfuellt').notNull().default('[]'),
bewertung: text('bewertung'),
rueckmeldung: text('rueckmeldung'),
begonnenAm: text('begonnen_am'),
abgegebenAm: text('abgegeben_am'),
abgenommenVon: text('abgenommen_von').references(() => benutzer.id),
abgenommenAm: text('abgenommen_am'),
geaendertAm: text('geaendert_am').notNull(),
},
(t) => [
uniqueIndex('aufgabe_status_idx').on(t.benutzerId, t.aufgabeId),
index('aufgabe_status_status_idx').on(t.status),
],
);
export const bezugTypen = ['aufgabe', 'checkliste', 'bericht'] as const;
export type BezugTyp = (typeof bezugTypen)[number];
export const anhang = sqliteTable(
'anhang',
{
id: text('id').primaryKey(),
bezugTyp: text('bezug_typ', { enum: bezugTypen }).notNull(),
bezugId: text('bezug_id').notNull(),
benutzerId: text('benutzer_id')
.notNull()
.references(() => benutzer.id, { onDelete: 'cascade' }),
dateiname: text('dateiname').notNull(),
mimeTyp: text('mime_typ').notNull(),
groesse: integer('groesse').notNull(),
pfad: text('pfad').notNull(),
beschriftung: text('beschriftung'),
hochgeladenAm: text('hochgeladen_am').notNull(),
},
(t) => [index('anhang_bezug_idx').on(t.bezugTyp, t.bezugId), index('anhang_benutzer_idx').on(t.benutzerId)],
);
export const checklisteLauf = sqliteTable(
'checkliste_lauf',
{
id: text('id').primaryKey(),
benutzerId: text('benutzer_id')
.notNull()
.references(() => benutzer.id, { onDelete: 'cascade' }),
checklisteId: text('checkliste_id').notNull(),
titel: text('titel').notNull(),
datum: text('datum').notNull(),
ticket: text('ticket'),
kunde: text('kunde'),
bemerkung: text('bemerkung'),
gestartetAm: text('gestartet_am').notNull(),
abgeschlossenAm: text('abgeschlossen_am'),
},
(t) => [
index('checkliste_lauf_benutzer_idx').on(t.benutzerId, t.datum),
index('checkliste_lauf_vorlage_idx').on(t.checklisteId),
],
);
export const checklisteSchritt = sqliteTable(
'checkliste_schritt',
{
id: text('id').primaryKey(),
laufId: text('lauf_id')
.notNull()
.references(() => checklisteLauf.id, { onDelete: 'cascade' }),
gruppeId: text('gruppe_id').notNull(),
schrittId: text('schritt_id').notNull(),
text: text('text').notNull(),
reihenfolge: integer('reihenfolge').notNull(),
erledigt: integer('erledigt').notNull().default(0),
bemerkung: text('bemerkung'),
erledigtAm: text('erledigt_am'),
},
(t) => [
uniqueIndex('checkliste_schritt_idx').on(t.laufId, t.schrittId),
index('checkliste_schritt_lauf_idx').on(t.laufId, t.reihenfolge),
],
);
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;
export type AufgabeStatus = typeof aufgabeStatus.$inferSelect;
export type Anhang = typeof anhang.$inferSelect;
export type ChecklisteLauf = typeof checklisteLauf.$inferSelect;
export type ChecklisteSchritt = typeof checklisteSchritt.$inferSelect;
/** Wird von der Schema-Abgleichprüfung im Test verwendet. */
export const alleTabellen = {
benutzer,
sitzung,
ausbildung,
handoutGelesen,
wochenplanung,
karte,
kartenVerlauf,
aufgabeStatus,
anhang,
checklisteLauf,
checklisteSchritt,
};