Files
planer/prisma/schema.prisma
T
Florian WildeandClaude Opus 5 63c3c15a3c feat(aufgaben): Datenmodell und Regeln für Aufgaben und Projekte
Aufgabe mit Typ, Status, Priorität, Aufwand, Fälligkeit und Herkunft;
Projekt als eigene Klammer darum.

Zwei Entwurfsentscheidungen:
- Die Inbox ist kein eigener Zustand, sondern schlicht eine Aufgabe ohne
  Bereich. Einsortieren heißt: einen Bereich setzen. Ein zusätzliches
  Feld könnte mit der Zuordnung auseinanderlaufen.
- Projekt ist ein eigenes Modell, kein Aufgabentyp. Ein Projekt hat
  selbst weder Aufwand noch Fälligkeit, sondern leitet beides aus dem
  ab, was drinsteckt. ARBEITSPAKET und MEILENSTEIN sind Typen der
  Aufgaben *innerhalb* eines Projekts.

@@unique([quelle, quelleRef]) ist die Grundlage für die Integrationen ab
M6: sie upserten darüber, statt zu doppeln. Von Hand erfasste Aufgaben
tragen quelleRef = null; PostgreSQL sieht NULL-Werte als verschieden an,
sie behindern sich also nicht.

Verworfene Aufgaben zählen bewusst gar nicht in den Fortschritt eines
Projekts — sonst sähe es fertig aus, weil die Hälfte gestrichen wurde.

Fälligkeit wird in Kalendertagen in Europe/Berlin gerechnet, nicht in
24-Stunden-Schritten: was heute um 23:00 fällig ist, ist heute fällig.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 16:17:41 +02:00

255 lines
6.5 KiB
Plaintext
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.
// 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")
}