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
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user