/** * 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, 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([...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), ) }