// Datenmodell des Planers. // Fachbegriffe auf Deutsch, technische Felder auf Englisch. generator client { provider = "prisma-client" output = "../src/generated/prisma" moduleFormat = "esm" } datasource db { provider = "postgresql" } enum Rolle { ADMIN BENUTZER } /// Ein Mensch, der sich anmelden darf. model Benutzer { id String @id @default(cuid()) email String @unique name String? /// Nur gesetzt, wenn die Anmeldung per Passwort erlaubt ist. passwortHash String? rolle Rolle @default(BENUTZER) aktiv Boolean @default(true) bildUrl String? emailGeprueft DateTime? zuletztAktiv DateTime? erstelltAm DateTime @default(now()) aktualisiertAm DateTime @updatedAt konten Konto[] passkeys Passkey[] audit AuditEintrag[] @@index([aktiv]) @@map("benutzer") } /// Verknuepfung zu einem OIDC-Anbieter (Entra ID, authentik). model Konto { id String @id @default(cuid()) benutzerId String type String provider String providerAccountId String refresh_token String? access_token String? expires_at Int? token_type String? scope String? id_token String? session_state String? benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) @@index([benutzerId]) @@map("konto") } /// Ein registrierter Passkey (WebAuthn) eines Benutzers. model Passkey { id String @id @default(cuid()) benutzerId String /// Base64URL der Credential-ID. credentialId String @unique publicKey Bytes counter BigInt @default(0) /// Kommagetrennt, z. B. "internal,hybrid". transports String? geraetetyp String? gesichert Boolean @default(false) bezeichnung String erstelltAm DateTime @default(now()) zuletztBenutzt DateTime? benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Cascade) @@index([benutzerId]) @@map("passkey") } /// Laufende WebAuthn-Herausforderung; kurzlebig, wird nach Gebrauch geloescht. model WebauthnChallenge { id String @id @default(cuid()) /// E-Mail bei der Anmeldung, Benutzer-ID bei der Registrierung. kennung String challenge String zweck String gueltigBis DateTime @@index([kennung, zweck]) @@map("webauthn_challenge") } /// Lebensbereich mit Farbe, Tageskapazitaet und Arbeitsfenster. model Bereich { id String @id @default(cuid()) name String @unique /// Hex-Farbe inklusive Raute, z. B. "#2563eb". farbe String reihenfolge Int @default(0) aktiv Boolean @default(true) /// Tageskapazitaet in Minuten, Montag bis Sonntag. kapazitaetMo Int @default(0) kapazitaetDi Int @default(0) kapazitaetMi Int @default(0) kapazitaetDo Int @default(0) kapazitaetFr Int @default(0) kapazitaetSa Int @default(0) kapazitaetSo Int @default(0) /// Arbeitsfenster als Minuten seit Mitternacht, Europe/Berlin. arbeitsfensterVon Int @default(480) arbeitsfensterBis Int @default(1020) erstelltAm DateTime @default(now()) aktualisiertAm DateTime @updatedAt aufgaben Aufgabe[] projekte Projekt[] @@index([aktiv, reihenfolge]) @@map("bereich") } enum AufgabenTyp { AUFGABE IDEE ARBEITSPAKET MEILENSTEIN SPIEGEL } enum AufgabenStatus { OFFEN GEPLANT WARTET ERLEDIGT VERWORFEN } /// Woher eine Aufgabe stammt. Integrationen upserten ueber [quelle, quelleRef]. enum Quelle { MANUELL ZAMMAD WARTUNGSPLANER TELEGRAM SPIEGEL MAIL API MCP } enum ProjektStatus { AKTIV PAUSIERT ABGESCHLOSSEN VERWORFEN } /// Klammer um mehrere Aufgaben. Hat selbst keinen Aufwand und keine /// Faelligkeit – beides ergibt sich aus den enthaltenen Aufgaben. model Projekt { id String @id @default(cuid()) name String beschreibung String? status ProjektStatus @default(AKTIV) bereichId String? /// Wunschtermin fuer den Abschluss, rein informativ. zielAm DateTime? erstelltAm DateTime @default(now()) aktualisiertAm DateTime @updatedAt bereich Bereich? @relation(fields: [bereichId], references: [id], onDelete: SetNull) aufgaben Aufgabe[] @@unique([name]) @@index([status]) @@index([bereichId]) @@map("projekt") } /// Eine Aufgabe, Idee, ein Arbeitspaket oder ein Meilenstein. /// /// Ohne Bereich liegt sie in der Inbox – das ist der einzige Unterschied, /// "einsortieren" heisst schlicht: einen Bereich setzen. model Aufgabe { id String @id @default(cuid()) titel String notiz String? typ AufgabenTyp @default(AUFGABE) status AufgabenStatus @default(OFFEN) /// 1 = dringend, 5 = irgendwann. 3 ist der Normalfall. prioritaet Int @default(3) /// Geschaetzter Aufwand in Minuten; null heisst "noch nicht geschaetzt". aufwandMinuten Int? faelligAm DateTime? erledigtAm DateTime? bereichId String? projektId String? /// Herkunft. Fuer von Hand erfasste Aufgaben MANUELL ohne Verweis. quelle Quelle @default(MANUELL) quelleUrl String? /// Kennung beim Ursprungssystem, z. B. die Ticketnummer. quelleRef String? erstelltAm DateTime @default(now()) aktualisiertAm DateTime @updatedAt bereich Bereich? @relation(fields: [bereichId], references: [id], onDelete: SetNull) projekt Projekt? @relation(fields: [projektId], references: [id], onDelete: SetNull) /// Integrationen upserten immer ueber diese beiden Felder. Von Hand /// erfasste Aufgaben haben quelleRef = null; PostgreSQL sieht NULL-Werte /// als verschieden an, sie behindern sich also nicht. @@unique([quelle, quelleRef]) @@index([status, faelligAm]) @@index([bereichId, status]) @@index([projektId]) @@index([typ, status]) @@map("aufgabe") } /// Unveraenderlicher Nachweis jeder schreibenden Aktion. model AuditEintrag { id String @id @default(cuid()) zeitpunkt DateTime @default(now()) benutzerId String? /// Bleibt erhalten, auch wenn der Benutzer spaeter geloescht wird. benutzerEmail String aktion String objektTyp String objektId String? /// Nur fachliche Felder, niemals Tokens, Mailinhalte oder Termin-Betreffs. details Json? instanz String benutzer Benutzer? @relation(fields: [benutzerId], references: [id], onDelete: SetNull) @@index([zeitpunkt]) @@index([objektTyp, objektId]) @@index([benutzerId]) @@map("audit_eintrag") }