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
This commit is contained in:
Florian Wilde
2026-08-22 06:22:44 +02:00
parent b69a12da8f
commit 5cc7f5c140
29 changed files with 5573 additions and 3076 deletions
+143
View File
@@ -0,0 +1,143 @@
# 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:**
```prisma
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.
## Landesrecht ohne Bundesgesetz-Link
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.