Richtlinien-Bearbeitungsmodus am Mockup ausgerichtet: Variablen + Vier-Augen-Freigabe

Korrektur nach Mockup-Abgleich: Der Bearbeitungsmodus editiert NICHT den Rohtext,
sondern (wie im Mockup und Akzeptanzkriterium „im Bearbeiten nur dokumentbezogene
Variablen") die dokument-bezogenen Variablen — freie Blocktext-Änderungen sind
dem KI-Wizard vorbehalten.

- Edit-Seite neu: gerendertes Dokument links, rechts Karte „Dokument-Variablen"
  (nur die im Dokument vorkommenden, inkl. BL-gebundene; zentrale Pflegestelle §8)
  + „Freigabe"-Leiste. Rohtext-Textarea entfernt.
- Freigabe-Workflow (Vier-Augen, §8): Status Entwurf → In Freigabe → Freigegeben;
  submitForApproval/approvePolicy/rejectPolicy; Freigabe erfordert policy:approve
  UND Freigebender ≠ Einreichender; Ablehnung mit Begründung → zurück auf Entwurf;
  alles im Audit-Log. Neue Felder submitted_by/approved_by/approved_at.
- updateScopedVariables: gebündeltes Speichern der Dokument-Variablen.

Verifiziert: Editor zeigt Variablen-Karte + Freigabe-Leiste (kein Rohtext);
R08 „Erneut einreichen" → In Freigabe; Vier-Augen-Sperre greift (Anna = Autor).

Offen (nächste Ausbaustufe): echte Versionierung mit Entwurf-neben-Freigegeben +
block-/klauselgenauer Diff; KI-Wizard für Blocktext; DOCX/PDF-Export; Word-Upload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 16:45:58 +02:00
co-authored by Claude Opus 4.8
parent 094229011d
commit 3cffd3f32e
6 changed files with 190 additions and 72 deletions
+60
View File
@@ -146,3 +146,63 @@ export async function updatePolicyVariable(key: string, formData: FormData) {
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "policy_variable", after: { key, value } });
revalidatePath("/policies", "layout");
}
/**
* Dokument-bezogene Variablen gebündelt speichern (Bearbeitungsmodus). Nur die
* übergebenen Schlüssel (Feld `keys` = kommagetrennt) werden aus dem Formular
* gelesen; Werte gelten zentral für alle Dokumente (eine Pflegestelle, §8).
*/
export async function updateScopedVariables(code: string, formData: FormData) {
const { session, db } = await guard();
const keys = String(formData.get("keys") ?? "").split(",").map((k) => k.trim()).filter(Boolean);
for (const key of keys) {
if (!formData.has(`var_${key}`)) continue;
const value = String(formData.get(`var_${key}`) ?? "");
await db.policyVariable.updateMany({ where: { key }, data: { value } });
}
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "policy_variables", entityId: code, after: { keys } });
revalidatePath("/policies", "layout");
redirect(`/policies/${encodeURIComponent(code)}/edit`);
}
/* ── Freigabe-Workflow (Vier-Augen, §8) ── */
export async function submitForApproval(code: string) {
const { session, db } = await guard();
const doc = await db.policyDocument.findFirst({ where: { code } });
if (!doc) throw new Error("Dokument nicht gefunden");
await db.policyDocument.update({ where: { id: doc.id }, data: { status: "IN_FREIGABE", submittedBy: session.user.id, approvedBy: null, approvedAt: null } });
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "policy_document", entityId: doc.id, after: { status: "IN_FREIGABE" } });
revalidatePath("/policies", "layout");
redirect(`/policies/${encodeURIComponent(code)}/edit`);
}
export async function approvePolicy(code: string) {
const session = await requireSession();
requirePermission(session, "policy:approve");
const db = dbForTenant(session.user.tenantId);
const doc = await db.policyDocument.findFirst({ where: { code } });
if (!doc) throw new Error("Dokument nicht gefunden");
if (doc.status !== "IN_FREIGABE") throw new Error("Dokument ist nicht in Freigabe");
// Vier-Augen: Freigebender ≠ Einreichender
if (doc.submittedBy && doc.submittedBy === session.user.id) {
throw new Error("Vier-Augen-Prinzip: Freigabe muss durch eine andere Person als den Einreichenden erfolgen.");
}
await db.policyDocument.update({ where: { id: doc.id }, data: { status: "FREIGEGEBEN", approvedBy: session.user.id, approvedAt: new Date() } });
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "policy_document", entityId: doc.id, after: { status: "FREIGEGEBEN" } });
revalidatePath("/policies", "layout");
redirect(`/policies/${encodeURIComponent(code)}`);
}
export async function rejectPolicy(code: string, formData: FormData) {
const session = await requireSession();
requirePermission(session, "policy:approve");
const db = dbForTenant(session.user.tenantId);
const doc = await db.policyDocument.findFirst({ where: { code } });
if (!doc) throw new Error("Dokument nicht gefunden");
const reason = str(formData.get("reason"));
await db.policyDocument.update({ where: { id: doc.id }, data: { status: "ENTWURF", submittedBy: null } });
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "policy_document", entityId: doc.id, after: { status: "ENTWURF", rejected: true, reason } });
revalidatePath("/policies", "layout");
redirect(`/policies/${encodeURIComponent(code)}/edit`);
}