Files
betreuungs-cockpit/docs/schema-wuensche/vorgaenge.md
T
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

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 —

  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

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.