Files
AfiS-Aktensystem/web/lib/sections/render.ts
T
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

124 lines
4.2 KiB
TypeScript

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<SectionRow, 'minClearance' | 'currentVersion' | 'value' | 'updatedById'> | 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 }
}