- 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>
246 lines
8.5 KiB
TypeScript
246 lines
8.5 KiB
TypeScript
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,
|
||
};
|