CLAUDE.md und README.md verweisen auf docs/, die Dateien lagen aber im Wurzelverzeichnis. Verschoben. docs/13-entscheidungen.md hält fest, wo die Dokumente sich widersprechen und wie es aufgelöst wurde. Der wichtigste Punkt: 04-datenmodell.md verlangt eine Regel, die jedes UPDATE auf CaseEntryVersion verwirft, 05-akten-und-vorlagen.md verlangt, dass eine Umklassifizierung rückwirkend auf alle Versionen wirkt. Beides zusammen geht nicht. Aufgelöst über einen Trigger, der die Tabelle anhängend hält, aber das Anheben von minClearance zulässt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
202 lines
8.6 KiB
Markdown
202 lines
8.6 KiB
Markdown
# Akten, Vorlagen und Versionierung
|
|
|
|
## Die vier Ebenen einer Akte
|
|
|
|
| Ebene | Änderbar | Versioniert | Zweck |
|
|
|---|---|---|---|
|
|
| Kopfdaten | ja | nein, aber im Audit-Log | Status, Klassifizierung, Frist, Beteiligte |
|
|
| Abschnitte | ja | ja, pro Feld | Aktueller Sachstand |
|
|
| Verlauf | ja, mit Version und Grabstein | ja, pro Eintrag | Wie es dazu kam |
|
|
| Chat | Bearbeiten mit Vermerk | nein | Informelle Absprache, kein Aktenbestandteil |
|
|
|
|
Die Trennung zwischen Abschnitt und Verlauf ist nicht verhandelbar. Ein Abschnitt beantwortet
|
|
„was wissen wir jetzt", der Verlauf „wie sind wir dahin gekommen".
|
|
|
|
Der Verlauf ist nachträglich änderbar, aber jede Änderung ist sichtbar und nachvollziehbar.
|
|
Siehe „Nachträgliche Änderungen" weiter unten.
|
|
|
|
## Vorlagen
|
|
|
|
```ts
|
|
type TemplateField = {
|
|
key: string // stabil, nie ändern
|
|
label: string
|
|
type: 'longtext' | 'shorttext' | 'select' | 'multiselect'
|
|
| 'date' | 'person' | 'number' | 'checklist' | 'table'
|
|
options?: string[] // für select/multiselect
|
|
required?: boolean
|
|
requiredToClose?: boolean // blockiert den Abschluss des Falls
|
|
minClearance?: number // Feld erst ab dieser Stufe sichtbar
|
|
help?: string
|
|
}
|
|
```
|
|
|
|
Vorlagen sind versioniert. Eine Akte merkt sich `templateId` und `templateKey`. Änderungen an
|
|
einer Vorlage erzeugen eine neue Version und lassen bestehende Akten unberührt. Im
|
|
Vorlagen-Editor gibt es dafür eine Vorschau „So sieht eine neue Akte damit aus".
|
|
|
|
Startvorlagen im Seed: Korruptionsverdacht, Dienstvergehen, Sicherheitsvorfall, Routineprüfung.
|
|
|
|
## Feldebene-Klassifizierung
|
|
|
|
`minClearance` pro Feld ist das, was das Stufensystem lebendig macht. Für Nutzer unterhalb der
|
|
Stufe wird der Wert serverseitig durch
|
|
|
|
```ts
|
|
{ redacted: true, requiredLevel: 4 }
|
|
```
|
|
|
|
ersetzt. Die Oberfläche zeigt einen Schwärzungsbalken mit „Geschwärzt, ab Stufe 4 sichtbar".
|
|
Der echte Wert verlässt den Server nicht.
|
|
|
|
## Schwärzung im Freitext
|
|
|
|
Im Editor markiert man einen Bereich und wählt eine Stufe. Gespeichert wird als Auszeichnung:
|
|
|
|
```
|
|
Der Hinweis kam von [[c4]]Wachmann Perreira, Sektor B[[/c4]] am 27.08.
|
|
```
|
|
|
|
Der Server ersetzt den Inhalt beim Rendern für niedrigere Stufen durch einen Balken gleicher
|
|
Länge (nicht gleicher Text, sonst lässt sich die Länge auswerten: immer auf ein festes Raster
|
|
runden). Der Parser gehört in `lib/redaction.ts` und wird sowohl für die Ansicht als auch für
|
|
den PDF-Export benutzt.
|
|
|
|
## Versionierung der Abschnitte
|
|
|
|
Jede Änderung schreibt `CaseSectionVersion` und erhöht `currentVersion`. Die UI zeigt
|
|
`v4 · Vogel` neben der Überschrift, Klick öffnet einen Diff (Zeilen- oder Wortdiff, `diff`-Paket).
|
|
|
|
Beim Speichern schickt der Client die Version mit, die er geladen hat. Weicht sie ab, kommt
|
|
409 zurück und die UI zeigt beide Fassungen nebeneinander mit „Meine übernehmen" oder
|
|
„Fremde übernehmen". Kein stilles Überschreiben.
|
|
|
|
## Verlauf
|
|
|
|
Statusänderungen und Klassifizierungsänderungen erzeugen automatisch einen `SYSTEM`-Eintrag.
|
|
`SYSTEM`-Einträge sind von jeder Änderung ausgenommen.
|
|
|
|
Neben der nachträglichen Änderung bleibt der Korrektureintrag bestehen: ein neuer Eintrag mit
|
|
`type = CORRECTION` und `correctsId`. Das ist der richtige Weg, wenn sich der **Sachverhalt**
|
|
geändert hat. Nachträgliches Ändern ist der richtige Weg, wenn sich die **Darstellung**
|
|
geändert hat, also Tippfehler, vergessener Zusatz, falsche Uhrzeit im eigenen Vermerk.
|
|
|
|
## Nachträgliche Änderungen
|
|
|
|
Verlaufseinträge dürfen geändert und zurückgezogen werden. Ein hartes Löschen gibt es nicht,
|
|
weder über die Oberfläche noch über eine Server Action.
|
|
|
|
### Änderungsrichtlinie
|
|
|
|
Global einstellbar im Adminbereich, `AppSetting.entryEditPolicy`:
|
|
|
|
| Wert | Bedeutung |
|
|
|---|---|
|
|
| `APPEND_ONLY` | Keine Änderung möglich, nur Korrektureinträge |
|
|
| `GRACE_WINDOW` | Eigene Einträge innerhalb von N Minuten frei änderbar, danach nur mit Recht **(Auslieferungszustand, N = 15)** |
|
|
| `OPEN` | Jederzeit änderbar, sofern das Recht vorliegt |
|
|
|
|
Zusätzlich: `entryEditGraceMinutes` (Standard 15) und `entryEditMarkGrace` (Standard `false`).
|
|
Steht letzteres auf `false`, bekommt eine Änderung innerhalb der Karenzzeit **keine sichtbare
|
|
Markierung**, wird aber trotzdem versioniert und protokolliert. Ein Tippfehler, der zwei
|
|
Minuten nach dem Absenden korrigiert wird, soll die Akte nicht mit „bearbeitet"-Marken
|
|
zupflastern. Alles danach ist sichtbar markiert.
|
|
|
|
### Was geändert werden darf
|
|
|
|
| Feld | Änderbar |
|
|
|---|---|
|
|
| `body` | ja |
|
|
| `minClearance` | ja, mit Recht `case.edit` |
|
|
| `type` | nein |
|
|
| `authorId` | **nie** |
|
|
| `createdAt` | **nie** |
|
|
| `caseId` | nein, ein Eintrag wandert nicht in eine andere Akte |
|
|
|
|
Autor und Entstehungszeit sind unantastbar. Wer das umgehen will, muss den Eintrag
|
|
zurückziehen und einen neuen schreiben, und genau das ist dann auch sichtbar.
|
|
|
|
### Versionierung
|
|
|
|
Jede Änderung schreibt den **vorherigen** Stand nach `CaseEntryVersion` und erhöht
|
|
`CaseEntry.version`. Die Versionstabelle ist strikt anhängend und wird auch von Admins nicht
|
|
gelöscht.
|
|
|
|
Die UI zeigt am Eintrag `bearbeitet · v3`. Ein Klick öffnet die Historie mit Wortdiff,
|
|
Zeitpunkt und bearbeitender Person. **Diese Historie sieht jeder, der den Eintrag sehen darf**,
|
|
nicht nur die Leitung. Das ist der ganze Sinn der Sache: In einer Abteilung, die Korruption
|
|
aufdeckt, muss nachprüfbar sein, dass niemand still etwas umgeschrieben hat.
|
|
|
|
Ab der zweiten Änderung oder bei Änderung eines fremden Eintrags ist eine **Begründung
|
|
Pflicht** (`changeReason`, mindestens 10 Zeichen). Sie steht in der Historie.
|
|
|
|
### Zurückziehen statt Löschen
|
|
|
|
`retractedAt`, `retractedById` und `retractionReason` (Pflicht) werden gesetzt. Der Eintrag
|
|
bleibt an seiner Stelle im Verlauf stehen und zeigt:
|
|
|
|
> Eintrag zurückgezogen am 31.08.2026 durch Ltr. Reinhardt · Begründung: Falscher Fall
|
|
|
|
Der ursprüngliche Text bleibt in der Datenbank und ist für Nutzer mit `audit.read` weiterhin
|
|
lesbar, für alle anderen nicht. Zurückziehen lässt sich rückgängig machen (`entry.retract`),
|
|
was wiederum protokolliert wird.
|
|
|
|
### Umklassifizierung wirkt rückwirkend
|
|
|
|
Wird `minClearance` eines Eintrags erhöht, **erben alle bestehenden Versionen den neuen Wert**.
|
|
Sonst liest jemand mit niedrigerer Stufe den ursprünglichen Text einfach in der Historie.
|
|
Das ist die Stelle, an der eine naive Umsetzung leckt.
|
|
|
|
### Gesperrte Zustände
|
|
|
|
- `CLOSED`: Änderungen nur mit `case.edit_closed`, erzeugen zusätzlich einen `SYSTEM`-Eintrag
|
|
„Nachträgliche Änderung nach Abschluss"
|
|
- `ARCHIVED` und `SEALED`: keine Änderungen, auch nicht für Admins
|
|
|
|
### Protokollierung
|
|
|
|
Jede Änderung schreibt einen `AuditLog`-Eintrag mit Aktion `entry.edit`, `entry.retract` oder
|
|
`entry.restore`, der Eintrags-ID, der neuen Versionsnummer, der Begründung und einem
|
|
kompakten Diff in `meta`. Der Audit-Log wird nie gelöscht und ist die zweite, unabhängige
|
|
Spur neben der Versionstabelle.
|
|
|
|
### Änderungsübersicht je Akte
|
|
|
|
Eigener Reiter an der Akte: alle nachträglichen Änderungen an Abschnitten und Verlauf,
|
|
chronologisch, mit Person, Zeitpunkt, Begründung und Diff. Für den Leiter ist das die Ansicht,
|
|
mit der er im Zweifel prüft, ob an einer Akte nachträglich gedreht wurde. Sichtbar für alle,
|
|
die die Akte lesen dürfen.
|
|
|
|
### Folgen für andere Teile
|
|
|
|
- **Export**: `updatedAt` ändert sich, der PDF-Cache verfällt automatisch. Auf dem Deckblatt
|
|
steht ein `Stand:`-Zeitstempel, damit eine ältere ausgedruckte Fassung erkennbar ist.
|
|
- **Suche**: Der Trigger auf `search_vector` muss auch bei `UPDATE` auf `CaseEntry` feuern.
|
|
- **Ungelesen**: Eine Änderung setzt die Akte für andere wieder auf ungelesen und löst
|
|
dieselbe Benachrichtigung aus wie ein neuer Eintrag, mit dem Zusatz „geändert".
|
|
- **Echtzeit**: Ereignis `entry.updated` über SSE, Clients tauschen den Eintrag anhand der ID.
|
|
|
|
## Lebenszyklus
|
|
|
|
```
|
|
DRAFT ──► OPEN ──► REVIEW ──► CLOSED ──► ARCHIVED
|
|
│ ▲ │
|
|
▼ │ └──► OPEN (Rückgabe zur Nachbesserung)
|
|
DORMANT
|
|
|
|
SEALED: aus jedem Zustand erreichbar, nur mit Recht 'admin' oder Stufe 5 lesbar
|
|
```
|
|
|
|
Regeln:
|
|
- `OPEN → REVIEW` braucht `case.edit`
|
|
- `REVIEW → CLOSED` braucht `case.close` und alle `requiredToClose`-Felder gefüllt
|
|
- `CLOSED → OPEN` braucht `case.reopen` und erzeugt einen Pflicht-Vermerk mit Begründung
|
|
- `ARCHIVED` ist schreibgeschützt, nur noch lesbar
|
|
|
|
## Rückverweise
|
|
|
|
Auf einer Personalakte stehen automatisch die Fälle, in denen die Person vorkommt, getrennt
|
|
nach Rolle. **Diese Liste läuft durch dieselbe Sichtbarkeitsprüfung wie die Fallliste.** Ein
|
|
Nutzer, der einen Fall nicht sehen darf, sieht ihn hier auch nicht und die Zählung enthält ihn
|
|
nicht. Das ist der häufigste Ort, an dem ein Aktensystem Informationen leaken lässt.
|