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:
Florian Wilde
2026-08-22 02:24:16 +02:00
parent 63352272b8
commit 1217725076
5 changed files with 1021 additions and 12 deletions
+11 -10
View File
@@ -746,15 +746,16 @@ export type VorlagenErgebnis = {
}
/**
* Der Tagesanteil eines Datums, auf Mitternacht UTC normiert.
* Bildet den örtlichen Kalendertag auf Mitternacht UTC ab.
*
* Fristen und Fälligkeiten sind in der Datenbank @db.Date - ohne diese
* Normierung verschiebt die Zeitzone eine Frist um einen Tag.
* Fristen und Fälligkeiten sind in der Datenbank @db.Date. Wer hier den
* Zeitstempel einfach abschneidet, verschiebt in Deutschland jede Frist um
* einen Tag nach hinten - der 22.08. um 01:00 Uhr Ortszeit ist in UTC noch
* der 21.08. Deshalb wird der Tag gelesen, wie ihn der Kalender an der Wand
* zeigt, und dann als Mitternacht UTC abgelegt.
*/
export function alsTag(datum: Date): Date {
return new Date(
Date.UTC(datum.getUTCFullYear(), datum.getUTCMonth(), datum.getUTCDate(), 0, 0, 0, 0),
)
return new Date(Date.UTC(datum.getFullYear(), datum.getMonth(), datum.getDate(), 0, 0, 0, 0))
}
export function vorlageAnwenden(schluessel: string, start: Date = new Date()): VorlagenErgebnis {
@@ -763,22 +764,22 @@ export function vorlageAnwenden(schluessel: string, start: Date = new Date()): V
throw new Error(`Unbekannte Vorgangs-Vorlage: ${schluessel}`)
}
const basis = alsTag(start)
// Gerechnet wird auf dem oertlichen Datum (date-fns ist dabei
// sommerzeitfest), erst das Ergebnis wird auf Mitternacht UTC normiert.
return {
vorlage,
aufgaben: vorlage.aufgaben.map((aufgabe, index) => ({
titel: aufgabe.titel,
beschreibung: aufgabe.beschreibung ?? null,
faelligAm:
aufgabe.tageBisFaellig === undefined ? null : alsTag(addDays(basis, aufgabe.tageBisFaellig)),
aufgabe.tageBisFaellig === undefined ? null : alsTag(addDays(start, aufgabe.tageBisFaellig)),
// Die Reihenfolge ist der fachliche Kern der Vorlage - sie wird
// festgeschrieben, damit spaeteres Sortieren sie nicht zerstoert.
reihenfolge: index,
})),
fristen: (vorlage.fristen ?? []).map((frist) => ({
titel: frist.titel,
datum: alsTag(addDays(basis, frist.tageBisFaellig)),
datum: alsTag(addDays(start, frist.tageBisFaellig)),
rechtsgrundlage: frist.rechtsgrundlage ?? null,
})),
}