Files
betreuungs-cockpit/prisma/schema.prisma
T
Florian Wilde 848aaf1b9f Absenderanschrift in jedem Schreiben
Jeder erzeugte Brief ging bisher ohne Ruecksendeadresse hinaus: Im
Absenderblock stand nur der Name, in der Datumszeile fehlte der Ort. Bei
einer Versicherung oder Behoerde wirkt das unserioes und macht es der
Gegenstelle unnoetig schwer zu antworten.

Neu unter "Mein Konto" ein Feld fuer Strasse, PLZ und Ort. Ab da stehen
sie im Absenderblock jedes Schreibens, und die Datumszeile lautet
"Bayreuth, den 23.08.2026".

Die Anschrift haengt am Benutzer, nicht an der betreuten Person: Sie
gehoert zu mir und gilt fuer alle Akten.

Nicht die Logik fehlte, sondern die Datenquelle - absenderBlock() und
datumsZeile() konnten die Felder schon, briefkopfBauen() hat sie mit
einem Kommentar auf null gesetzt, weil es sie im Datenmodell nicht gab.

Aufbau:
- Drei Felder am Benutzer, Migration ueber den Wrapper aus #24. Das war
  sein erster echter Einsatz: erzeugte Datei ohne Handarbeit brauchbar,
  DROP-INDEX-Zeilen bereits heraus.
- Ein Absender-Typ statt eines blossen Namens, durchgereicht bis in den
  Serienlauf - der hat bisher {name} gebaut und haette die Anschrift
  sonst still verloren.
- Zwei Platzhalter fuer eigene Vorlagen: betreuer_anschrift,
  betreuer_ort.

Was fehlt, bleibt weg: Jede der drei Angaben ist einzeln freiwillig. Nur
ein Ort ergibt eine Datumszeile mit Ort und einen Absenderblock, der
weiterhin nur aus dem Namen besteht. Eine halb ausgefuellte Adresse
sieht nach Fehler aus, ein blosser Name nach Absicht.

Geprueft: sieben Einheitstests fuer den Absenderblock inklusive jeder
Teilbefuellung und der Gegenprobe, dass nie die Heimadresse der
betreuten Person im Absender landet; vier End-to-End-Tests vom leeren
Zustand ueber das Speichern bis zum fertigen Brief.

Refs #21
2026-08-23 20:29:17 +02:00

1770 lines
52 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)
/// Die eigene Anschrift - Absenderblock und Ort in der Datumszeile
/// jedes erzeugten Schreibens. Sie gehoert zum Betreuer und nicht zur
/// betreuten Person: Sie gilt fuer alle Akten.
strasse String?
plz String?
ort String?
// Oberflaechen-Einstellungen
theme String @default("system") // "system" | "hell" | "dunkel"
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
zugaenge BetreuungsZugang[]
passkeys Passkey[]
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")
}
/// Ein registrierter Passkey (WebAuthn/FIDO2).
///
/// Der private Schluessel verlaesst das Geraet nie - hier liegt nur der
/// oeffentliche Teil. Ein gestohlener Datenbankabzug erlaubt deshalb keine
/// Anmeldung, anders als bei einem Passworthash, den man offline angreifen
/// kann.
///
/// Ein Passkey zaehlt bei uns als ZWEI Faktoren: Besitz des Geraets plus
/// die Nutzerverifikation darauf (Fingerabdruck, Gesicht oder Geraete-PIN).
/// Deshalb wird bei der Anmeldung "userVerified" erzwungen - siehe
/// src/lib/auth/passkey.ts. Ohne diese Pruefung waere es nur ein Faktor.
model Passkey {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
/// Kennung des Authenticators, base64url. Eindeutig ueber alle Benutzer -
/// derselbe Schluessel darf nicht an zwei Zugaengen haengen.
credentialId String @unique
/// Oeffentlicher Schluessel im COSE-Format. Kein Geheimnis.
publicKey Bytes
/// Signaturzaehler des Authenticators. Zaehlt er nicht hoch, obwohl er
/// es vorher tat, ist der Schluessel womoeglich geklont worden.
/// BigInt, weil der Zaehler bis 2^32-1 geht.
counter BigInt @default(0)
/// "usb", "nfc", "ble", "internal", "hybrid" - hilft dem Browser beim
/// naechsten Mal, das richtige Geraet vorzuschlagen.
transports String[] @default([])
/// Vom Benutzer vergeben ("iPhone von Florian"). Bei mehreren Geraeten
/// ist das die einzige Moeglichkeit, sie auseinanderzuhalten.
name String
/// "singleDevice" = liegt nur auf diesem Geraet, "multiDevice" = wird vom
/// Schluesselbund synchronisiert. Steht in der Uebersicht, weil es den
/// Unterschied macht, ob ein verlorenes Geraet den Zugang mitnimmt.
geraeteArt String?
/// Ob der Anbieter den Schluessel gesichert hat (iCloud, Google, Bitwarden).
gesichert Boolean @default(false)
erstelltAm DateTime @default(now())
zuletztBenutztAm DateTime?
@@index([userId])
@@map("passkey")
}
enum PasskeyAnfrageArt {
REGISTRIERUNG
ANMELDUNG
}
/// Kurzlebiger Speicher fuer die Challenge einer laufenden Passkey-Aktion.
///
/// Die Challenge muss serverseitig liegen und genau einmal gelten, sonst
/// liesse sich eine mitgeschnittene Antwort erneut einspielen. Verbraucht
/// wird sie durch DELETE - dass genau eine Zeile geloescht wurde, ist der
/// Beweis, dass sie noch nicht benutzt war.
///
/// Bewusst ohne Fremdschluessel auf User: die Zeile lebt fuenf Minuten und
/// ist kein Fachdatum. Der Wartungslauf raeumt Liegengebliebenes weg.
model PasskeyAnfrage {
id String @id @default(cuid())
/// base64url, zufaellig. Eindeutig, damit das Verbrauchen atomar ist.
challenge String @unique
art PasskeyAnfrageArt
/// Bei der Registrierung bekannt. Bei der Anmeldung nicht - dort weiss
/// der Server erst nach der Antwort, wer sich anmeldet.
userId String?
ablaufAm DateTime
erstelltAm DateTime @default(now())
@@index([ablaufAm])
@@map("passkey_anfrage")
}
// =============================================================================
// 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[]
zahlungsgruppen ZahlungsgruppeGeklaert[]
schreibvorlagen Schreibvorlage[]
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?
/// Die Person bei der Gegenstelle, die den Vorgang bearbeitet. Ein
/// namentlich adressiertes Schreiben landet bei ihr statt in der
/// Poststelle - beim Bezirk ist das der Unterschied zwischen zwei
/// Wochen und zwei Monaten. SetNull: Ein Vorgang darf nicht daran
/// haengen, dass es den Kontakt noch gibt.
ansprechpartnerId String?
ansprechpartner Kontakt? @relation("VorgangAnsprechpartner", fields: [ansprechpartnerId], references: [id], onDelete: SetNull)
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
}
/// Eine Gruppe wiederkehrender Zahlungen, zu der bewusst KEIN Vertrag
/// gefuehrt wird - Miete, Strom, Rundfunkbeitrag, Finanzamt.
///
/// Die Entscheidung haengt am normalisierten Namen der Gegenstelle, nicht
/// an den Buchungen. Nur so ueberlebt sie den naechsten Kontoauszug: Beim
/// Import entstehen neue Buchungszeilen, der Empfaenger bleibt derselbe.
///
/// ACHTUNG: Der Schluessel wird von normalisieren() in
/// src/lib/finanzen/wiederkehrend.ts gebildet. Aendert sich diese Funktion,
/// passen bestehende Schluessel nicht mehr und abgehakte Gruppen tauchen
/// wieder auf. Das ist unangenehm, aber ungefaehrlich - schlimmer waere,
/// eine Gruppe stillschweigend weiter zu verbergen, die inzwischen anders
/// zugeschnitten ist.
/// Eine eigene Schreibvorlage.
///
/// Die mitgelieferten Vorlagen (Rueckkaufswert, Kuendigung,
/// Sachstandsanfrage) stehen bewusst NICHT in dieser Tabelle - sie sind
/// Code, getestet und unveraenderlich. "Wiederherstellen" heisst: die
/// mitgelieferte Fassung als neue Kopie hierher holen.
///
/// Der Text traegt Platzhalter im Format {{name}} - aufgeloest in
/// src/lib/schreiben/platzhalter.ts. Er ist bewusst NICHT verschluesselt:
/// Eine Vorlage ist ein Textbaustein ohne Fallbezug; die Fachdaten kommen
/// erst beim Erzeugen hinein.
model Schreibvorlage {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
name String
betreff String @default("")
text String
/// Beim Speichern bestaetigt, dass der fehlende Bestellungsabsatz
/// Absicht ist - dann wird nicht bei jedem Bearbeiten erneut gewarnt.
ohneBestellungsabsatz Boolean @default(false)
erstelltAm DateTime @default(now())
aktualisiertAm DateTime @updatedAt
geloeschtAm DateTime?
@@index([betreuteId])
@@map("schreibvorlage")
}
model ZahlungsgruppeGeklaert {
id String @id @default(cuid())
betreuteId String
betreute Betreute @relation(fields: [betreuteId], references: [id], onDelete: Cascade)
/// Normalisierter Name der Gegenstelle.
schluessel String
/// Die Schreibweise, wie sie beim Abhaken auf dem Bildschirm stand -
/// damit spaeter noch erkennbar ist, worum es ging.
anzeigename String
/// Warum kein Vertrag? Freiwillig, aber in einem Jahr Gold wert.
begruendung String?
erstelltAm DateTime @default(now())
@@unique([betreuteId, schluessel])
@@index([betreuteId])
@@map("zahlungsgruppe_geklaert")
}
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?
/// Dauerzahlung zu einem Vertrag. Bei einem Dauerschuldverhaeltnis IST
/// der Vertrag der Beleg - fuer zwoelf gleiche Lastschriften gibt es
/// keine zwoelf Quittungen, sondern eine Police.
///
/// onDelete: SetNull ist Absicht und Teil der Zusage: Verschwindet der
/// Vertrag, faellt die Buchung zurueck in "Beleg fehlt". Ein Nachweis,
/// der auf etwas zeigt, das es nicht mehr gibt, waere schlimmer als
/// gar keiner.
vertragId String?
vertrag Vertrag? @relation("Vertragszahlung", fields: [vertragId], references: [id], onDelete: SetNull)
/// Wurde die Zuordnung beim Import automatisch gesetzt? Dann ist sie
/// eine Vermutung der Maschine und in der Liste als solche erkennbar.
vertragAutomatisch Boolean @default(false)
/// 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])
@@index([vertragId])
@@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[]
zahlungen Buchung[] @relation("Vertragszahlung")
@@index([betreuteId, status])
@@map("vertrag")
}
// =============================================================================
// Kontakte und Kontaktprotokoll
// =============================================================================
/// Anrede fuer die Briefanrede eines Kontakts.
enum KontaktAnrede {
OHNE
FRAU
HERR
}
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)
/// Fuer die Briefanrede. Ohne Angabe wird mit vollem Namen angeredet -
/// nie geraten, das faellt dem Empfaenger sofort auf.
anrede KontaktAnrede @default(OHNE)
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")
betreuteVorgaenge Vorgang[] @relation("VorgangAnsprechpartner")
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
PASSKEY_REGISTRIERT
PASSKEY_ENTFERNT
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")
}