import { de } from '@/lib/i18n/de' import { dossierRows, dossierSpan, fillPlaceholders, isInputField, type TemplateField, } from '@/lib/templates/fields' import { RedactedText } from '@/components/Redacted' import { FoundationMark } from './FoundationMark' import type { RenderedSection } from '@/lib/sections/render' /** * Das Aktenblatt: ein Formular, kein Bildschirmelement. * * Nachgebaut nach dem Blatt, das die Personalabteilung bislang von Hand * ausgefüllt hat — Raster, Rahmenstärken und Beschriftungen entsprechen ihm. * Es steht bewusst außerhalb des Design-Systems der Oberfläche, weil es * gedruckt weitergereicht wird und auf Papier gleich aussehen muss. * * Welche Felder wo stehen, kommt aus der Vorlage, nicht von hier. Damit trägt * dieselbe Bauform später einen Vorfallsbericht oder einen Bußgeldbescheid. * Siehe docs/13-entscheidungen.md. */ export function Sheet({ sections, title, subtitle, documentNumber, issuedAt, foundationLevels, footerNote, photoUrls, }: { sections: RenderedSection[] title: string subtitle: string documentNumber: string issuedAt: Date /** Aus FoundationClearance. Steuert keinen Zugriff, nur den Balken. */ foundationLevels: { level: number; name: string; colorHex: string }[] footerNote: string /** Nur geprüfte Lichtbilder, je Feldschlüssel. Serverseitig aufgelöst. */ photoUrls?: Record }) { const fields = sections.map((section) => section.field) const byKey = new Map(sections.map((section) => [section.fieldKey, section])) const werte = Object.fromEntries( sections.filter((s) => s.visible).map((s) => [s.fieldKey, s.value]), ) const { photo, photoRows, rows } = dossierRows(fields) const klassifikation = sections.find( (section) => isInputField(section.field) && section.field.slot === 'classbar', ) const stufe = klassifikation?.visible ? Number(klassifikation.value) : NaN const balken = foundationLevels.find((l) => l.level === stufe) return (

{title}

{subtitle}

{de.document.number}: {documentNumber}
{subtitle} — {balken ? `${balken.name} [Level ${balken.level}]` : de.document.unclassified} {de.document.issuedAt}: {formatiereDatum(issuedAt)}
{rows.map((row, rowIndex) => { const inPhotoRows = rowIndex < photoRows return ( {photo && rowIndex === 0 ? ( ) : null} {row.map((field) => ( ))} ) })}
{photo.label}
{footerNote} {documentNumber}
) } function PhotoBox({ section, url, }: { section: RenderedSection | undefined url: string | null }) { if (section && !section.visible) { return (
{de.section.redacted}
) } // Der Wert eines Lichtbildfeldes ist die Kennung eines geprüften Anhangs, // nie das Bild selbst. Die Adresse dazu ist serverseitig aufgelöst worden, // und nur für einen Anhang, der die Prüfkette bestanden hat. return (
{url ? ( // eslint-disable-next-line @next/next/no-img-element {section?.field.label ) : ( {de.document.photoEmpty} )}
) } function Cell({ field, section, dateSection, span, werte, }: { field: TemplateField section: RenderedSection | undefined dateSection: RenderedSection | undefined span: number werte: Record }) { if (!isInputField(field)) { const text = fillPlaceholders(field.text ?? '', werte) return ( {field.type === 'heading' ? (
{text || field.label}
) : (
{text}
)} ) } // Serverseitig ersetzt: der echte Wert ist gar nicht erst hier. if (section && !section.visible) { return (
{field.label}
) } if (field.type === 'signature') { return (
{fillPlaceholders(field.label, werte)}
{asText(section?.value)}
{field.dateKey ? (
{de.document.signedOn}: {asText(dateSection?.value) || '—'}
) : null} ) } const lang = field.type === 'longtext' return (
{field.label}
{section?.segments ? ( ) : ( asText(section?.value) || '---' )}
) } /** * Zweistellig, wie auf dem Formular: `14.03.2026`, nicht `14.3.2026`. Die * deutsche Voreinstellung kürzt, ein Formular tut das nicht. */ function formatiereDatum(value: Date): string { return value.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }) } function asText(value: unknown): string { if (value === null || value === undefined) return '' if (Array.isArray(value)) return value.join(', ') return String(value) }