import { db } from '@/lib/db' import { writeAudit } from '@/lib/audit/write' import { canEditCase } from '@/lib/access/caseWrite' import { loadVisibleCase } from '@/lib/cases/service' import { AccessError } from '@/lib/errors' import { parseTemplateFields } from '@/lib/templates/schema' import { asJson, planSectionSave, renderSections, type RenderedSection, } from '@/lib/sections/render' import type { TemplateField } from '@/lib/templates/fields' import type { EffectivePermissions } from '@/lib/auth/permissions' /** * Abschnitte einer Fallakte: der aktuelle Sachstand, aus der Vorlage erzeugt * und je Feld versioniert. Siehe docs/05-akten-und-vorlagen.md. * * Die Regeln stehen in `lib/sections/render.ts`, weil Dokumente dieselben * haben. Hier steht nur, welche Tabelle gelesen und geschrieben wird. */ export type { RenderedSection, Placeholder } from '@/lib/sections/render' export { ConflictError } from '@/lib/errors' export function templateFields(raw: unknown): TemplateField[] { return parseTemplateFields(raw) } export async function loadSections( p: EffectivePermissions, caseId: string, templateId: string, ): Promise { const [template, rows] = await Promise.all([ db.documentTemplate.findUnique({ where: { id: templateId }, select: { fields: true } }), db.caseSection.findMany({ where: { caseId } }), ]) return renderSections(templateFields(template?.fields), rows, p) } /** * Speichert einen Abschnitt. * * Der Client schickt die Version mit, die er geladen hat. Weicht sie ab, gibt * es kein stilles Überschreiben, sondern einen Konflikt mit beiden Fassungen. */ export async function saveSection( p: EffectivePermissions, input: { caseNumber: string fieldKey: string value: unknown /** Version, die der Client geladen hat. 0 heisst: Feld war noch leer. */ baseVersion: number minClearance?: number }, ): Promise<{ version: number }> { const target = await loadVisibleCase(p, input.caseNumber) if (!target) throw new AccessError('not-found') const check = canEditCase(p, target) if (!check.ok) throw new AccessError(check.reason) const template = await db.documentTemplate.findUnique({ where: { id: target.templateId }, select: { fields: true }, }) const field = templateFields(template?.fields).find((f) => f.key === input.fieldKey) if (!field) throw new AccessError('field-unknown') const existing = await db.caseSection.findUnique({ where: { caseId_fieldKey: { caseId: target.id, fieldKey: input.fieldKey } }, }) const { nextClearance, nextVersion } = planSectionSave({ p, field, existing, baseVersion: input.baseVersion, minClearance: input.minClearance, }) await db.$transaction(async (tx) => { if (existing) { // Der vorherige Stand wandert in die Versionstabelle, bevor überschrieben // wird. Die Historie ist damit lückenlos. await tx.caseSectionVersion.create({ data: { sectionId: existing.id, version: existing.currentVersion, value: asJson(existing.value), authorId: existing.updatedById, }, }) await tx.caseSection.update({ where: { id: existing.id }, data: { value: asJson(input.value), currentVersion: nextVersion, updatedById: p.userId, minClearance: nextClearance, }, }) } else { await tx.caseSection.create({ data: { caseId: target.id, fieldKey: input.fieldKey, value: asJson(input.value), currentVersion: 1, updatedById: p.userId, minClearance: nextClearance, }, }) } await writeAudit(tx, { actorId: p.userId, action: 'case.update', targetType: 'CaseSection', targetId: `${target.id}:${input.fieldKey}`, clearanceAtTime: p.clearanceLevel, meta: { caseNumber: target.caseNumber, fieldKey: input.fieldKey, version: nextVersion, minClearance: nextClearance, }, }) }) return { version: nextVersion } } /** Die Versionen eines Abschnitts, für die Diff-Ansicht. */ export async function loadSectionHistory( p: EffectivePermissions, caseNumber: string, fieldKey: string, ) { const target = await loadVisibleCase(p, caseNumber) if (!target) throw new AccessError('not-found') const section = await db.caseSection.findUnique({ where: { caseId_fieldKey: { caseId: target.id, fieldKey } }, select: { id: true, minClearance: true, value: true, currentVersion: true, updatedById: true }, }) if (!section) return null if (!p.isAdmin && section.minClearance > p.clearanceLevel) throw new AccessError('clearance-too-low') const versions = await db.caseSectionVersion.findMany({ where: { sectionId: section.id }, orderBy: { version: 'desc' }, take: 50, }) return { section, versions } }