Files
betreuungs-cockpit/prisma/schema.prisma
T
Florian Wilde 6c1915ab9c feat: Vollstaendiges Datenmodell als Prisma-Schema
Alle Fachentitaeten der Betreuung in einem Schema - bewusst komplett,
damit spaetere Etappen keine Migration am Fundament brauchen.

Kernentscheidungen:
- Geldbetraege durchgaengig als BigInt in Cent. Eine Rechnungslegung nach
  § 1865 BGB darf keine Rundungsfehler erben.
- Felder mit Suffix Verschluesselt tragen einen AES-256-GCM-Umschlag.
  Wo trotzdem gesucht werden muss (IBAN), steht ein HMAC-Blindindex daneben.
- Fristen und Stichtage sind @db.Date, nicht DateTime - sonst verschiebt
  die Zeitzone eine Frist um einen Tag.
- Frist.systemSchluessel macht die aus dem Bestellungsdatum abgeleiteten
  Pflichtfristen idempotent.
- Genehmigung.beschlussDokumentId ist die technische Voraussetzung dafuer,
  dass ein Vorgang ohne Beschluss nicht auf VOLLZOGEN gesetzt werden kann.
- DokumentVerknuepfung mit echten Fremdschluesseln statt loser Typ/ID-
  Spalte: eine Akte muss referenzsicher sein.
- Wertstand und Saldostand sind Historien - Werte werden nie ueberschrieben.

Dazu zwei SQL-Skripte, die Prisma nicht abbilden kann:
- volltextsuche.sql: tsvector-Trigger (deutsch) mit Gewichtung A/B/C,
  plus Trigram-Indizes fuer unscharfe Absendersuche.
- audit-schutz.sql: Trigger, die UPDATE und DELETE auf audit_log
  verweigern. Das Log ist Nachweis gegenueber Gericht und Miterben,
  also nur anhaengend - auch gegen psql.

Etappe: 1
Phase: Fundament
2026-08-22 01:36:36 +02:00

1568 lines
44 KiB
Plaintext

// =============================================================================
// Betreuungs-Cockpit - Datenmodell
// =============================================================================
//
// Konventionen in diesem Schema:
//
// 1. Geldbetraege liegen IMMER als BigInt in Cent vor ("...Cent"). Kein Float,
// keine Decimal-Konvertierung im Anzeigepfad - die Rechnungslegung nach
// § 1865 BGB darf keine Rundungsfehler erben.
//
// 2. Felder mit dem Suffix "Verschluesselt" enthalten einen AES-256-GCM-
// Umschlag (siehe src/lib/crypto/feld.ts), niemals Klartext. Sie lassen
// sich nicht filtern oder sortieren. Wo trotzdem gesucht werden muss,
// steht daneben ein "...Index"-Feld mit einem HMAC-Blindindex.
//
// 3. Alles, was fachlich zu einer betreuten Person gehoert, traegt eine
// betreuteId. Der Zugriffsschutz prueft grundsaetzlich gegen dieses Feld
// (Row-Level-Pruefung), nie nur im UI.
//
// 4. Loeschen ist ein Papierkorb: "geloeschtAm" wird gesetzt, der Datensatz
// bleibt 30 Tage bestehen und wird danach hart entfernt.
//
// 5. Datumsfelder ohne Uhrzeitbezug (Fristen, Bescheide, Stichtage) sind
// @db.Date - sonst verschiebt die Zeitzone eine Frist um einen Tag.
//
// =============================================================================
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// =============================================================================
// Benutzer, Rollen, Zugriff
// =============================================================================
enum Rolle {
BETREUER // voller Zugriff auf die zugeordneten Betreuten
MITLESER // nur lesend, je Modul freigegeben (z.B. Geschwister)
ADMIN // Benutzerverwaltung, Systemeinstellungen
}
enum Modul {
STAMMDATEN
VORGAENGE
FRISTEN
DOKUMENTE
FINANZEN
SOZIALHILFE
IMMOBILIE
VERTRAEGE
GESUNDHEIT
KONTAKTE
BERICHTE
}
model User {
id String @id @default(cuid())
email String @unique
name String
// Argon2id. Die Parameter stehen im Hash-String selbst.
passwortHash String
passwortGeaendertAm DateTime @default(now())
// TOTP ist Pflicht. Das Secret liegt verschluesselt, nicht im Klartext.
totpSecretVerschluesselt String?
totpAktiviertAm DateTime?
// Einmal-Wiederherstellungscodes, einzeln Argon2id-gehasht.
wiederherstellungscodes String[] @default([])
rolle Rolle @default(BETREUER)
aktiv Boolean @default(true)
letzteAnmeldung DateTime?
// Wird hochgezaehlt, um alle bestehenden Sitzungen ungueltig zu machen
// (Passwortwechsel, Rollenaenderung, Verdacht auf Kompromittierung).
sitzungsGeneration Int @default(0)
// Oberflaechen-Einstellungen
theme String @default("system") // "system" | "hell" | "dunkel"
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
zugaenge BetreuungsZugang[]
auditEintraege AuditLog[]
angelegteVorgaenge Vorgang[] @relation("VorgangAngelegtVon")
berichte Bericht[]
@@index([email])
@@map("benutzer")
}
/// Verbindet einen Benutzer mit einer betreuten Person und legt fest,
/// welche Module ein Mitleser sehen darf.
model BetreuungsZugang {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
rolle Rolle @default(BETREUER)
/// Nur fuer MITLESER ausgewertet. BETREUER sieht immer alles.
freigegebeneModule Modul[] @default([])
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
@@unique([userId, betreuteId])
@@index([betreuteId])
@@map("betreuungs_zugang")
}
/// Fehlgeschlagene und erfolgreiche Anmeldeversuche - Grundlage fuer das
/// Rate-Limiting am Login. Wird nach 30 Tagen aufgeraeumt.
model LoginVersuch {
id String @id @default(cuid())
email String
ipHash String?
erfolg Boolean
grund String?
zeitpunkt DateTime @default(now())
@@index([email, zeitpunkt])
@@index([zeitpunkt])
@@map("login_versuch")
}
// =============================================================================
// Betreute Person
// =============================================================================
enum Aufgabenkreis {
VERMOEGENSSORGE
GESUNDHEITSSORGE
AUFENTHALTSBESTIMMUNG
WOHNUNGSANGELEGENHEITEN
BEHOERDENANGELEGENHEITEN
POST_UND_FERNMELDEVERKEHR
RECHTS_ANTRAGS_UND_BEHOERDENVERTRETUNG
ENTSCHEIDUNG_UEBER_FREIHEITSENTZIEHENDE_MASSNAHMEN
SONSTIGES
}
model Betreute {
id String @id @default(cuid())
// --- Person -------------------------------------------------------------
vorname String
nachname String
geburtsname String?
geburtsdatum DateTime @db.Date
geburtsort String?
anrede String? // "Frau", "Herr", ...
// --- Aufenthalt / Heim ---------------------------------------------------
heimName String?
heimStrasse String?
heimPlz String?
heimOrt String?
heimZimmer String?
heimTelefon String?
heimEinzugAm DateTime? @db.Date
// Letzte eigene Wohnanschrift - bleibt fuer Behoerden relevant.
fruehereStrasse String?
frueherePlz String?
fruehererOrt String?
// --- Gesundheit (besondere Kategorie nach Art. 9 DSGVO) ------------------
krankenkasse String?
krankenversichertennummerVerschluesselt String?
pflegekasse String?
pflegegrad Int?
pflegegradSeit DateTime? @db.Date
pflegegradBescheidVom DateTime? @db.Date
schwerbehinderungGdB Int?
schwerbehinderungMerkzeichen String?
diagnosenVerschluesselt String?
/// Der nach § 1821 BGB zu ermittelnde Wille der betreuten Person.
/// Seit der Reform 2023 Leitschnur des Betreuerhandelns.
ermittelterWilleVerschluesselt String?
patientenverfuegungVorhanden Boolean @default(false)
vorsorgevollmachtVorhanden Boolean @default(false)
// --- Betreuungsgericht ---------------------------------------------------
betreuungsgericht String?
aktenzeichenGericht String?
/// Steuert das Betreuungsjahr und damit fast alle gesetzlichen Fristen.
bestellungAm DateTime? @db.Date
bestellungBis DateTime? @db.Date
aufgabenkreise Aufgabenkreis[] @default([])
aufgabenkreiseFreitext String?
/// Befreiter Betreuer nach § 1859 BGB (Angehoerige) - steuert, ob die
/// knappe Vermoegensuebersicht oder die volle Rechnungslegung faellig ist.
befreiterBetreuer Boolean @default(true)
ehrenamtlich Boolean @default(true)
// --- Sozialleistungstraeger ---------------------------------------------
aktenzeichenBezirk String?
aktenzeichenSozialamt String?
rentenversicherungsnummerVerschluesselt String?
steuerIdVerschluesselt String?
notizenVerschluesselt String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
zugaenge BetreuungsZugang[]
vorgaenge Vorgang[]
fristen Frist[]
dokumente Dokument[]
konten Konto[]
buchungen Buchung[]
vermoegensposten Vermoegensposten[]
immobilien Immobilie[]
vertraege Vertrag[]
kontakte Kontakt[]
kontaktprotokolle Kontaktprotokoll[]
gesundheitsereignisse Gesundheitsereignis[]
genehmigungen Genehmigung[]
sozialhilfeAntraege SozialhilfeAntrag[]
leistungen Leistung[]
heimkosten Heimkostenmonat[]
heimvertrag Heimvertrag?
tags Tag[]
verknuepfungen DokumentVerknuepfung[]
auditEintraege AuditLog[]
berichte Bericht[]
@@index([nachname, vorname])
@@map("betreute")
}
// =============================================================================
// Vorgaenge (Fallakte) und Genehmigungen
// =============================================================================
enum VorgangKategorie {
GERICHT
SOZIALHILFE
IMMOBILIE
VERSICHERUNG
HEIM_PFLEGE
VERTRAEGE
STEUERN
SONSTIGES
}
enum VorgangStatus {
OFFEN
IN_ARBEIT
WARTET_AUF_DRITTE
WARTET_AUF_GENEHMIGUNG
ERLEDIGT
}
model Vorgang {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
titel String
beschreibungVerschluesselt String?
kategorie VorgangKategorie
status VorgangStatus @default(OFFEN)
prioritaet Int @default(2) // 1 hoch, 2 normal, 3 niedrig
/// Behoerde oder Gegenstelle, die am Zug ist.
gegenstelle String?
aktenzeichen String?
verantwortlich String?
angelegtVonId String?
angelegtVon User? @relation("VorgangAngelegtVon", fields: [angelegtVonId], references: [id], onDelete: SetNull)
/// "Was ist der naechste konkrete Handgriff?" - treibt das Dashboard.
naechsterSchritt String?
faelligAm DateTime? @db.Date
/// Wann wurde zuletzt mit der Gegenstelle gesprochen/geschrieben?
/// Grundlage fuer die Kachel "wartet auf andere" und die Sachstandsanfrage.
letzterKontaktAm DateTime? @db.Date
/// Loest die feste Ablaufkette Mitteilung -> Antrag -> Anhoerung ->
/// Beschluss -> Rechtskraft -> Vollzug aus.
genehmigungspflichtig Boolean @default(false)
/// Aus welcher Vorlage wurde der Vorgang erzeugt (fuer Auswertung)?
vorlageSchluessel String?
erledigtAm DateTime?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
fristen Frist[]
buchungen Buchung[]
genehmigungen Genehmigung[]
kontaktprotokolle Kontaktprotokoll[]
aufgaben Aufgabe[]
verknuepfungen DokumentVerknuepfung[]
tags Tag[] @relation("VorgangTags")
kontakte Kontakt[] @relation("VorgangKontakte")
@@index([betreuteId, status])
@@index([betreuteId, kategorie])
@@index([faelligAm])
@@map("vorgang")
}
/// Einzelner Haken innerhalb eines Vorgangs. Vorgangs-Vorlagen bringen
/// ihre Aufgabenliste vorbefuellt mit.
model Aufgabe {
id String @id @default(cuid())
vorgangId String
vorgang Vorgang @relation(fields: [vorgangId], references: [id], onDelete: Cascade)
titel String
beschreibung String?
erledigt Boolean @default(false)
erledigtAm DateTime?
faelligAm DateTime? @db.Date
reihenfolge Int @default(0)
erstelltAm DateTime @default(now())
@@index([vorgangId])
@@map("aufgabe")
}
enum GenehmigungStatus {
GEPLANT // erkannt, noch nichts unternommen
MITGETEILT // Mitteilung ans Gericht raus
BEANTRAGT
ANGEHOERT // Anhoerung der Betreuten / des Verfahrenspflegers erfolgt
ERTEILT
VERSAGT
RECHTSKRAEFTIG
VOLLZOGEN // erst moeglich, wenn ein Beschlussdokument hinterlegt ist
}
model Genehmigung {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
vorgangId String
vorgang Vorgang @relation(fields: [vorgangId], references: [id], onDelete: Cascade)
titel String
/// Freitext, z.B. "§ 1850 Nr. 1 BGB" - bewusst nicht als Enum, damit
/// auch seltene Tatbestaende erfasst werden koennen.
rechtsgrundlage String
status GenehmigungStatus @default(GEPLANT)
mitteilungAm DateTime? @db.Date
antragAm DateTime? @db.Date
anhoerungAm DateTime? @db.Date
beschlussAm DateTime? @db.Date
rechtskraftAm DateTime? @db.Date
vollzugAm DateTime? @db.Date
/// Ohne dieses Beschlussdokument darf der Status nicht auf VOLLZOGEN
/// wechseln (§ 1858 BGB: Genehmigung vor dem Rechtsgeschaeft).
beschlussDokumentId String? @unique
beschlussDokument Dokument? @relation("Beschlussdokument", fields: [beschlussDokumentId], references: [id], onDelete: SetNull)
notizenVerschluesselt String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
verknuepfungen DokumentVerknuepfung[]
@@index([betreuteId, status])
@@index([vorgangId])
@@map("genehmigung")
}
// =============================================================================
// Fristen
// =============================================================================
enum FristTyp {
GESETZLICH // z.B. Jahresbericht § 1863 BGB
BEHOERDLICH // vom Bezirk oder Gericht gesetzt
SELBST_GESETZT
WIEDERVORLAGE
}
enum FristStatus {
OFFEN
ERLEDIGT
VERSCHOBEN
ENTFALLEN
}
model Frist {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
vorgangId String?
vorgang Vorgang? @relation(fields: [vorgangId], references: [id], onDelete: SetNull)
titel String
beschreibung String?
datum DateTime @db.Date
typ FristTyp @default(SELBST_GESETZT)
status FristStatus @default(OFFEN)
/// Freitext, z.B. "§ 1863 Abs. 3 BGB".
rechtsgrundlage String?
/// Tage vor dem Termin, an denen gewarnt wird.
vorwarnungTage Int[] @default([60, 30, 7, 1])
/// iCalendar-RRULE, z.B. "FREQ=YEARLY;INTERVAL=1". Beim Erledigen einer
/// wiederkehrenden Frist wird daraus die naechste erzeugt.
wiederholungRrule String?
/// Zeigt auf die Frist, aus der diese hervorgegangen ist.
vorgaengerFristId String? @unique
vorgaengerFrist Frist? @relation("Fristkette", fields: [vorgaengerFristId], references: [id], onDelete: SetNull)
nachfolgerFrist Frist? @relation("Fristkette")
/// Ausschlussfrist: laeuft sie ab, ist der Anspruch weg
/// (z.B. Aufwandspauschale § 1878 BGB). Wird deutlich rot markiert.
ausschlussfrist Boolean @default(false)
/// Automatisch aus dem Bestellungsdatum abgeleitete Pflichtfrist.
/// Verhindert doppeltes Anlegen beim erneuten Ableiten.
systemSchluessel String?
/// Wann wurde zuletzt eine Erinnerung verschickt (Vorwarnstufe in Tagen)?
letzteErinnerungStufe Int?
erledigtAm DateTime?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
verknuepfungen DokumentVerknuepfung[]
@@unique([betreuteId, systemSchluessel])
@@index([betreuteId, status, datum])
@@index([datum])
@@map("frist")
}
// =============================================================================
// Dokumente
// =============================================================================
enum DokumentRichtung {
EINGANG
AUSGANG
INTERN
}
enum DokumentArt {
BESCHEID
ANTRAG
SCHREIBEN
RECHNUNG
KONTOAUSZUG
VERTRAG
BESCHLUSS
URKUNDE
GUTACHTEN
AUSWEIS
PROTOKOLL
QUITTUNG
MAHNUNG
VOLLMACHT
ATTEST
GRUNDBUCHAUSZUG
FOTO
SONSTIGES
}
enum PosteingangStatus {
NEU
GESICHTET
ZUGEORDNET
BEANTWORTET
}
model Dokument {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
titel String
art DokumentArt @default(SONSTIGES)
richtung DokumentRichtung @default(EINGANG)
/// Datum des Schreibens (steht oben auf dem Brief).
schreibenVom DateTime? @db.Date
/// Wann es bei mir eingegangen ist - massgeblich fuer Rechtsbehelfsfristen.
eingangAm DateTime? @db.Date
versandAm DateTime? @db.Date
absender String?
empfaenger String?
aktenzeichen String?
// --- Datei im Objektspeicher --------------------------------------------
/// Schluessel im S3-Bucket. Ausgeliefert wird nur ueber kurzlebige
/// signierte URLs, niemals oeffentlich.
objektSchluessel String?
dateiname String?
mimeTyp String?
groesseBytes Int?
/// SHA-256 der Datei - Nachweis der Unveraendertheit gegenueber Gericht.
pruefsumme String?
seitenzahl Int?
/// Volltext aus der OCR (Tesseract, deutsch). Bleibt im Klartext, weil er
/// sonst nicht durchsuchbar waere; die Datei selbst liegt verschluesselt
/// im Objektspeicher. In der README als bewusste Abwaegung dokumentiert.
ocrText String?
ocrStatus String? // "ausstehend" | "laeuft" | "fertig" | "fehlgeschlagen" | "nicht_verfuegbar"
/// Von Postgres per Trigger gepflegter Suchvektor (deutsche Konfiguration).
suchvektor Unsupported("tsvector")?
posteingangStatus PosteingangStatus @default(NEU)
/// Wo liegt das Original im Papierordner? Papier bleibt Papier.
ablageortPapier String?
/// Wiedervorlage bzw. Ablaufdatum (z.B. Gueltigkeit Betreuerausweis).
wiedervorlageAm DateTime? @db.Date
ablaufAm DateTime? @db.Date
notizenVerschluesselt String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
verknuepfungen DokumentVerknuepfung[]
belegFuer Buchung[] @relation("Buchungsbeleg")
beschlussFuer Genehmigung? @relation("Beschlussdokument")
nachweisFuer Wertstand[] @relation("Wertnachweis")
tags Tag[] @relation("DokumentTags")
@@index([betreuteId, posteingangStatus])
@@index([betreuteId, eingangAm])
@@index([suchvektor], type: Gin)
@@map("dokument")
}
/// Verbindet ein Dokument mit genau einer Fachentitaet. Bewusst als eigene
/// Tabelle mit echten Fremdschluesseln - eine Akte muss referenzsicher sein.
/// Die Anwendung stellt sicher, dass genau eine Ziel-Spalte gesetzt ist.
model DokumentVerknuepfung {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
dokumentId String
dokument Dokument @relation(fields: [dokumentId], references: [id], onDelete: Cascade)
vorgangId String?
vorgang Vorgang? @relation(fields: [vorgangId], references: [id], onDelete: Cascade)
fristId String?
frist Frist? @relation(fields: [fristId], references: [id], onDelete: Cascade)
buchungId String?
buchung Buchung? @relation(fields: [buchungId], references: [id], onDelete: Cascade)
vermoegenspostenId String?
vermoegensposten Vermoegensposten? @relation(fields: [vermoegenspostenId], references: [id], onDelete: Cascade)
immobilieId String?
immobilie Immobilie? @relation(fields: [immobilieId], references: [id], onDelete: Cascade)
vertragId String?
vertrag Vertrag? @relation(fields: [vertragId], references: [id], onDelete: Cascade)
kontaktprotokollId String?
kontaktprotokoll Kontaktprotokoll? @relation(fields: [kontaktprotokollId], references: [id], onDelete: Cascade)
gesundheitsereignisId String?
gesundheitsereignis Gesundheitsereignis? @relation(fields: [gesundheitsereignisId], references: [id], onDelete: Cascade)
genehmigungId String?
genehmigung Genehmigung? @relation(fields: [genehmigungId], references: [id], onDelete: Cascade)
nachforderungId String?
nachforderung Nachforderung? @relation(fields: [nachforderungId], references: [id], onDelete: Cascade)
bescheidId String?
bescheid Bescheid? @relation(fields: [bescheidId], references: [id], onDelete: Cascade)
kontaktId String?
kontakt Kontakt? @relation(fields: [kontaktId], references: [id], onDelete: Cascade)
bemerkung String?
erstelltAm DateTime @default(now())
@@index([dokumentId])
@@index([betreuteId])
@@index([vorgangId])
@@map("dokument_verknuepfung")
}
model Tag {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
name String
farbe String @default("slate")
erstelltAm DateTime @default(now())
dokumente Dokument[] @relation("DokumentTags")
vorgaenge Vorgang[] @relation("VorgangTags")
@@unique([betreuteId, name])
@@map("tag")
}
// =============================================================================
// Geld: Konten, Kassenbuch, Vermoegen
// =============================================================================
enum KontoArt {
GIRO
SPAR
TAGESGELD
FESTGELD
DEPOT
VERWAHRGELD_HEIM // Taschengeldkonto in der Einrichtung
BARKASSE
SONSTIGES
}
model Konto {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
bezeichnung String
bank String?
art KontoArt @default(GIRO)
ibanVerschluesselt String?
/// HMAC-Blindindex der IBAN - erlaubt "gibt es dieses Konto schon?",
/// ohne die IBAN im Klartext zu speichern.
ibanIndex String?
bic String?
/// Konto des Betreuers, nicht der Betreuten. Buchungen darauf werden
/// als Auslage gefuehrt und loesen eine Warnung aus.
istBetreuerkonto Boolean @default(false)
eroeffnetAm DateTime? @db.Date
geschlossenAm DateTime? @db.Date
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
buchungen Buchung[]
saldostaende Saldostand[]
vermoegensposten Vermoegensposten[]
@@index([betreuteId])
@@index([ibanIndex])
@@map("konto")
}
/// Kontostand zu einem Stichtag. Wird nie ueberschrieben - jede
/// Vermoegensuebersicht muss spaeter reproduzierbar bleiben.
model Saldostand {
id String @id @default(cuid())
kontoId String
konto Konto @relation(fields: [kontoId], references: [id], onDelete: Cascade)
stichtag DateTime @db.Date
betragCent BigInt
quelle String? // "Kontoauszug Nr. 3", "Online-Banking", ...
erstelltAm DateTime @default(now())
@@unique([kontoId, stichtag])
@@index([kontoId, stichtag])
@@map("saldostand")
}
enum BuchungRichtung {
EINNAHME
AUSGABE
UMBUCHUNG
}
enum BuchungKategorie {
RENTE
PFLEGEKASSE
SOZIALHILFE
LANDESPFLEGEGELD
ZINSEN
MIETEINNAHME
SONSTIGE_EINNAHME
HEIMKOSTEN
BARBETRAG
BEKLEIDUNG
ARZT_UND_MEDIKAMENTE
ZUZAHLUNG
VERSICHERUNG
IMMOBILIE_BETRIEBSKOSTEN
IMMOBILIE_INSTANDHALTUNG
GRUNDSTEUER
STEUERN
GEBUEHREN_GERICHT
PORTO_UND_KOPIEN
FAHRTKOSTEN
TELEFON_INTERNET
RUNDFUNKBEITRAG
BESTATTUNGSVORSORGE
BANKGEBUEHREN
SONSTIGE_AUSGABE
UMBUCHUNG
}
model Buchung {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
kontoId String?
konto Konto? @relation(fields: [kontoId], references: [id], onDelete: SetNull)
vorgangId String?
vorgang Vorgang? @relation(fields: [vorgangId], references: [id], onDelete: SetNull)
datum DateTime @db.Date
wertstellung DateTime? @db.Date
betragCent BigInt
richtung BuchungRichtung
kategorie BuchungKategorie @default(SONSTIGE_AUSGABE)
gegenstelle String?
verwendungszweckVerschluesselt String?
belegId String?
beleg Dokument? @relation("Buchungsbeleg", fields: [belegId], references: [id], onDelete: SetNull)
/// Buchung ohne Beleg wird in der Liste markiert - Belegzwang.
belegFehltBegruendung String?
/// false = Auslage des Betreuers, die erstattet werden muss.
betrifftBetreute Boolean @default(true)
/// Verschiebt das Vermoegen (z.B. Verkaufserloes), nicht nur laufende
/// Einnahme/Ausgabe. Relevant fuer die Vermoegensuebersicht.
vermoegenswirksam Boolean @default(false)
/// Herkunft eines CSV-/CAMT-Imports, verhindert Doppelbuchungen.
importReferenz String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
verknuepfungen DokumentVerknuepfung[]
@@unique([kontoId, importReferenz])
@@index([betreuteId, datum])
@@index([betreuteId, kategorie])
@@map("buchung")
}
enum VermoegensArt {
KONTO
IMMOBILIE
VERSICHERUNG
FAHRZEUG
WERTGEGENSTAND
FORDERUNG
VERBINDLICHKEIT
SONSTIGES
}
/// Bewertung eines Postens im Sozialhilfeverfahren. Das ist MEINE
/// Dokumentation und Argumentation, keine automatische Rechtsauskunft.
enum SozialhilfeBewertung {
UNGEPRUEFT
VERWERTBAR
GESCHUETZT
STRITTIG
}
model Vermoegensposten {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
bezeichnung String
art VermoegensArt
// Optionale Verbindung zum konkreten Objekt.
kontoId String?
konto Konto? @relation(fields: [kontoId], references: [id], onDelete: SetNull)
immobilieId String?
immobilie Immobilie? @relation(fields: [immobilieId], references: [id], onDelete: SetNull)
vertragId String?
vertrag Vertrag? @relation(fields: [vertragId], references: [id], onDelete: SetNull)
/// Passiva werden als eigener Posten mit Art VERBINDLICHKEIT gefuehrt;
/// der Wert bleibt positiv, das Vorzeichen ergibt sich aus der Art.
bewertungSozialhilfe SozialhilfeBewertung @default(UNGEPRUEFT)
/// Warum halte ich den Posten fuer geschuetzt? Verweis auf § 90 SGB XII,
/// Schonvermoegen, Bestattungsvorsorge, selbst bewohntes Hausgrundstueck.
bewertungBegruendungVerschluesselt String?
notizenVerschluesselt String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
wertstaende Wertstand[]
verknuepfungen DokumentVerknuepfung[]
@@index([betreuteId, art])
@@map("vermoegensposten")
}
/// Ein Wert zu einem Stichtag. Historie, nichts wird ueberschrieben.
model Wertstand {
id String @id @default(cuid())
vermoegenspostenId String
vermoegensposten Vermoegensposten @relation(fields: [vermoegenspostenId], references: [id], onDelete: Cascade)
stichtag DateTime @db.Date
wertCent BigInt
/// Woher stammt der Wert? "Rueckkaufswertmitteilung vom 12.03.2026",
/// "Maklergutachten", "Bodenrichtwert", "eigene Schaetzung".
wertquelle String
nachweisDokumentId String?
nachweisDokument Dokument? @relation("Wertnachweis", fields: [nachweisDokumentId], references: [id], onDelete: SetNull)
bemerkung String?
erstelltAm DateTime @default(now())
@@unique([vermoegenspostenId, stichtag])
@@index([vermoegenspostenId, stichtag])
@@map("wertstand")
}
// =============================================================================
// Immobilie
// =============================================================================
enum ImmobilienStatusSozialhilfe {
UNGEKLAERT
SELBST_BEWOHNT_GESCHUETZT
HAERTEFALL_GELTEND_GEMACHT
DARLEHEN_91_SGB_XII
VERKAUF_LAEUFT
VERWERTET
}
model Immobilie {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
bezeichnung String
strasse String?
plz String?
ort String?
grundbuchamt String?
grundbuchBlatt String?
gemarkung String?
flurstueck String?
eigentumsanteil String? // "1/1", "1/2", ...
wohnflaecheQm Int?
grundstueckQm Int?
baujahr Int?
bewohntStatus String? // "leerstehend", "vermietet", "selbst bewohnt"
leerstandSeit DateTime? @db.Date
statusSozialhilfe ImmobilienStatusSozialhilfe @default(UNGEKLAERT)
statusBegruendungVerschluesselt String?
notizenVerschluesselt String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
belastungen Belastung[]
checklistenPunkte ImmobilienCheckpunkt[]
vermoegensposten Vermoegensposten[]
verknuepfungen DokumentVerknuepfung[]
@@index([betreuteId])
@@map("immobilie")
}
model Belastung {
id String @id @default(cuid())
immobilieId String
immobilie Immobilie @relation(fields: [immobilieId], references: [id], onDelete: Cascade)
art String // "Grundschuld", "Wohnrecht", "Niessbrauch", "Reallast"
abteilung String? // Grundbuch-Abteilung II oder III
laufendeNr String?
glaeubiger String?
betragCent BigInt?
bemerkung String?
geloeschtImGrundbuchAm DateTime? @db.Date
erstelltAm DateTime @default(now())
@@index([immobilieId])
@@map("belastung")
}
/// Pflichten bei Leerstand: Versicherung informiert, Frostschutz,
/// Winterdienst, Zaehlerstaende, Schluesselverzeichnis, Fotodoku.
model ImmobilienCheckpunkt {
id String @id @default(cuid())
immobilieId String
immobilie Immobilie @relation(fields: [immobilieId], references: [id], onDelete: Cascade)
titel String
beschreibung String?
erledigt Boolean @default(false)
erledigtAm DateTime? @db.Date
faelligAm DateTime? @db.Date
/// Wiederkehrend (z.B. Kontrollgang, Zaehlerstand) als RRULE.
wiederholungRrule String?
reihenfolge Int @default(0)
erstelltAm DateTime @default(now())
@@index([immobilieId])
@@map("immobilien_checkpunkt")
}
// =============================================================================
// Versicherungen und Vertraege
// =============================================================================
enum VertragArt {
LEBENSVERSICHERUNG
STERBEGELDVERSICHERUNG
BESTATTUNGSVORSORGE
RENTENVERSICHERUNG_PRIVAT
UNFALLVERSICHERUNG
HAFTPFLICHT
HAUSRAT
WOHNGEBAEUDE
KRANKENZUSATZ
PFLEGEZUSATZ
RECHTSSCHUTZ
STERBEKASSE
STROM
GAS
WASSER
TELEFON_INTERNET
RUNDFUNKBEITRAG
MITGLIEDSCHAFT
ABONNEMENT
SONSTIGES
}
enum VertragStatus {
AKTIV
BEITRAGSFREI
RUHEND
GEKUENDIGT
ABGETRETEN
VERWERTET
ABGELAUFEN
}
enum ZahlRhythmus {
MONATLICH
VIERTELJAEHRLICH
HALBJAEHRLICH
JAEHRLICH
EINMALIG
}
model Vertrag {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
bezeichnung String
gesellschaft String?
art VertragArt
status VertragStatus @default(AKTIV)
vertragsnummerVerschluesselt String?
beginnAm DateTime? @db.Date
endeAm DateTime? @db.Date
/// Kuendigungsfrist in Monaten zum Ende der Laufzeit.
kuendigungsfristMonate Int?
kuendigungZum DateTime? @db.Date
gekuendigtAm DateTime? @db.Date
beitragCent BigInt?
rhythmus ZahlRhythmus?
/// Bewertung fuer das Sozialamt - meine Argumentation, keine Auskunft.
bewertungSozialhilfe SozialhilfeBewertung @default(UNGEPRUEFT)
bewertungBegruendungVerschluesselt String?
/// Wann wurde zuletzt ein Rueckkaufswert angefordert? Steuert die
/// Wiedervorlage nach vier Wochen.
rueckkaufswertAngefragtAm DateTime? @db.Date
notizenVerschluesselt String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
vermoegensposten Vermoegensposten[]
verknuepfungen DokumentVerknuepfung[]
@@index([betreuteId, status])
@@map("vertrag")
}
// =============================================================================
// Kontakte und Kontaktprotokoll
// =============================================================================
enum KontaktRolle {
RECHTSPFLEGER
RICHTER
BETREUUNGSBEHOERDE
SACHBEARBEITER_BEZIRK
SACHBEARBEITER_SOZIALAMT
HEIMLEITUNG
PFLEGEDIENSTLEITUNG
SOZIALDIENST_HEIM
HAUSARZT
FACHARZT
KRANKENKASSE
PFLEGEKASSE
RENTENVERSICHERUNG
NOTAR
MAKLER
VERSICHERUNGSMAKLER
VERSICHERUNG
STEUERBERATER
RECHTSANWALT
BANK
HANDWERKER
ANGEHOERIGER
SONSTIGES
}
model Kontakt {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
name String
organisation String?
rolle KontaktRolle @default(SONSTIGES)
telefon String?
mobil String?
fax String?
email String?
strasse String?
plz String?
ort String?
/// Aktenzeichen, unter dem dieser Kontakt den Vorgang fuehrt.
aktenzeichen String?
notizenVerschluesselt String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
protokolle Kontaktprotokoll[]
vorgaenge Vorgang[] @relation("VorgangKontakte")
verknuepfungen DokumentVerknuepfung[]
@@index([betreuteId, rolle])
@@map("kontakt")
}
enum KontaktArt {
BESUCH
TELEFONAT
EMAIL
BRIEF
VIDEOCALL
FAX
VORSPRACHE
}
/// Grundlage des Jahresberichts nach § 1863 BGB: persoenliche Kontakte,
/// Wuensche der betreuten Person, daraus folgende Aufgaben.
model Kontaktprotokoll {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
kontaktId String?
kontakt Kontakt? @relation(fields: [kontaktId], references: [id], onDelete: SetNull)
vorgangId String?
vorgang Vorgang? @relation(fields: [vorgangId], references: [id], onDelete: SetNull)
datum DateTime
art KontaktArt
dauerMinuten Int?
gespraechspartner String?
/// true = persoenlicher Kontakt zur betreuten Person selbst.
mitBetreuter Boolean @default(false)
inhaltVerschluesselt String?
/// Geaeusserte Wuensche - fuer § 1821 BGB und den Jahresbericht.
wuenscheVerschluesselt String?
folgeaufgabenVerschluesselt String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
verknuepfungen DokumentVerknuepfung[]
@@index([betreuteId, datum])
@@map("kontaktprotokoll")
}
// =============================================================================
// Gesundheit und Pflege
// =============================================================================
enum GesundheitsArt {
ARZTBESUCH
KRANKENHAUS
EXAZERBATION
STURZ
MEDIKATIONSAENDERUNG
SAUERSTOFFVERSORGUNG
INHALATION
HILFSMITTEL
IMPFUNG
REHA
PFLEGEBEGUTACHTUNG
HEILMITTEL
SONSTIGES
}
model Gesundheitsereignis {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
datum DateTime @db.Date
bisDatum DateTime? @db.Date // z.B. Ende eines Krankenhausaufenthalts
art GesundheitsArt
titel String
beschreibungVerschluesselt String?
behandler String?
einrichtung String?
/// Abwesenheitstage mindern den Heimeigenanteil - deshalb hier zaehlbar.
abwesenheitstage Int?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
verknuepfungen DokumentVerknuepfung[]
@@index([betreuteId, datum])
@@map("gesundheitsereignis")
}
model Heimvertrag {
id String @id @default(cuid())
betreuteId String @unique
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
einrichtung String
traeger String?
strasse String?
plz String?
ort String?
vertragAm DateTime? @db.Date
einzugAm DateTime? @db.Date
auszugAm DateTime? @db.Date
kuendigungsfristTage Int? @default(14)
// Aktuelle Saetze - historische Werte stehen in Heimkostenmonat.
pflegeEigenanteilCent BigInt? // einrichtungseinheitlicher Eigenanteil
unterkunftVerpflegungCent BigInt?
investitionskostenCent BigInt?
ausbildungsumlageCent BigInt?
notizenVerschluesselt String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
@@map("heimvertrag")
}
/// Monatliche Gegenueberstellung Heimrechnung gegen Deckungsquellen.
model Heimkostenmonat {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
jahr Int
monat Int // 1..12
// Kostenseite
pflegeEigenanteilCent BigInt @default(0)
unterkunftVerpflegungCent BigInt @default(0)
investitionskostenCent BigInt @default(0)
ausbildungsumlageCent BigInt @default(0)
zusatzleistungenCent BigInt @default(0)
// Deckungsseite
pflegekasseCent BigInt @default(0) // § 43 SGB XI
/// Leistungszuschlag nach § 43c SGB XI, abhaengig von der Verweildauer.
leistungszuschlagProzent Int @default(0)
leistungszuschlagCent BigInt @default(0)
renteCent BigInt @default(0)
sonstigeEinkuenfteCent BigInt @default(0)
sozialhilfeCent BigInt @default(0)
eigenmittelCent BigInt @default(0)
// Der betreuten Person verbleibende Betraege
barbetragCent BigInt @default(0)
bekleidungspauschaleCent BigInt @default(0)
abwesenheitstage Int @default(0)
rechnungDokumentId String?
bemerkung String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
@@unique([betreuteId, jahr, monat])
@@index([betreuteId, jahr])
@@map("heimkostenmonat")
}
// =============================================================================
// Sozialhilfe (Bezirk Oberfranken)
// =============================================================================
enum NachforderungStatus {
OFFEN
IN_BESCHAFFUNG
UEBERSANDT
ANERKANNT
NICHT_VORHANDEN
}
enum BescheidArt {
BEWILLIGUNG
ABLEHNUNG
AENDERUNG
AUFHEBUNG
ERSTATTUNG
VORSCHUSS
WIDERSPRUCHSBESCHEID
}
enum BescheidPruefStatus {
UNGEPRUEFT
GEPRUEFT_OK
WIDERSPRUCH_EINGELEGT
WIDERSPRUCH_ERLEDIGT
FRIST_VERSTRICHEN
}
model SozialhilfeAntrag {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
traeger String @default("Bezirk Oberfranken")
leistungsart String @default("Hilfe zur Pflege nach dem 7. Kapitel SGB XII")
aktenzeichen String?
sachbearbeiter String?
telefon String?
email String?
antragAm DateTime? @db.Date
eingangBeimTraegerAm DateTime? @db.Date
bewilligtVon DateTime? @db.Date
bewilligtBis DateTime? @db.Date
notizenVerschluesselt String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
nachforderungen Nachforderung[]
bescheide Bescheid[]
@@index([betreuteId])
@@map("sozialhilfe_antrag")
}
/// Jede vom Traeger angeforderte Unterlage als eigene Zeile.
model Nachforderung {
id String @id @default(cuid())
antragId String
antrag SozialhilfeAntrag @relation(fields: [antragId], references: [id], onDelete: Cascade)
bezeichnung String
erlaeuterung String?
status NachforderungStatus @default(OFFEN)
angefordertAm DateTime? @db.Date
fristBis DateTime? @db.Date
uebersandtAm DateTime? @db.Date
/// Wenn die Unterlage nicht existiert: warum nicht? Geht als Erklaerung
/// mit ins Unterlagenpaket.
begruendungNichtVorhanden String?
reihenfolge Int @default(0)
/// Vorbefuellte Standardunterlage aus der Checkliste.
ausCheckliste Boolean @default(false)
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
verknuepfungen DokumentVerknuepfung[]
@@index([antragId, status])
@@map("nachforderung")
}
model Bescheid {
id String @id @default(cuid())
antragId String
antrag SozialhilfeAntrag @relation(fields: [antragId], references: [id], onDelete: Cascade)
art BescheidArt
bescheidVom DateTime @db.Date
/// Massgeblich fuer den Fristbeginn - nicht das Datum auf dem Bescheid.
zugegangenAm DateTime? @db.Date
zeitraumVon DateTime? @db.Date
zeitraumBis DateTime? @db.Date
betragCent BigInt?
/// In der Regel ein Monat ab Bekanntgabe (§ 84 SGG). Bei fehlender oder
/// falscher Rechtsbehelfsbelehrung ein Jahr - deshalb speicherbar.
rechtsbehelfsfristMonate Int @default(1)
widerspruchMoeglichBis DateTime? @db.Date
pruefStatus BescheidPruefStatus @default(UNGEPRUEFT)
widerspruchEingelegtAm DateTime? @db.Date
notizenVerschluesselt String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
verknuepfungen DokumentVerknuepfung[]
@@index([antragId, bescheidVom])
@@map("bescheid")
}
enum LeistungStatus {
NICHT_GEPRUEFT
PRUEFUNG_LAEUFT
BEANTRAGT
BEWILLIGT
ABGELEHNT
LAEUFT
NICHT_ZUTREFFEND
}
/// Katalog weiterer Leistungen: Landespflegegeld, Zuzahlungsbefreiung,
/// Rundfunkbeitragsbefreiung, Schwerbehindertenausweis, Grundsicherung.
model Leistung {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
bezeichnung String
traeger String?
rechtsgrundlage String?
status LeistungStatus @default(NICHT_GEPRUEFT)
beantragtAm DateTime? @db.Date
bewilligtAm DateTime? @db.Date
gueltigBis DateTime? @db.Date
betragCent BigInt?
rhythmus ZahlRhythmus?
/// Muss jaehrlich neu beantragt werden (Landespflegegeld, Zuzahlung).
jaehrlichNeuBeantragen Boolean @default(false)
/// Stabiler Schluessel aus dem Katalog, verhindert Dubletten beim Seed.
katalogSchluessel String?
notizen String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
@@unique([betreuteId, katalogSchluessel])
@@index([betreuteId, status])
@@map("leistung")
}
// =============================================================================
// Berichte
// =============================================================================
enum BerichtArt {
VERMOEGENSVERZEICHNIS // § 1835 BGB
JAHRESBERICHT // § 1863 BGB
VERMOEGENSUEBERSICHT // § 1859 BGB (befreiter Betreuer)
RECHNUNGSLEGUNG // § 1865 BGB
BELEGMAPPE
UNTERLAGENPAKET_BEZIRK
VOLLEXPORT
}
/// Erzeugte Berichte werden protokolliert - was habe ich wann in welcher
/// Fassung abgegeben? Der Entwurfstext bleibt fuer die Ueberarbeitung stehen.
model Bericht {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
art BerichtArt
titel String
zeitraumVon DateTime? @db.Date
zeitraumBis DateTime? @db.Date
stichtag DateTime? @db.Date
/// Vom Nutzer ueberarbeiteter Freitext (Jahresbericht) - verschluesselt.
entwurfVerschluesselt String?
/// Abgelegte PDF-Fassung im Objektspeicher.
objektSchluessel String?
dateiname String?
pruefsumme String?
erstelltVonId String?
erstelltVon User? @relation(fields: [erstelltVonId], references: [id], onDelete: SetNull)
abgegebenAm DateTime? @db.Date
abgegebenAn String?
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
@@index([betreuteId, art])
@@map("bericht")
}
// =============================================================================
// Audit-Log
// =============================================================================
enum AuditAktion {
ERSTELLT
GEAENDERT
GELOESCHT
WIEDERHERGESTELLT
ENDGUELTIG_GELOESCHT
ANGESEHEN
EXPORTIERT
DOKUMENT_HERUNTERGELADEN
ANMELDUNG
ANMELDUNG_FEHLGESCHLAGEN
ABMELDUNG
ZWEITER_FAKTOR_EINGERICHTET
PASSWORT_GEAENDERT
ROLLE_GEAENDERT
FREIGABE_GEAENDERT
}
/// Unveraenderlich und nur anhaengend. Ein Datenbank-Trigger
/// (prisma/sql/audit-schutz.sql) verhindert UPDATE und DELETE.
/// Zugleich mein Nachweis gegenueber Gericht und moeglichen Miterben.
model AuditLog {
id String @id @default(cuid())
zeitpunkt DateTime @default(now())
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
/// Momentaufnahme - bleibt lesbar, auch wenn der Benutzer geloescht wird.
userEmail String?
userName String?
betreuteId String?
betreute Betreute? @relation(fields: [betreuteId], references: [id], onDelete: SetNull)
aktion AuditAktion
entitaet String // Modellname, z.B. "Betreute"
entitaetId String?
bezeichnung String? // lesbarer Titel des betroffenen Datensatzes
/// Geaenderte Felder als JSON {feld: {alt, neu}} - verschluesselt, weil
/// dort Gesundheits- und Finanzdaten stehen koennen.
aenderungVerschluesselt String?
ipHash String?
userAgent String?
@@index([betreuteId, zeitpunkt])
@@index([userId, zeitpunkt])
@@index([entitaet, entitaetId])
@@map("audit_log")
}