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
4.7 KiB
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:
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:
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 —
- 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.
- Das Audit-Log ist ein Nachweis, keine Fachtabelle. Es fachlich auszuwerten vermischt zwei Zwecke.
- Ein Log-Eintrag, der sich nicht entschlüsseln lässt (Schlüsselrotation), reißt still ein Loch in die Chronik.
Wunsch: eine schmale Tabelle
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:
/// 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.