Files
betreuungs-cockpit/docs/schema-wuensche/berichte.md
T
Florian Wilde 5cc7f5c140 feat: Jahresbericht, Vollexport, Wissensbasis und Mitleser-Freigaben
Etappe 7. Die Ausgabeschicht - alles, was diese Anwendung nach aussen
abgibt.

Der Jahresbericht nach § 1863 BGB ist ein ENTWURF zum Ueberarbeiten,
kein fertiger Text. Er zieht sich die fuenf Gliederungspunkte aus
Kontaktprotokollen, Gesundheitsereignissen und Vorgangsstatus zusammen;
was davor abgegeben wird, entscheidet der Betreuer. Das sagt auch die
Oberflaeche. Ausgabe als PDF und als DOCX, damit sich weiterschreiben
laesst.

Vollexport als ZIP-Strom mit Gegendruck, damit auch grosse Bestaende
ohne Speicherausbruch durchgehen: Originaldateien nach Jahren sortiert,
alle Tabellen als entschluesseltes JSON, die erzeugten Berichte, ein
LIESMICH und SHA-256 je Datei. Damit laesst sich die Anwendung
verlassen - kein Datensilo.

Der Export ist eine sensible Aktion: nur BETREUER und ADMIN, nur nach
frischer Anmeldung, immer mit Audit-Eintrag. Ein Mitleser kommt nicht
heran.

37 Hinweiskarten in daten/hinweise.json, Zod-geprueft und ohne
Neustart pflegbar. Jede mit Rechtsgrundlage, Link und dem stehenden
Vermerk, dass sie keine Rechtsberatung ersetzt.

Modulweise Freigaben fuer Mitleser, mit ausdruecklicher Warnung bei
Gesundheit, Finanzen und Sozialhilfe. Rollen- und Freigabeaenderungen
erfordern eine frische Anmeldung und stehen im Audit-Log.

Nebenbei geloest: Webpack loest .tsx vor .ts auf, TypeScript und Vitest
umgekehrt. Ein Modul, das beide Endungen traegt, importiert sich unter
einem der beiden Werkzeuge selbst. Deshalb heissen die Bausteine jetzt
eindeutig (jahresbericht.entwurf.ts, .inhalt.tsx, .ausgabe.ts) und
werden ueber eine gemeinsame Fassade weitergereicht.

41 Tests.

Etappe: 7
Phase: Ausgabe
2026-08-22 06:22:44 +02:00

5.4 KiB
Raw Blame History

Schema-Wünsche aus der Ausgabeschicht (Etappe 7)

prisma/schema.prisma wurde nicht angefasst. Was beim Bauen der Berichte, des Exports und der Freigaben gefehlt hat, steht hier – mit Begründung und mit dem Behelf, der jetzt an seiner Stelle steht.


1. Mehrere Fassungen je Bericht

Jetzt: Bericht trägt genau ein objektSchluessel / dateiname / pruefsumme. Wird das PDF nach einer Rückfrage des Gerichts neu erzeugt, überschreibt es die Angaben der Fassung, die bereits vorgelegt wurde.

Warum das stört: Die Leitfrage der Tabelle ist „was habe ich wann in welcher Fassung abgegeben?“. Genau die lässt sich nach dem zweiten Erzeugen nicht mehr beantworten. Fragt der Rechtspfleger in zwei Jahren nach, welcher Text ihm vorlag, steht der neue da.

Wunsch:

model Berichtsfassung {
  id         String   @id @default(cuid())
  berichtId  String
  bericht    Bericht  @relation(fields: [berichtId], references: [id], onDelete: Cascade)
  nummer     Int      // 1, 2, 3 …
  format     String   // "pdf" | "docx"
  objektSchluessel String
  dateiname        String
  pruefsumme       String
  erstelltAm DateTime @default(now())
  abgegebenAm DateTime? @db.Date
  abgegebenAn String?

  @@unique([berichtId, nummer, format])
}

Behelf bis dahin: Die PDF-Fassung wird am Bericht vermerkt, die DOCX-Arbeitsfassung bewusst nicht – sonst verdrängte sie die abgegebene Fassung. Die Word-Datei liegt im Objektspeicher und taucht im Vollexport auf, ist aber über die Oberfläche nur bis zum nächsten Erzeugen erreichbar.


2. Zusammenhang zwischen Unterlagenpaket und Nachforderung

Jetzt: Beim Erzeugen eines Pakets werden die beigefügten Nachforderung-Zeilen auf UEBERSANDT gesetzt und bekommen ein uebersandtAm. In welchem Paket sie mitgegangen sind, steht nirgends.

Warum das stört: Der Träger fragt regelmäßig nach einer einzelnen Position („zu Nr. 7 fehlt uns noch etwas“). Ohne Rückbezug muss man raten, welches Anschreiben Nr. 7 enthielt.

Wunsch: Nachforderung.uebersandtMitBerichtId String? mit Relation auf Bericht.

Behelf: Das Anschreiben nennt die laufende Nummer, und der Bericht-Datensatz trägt Datum und Positionsanzahl im Titel.


3. Betreuungsjahr am Bericht

Wunsch: Bericht.betreuungsjahr Int? – die laufende Nummer des Betreuungsjahres, aus dem der Jahresbericht stammt.

Warum: Das Betreuungsjahr läuft ab dem Tag der Bestellung. Zeiträume allein sind für die Dublettenprüfung unhandlich, sobald jemand den Vorschlag im Formular um einen Tag verschiebt.

Behelf: Geprüft wird auf art + zeitraumVon + zeitraumBis. Ein um einen Tag verschobener Zeitraum erzeugt deshalb einen zweiten Entwurf.


4. Index für die Dublettenprüfung

Wunsch: @@index([betreuteId, art, zeitraumVon, zeitraumBis]) auf Bericht. Vorhanden ist nur @@index([betreuteId, art]).


5. Aufbewahrungsfrist am Papierkorb

Wunsch: geloeschtVonId String? auf allen Modellen mit geloeschtAm.

Warum: Das Audit-Log weiß, wer gelöscht hat, aber die Papierkorbliste müsste dafür je Zeile einen Log-Eintrag suchen. Für die Frage „wer hat das weggeräumt?“ direkt an der Zeile wäre ein Feld billiger.


Entscheidungen, die keine Schemaänderung brauchen

Zwei gleichnamige Dateien: jahresbericht.ts und jahresbericht.tsx

Der Bauplan verlangt beide. Beide nebeneinander sind aber eine Falle: Beim Import ohne Endung probiert Webpack .tsx vor .ts, TypeScript und Vitest umgekehrt. Derselbe Importpfad wäre je nach Werkzeug eine andere Datei – und ein export * from './jahresbericht' innerhalb der .tsx würde unter Webpack die Datei sich selbst importieren.

Gelöst über eindeutige Namen für die Bausteine:

Datei Inhalt
jahresbericht.entwurf.ts Textbausteine, rein, ohne Datenbank – das, was getestet wird
jahresbericht.inhalt.tsx Satz für das PDF (React)
jahresbericht.ausgabe.ts Daten laden, PDF erzeugen
jahresbericht.ts reicht alle drei weiter
jahresbericht.tsx reicht alle drei weiter (identisch)

Damit bekommt jeder Aufrufer von @/lib/berichte/jahresbericht dieselbe Schnittstelle, gleich welches Werkzeug auflöst. Wer nur die Textbausteine braucht – die Tests, die DOCX-Ausgabe – importiert direkt @/lib/berichte/jahresbericht.entwurf und spart sich Datenbank und PDF-Bibliothek.

Alternative, falls das später stört: allowImportingTsExtensions: true in der tsconfig.json (mit noEmit: true unbedenklich, das ist bereits gesetzt). Dann ließen sich die Endungen ausschreiben und die beiden weiterreichenden Dateien entfielen.

Vollexport ohne Serverkopie

Das ZIP wird als Strom ausgeliefert und nicht im Objektspeicher abgelegt. Ein Bericht-Datensatz der Art VOLLEXPORT hält nur fest, dass und wann exportiert wurde; objektSchluessel bleibt leer.

Grund: Eine unverschlüsselte Vollkopie des gesamten Bestands dauerhaft auf dem Server liegen zu lassen, wäre die größte Angriffsfläche der ganzen Anwendung – und niemand bräuchte sie ein zweites Mal.

Zwei Hinweiskarten betreffen bayerisches Landesrecht bzw. den Rundfunkbeitragsstaatsvertrag (landespflegegeld, rundfunkbeitragsbefreiung). Beides steht nicht auf gesetze-im-internet.de; die Karten verlinken deshalb auf gesetze-bayern.de. Das Schema verlangt nur einen https-Link.