import { db } from '@/lib/db' import { visibleEntryWhere } from '@/lib/access/caseFilter' import { visibleAttachmentWhere } from '@/lib/access/attachmentFilter' import { loadSections } from '@/lib/cases/sectionService' import { isInputField, fillPlaceholders } from '@/lib/templates/fields' import { de } from '@/lib/i18n/de' import type { RenderedSection } from '@/lib/sections/render' import type { EffectivePermissions } from '@/lib/auth/permissions' /** * Der Druckbogen einer Fallakte, ohne die Seite drumherum. * * Ausgelagert, weil er an drei Stellen gebraucht wird: in der Druckansicht der * Akte selbst, im Dossier einer Person und im Bogen einer Abteilung. Stünde * das Layout dort jeweils noch einmal, liefe es auseinander - und ein Export, * der je nach Weg anders aussieht, ist keiner. * * **Ein Export darf nie sauberer aussehen als die Akte ist.** Deshalb: * gesperrte Felder erscheinen als Balken mit Vermerk statt zu fehlen, * geänderte Einträge sind als geändert gekennzeichnet, zurückgezogene stehen * als Grabstein mit Begründung. Siehe docs/08-suche-und-export.md. */ export type CaseForPrint = { id: string caseNumber: string title: string status: string templateId: string templateKey: string classification: number createdById: string dueAt: Date | null } export async function CaseSheet({ permissions, displayName, akte, /** Im Dossier steht der Deckel schon vorn; dann nur der Inhalt. */ withCover = true, authors, }: { permissions: EffectivePermissions displayName: string akte: CaseForPrint withCover?: boolean /** Einmal geladen und durchgereicht, statt je Akte erneut. */ authors?: Map }) { const [sections, entries, anhaenge, levels, autoren] = await Promise.all([ loadSections(permissions, akte.id, akte.templateId), db.caseEntry.findMany({ where: { AND: [visibleEntryWhere(permissions), { caseId: akte.id }] }, orderBy: [{ createdAt: 'asc' }, { id: 'asc' }], select: { id: true, type: true, body: true, authorId: true, createdAt: true, version: true, editedAt: true, editVisible: true, retractedAt: true, retractionReason: true, }, }), db.attachment.findMany({ where: { AND: [visibleAttachmentWhere(permissions), { caseId: akte.id }] }, orderBy: { createdAt: 'asc' }, select: { id: true, originalName: true, sha256: true, sizeBytes: true, scanStatus: true, retractedAt: true, retractionReason: true, }, }), db.clearanceLevelConfig.findMany({ orderBy: { level: 'asc' } }), authors ? [] : db.user.findMany({ select: { id: true, displayName: true } }), ]) const levelName = new Map(levels.map((l) => [l.level, l.name])) const nameVon = authors ?? new Map(autoren.map((a) => [a.id, a.displayName])) const stand = new Date() return ( <> {withCover ? (
{akte.classification} · {levelName.get(akte.classification) ?? de.common.unknown}

{akte.title}

{akte.caseNumber}

{de.export.coverNote}

) : (
{akte.classification} · {levelName.get(akte.classification) ?? de.common.unknown}

{akte.title}

{akte.caseNumber} · {de.case.statusNames[akte.status] ?? akte.status}

)}

{de.section.heading}

{de.case.timeline}

{entries.length === 0 ? (

{de.entry.empty}

) : ( entries.map((eintrag) => (

{de.entry.types[eintrag.type] ?? eintrag.type} ·{' '} {nameVon.get(eintrag.authorId) ?? eintrag.authorId} ·{' '} {eintrag.createdAt.toLocaleString('de-DE')} {eintrag.editVisible && eintrag.editedAt ? ` · ${de.export.editedAt(eintrag.version, eintrag.editedAt.toLocaleDateString('de-DE'))}` : ''}

{eintrag.retractedAt ? (

{de.entry.retracted( eintrag.retractedAt.toLocaleDateString('de-DE'), nameVon.get(eintrag.authorId) ?? eintrag.authorId, eintrag.retractionReason ?? '', )}

) : (

{eintrag.body}

)}
)) )}

{de.attachment.heading}

{anhaenge.length === 0 ? (

{de.attachment.empty}

) : ( {anhaenge.map((anhang) => ( {/* Ein zurückgezogener Anhang bleibt im Verzeichnis stehen, mit Grund. Ihn wegzulassen hiesse, den Ausdruck sauberer zu machen als die Akte - siehe docs/08. */} {/* Der Hash macht nachprüfbar, dass die Datei zum Ausdruck gehört - deshalb steht er im Verzeichnis. */} ))}
{de.export.fileName} {de.export.fileSize} {de.export.fileHash}
{anhang.originalName} {anhang.retractedAt ? ( {' '} {de.attachment.retractedBy( anhang.retractedAt.toLocaleDateString('de-DE'), anhang.retractionReason ?? '', )} ) : null} {anhang.sizeBytes} {anhang.sha256}
)} ) } function Zeile({ label, value }: { label: string; value: string }) { return ( {label} {value} ) } /** * Die Abschnitte im Druck. Ein gesperrtes Feld wird als Balken ausgegeben, mit * dem Vermerk, ab welcher Stufe es lesbar wäre - sonst merkt der Empfänger * nicht, dass er eine gekürzte Fassung in der Hand hält. */ function PrintSections({ sections, levelName, }: { sections: RenderedSection[] levelName: Map }) { const werte = Object.fromEntries( sections.filter((s) => s.visible).map((s) => [s.fieldKey, s.value]), ) return (
{sections.map((section) => { const field = section.field if (!isInputField(field)) { if (field.type === 'pagebreak') { return
} const text = fillPlaceholders(field.text ?? '', werte) return field.type === 'heading' ? (

{text || field.label}

) : (

{text}

) } return (

{field.label}

{!section.visible ? (

 {' '} {de.export.redactedAt( section.value.requiredLevel, levelName.get(section.value.requiredLevel) ?? '', )}

) : section.segments ? (

{section.segments.map((segment, index) => segment.kind === 'text' ? ( {segment.text} ) : ( // Balken statt Auslassung, mit der Stufe daneben: der // Empfaenger soll sehen, dass hier etwas fehlt.   [{segment.requiredLevel}] ), )}

) : (

{String(section.value ?? '-')}

)}
) })}
) }