// ============================================================================= // 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[] 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[] 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 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") }