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. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
/**
|
|
* Bausteine einer Vorlage — Typen und reine Funktionen.
|
|
*
|
|
* **Ohne zod.** Dieses Modul wird auch im Client geladen (Vorlagen-Editor,
|
|
* Vorschau), und eine Validierungsbibliothek im Bündel kostet mehr als das
|
|
* Budget aus docs/09-performance-und-ux.md hergibt. Die verbindliche Prüfung
|
|
* steht in `schema.ts` und läuft ausschliesslich serverseitig — dort, wo sie
|
|
* ohnehin hingehört.
|
|
*
|
|
* Eine Vorlage ist eine **geordnete Liste von Bausteinen**. Manche nehmen eine
|
|
* Eingabe entgegen, andere liefern festen Text. Damit tragen dieselbe Struktur
|
|
* ein Vorfallsbericht und ein Sonderarbeitsvertrag.
|
|
*/
|
|
|
|
/** Bausteine, die der Nutzer ausfüllt. Ihr Wert landet in einem Abschnitt. */
|
|
export const INPUT_TYPES = [
|
|
'shorttext',
|
|
'longtext',
|
|
'select',
|
|
'multiselect',
|
|
'date',
|
|
'person',
|
|
'number',
|
|
'checklist',
|
|
'table',
|
|
] as const
|
|
|
|
/** Bausteine, die aus der Vorlage kommen und nicht ausgefüllt werden. */
|
|
export const STATIC_TYPES = ['heading', 'paragraph', 'signature', 'pagebreak'] as const
|
|
|
|
export type InputType = (typeof INPUT_TYPES)[number]
|
|
export type StaticType = (typeof STATIC_TYPES)[number]
|
|
|
|
export type InputField = {
|
|
key: string
|
|
label: string
|
|
type: InputType
|
|
help?: string
|
|
minClearance?: number
|
|
required?: boolean
|
|
requiredToClose?: boolean
|
|
options?: string[]
|
|
columns?: string[]
|
|
}
|
|
|
|
export type StaticBlock = {
|
|
key: string
|
|
label: string
|
|
type: StaticType
|
|
help?: string
|
|
minClearance?: number
|
|
text?: string
|
|
}
|
|
|
|
export type TemplateField = InputField | StaticBlock
|
|
|
|
export function isInputField(field: TemplateField): field is InputField {
|
|
return (INPUT_TYPES as readonly string[]).includes(field.type)
|
|
}
|
|
|
|
/** Platzhalter `{{schlüssel}}` in einem festen Textbaustein. */
|
|
export const PLACEHOLDER = /\{\{\s*([a-z][a-z0-9_]*)\s*\}\}/g
|
|
|
|
export function fillPlaceholders(
|
|
text: string,
|
|
values: Record<string, unknown>,
|
|
fallback = (key: string) => `[${key}]`,
|
|
): string {
|
|
return text.replace(PLACEHOLDER, (_match, name: string) => {
|
|
const value = values[name]
|
|
if (value === undefined || value === null || value === '') return fallback(name)
|
|
return Array.isArray(value) ? value.join(', ') : String(value)
|
|
})
|
|
}
|
|
|
|
/** Die Bausteine, die vor dem Abschluss eines Falls gefüllt sein müssen. */
|
|
export function requiredToCloseKeys(fields: TemplateField[]): string[] {
|
|
return fields
|
|
.filter((field): field is InputField => isInputField(field) && field.requiredToClose === true)
|
|
.map((field) => field.key)
|
|
}
|
|
|
|
/**
|
|
* Grobe Formprüfung für die Vorschau im Editor. Sagt, ob sich die Eingabe
|
|
* überhaupt darstellen lässt — nicht, ob sie gespeichert werden darf. Das
|
|
* entscheidet allein der Server.
|
|
*/
|
|
export function looksLikeFields(value: unknown): value is TemplateField[] {
|
|
if (!Array.isArray(value)) return false
|
|
const known = new Set<string>([...INPUT_TYPES, ...STATIC_TYPES])
|
|
return value.every(
|
|
(item) =>
|
|
typeof item === 'object' &&
|
|
item !== null &&
|
|
typeof (item as TemplateField).key === 'string' &&
|
|
typeof (item as TemplateField).label === 'string' &&
|
|
known.has((item as TemplateField).type),
|
|
)
|
|
}
|