import { isInputField, type TemplateField } from '@/lib/templates/fields' import { redactForLevel, type Segment } from '@/lib/redaction' import { AccessError, ConflictError } from '@/lib/errors' import type { EffectivePermissions } from '@/lib/auth/permissions' import type { Prisma } from '@afis/db' /** * Abschnitte: der ausgefüllte Inhalt einer Vorlage. * * Eine Fallakte trägt sie direkt, ein Dokument ebenso. Die Regeln sind * dieselben, deshalb stehen sie hier einmal und nicht zweimal: welche Stufe * ein Feld verlangt, was ein zu niedrig eingestufter Leser bekommt, und wann * ein Speichern ein Konflikt ist. */ /** Was ein Leser sieht, dessen Stufe für ein Feld nicht reicht. */ export type Placeholder = { redacted: true; requiredLevel: number } /** Die Spalten, die `CaseSection` und `DocumentSection` gemeinsam haben. */ export type SectionRow = { fieldKey: string value: unknown minClearance: number currentVersion: number updatedAt: Date updatedById: string | null } export type RenderedSection = { fieldKey: string field: TemplateField version: number updatedAt: Date | null updatedById: string | null } & ( | { visible: true; value: unknown; segments: Segment[] | null } | { visible: false; value: Placeholder } ) /** * Prisma trennt „Feld nicht setzen" von „JSON null". Ein leerer Abschnitt ist * inhaltlich JSON null, nicht das Fehlen des Feldes. */ export function asJson(value: unknown): Prisma.InputJsonValue { return (value ?? null) as Prisma.InputJsonValue } /** * Wendet die Stufe des Lesers auf die Abschnitte an. * * Felder über seiner Stufe werden **hier**, also serverseitig, durch einen * Platzhalter ersetzt. Der echte Wert verlässt den Server nicht - nie im * Client ausblenden. Dasselbe gilt für Schwärzungen im Fließtext, die als * Segmente zurückkommen. */ export function renderSections( fields: TemplateField[], rows: SectionRow[], p: EffectivePermissions, ): RenderedSection[] { const byKey = new Map(rows.map((row) => [row.fieldKey, row])) return fields.map((field): RenderedSection => { const row = byKey.get(field.key) const required = Math.max(field.minClearance ?? 0, row?.minClearance ?? 0) const common = { fieldKey: field.key, field, version: row?.currentVersion ?? 0, updatedAt: row?.updatedAt ?? null, updatedById: row?.updatedById ?? null, } if (!p.isAdmin && required > p.clearanceLevel) { return { ...common, visible: false, value: { redacted: true, requiredLevel: required } } } const value = row?.value ?? null const segments = isInputField(field) && field.type === 'longtext' && typeof value === 'string' ? redactForLevel(value, p.clearanceLevel) : null return { ...common, visible: true, value, segments } }) } /** * Prüft ein Speichern, bevor es geschrieben wird, und liefert die neue * Feldstufe und Versionsnummer. * * Wirft `AccessError`, wenn die Stufe nicht reicht, und `ConflictError`, wenn * der Abschnitt sich seit dem Laden verändert hat. Beides gilt für Fallakten * und Dokumente gleichermaßen. */ export function planSectionSave(input: { p: EffectivePermissions field: TemplateField existing: Pick | null baseVersion: number minClearance?: number }): { nextClearance: number; nextVersion: number } { const { p, field, existing } = input if (!isInputField(field)) throw new AccessError('field-unknown') // Wer ein Feld nicht lesen darf, darf es auch nicht überschreiben. const required = Math.max(field.minClearance ?? 0, existing?.minClearance ?? 0) if (!p.isAdmin && required > p.clearanceLevel) throw new AccessError('clearance-too-low') const nextClearance = input.minClearance ?? existing?.minClearance ?? field.minClearance ?? 0 if (nextClearance > p.clearanceLevel && !p.isAdmin) throw new AccessError('clearance-too-low') if ((existing?.currentVersion ?? 0) !== input.baseVersion) { throw new ConflictError({ value: existing?.value ?? null, version: existing?.currentVersion ?? 0, updatedById: existing?.updatedById ?? null, }) } return { nextClearance, nextVersion: (existing?.currentVersion ?? 0) + 1 } }