Files
Florian Wilde 1217725076 feat: Vorgaenge als Fallakte mit Genehmigungskette und Vorlagen
Jeder Vorgang ist eine kleine Akte: Kopfdaten, Aufgabenliste zum
Abhaken, Genehmigungsstufen und ein Zeitstrahl, der Dokumente,
Kontaktprotokolle, Fristen, Buchungen und Statuswechsel zusammenfuehrt.

Die abnahmerelevante Regel: Ein genehmigungspflichtiger Vorgang laesst
sich nicht auf VOLLZOGEN setzen, solange kein Beschlussdokument
hinterlegt ist. Geprueft wird in darfStatusWechseln() UND in der Server
Action - nicht nur im UI, wo das Auswahlfeld die Stufe ohnehin gar nicht
erst anbietet. Hintergrund ist § 1858 BGB: die Genehmigung muss vor dem
Rechtsgeschaeft vorliegen, nicht danach.

Neun Vorlagen mit durchdachter Aufgabenreihenfolge. Bei Wohnungs-
aufloesung und Hausverkauf erzwingen sie die Kette Mitteilung ->
Genehmigung -> Rechtsgeschaeft; ein Test prueft das.

Kanban ohne Drag-and-Drop: Statuswechsel laufen ueber Auswahlfeld und
Knopf. Am Handy ist das verlaesslicher, und am Schreibtisch kostet es
einen Klick mehr statt einer verlorenen Karte.

Zeitzonen: alsTag() bildet den oertlichen Kalendertag auf Mitternacht
UTC ab. Ohne das rutscht in Deutschland jede Faelligkeit einen Tag
zurueck.

33 Tests.

Etappe: 2
Phase: Alltag
2026-08-22 02:24:16 +02:00

137 lines
4.7 KiB
Markdown

# Schema-Wünsche: Vorgänge, Aufgaben, Genehmigungen
Beim Bau von Etappe 2 („Vorgänge (Fallakte)“) sind vier Stellen aufgefallen,
an denen das Datenmodell die Fachlichkeit nur mit Umwegen abbildet. Nichts
davon blockiert die Umsetzung — alles ist im Code sauber umschifft. Die
Umwege stehen unten mit dabei, damit man beim Ändern weiß, was wegfällt.
`prisma/schema.prisma` wurde **nicht** angefasst.
---
## 1. `Genehmigung`: Die Versagung hat kein eigenes Datum
**Ist**: `mitteilungAm`, `antragAm`, `anhoerungAm`, `beschlussAm`,
`rechtskraftAm`, `vollzugAm` — für jede Stufe der Kette ein Feld. Nur der
Seitenausgang `VERSAGT` hat keins.
**Warum das stört**: Der Zeitstrahl der Fallakte baut die Chronik des
Genehmigungsverfahrens aus genau diesen Datumsfeldern. Eine Versagung ist
aber der Ausgang, der später am meisten Erklärung braucht — gegenüber dem
Gericht, gegenüber Miterben, und für die Frage, ob eine Beschwerde noch
läuft.
**Wunsch**:
```prisma
versagungAm DateTime? @db.Date
versagungsgrund String?
```
**Umweg bis dahin**: `src/lib/vorgaenge/zeitstrahl.ts` zeigt eine Versagung
am `beschlussAm` an — das Gericht versagt ja mit einem Beschluss. Der Grund
landet in `notizenVerschluesselt`.
---
## 2. `Aufgabe`: kein Papierkorb
**Ist**: `Aufgabe` hat kein `geloeschtAm`. Sie hängt per `onDelete: Cascade`
am Vorgang.
**Warum das stört**: Vorlagen bringen bis zu zehn Aufgaben mit. Manche davon
passen im Einzelfall nicht („Rundfunkbeitrag“, wenn nie einer gezahlt
wurde). Sie einfach abzuhaken ist unehrlich — sie wurden nicht erledigt,
sie waren gegenstandslos. Löschen wiederum ist im Rest der Anwendung immer
ein Papierkorb, hier wäre es endgültig.
**Wunsch**:
```prisma
geloeschtAm DateTime?
entfaellt Boolean @default(false)
entfallGrund String?
```
`entfaellt` wäre das Ehrlichere: Die Aufgabe bleibt in der Akte sichtbar,
zählt aber nicht gegen den Fortschritt.
**Umweg bis dahin**: Es gibt in dieser Etappe keine Möglichkeit, eine
Aufgabe zu entfernen. Nicht passende Aufgaben bleiben offen stehen.
---
## 3. Statuswechsel des Vorgangs stehen nur im Audit-Log
**Ist**: Der Zeitstrahl liest die Statuschronik eines Vorgangs aus
`AuditLog`, entschlüsselt `aenderungVerschluesselt` und sucht darin das
Feld `status`.
**Warum das stört**: Drei Dinge auf einmal —
1. Es werden bis zu 200 Log-Einträge je Aufruf entschlüsselt, nur um ein
Feld zu lesen. Das skaliert schlecht und lässt sich nicht filtern,
weil der Umschlag nicht durchsuchbar ist.
2. Das Audit-Log ist ein Nachweis, keine Fachtabelle. Es fachlich
auszuwerten vermischt zwei Zwecke.
3. Ein Log-Eintrag, der sich nicht entschlüsseln lässt (Schlüsselrotation),
reißt still ein Loch in die Chronik.
**Wunsch**: eine schmale Tabelle
```prisma
model VorgangStatuswechsel {
id String @id @default(cuid())
vorgangId String
vorgang Vorgang @relation(fields: [vorgangId], references: [id], onDelete: Cascade)
von VorgangStatus?
nach VorgangStatus
zeitpunkt DateTime @default(now())
userId String?
userName String?
@@index([vorgangId, zeitpunkt])
}
```
**Umweg bis dahin**: `src/app/(app)/vorgaenge/[id]/page.tsx` liest das
Audit-Log und fängt unlesbare Einträge einzeln ab. Die Schnittstelle von
`zeitstrahlBauen()` nimmt die Wechsel bereits als eigene Quelle entgegen —
ein Umstieg auf die Tabelle wäre eine Änderung in einer Datei.
---
## 4. `Vorgang`: kein Feld für den Ansprechpartner bei der Gegenstelle
**Ist**: `gegenstelle` (Text) und `aktenzeichen` (Text), dazu die n:m-
Beziehung `kontakte`. Wer bei der Behörde konkret zuständig ist, steht
nirgends direkt am Vorgang.
**Warum das stört**: Die Sachstandsanfrage
(`src/lib/vorgaenge/sachstandsanfrage.ts`) kann eine namentliche Anrede
erzeugen — genau die, die erfahrungsgemäß schneller beantwortet wird. Sie
bekommt den Namen aber nicht, ohne über `kontakte` zu raten, welcher der
verknüpften Kontakte gemeint ist.
**Wunsch**:
```prisma
/// Der eine Kontakt, der diesen Vorgang bei der Gegenstelle führt.
ansprechpartnerId String?
ansprechpartner Kontakt? @relation("VorgangAnsprechpartner", fields: [ansprechpartnerId], references: [id], onDelete: SetNull)
```
**Umweg bis dahin**: `sachstandsanfrageErzeugen()` nimmt den
Ansprechpartner als optionalen Parameter entgegen und fällt sonst auf
„Sehr geehrte Damen und Herren“ zurück. Die Seite übergibt ihn derzeit
nicht.
---
## Nicht gewünscht
Ausdrücklich **kein** Wunsch ist ein Enum für
`Genehmigung.rechtsgrundlage`. Der Freitext ist richtig: Die Auswahlliste
in `src/lib/vorgaenge/genehmigungskette.ts` deckt den Alltag ab, aber
seltene Tatbestände muss man eintragen können, ohne eine Migration zu
schreiben.