From 399988596f896e06c5fa5d6f89b99008c9cd7ae3 Mon Sep 17 00:00:00 2001 From: Harlekin Date: Wed, 2 Sep 2026 12:52:53 +0200 Subject: [PATCH] =?UTF-8?q?Phase=203=20abgeschlossen:=20Vorlagen,=20Abschn?= =?UTF-8?q?itte,=20Schw=C3=A4rzung,=20Diff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eine Vorlage ist eine geordnete Liste von Bausteinen: Eingabefelder und feste Textbausteine mit Platzhaltern. Damit trägt dieselbe Struktur den Vorfallsbericht und den Sonderarbeitsvertrag. Die Startvorlagen bilden jetzt den echten Bericht der Abteilung ab, samt Berichtsart, Gefahreneinschätzung und Foundation-Freigabe — letztere als gewöhnliches Feld ohne Wirkung auf den Zugriff. Abschnitte werden gerendert und bearbeitet, Entwürfe alle zwei Sekunden lokal gesichert. Gespeichert wird auf Knopfdruck: ein Abschnitt ist der Sachstand der Akte, kein Chatbeitrag. Die geladene Version geht mit; weicht sie ab, gibt es kein stilles Überschreiben, sondern beide Fassungen nebeneinander mit "Meine übernehmen" und "Fremde übernehmen". Felder über der Stufe des Lesers werden serverseitig durch einen Platzhalter ersetzt, Schwärzungen im Fliesstext kommen als Segmente. Beides so, dass der echte Wert den Server nicht verlässt — nicht als Ausblenden im Client. Der Wortdiff ist selbst geschrieben statt eingekauft: ein Dutzend Zeilen gegen 30 kB im Bündel für etwas, das nur serverseitig läuft. Beim Bauen des Vorlagen-Editors riss das Bündel-Budget auf 127,9 kB, weil zod über das Vorlagenmodul in den Client geriet. Typen und Validierung sind jetzt getrennt: fields.ts ohne zod für Client und Server, schema.ts nur serverseitig. Wieder bei 111,9 kB von 120. 85 Tests, 22 E2E. --- packages/db/prisma/seed.ts | 41 +++- .../(app)/admin/vorlagen/TemplateEditor.tsx | 200 ++++++++++++++++ web/app/(app)/admin/vorlagen/actions.ts | 80 +++++++ web/app/(app)/admin/vorlagen/page.tsx | 46 ++++ .../faelle/[caseNumber]/SectionEditor.tsx | 210 ++++++++++++++++ .../(app)/faelle/[caseNumber]/Sections.tsx | 114 +++++++++ .../faelle/[caseNumber]/aenderungen/page.tsx | 32 +-- web/app/(app)/faelle/[caseNumber]/page.tsx | 46 +++- .../faelle/[caseNumber]/sectionActions.ts | 47 ++++ web/app/(app)/layout.tsx | 4 + web/components/Redacted.tsx | 37 +++ web/lib/cases/sectionService.ts | 224 ++++++++++++++++++ web/lib/diff.ts | 65 +++++ web/lib/i18n/de.ts | 43 ++++ web/lib/templates/fields.ts | 120 ++++------ web/lib/templates/schema.ts | 73 ++++++ web/tests/abschnitte.test.ts | 171 +++++++++++++ web/tests/diff.test.ts | 38 +++ 18 files changed, 1498 insertions(+), 93 deletions(-) create mode 100644 web/app/(app)/admin/vorlagen/TemplateEditor.tsx create mode 100644 web/app/(app)/admin/vorlagen/actions.ts create mode 100644 web/app/(app)/admin/vorlagen/page.tsx create mode 100644 web/app/(app)/faelle/[caseNumber]/SectionEditor.tsx create mode 100644 web/app/(app)/faelle/[caseNumber]/Sections.tsx create mode 100644 web/app/(app)/faelle/[caseNumber]/sectionActions.ts create mode 100644 web/components/Redacted.tsx create mode 100644 web/lib/cases/sectionService.ts create mode 100644 web/lib/diff.ts create mode 100644 web/lib/templates/schema.ts create mode 100644 web/tests/abschnitte.test.ts create mode 100644 web/tests/diff.test.ts diff --git a/packages/db/prisma/seed.ts b/packages/db/prisma/seed.ts index d954f10..ec2ca52 100644 --- a/packages/db/prisma/seed.ts +++ b/packages/db/prisma/seed.ts @@ -66,9 +66,39 @@ const TEMPLATES: { ] const TEMPLATE_FIELDS = [ + // Nachgebaut nach dem Vorfallsbericht der Abteilung. Die Foundation-Freigabe + // steht im Kopf des Dokuments, steuert aber keinen Zugriff — das tut allein + // die interne Freigabe. Siehe docs/13-entscheidungen.md. + { + key: 'foundation_freigabe', + label: 'Foundation-Freigabe', + type: 'select', + options: ['0', '1', '2', '3', '4', '5'], + }, + { + key: 'berichtsart', + label: 'Berichtsart', + type: 'multiselect', + options: [ + 'Vorfallsbericht', + 'Sicherheitsanalyse', + 'Interne Untersuchung', + 'Allg. Bericht', + 'Sonstiges', + ], + }, + { + key: 'gefahreneinschaetzung', + label: 'Gefahreneinschätzung', + type: 'select', + options: ['Keine', 'Leichte', 'Mittlere', 'Erhöhte', 'Kritische'], + }, + { key: 'vorfallsdatum', label: 'Datum des Vorfalls', type: 'date' }, { key: 'sachverhalt', label: 'Sachverhalt', type: 'longtext', required: true }, { key: 'beteiligte', label: 'Beteiligte Personen', type: 'longtext' }, { key: 'feststellungen', label: 'Feststellungen', type: 'longtext' }, + { key: 'anmerkungen', label: 'Anmerkungen', type: 'longtext' }, + { key: 'massnahmen', label: 'Maßnahmen', type: 'longtext' }, { key: 'bewertung', label: 'Abschließende Bewertung', @@ -110,9 +140,14 @@ async function main(): Promise { fields: TEMPLATE_FIELDS, isActive: true, }, - // Muster und Felder bleiben unangetastet: was der Leiter im - // Vorlagen-Editor eingestellt hat, überschreibt der Seed nicht. - update: { name: template.name, description: template.description }, + // Version 1 gehört dem Seed und wird von ihm aktuell gehalten. Ändert + // der Leiter eine Vorlage im Editor, entsteht Version 2 — die fasst der + // Seed nie an, und bestehende Akten behalten ohnehin ihre Version. + update: { + name: template.name, + description: template.description, + fields: TEMPLATE_FIELDS, + }, }) } console.log(`[seed] ${TEMPLATES.length} Startvorlagen`) diff --git a/web/app/(app)/admin/vorlagen/TemplateEditor.tsx b/web/app/(app)/admin/vorlagen/TemplateEditor.tsx new file mode 100644 index 0000000..1eb7a6c --- /dev/null +++ b/web/app/(app)/admin/vorlagen/TemplateEditor.tsx @@ -0,0 +1,200 @@ +'use client' + +import { useMemo, useState, useTransition } from 'react' +import { de } from '@/lib/i18n/de' +import { isInputField, looksLikeFields, type TemplateField } from '@/lib/templates/fields' +import { saveTemplate } from './actions' + +type Vorlage = { + key: string + name: string + description: string | null + version: number + numberPrefix: string + numberPadding: number + fields: unknown +} + +/** + * Vorlagen-Editor. + * + * Die Bausteine werden als JSON bearbeitet. Das ist bewusst: ein + * Baukasten-Editor mit Ziehen und Ablegen wäre eine eigene Phase, und die + * Vorlagen ändern sich selten. Geprüft wird gegen dasselbe Schema wie + * serverseitig, und die Vorschau zeigt sofort, was dabei herauskommt. + * + * Nicht optimistisch: eine neue Version wirkt auf alle künftigen Akten. + */ +export function TemplateEditor({ vorlage }: { vorlage: Vorlage }) { + const [name, setName] = useState(vorlage.name) + const [description, setDescription] = useState(vorlage.description ?? '') + const [numberPrefix, setNumberPrefix] = useState(vorlage.numberPrefix) + const [numberPadding, setNumberPadding] = useState(vorlage.numberPadding) + const [json, setJson] = useState(() => JSON.stringify(vorlage.fields, null, 2)) + const [result, setResult] = useState(null) + const [error, setError] = useState(null) + const [pending, startTransition] = useTransition() + + const geparst = useMemo(() => { + try { + const roh = JSON.parse(json) as unknown + // Nur eine grobe Formprüfung für die Vorschau. Ob die Vorlage + // gespeichert werden darf, entscheidet allein der Server. + if (!looksLikeFields(roh)) { + return { ok: false as const, fehler: de.templateAdmin.fieldsInvalid } + } + return { ok: true as const, fields: roh } + } catch { + return { ok: false as const, fehler: de.templateAdmin.invalid } + } + }, [json]) + + function submit() { + if (!geparst.ok) return + setError(null) + setResult(null) + startTransition(async () => { + const antwort = await saveTemplate({ + key: vorlage.key, + name, + description: description || undefined, + numberPrefix, + numberPadding, + fields: geparst.fields, + }) + if (antwort.ok) setResult(de.templateAdmin.saved(antwort.version)) + else setError(antwort.error) + }) + } + + return ( +
+
+

{vorlage.name}

+ {vorlage.key} + + {de.templateAdmin.version(vorlage.version)} + +
+ +
+
+ + + + +
+ + +
+ +