Phase 3 abgeschlossen: Vorlagen, Abschnitte, Schwärzung, Diff
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.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { de } from '@/lib/i18n/de'
|
||||
import { fillPlaceholders, isInputField } from '@/lib/templates/fields'
|
||||
import { RedactedText } from '@/components/Redacted'
|
||||
import { SectionEditor } from './SectionEditor'
|
||||
import type { RenderedSection } from '@/lib/cases/sectionService'
|
||||
|
||||
/**
|
||||
* Abschnitte einer Fallakte: der aktuelle Sachstand, aus der Vorlage erzeugt.
|
||||
*
|
||||
* Feste Textbausteine der Vorlage werden gerendert, nicht bearbeitet — ihre
|
||||
* Platzhalter ziehen die Werte der Eingabefelder. Damit trägt dieselbe
|
||||
* Darstellung ein Formular und einen Vertragstext.
|
||||
*/
|
||||
export function Sections({
|
||||
caseNumber,
|
||||
sections,
|
||||
canEdit,
|
||||
}: {
|
||||
caseNumber: string
|
||||
sections: RenderedSection[]
|
||||
canEdit: boolean
|
||||
}) {
|
||||
if (sections.length === 0) {
|
||||
return <p className="text-text-dim">{de.section.empty}</p>
|
||||
}
|
||||
|
||||
const werte = Object.fromEntries(
|
||||
sections.filter((s) => s.visible).map((s) => [s.fieldKey, s.value]),
|
||||
)
|
||||
|
||||
return (
|
||||
<dl className="space-y-8">
|
||||
{sections.map((section) => {
|
||||
const field = section.field
|
||||
|
||||
// Feste Bausteine der Vorlage.
|
||||
if (!isInputField(field)) {
|
||||
if (field.type === 'pagebreak') return <hr key={field.key} className="border-line" />
|
||||
if (field.type === 'signature') {
|
||||
return (
|
||||
<div key={field.key} className="pt-8">
|
||||
<div className="w-64 border-t border-line" />
|
||||
<p className="mt-1 text-2xs text-text-dim">{field.label}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const text = fillPlaceholders(field.text ?? '', werte)
|
||||
return field.type === 'heading' ? (
|
||||
<h3 key={field.key} className="font-condensed">
|
||||
{text || field.label}
|
||||
</h3>
|
||||
) : (
|
||||
<p key={field.key} className="max-w-(--container-measure) text-sm whitespace-pre-wrap">
|
||||
{text}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={field.key}>
|
||||
<dt className="flex flex-wrap items-baseline gap-x-3">
|
||||
<span className="font-condensed text-xs text-text-dim">{field.label}</span>
|
||||
{field.requiredToClose ? (
|
||||
<span className="font-condensed text-2xs text-stamp-3">
|
||||
{de.section.requiredToClose}
|
||||
</span>
|
||||
) : null}
|
||||
</dt>
|
||||
<dd>
|
||||
{!section.visible ? (
|
||||
// Serverseitig ersetzt: der echte Wert ist gar nicht erst hier.
|
||||
<p className="mt-1.5 flex items-center gap-2">
|
||||
<span className="redaction inline-block h-[1.1rem] w-40" aria-hidden />
|
||||
<span className="font-mono text-2xs text-text-dim">
|
||||
{de.section.lockedField(section.value.requiredLevel)}
|
||||
</span>
|
||||
</p>
|
||||
) : section.segments ? (
|
||||
<div className="mt-1.5">
|
||||
<RedactedText segments={section.segments} />
|
||||
{canEdit ? (
|
||||
<SectionEditor
|
||||
caseNumber={caseNumber}
|
||||
fieldKey={field.key}
|
||||
label={field.label}
|
||||
type={field.type}
|
||||
options={field.options}
|
||||
requiredToClose={field.requiredToClose}
|
||||
initialValue={section.value}
|
||||
initialVersion={section.version}
|
||||
readOnly={false}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<SectionEditor
|
||||
caseNumber={caseNumber}
|
||||
fieldKey={field.key}
|
||||
label={field.label}
|
||||
type={field.type}
|
||||
options={field.options}
|
||||
requiredToClose={field.requiredToClose}
|
||||
initialValue={section.value}
|
||||
initialVersion={section.version}
|
||||
readOnly={!canEdit}
|
||||
/>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user