Richtlinien-Fixes: zentrale Variablen, Schutzbedarf zentral, Coverage nach Level
- Zentrale Variablen (Organisation + Rollen + Schutzbedarf-Flags, lib/policy-variables.ts) sind im Richtlinien-Editor ausgeblendet und werden nur noch in den Einstellungen gepflegt; updatePolicyVariable/updateScopedVariables blocken zentrale Keys zusätzlich serverseitig. - Schutzbedarf/TISAX wird ausschließlich zentral gesteuert (Superadmin): Override je Richtlinie entfernt (setProtectionOverride + UI weg), globaler TISAX-Schalter im Policy-Modul entfernt (setGlobalTisaxLevel weg) → nur noch Read-only-Anzeige im Editor und in der Bibliothek. Rendering nutzt stets den globalen Level. - Coverage-/Referenzmatrix zeigt nur Anforderungen des aktiven Assessment-Levels (Bedingung erfüllt): bei AL2 keine „sehr hoch"-Controls (browser-verifiziert: 297 statt 316 Anforderungen, 0 SEHR HOCH); AL3 zeigt alle. tsc + lint + build grün; Edit-Seite: Schutzbedarf read-only, zentrale Variablen gesperrt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,10 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Pill } from "@/components/mockup-ui";
|
import { Pill } from "@/components/mockup-ui";
|
||||||
import { applyProtection, buildContext, effectiveLevel, renderPolicyHtml, splitPolicyDoc } from "@/lib/policy-render";
|
import { applyProtection, buildContext, effectiveLevel, renderPolicyHtml, splitPolicyDoc } from "@/lib/policy-render";
|
||||||
|
import { isCentralVariable } from "@/lib/policy-variables";
|
||||||
import { POLICY_STATUS_LABEL, POLICY_STATUS_TONE, POLICY_TYPE_LABEL } from "@/components/policy-modals";
|
import { POLICY_STATUS_LABEL, POLICY_STATUS_TONE, POLICY_TYPE_LABEL } from "@/components/policy-modals";
|
||||||
import { PolicyExpertEditor } from "@/components/policy-expert-editor";
|
import { PolicyExpertEditor } from "@/components/policy-expert-editor";
|
||||||
import { approvePolicy, rejectPolicy, setProtectionOverride, submitForApproval, updatePolicyTemplate, updateScopedVariables } from "@/server/actions/policies";
|
import { approvePolicy, rejectPolicy, submitForApproval, updatePolicyTemplate, updateScopedVariables } from "@/server/actions/policies";
|
||||||
|
|
||||||
const TEMPLATE_TYPES = new Set(["LEITLINIE", "RICHTLINIE", "VERFAHREN"]);
|
const TEMPLATE_TYPES = new Set(["LEITLINIE", "RICHTLINIE", "VERFAHREN"]);
|
||||||
|
|
||||||
@@ -59,12 +60,14 @@ export default async function PolicyEditPage({
|
|||||||
const userName = (id: string | null) => users.find((u) => u.id === id)?.name ?? id ?? "—";
|
const userName = (id: string | null) => users.find((u) => u.id === id)?.name ?? id ?? "—";
|
||||||
|
|
||||||
const usedKeys = docVariableKeys(doc.rawMarkdown, baseline);
|
const usedKeys = docVariableKeys(doc.rawMarkdown, baseline);
|
||||||
const docVars = variables.filter((v) => usedKeys.has(v.key) && v.kind !== "boolean");
|
// Zentrale Variablen (Organisation/Rollen) sind nur in den Einstellungen pflegbar → hier ausblenden.
|
||||||
|
const docVars = variables.filter((v) => usedKeys.has(v.key) && v.kind !== "boolean" && !isCentralVariable(v));
|
||||||
const keysCsv = docVars.map((v) => v.key).join(",");
|
const keysCsv = docVars.map((v) => v.key).join(",");
|
||||||
|
|
||||||
const ctx = applyProtection(buildContext(variables), doc.protectionOverride);
|
// Schutzbedarf wird zentral gesteuert (Superadmin) — kein Override je Richtlinie mehr.
|
||||||
|
const ctx = applyProtection(buildContext(variables));
|
||||||
const globalVeryHigh = variables.find((v) => v.key === "FLAG_VERY_HIGH_PROTECTION")?.value === "true";
|
const globalVeryHigh = variables.find((v) => v.key === "FLAG_VERY_HIGH_PROTECTION")?.value === "true";
|
||||||
const effLevel = effectiveLevel(globalVeryHigh, doc.protectionOverride);
|
const effLevel = effectiveLevel(globalVeryHigh);
|
||||||
const { infoMd, bodyMd } = splitPolicyDoc(doc.rawMarkdown);
|
const { infoMd, bodyMd } = splitPolicyDoc(doc.rawMarkdown);
|
||||||
const stripBaseline = doc.code !== "BASELINE";
|
const stripBaseline = doc.code !== "BASELINE";
|
||||||
const bodyHtml = renderPolicyHtml(bodyMd, ctx, { readMode: true, stripBaseline });
|
const bodyHtml = renderPolicyHtml(bodyMd, ctx, { readMode: true, stripBaseline });
|
||||||
@@ -182,25 +185,18 @@ export default async function PolicyEditPage({
|
|||||||
<p className="mt-3 text-[11px] text-muted-foreground">{t("approvalNote")}</p>
|
<p className="mt-3 text-[11px] text-muted-foreground">{t("approvalNote")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Schutzbedarf / TISAX-Level-Override je Richtlinie (Delta-Update) */}
|
{/* Schutzbedarf / TISAX-Level — zentral gesteuert (nur Anzeige) */}
|
||||||
<form action={setProtectionOverride.bind(null, decoded)} className="shadow-card rounded-2xl border bg-card p-4">
|
<div className="shadow-card rounded-2xl border bg-card p-4">
|
||||||
<p className="font-heading text-sm font-semibold">{t("protectionLevel")}</p>
|
<p className="font-heading text-sm font-semibold">{t("protectionLevel")}</p>
|
||||||
<p className="mt-1 mb-2 text-[11.5px] text-muted-foreground">
|
<p className="mt-1 text-[12.5px]">{t("effectiveLevel")}: <b>{effLevel}</b></p>
|
||||||
{t("effectiveLevel")}: <b>{effLevel}</b> · {t("globalLevel")}: {globalVeryHigh ? "AL3" : "AL2"}
|
<p className="mt-2 text-[11px] text-muted-foreground">Der Schutzbedarf wird zentral vom Plattform-Betreiber gesteuert und ist hier nicht änderbar.</p>
|
||||||
</p>
|
</div>
|
||||||
<select name="override" defaultValue={doc.protectionOverride ?? ""} className="h-8 w-full rounded-md border border-input bg-transparent px-2 text-[12.5px]">
|
|
||||||
<option value="">{t("levelGlobal")} ({globalVeryHigh ? "AL3" : "AL2"})</option>
|
|
||||||
<option value="AL2">AL2 — {t("levelAl2")}</option>
|
|
||||||
<option value="AL3">AL3 — {t("levelAl3")}</option>
|
|
||||||
</select>
|
|
||||||
<Button type="submit" size="sm" className="mt-3 w-full justify-center">{tc("save")}</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Dokument-Variablen (§8 Variablen-Scoping) */}
|
{/* Dokument-Variablen (§8 Variablen-Scoping) — zentrale Variablen ausgenommen */}
|
||||||
<form action={updateScopedVariables.bind(null, decoded)} className="shadow-card rounded-2xl border bg-card p-4">
|
<form action={updateScopedVariables.bind(null, decoded)} className="shadow-card rounded-2xl border bg-card p-4">
|
||||||
<input type="hidden" name="keys" value={keysCsv} />
|
<input type="hidden" name="keys" value={keysCsv} />
|
||||||
<p className="font-heading text-sm font-semibold">{t("docVariables")}</p>
|
<p className="font-heading text-sm font-semibold">{t("docVariables")}</p>
|
||||||
<p className="mt-1 mb-3 text-[11.5px] text-muted-foreground">{t("docVariablesHint")}</p>
|
<p className="mt-1 mb-3 text-[11.5px] text-muted-foreground">Zentrale Variablen (Unternehmensdaten, Rollen) werden ausschließlich in den Einstellungen gepflegt.</p>
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
{docVars.length === 0 && <p className="text-[12.5px] text-muted-foreground">{tc("none")}</p>}
|
{docVars.length === 0 && <p className="text-[12.5px] text-muted-foreground">{tc("none")}</p>}
|
||||||
{docVars.map((v) => (
|
{docVars.map((v) => (
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { FilterTabs } from "@/components/filter-tabs";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { controlTitle, compareControl } from "@/lib/control-titles";
|
import { controlTitle, compareControl } from "@/lib/control-titles";
|
||||||
import { applyProtection, buildContext, renderPolicyMarkdown } from "@/lib/policy-render";
|
import { applyProtection, buildContext, renderPolicyMarkdown } from "@/lib/policy-render";
|
||||||
import { setGlobalTisaxLevel } from "@/server/actions/policies";
|
|
||||||
import type { PolicyRequirement } from "@prisma/client";
|
import type { PolicyRequirement } from "@prisma/client";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
@@ -55,22 +54,13 @@ function ObligationBadge({ type }: { type: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Zentrale Schutzbedarf-/TISAX-Level-Einstellung (AL2/AL3) — mandantenweit. */
|
/** Schutzbedarf-/TISAX-Level — zentral vom Betreiber gesteuert (nur Anzeige). */
|
||||||
function TisaxLevelCard({ level }: { level: "AL2" | "AL3" }) {
|
function TisaxLevelCard({ level }: { level: "AL2" | "AL3" }) {
|
||||||
const opt = (lvl: "AL2" | "AL3", label: string) => (
|
|
||||||
<form action={setGlobalTisaxLevel}>
|
|
||||||
<input type="hidden" name="level" value={lvl} />
|
|
||||||
<button type="submit" className={`rounded-md px-2.5 py-1 text-[12px] font-bold ${level === lvl ? "bg-[var(--sidebar-accent)] text-[var(--primary)]" : "text-muted-foreground hover:text-foreground"}`}>{label}</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<div className="shadow-card rounded-xl border bg-card p-4">
|
<div className="shadow-card rounded-xl border bg-card p-4">
|
||||||
<p className="text-[12px] text-muted-foreground">Schutzbedarf / TISAX-Level</p>
|
<p className="text-[12px] text-muted-foreground">Schutzbedarf / TISAX-Level</p>
|
||||||
<div className="mt-1.5 inline-flex rounded-lg border p-1">
|
<p className="mt-1.5 font-heading text-lg font-bold">{level}</p>
|
||||||
{opt("AL2", "AL2")}
|
<p className="mt-1 text-[11px] text-muted-foreground">{level === "AL3" ? "MUSS · SOLL · HOCH · SEHR HOCH" : "MUSS · SOLL · HOCH"} · zentral gesteuert</p>
|
||||||
{opt("AL3", "AL3")}
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-[11px] text-muted-foreground">{level === "AL3" ? "MUSS · SOLL · HOCH · SEHR HOCH" : "MUSS · SOLL · HOCH"}</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -133,6 +123,9 @@ export default async function PoliciesPage({
|
|||||||
]);
|
]);
|
||||||
const titleOf = (code: string) => docs.find((d) => d.code === code)?.title ?? code;
|
const titleOf = (code: string) => docs.find((d) => d.code === code)?.title ?? code;
|
||||||
const ctx = applyProtection(buildContext(variables));
|
const ctx = applyProtection(buildContext(variables));
|
||||||
|
// Coverage nach Assessment-Level: nur Anforderungen, deren Bedingung im effektiven
|
||||||
|
// Level erfüllt ist (AL2 blendet „sehr hoch" = FLAG_VERY_HIGH_PROTECTION aus).
|
||||||
|
const activeRequirements = requirements.filter((r) => !r.condition || ctx[r.condition] === true);
|
||||||
const clean = (s: string, n = 160) =>
|
const clean = (s: string, n = 160) =>
|
||||||
renderPolicyMarkdown(s, ctx, { readMode: true, stripBaseline: true })
|
renderPolicyMarkdown(s, ctx, { readMode: true, stripBaseline: true })
|
||||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||||
@@ -170,14 +163,14 @@ export default async function PoliciesPage({
|
|||||||
/* ── Referenz-/Coverage-Matrix (§9.3, nach Mockup) ── */
|
/* ── Referenz-/Coverage-Matrix (§9.3, nach Mockup) ── */
|
||||||
if (isCoverage) {
|
if (isCoverage) {
|
||||||
const dir = params.dir === "doc" ? "doc" : "ctrl";
|
const dir = params.dir === "doc" ? "doc" : "ctrl";
|
||||||
const controlCount = new Set(requirements.map((r) => r.control)).size;
|
const controlCount = new Set(activeRequirements.map((r) => r.control)).size;
|
||||||
const vasOf = (ids: string[]) => [...new Set(ids)].sort();
|
const vasOf = (ids: string[]) => [...new Set(ids)].sort();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 p-6">
|
<main className="flex-1 p-6">
|
||||||
{head}
|
{head}
|
||||||
<div className="mt-3 flex flex-wrap items-center justify-between gap-3">
|
<div className="mt-3 flex flex-wrap items-center justify-between gap-3">
|
||||||
<p className="text-[12.5px] text-muted-foreground">{t("coverageHint", { controls: controlCount, reqs: requirements.length })}</p>
|
<p className="text-[12.5px] text-muted-foreground">{t("coverageHint", { controls: controlCount, reqs: activeRequirements.length })}</p>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="inline-flex rounded-lg border bg-card p-1 text-[12px] font-semibold">
|
<div className="inline-flex rounded-lg border bg-card p-1 text-[12px] font-semibold">
|
||||||
<Link href="/policies?view=coverage" className={dir === "ctrl" ? "rounded-md bg-[var(--sidebar-accent)] px-3 py-1.5 text-[var(--primary)]" : "px-3 py-1.5 text-muted-foreground hover:text-foreground"}>{t("byControl")}</Link>
|
<Link href="/policies?view=coverage" className={dir === "ctrl" ? "rounded-md bg-[var(--sidebar-accent)] px-3 py-1.5 text-[var(--primary)]" : "px-3 py-1.5 text-muted-foreground hover:text-foreground"}>{t("byControl")}</Link>
|
||||||
@@ -199,8 +192,8 @@ export default async function PoliciesPage({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[...new Set(requirements.map((r) => r.control))].sort(compareControl).map((control) => {
|
{[...new Set(activeRequirements.map((r) => r.control))].sort(compareControl).map((control) => {
|
||||||
const list = requirements.filter((r) => r.control === control);
|
const list = activeRequirements.filter((r) => r.control === control);
|
||||||
const vas = vasOf(list.flatMap((r) => r.vaCodes));
|
const vas = vasOf(list.flatMap((r) => r.vaCodes));
|
||||||
return (
|
return (
|
||||||
<tr key={control} className="border-b align-top last:border-0">
|
<tr key={control} className="border-b align-top last:border-0">
|
||||||
@@ -241,8 +234,8 @@ export default async function PoliciesPage({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{[...new Set(requirements.map((r) => r.policyCode))].map((policy) => (
|
{[...new Set(activeRequirements.map((r) => r.policyCode))].map((policy) => (
|
||||||
<PolicyGroup key={policy} policy={policy} title={titleOf(policy)} rows={requirements.filter((r) => r.policyCode === policy)} clean={clean} />
|
<PolicyGroup key={policy} policy={policy} title={titleOf(policy)} rows={activeRequirements.filter((r) => r.policyCode === policy)} clean={clean} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export interface PolicyReadData {
|
|||||||
export async function PolicyReadView({ data }: { data: PolicyReadData }) {
|
export async function PolicyReadView({ data }: { data: PolicyReadData }) {
|
||||||
const t = await getTranslations("policies");
|
const t = await getTranslations("policies");
|
||||||
const { doc, variables, controls, relatedVas } = data;
|
const { doc, variables, controls, relatedVas } = data;
|
||||||
const ctx = applyProtection(buildContext(variables), doc.protectionOverride);
|
const ctx = applyProtection(buildContext(variables));
|
||||||
const { infoMd, bodyMd } = splitPolicyDoc(doc.rawMarkdown);
|
const { infoMd, bodyMd } = splitPolicyDoc(doc.rawMarkdown);
|
||||||
|
|
||||||
const stripBaseline = doc.code !== "BASELINE";
|
const stripBaseline = doc.code !== "BASELINE";
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Abgrenzung „zentraler" Richtlinien-Variablen (nur in den Einstellungen pflegbar,
|
||||||
|
* im Richtlinien-Editor gesperrt): Organisation (Unternehmensname/Scope, …),
|
||||||
|
* verantwortliche Rollen (ROLE_*) und die Schutzbedarf-Flags. Alles andere
|
||||||
|
* (dokument-/paketspezifische Variablen, eigene) bleibt im Editor pflegbar.
|
||||||
|
*
|
||||||
|
* Reine Funktion ohne Server-/Render-Abhängigkeiten (auch in Actions nutzbar).
|
||||||
|
*/
|
||||||
|
export const CENTRAL_VARIABLE_GROUPS = new Set(["Organisation", "Rollen"]);
|
||||||
|
export const CENTRAL_VARIABLE_KEYS = new Set(["FLAG_HIGH_PROTECTION", "FLAG_VERY_HIGH_PROTECTION"]);
|
||||||
|
|
||||||
|
export function isCentralVariable(v: { key: string; groupName?: string | null }): boolean {
|
||||||
|
if (v.groupName && CENTRAL_VARIABLE_GROUPS.has(v.groupName)) return true;
|
||||||
|
return CENTRAL_VARIABLE_KEYS.has(v.key);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { redirect } from "next/navigation";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { moduleGuard } from "@/server/action-guard";
|
import { moduleGuard } from "@/server/action-guard";
|
||||||
import { type Permission } from "@/server/rbac";
|
import { type Permission } from "@/server/rbac";
|
||||||
|
import { isCentralVariable } from "@/lib/policy-variables";
|
||||||
import { writeAuditLog } from "@/server/audit";
|
import { writeAuditLog } from "@/server/audit";
|
||||||
|
|
||||||
const optDate = (v: FormDataEntryValue | null) => (v && String(v) ? new Date(String(v)) : null);
|
const optDate = (v: FormDataEntryValue | null) => (v && String(v) ? new Date(String(v)) : null);
|
||||||
@@ -179,6 +180,8 @@ export async function updatePolicyTemplate(code: string, formData: FormData) {
|
|||||||
/** Eine dokument-bezogene Variable ändern (eine Pflegestelle, §7/§8) — propagiert in alle Dokumente. */
|
/** Eine dokument-bezogene Variable ändern (eine Pflegestelle, §7/§8) — propagiert in alle Dokumente. */
|
||||||
export async function updatePolicyVariable(key: string, formData: FormData) {
|
export async function updatePolicyVariable(key: string, formData: FormData) {
|
||||||
const { session, db } = await guard();
|
const { session, db } = await guard();
|
||||||
|
const variable = await db.policyVariable.findFirst({ where: { key }, select: { key: true, groupName: true } });
|
||||||
|
if (variable && isCentralVariable(variable)) throw new Error("Zentrale Variablen werden ausschließlich in den Einstellungen gepflegt.");
|
||||||
const value = String(formData.get("value") ?? "");
|
const value = String(formData.get("value") ?? "");
|
||||||
await db.policyVariable.updateMany({ where: { key }, data: { 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 } });
|
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "policy_variable", after: { key, value } });
|
||||||
@@ -193,8 +196,11 @@ export async function updatePolicyVariable(key: string, formData: FormData) {
|
|||||||
export async function updateScopedVariables(code: string, formData: FormData) {
|
export async function updateScopedVariables(code: string, formData: FormData) {
|
||||||
const { session, db } = await guard();
|
const { session, db } = await guard();
|
||||||
const keys = String(formData.get("keys") ?? "").split(",").map((k) => k.trim()).filter(Boolean);
|
const keys = String(formData.get("keys") ?? "").split(",").map((k) => k.trim()).filter(Boolean);
|
||||||
|
// Zentrale Variablen (Organisation/Rollen) niemals aus dem Richtlinien-Editor überschreiben.
|
||||||
|
const rows = keys.length ? await db.policyVariable.findMany({ where: { key: { in: keys } }, select: { key: true, groupName: true } }) : [];
|
||||||
|
const centralKeys = new Set(rows.filter(isCentralVariable).map((r) => r.key));
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
if (!formData.has(`var_${key}`)) continue;
|
if (centralKeys.has(key) || !formData.has(`var_${key}`)) continue;
|
||||||
const value = String(formData.get(`var_${key}`) ?? "");
|
const value = String(formData.get(`var_${key}`) ?? "");
|
||||||
await db.policyVariable.updateMany({ where: { key }, data: { value } });
|
await db.policyVariable.updateMany({ where: { key }, data: { value } });
|
||||||
}
|
}
|
||||||
@@ -203,31 +209,6 @@ export async function updateScopedVariables(code: string, formData: FormData) {
|
|||||||
redirect(`/policies/${encodeURIComponent(code)}/edit`);
|
redirect(`/policies/${encodeURIComponent(code)}/edit`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Schutzbedarf / TISAX-Level (Delta-Update) ── */
|
|
||||||
|
|
||||||
/** Globaler TISAX-Level: AL2 (very-high aus) / AL3 (very-high an); HIGH bleibt stets an. */
|
|
||||||
export async function setGlobalTisaxLevel(formData: FormData) {
|
|
||||||
const { session, db } = await guard();
|
|
||||||
const level = String(formData.get("level")) === "AL3" ? "AL3" : "AL2";
|
|
||||||
await db.policyVariable.updateMany({ where: { key: "FLAG_VERY_HIGH_PROTECTION" }, data: { value: level === "AL3" ? "true" : "false" } });
|
|
||||||
await db.policyVariable.updateMany({ where: { key: "FLAG_HIGH_PROTECTION" }, data: { value: "true" } });
|
|
||||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "tisax_level", after: { level } });
|
|
||||||
revalidatePath("/policies", "layout");
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Schutzbedarf-Override je Richtlinie: "" = global, "AL2", "AL3". */
|
|
||||||
export async function setProtectionOverride(code: string, formData: FormData) {
|
|
||||||
const { session, db } = await guard();
|
|
||||||
const raw = String(formData.get("override") ?? "");
|
|
||||||
const override = raw === "AL2" || raw === "AL3" ? raw : null;
|
|
||||||
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: { protectionOverride: override } });
|
|
||||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "policy_document", entityId: doc.id, after: { protectionOverride: override } });
|
|
||||||
revalidatePath("/policies", "layout");
|
|
||||||
redirect(`/policies/${encodeURIComponent(code)}/edit`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Freigabe-Workflow (Vier-Augen, §8) ── */
|
/* ── Freigabe-Workflow (Vier-Augen, §8) ── */
|
||||||
|
|
||||||
export async function submitForApproval(code: string) {
|
export async function submitForApproval(code: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user