Richtlinien: Lesemodus als eigene Seite + Bearbeitungsmodus (§8)

Auf Wunsch: Dokumente öffnen nicht mehr als Popup, sondern als eigene Seite
mit Zurück-Navigation.

- Neue Routen /policies/[code] (Lesen) und /policies/[code]/edit (Bearbeiten);
  Popup-Modals durch PolicyReadView/PolicyRegisterView ersetzt. Alle Deeplinks
  ({{LINK}}, Coverage, Bibliothek, Register-Callouts) zeigen auf /policies/CODE.
- Detailseite: „← Zurück zur Bibliothek", Titel/Status/Version, Control-Chips,
  „Bearbeiten"-Button (nur Vorlagen-Dokumente, policy:write).
- Bearbeitungsmodus für Leitlinie/Richtlinien/Verfahren:
  * Vorlagen-Markdown-Editor (Textarea) mit Live-Vorschau (Lesemodus),
    Speichern via updatePolicyTemplate.
  * Dokument-bezogene Variablen (§8 Variablen-Scoping): nur die im Dokument
    vorkommenden Variablen — inkl. über BL-Referenzen gebundene — einzeln
    editierbar; Änderung propagiert zentral in alle Dokumente (eine Pflegestelle).
- Register-Actions revalidieren jetzt /policies per layout (Detailseiten aktuell).

Verifiziert: R08 als Seite (kein Modal), Editor mit 23 dokument-bezogenen
Variablen; PW_MIN_LENGTH 12→14 propagiert in R08 und Handbuch; Template-Speichern
mit Redirect zur Ansicht; zurückgesetzt.

Hinweis: Der Vier-Augen-Freigabe-Workflow mit Versionierung/Diff (§8) folgt als
nächster Schritt — aktuell speichert der Editor direkt (mit Audit-Log).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 16:29:18 +02:00
co-authored by Claude Opus 4.8
parent e2b6dee788
commit 094229011d
9 changed files with 365 additions and 163 deletions
+31 -7
View File
@@ -1,6 +1,7 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";
import { requireSession } from "@/server/auth";
import { dbForTenant } from "@/server/db";
@@ -35,7 +36,7 @@ export async function addCryptoEntry(formData: FormData) {
},
});
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "crypto_entry" });
revalidatePath("/policies");
revalidatePath("/policies", "layout");
}
export async function updateCryptoEntry(id: string, formData: FormData) {
@@ -53,14 +54,14 @@ export async function updateCryptoEntry(id: string, formData: FormData) {
},
});
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "crypto_entry", entityId: id });
revalidatePath("/policies");
revalidatePath("/policies", "layout");
}
export async function deleteCryptoEntry(id: string) {
const { session, db } = await guard();
await db.cryptoEntry.delete({ where: { id } });
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "delete", entity: "crypto_entry", entityId: id });
revalidatePath("/policies");
revalidatePath("/policies", "layout");
}
/* ── Klassifizierungs-Handhabungsmatrix (R02 / VA-08) ── */
@@ -74,7 +75,7 @@ export async function updateHandlingRule(classId: string, aspectId: string, form
create: { tenantId: session.user.tenantId, classId, aspectId, text },
});
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "handling_rule" });
revalidatePath("/policies");
revalidatePath("/policies", "layout");
}
export async function addHandlingAspect(formData: FormData) {
@@ -89,7 +90,7 @@ export async function addHandlingAspect(formData: FormData) {
},
});
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "handling_aspect" });
revalidatePath("/policies");
revalidatePath("/policies", "layout");
}
export async function addClassificationClass(formData: FormData) {
@@ -104,7 +105,7 @@ export async function addClassificationClass(formData: FormData) {
},
});
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "classification_class" });
revalidatePath("/policies");
revalidatePath("/policies", "layout");
}
/* ── Risiko-Bewertungsmatrix (R03 / VA-09) ── */
@@ -120,5 +121,28 @@ export async function updateRiskClass(id: string, formData: FormData) {
},
});
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "risk_matrix_class", entityId: id });
revalidatePath("/policies");
revalidatePath("/policies", "layout");
}
/* ── Bearbeitungsmodus Richtlinien/Verfahren ── */
/** Vorlagen-Markdown eines Dokuments speichern. */
export async function updatePolicyTemplate(code: string, formData: FormData) {
const { session, db } = await guard();
const doc = await db.policyDocument.findFirst({ where: { code } });
if (!doc) throw new Error("Dokument nicht gefunden");
const rawMarkdown = String(formData.get("rawMarkdown") ?? "");
await db.policyDocument.update({ where: { id: doc.id }, data: { rawMarkdown } });
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "policy_document", entityId: doc.id });
revalidatePath("/policies", "layout");
redirect(`/policies/${encodeURIComponent(code)}`);
}
/** Eine dokument-bezogene Variable ändern (eine Pflegestelle, §7/§8) — propagiert in alle Dokumente. */
export async function updatePolicyVariable(key: string, formData: FormData) {
const { session, db } = await guard();
const value = String(formData.get("value") ?? "");
await db.policyVariable.updateMany({ where: { key }, data: { value } });
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "policy_variable", after: { key, value } });
revalidatePath("/policies", "layout");
}