Files
betreuungs-cockpit/docs/schema-wuensche/substanz.md
T
Florian Wilde 22b3a9b9e9 feat: Stammdaten, Ersteinrichtung, Zweitfaktor und Protokollansicht
Damit ist Etappe 1 fachlich vollstaendig.

Ersteinrichtung in drei Schritten, bewusst ausserhalb des
Anwendungsrahmens: der setzt Benutzer, zweiten Faktor UND eine betreute
Person voraus - beim allerersten Start gibt es nichts davon. Die Aktion
fuer den ersten Zugang sperrt sich selbst, sobald ein Benutzer existiert.

Zweiter Faktor: Das Geheimnis wird zuerst nur abgelegt, ohne
totpAktiviertAm. Erst ein echter Code aus der App schaltet ihn scharf.
Wer sich beim Abtippen vertut oder die falsche App nimmt, wuerde sich
sonst dauerhaft aussperren. Erst danach gibt es die
Wiederherstellungscodes - einmal angezeigt, danach nur noch als Hash.

Betreuungsjahr (src/lib/stammdaten/betreuungsjahr.ts): laeuft ab dem Tag
der Bestellung, nicht ab dem Kalenderjahr. Schrittweise vorwaerts
gerechnet statt ueber eine Differenz, damit Schaltjahre und der
29. Februar von selbst stimmen. Daran haengen Jahresbericht,
Rechnungslegung und die Ausschlussfrist der Aufwandspauschale.

Aendert sich das Bestellungsdatum, werden die Pflichtfristen neu
abgeleitet - ueber einen dynamischen Import, damit die Stammdaten auch
dann speicherbar bleiben, wenn das Fristenmodul fehlt.

Protokollansicht zeigt je Eintrag die geaenderten Felder als alt -> neu.
Die Aenderungen liegen verschluesselt, weil dort Diagnosen und
Kontostaende stehen koennen.

Seed legt bewusst keine echten Personendaten an, nur den ersten Zugang.
Mit SEED_DEMO=true kommt ein vollstaendiger Beispielfall dazu - Erika
Musterfrau, COPD GOLD III, Pflegegrad 3, Heimeinzug Januar 2026,
leerstehendes Haus, zwei Versicherungen, laufender Antrag beim Bezirk
mit acht Nachforderungen. Nur ausserhalb der Produktion.

Docker Compose ohne Reverse Proxy - der ist auf dem Zielserver
vorhanden. Die Datenbank hat keine Portfreigabe und ist nur ueber das
interne Netz erreichbar; die Anwendung horcht auf 127.0.0.1.

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

5.7 KiB
Raw Blame History

Schema-Wünsche aus Etappe 6 „Substanz“

Das Schema wurde nicht angefasst. Hier steht, was beim Bauen von Immobilie, Verträgen und Pflege gefehlt hat — mit der Umgehung, die stattdessen gewählt wurde, damit später klar ist, was zu migrieren wäre.


1. Buchung.immobilieId

Problem. Das Kostenkonto der Immobilie (Grundsteuer, Gebäude­versicherung, Energie, Wasser, Instandhaltung, Schornsteinfeger) kann nur über die Kategorien GRUNDSTEUER, IMMOBILIE_BETRIEBSKOSTEN, IMMOBILIE_INSTANDHALTUNG und MIETEINNAHME zusammengestellt werden. Eine Zuordnung „diese Buchung gehört zu diesem Objekt“ gibt es nicht.

Folge. Solange nur ein Objekt geführt wird, ist die Zuordnung eindeutig. Bei einem zweiten Grundstück wären die Zahlen falsch. Die Oberfläche sagt das an der Auswertung ausdrücklich dazu.

Wunsch.

model Buchung {
  immobilieId String?
  immobilie   Immobilie? @relation(fields: [immobilieId], references: [id], onDelete: SetNull)
}

2. Eigene Buchungskategorie für die Gebäudeversicherung

BuchungKategorie.VERSICHERUNG sammelt alle Versicherungen ein, auch Sterbegeld und Haftpflicht. Für das Kostenkonto eines Hauses ist das zu grob; die Gebäudeversicherung muss dort erscheinen, die private Unfallversicherung nicht.

Umgehung. Die Gebäudeversicherung wird als IMMOBILIE_BETRIEBSKOSTEN gebucht. Das steht als Hinweis an der Auswertung.

Wunsch. IMMOBILIE_VERSICHERUNG in BuchungKategorie.


3. Inventarstueck — Inventarliste der Wertgegenstände

Problem. Für die Fotodokumentation und die Inventarliste eines leerstehenden Hauses fehlt ein Modell mit bezeichnung, raum, geschaetzterWertCent und verbleib. Vermoegensposten passt fachlich (Art WERTGEGENSTAND, Verknüpfung auf immobilieId, Wert über Wertstand, Fotos über DokumentVerknuepfung), hat aber keine Felder für Raum und Verbleib.

Umgehung. Raum, Verbleib und Bemerkung stehen als beschriftete Zeilen im ohnehin verschlüsselten notizenVerschluesselt; gelesen und geschrieben wird das ausschließlich über src/lib/immobilie/inventar.ts. Der geschätzte Wert liegt als Wertstand mit der Quelle „eigene Schätzung“.

Wunsch.

model Inventarstueck {
  id                   String    @id @default(cuid())
  betreuteId           String
  immobilieId          String?
  bezeichnung          String
  raum                 String?
  geschaetzterWertCent BigInt?
  verbleib             String?   // "im Haus", "Heimzimmer", "verschenkt", "entsorgt", "verkauft"
  bemerkungVerschluesselt String?
}

4. Immobilie.checklisteUebernommenAm

Ob die Leerstands-Checkliste schon einmal übernommen wurde, lässt sich nur daran ablesen, ob ImmobilienCheckpunkt-Zeilen existieren. Das genügt, weil die Übernahme über den stabilen Titel abgleicht und keine Dubletten erzeugt — sauberer wäre ein systemSchluessel am Checkpunkt.

Wunsch.

model ImmobilienCheckpunkt {
  systemSchluessel String?
  @@unique([immobilieId, systemSchluessel])
}

Bis dahin dient der titel als Schlüssel; er darf deshalb beim Bearbeiten eines übernommenen Punktes nicht geändert werden, ohne dass die nächste Übernahme ihn erneut anlegt.


5. Pflegegrad-Historie als eigenes Modell

Betreute trägt den aktuellen Pflegegrad, seit wann und das Bescheiddatum. Frühere Grade gehen verloren, sobald der Grad hochgestuft wird — für den Jahresbericht und für eine Höherstufung ist aber genau die Kette interessant.

Umgehung. Jede Begutachtung wird als Gesundheitsereignis mit der Art PFLEGEBEGUTACHTUNG erfasst; der Verlauf zeigt daraus die Historie. Der Grad selbst steht im Titel und in der verschlüsselten Beschreibung, ist also nicht auswertbar.

Wunsch.

model Pflegegradstand {
  id            String    @id @default(cuid())
  betreuteId    String
  grad          Int
  gueltigAb     DateTime  @db.Date
  bescheidVom   DateTime? @db.Date
  antragAm      DateTime? @db.Date
  ergebnis      String?   // "hochgestuft", "abgelehnt", "unverändert"
}

6. Strukturierte Felder am Gesundheitsereignis

Die Schnellerfassung für COPD braucht Zahlen, die man später auswerten will: Sauerstofffluss in l/min, Stunden pro Tag, Schweregrad einer Exazerbation, ob stationär behandelt wurde.

Umgehung. Diese Angaben werden als beschriftete Zeilen in die verschlüsselte Beschreibung geschrieben (src/lib/pflege/ereignis-details.ts). Sie sind damit lesbar, aber nicht summierbar. Auswertbar bleiben nur Art, Datum, bisDatum und abwesenheitstage — das reicht für Exazerbationen je Jahr und Krankenhaustage je Jahr.

Wunsch. Ein details Json?-Feld am Gesundheitsereignis wäre falsch (Gesundheitsdaten gehören verschlüsselt). Besser wären wenige typisierte, unverschlüsselte Kennzahlen ohne Diagnosebezug:

model Gesundheitsereignis {
  stationaer        Boolean @default(false)
  schweregrad       Int?    // 1 leicht, 2 mittel, 3 schwer
}

7. Vertrag.vertragsnummerVerschluesselt war nicht registriert

Das Feld steht im Schema, fehlte aber in VERSCHLUESSELTE_FELDER in src/lib/crypto/feld.ts — sealen('Vertrag', { vertragsnummer }) hätte geworfen. Der Eintrag wurde ergänzt; das Schema blieb unverändert.


8. Vertrag.laufzeitJahre

Der Kündigungsrechner braucht die Vertragsdauer, um die Verlängerung zu bestimmen. Das Schema kennt nur beginnAm, endeAm und kuendigungsfristMonate.

Umgehung. Fehlt endeAm, rechnet src/lib/vertraege/kuendigung.ts mit einer Verlängerung um ein Jahr — der Regelfall bei Versicherungen — und schreibt diese Annahme als Hinweis in das Ergebnis.

Wunsch.

model Vertrag {
  laufzeitJahre            Int?
  verlaengertSichAutomatisch Boolean @default(true)
}