Richtlinien-Update: 316 Anforderungen/45 Controls + Schutzbedarf-/TISAX-Schalter
Delta-Update des VDA-ISA-2027-Vorlagenpakets eingepflegt: - Aktualisiertes Seed-Paket (ersetzt bisherigen Stand): 316 Anforderungen (122 MUSS · 132 SOLL · 43 HOCH · 19 SEHR HOCH) über 45 Controls; VA-01/VA-05 jetzt enthalten (13 Verfahren vollständig). Beschädigte RACI-Tokens (VA-05/08/09/ 10/12/13) repariert. Importer: Umsetzungstext aus den .md-IMPL-Ankern extrahiert (mapping.json führt ihn nicht mehr), neue Obligation-Typen HOCH/SEHR HOCH. - Neue Schutzbedarf-Flags (variables.schema): FLAG_HIGH_PROTECTION, FLAG_VERY_HIGH_PROTECTION, FLAG_ELEVATED_PROTECTION (abgeleitet). Render-Helper applyProtection: HIGH stets an, VERY_HIGH aus Global/Override, ELEVATED = HIGH||VH (nie manuell) — angewandt in Lese-, Bearbeiten-, Handbuch- und Coverage-Rendering. - TISAX-Level-Schalter (AL2/AL3) zentral auf der Bibliothek (setGlobalTisaxLevel); AL2 = MUSS/SOLL/HOCH, AL3 = zusätzlich SEHR HOCH. Override je Richtlinie im Bearbeitungsmodus (setProtectionOverride, Feld protection_override); effektiver Wert = Dokument-Override sonst global. - KPIs zeigen 316 Anforderungen mit Aufschlüsselung; Coverage/Badges für HOCH/SEHR HOCH; Control-Titel-Fallback. Verifiziert: Import 316/45; Rendering rückstandsfrei über AL2/AL3 × Flag-Kombis; Override R04→AL3 zeigt SEHR-HOCH-Inhalt, R02 (global AL2) nicht; global bleibt AL2. Architektur-Hinweis: applyProtection kapselt das Level→Flags-Mapping, sodass die globale Ebene später ohne Umbau zur TISAX-AL2/AL3-Auswahl wird (bereits so gebaut). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,10 +8,10 @@ import { hasPermission, requirePermission } from "@/server/rbac";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Pill } from "@/components/mockup-ui";
|
||||
import { buildContext, renderPolicyHtml, splitPolicyDoc } from "@/lib/policy-render";
|
||||
import { applyProtection, buildContext, effectiveLevel, renderPolicyHtml, splitPolicyDoc } from "@/lib/policy-render";
|
||||
import { POLICY_STATUS_LABEL, POLICY_STATUS_TONE, POLICY_TYPE_LABEL } from "@/components/policy-modals";
|
||||
import { PolicyExpertEditor } from "@/components/policy-expert-editor";
|
||||
import { approvePolicy, rejectPolicy, submitForApproval, updatePolicyTemplate, updateScopedVariables } from "@/server/actions/policies";
|
||||
import { approvePolicy, rejectPolicy, setProtectionOverride, submitForApproval, updatePolicyTemplate, updateScopedVariables } from "@/server/actions/policies";
|
||||
|
||||
const TEMPLATE_TYPES = new Set(["LEITLINIE", "RICHTLINIE", "VERFAHREN"]);
|
||||
|
||||
@@ -62,7 +62,9 @@ export default async function PolicyEditPage({
|
||||
const docVars = variables.filter((v) => usedKeys.has(v.key) && v.kind !== "boolean");
|
||||
const keysCsv = docVars.map((v) => v.key).join(",");
|
||||
|
||||
const ctx = buildContext(variables);
|
||||
const ctx = applyProtection(buildContext(variables), doc.protectionOverride);
|
||||
const globalVeryHigh = variables.find((v) => v.key === "FLAG_VERY_HIGH_PROTECTION")?.value === "true";
|
||||
const effLevel = effectiveLevel(globalVeryHigh, doc.protectionOverride);
|
||||
const { infoMd, bodyMd } = splitPolicyDoc(doc.rawMarkdown);
|
||||
const stripBaseline = doc.code !== "BASELINE";
|
||||
const bodyHtml = renderPolicyHtml(bodyMd, ctx, { readMode: true, stripBaseline });
|
||||
@@ -180,6 +182,20 @@ export default async function PolicyEditPage({
|
||||
<p className="mt-3 text-[11px] text-muted-foreground">{t("approvalNote")}</p>
|
||||
</div>
|
||||
|
||||
{/* Schutzbedarf / TISAX-Level-Override je Richtlinie (Delta-Update) */}
|
||||
<form action={setProtectionOverride.bind(null, decoded)} className="shadow-card rounded-2xl border bg-card p-4">
|
||||
<p className="font-heading text-sm font-semibold">{t("protectionLevel")}</p>
|
||||
<p className="mt-1 mb-2 text-[11.5px] text-muted-foreground">
|
||||
{t("effectiveLevel")}: <b>{effLevel}</b> · {t("globalLevel")}: {globalVeryHigh ? "AL3" : "AL2"}
|
||||
</p>
|
||||
<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) */}
|
||||
<form action={updateScopedVariables.bind(null, decoded)} className="shadow-card rounded-2xl border bg-card p-4">
|
||||
<input type="hidden" name="keys" value={keysCsv} />
|
||||
|
||||
@@ -7,7 +7,8 @@ import { PageHead, Pill, Tag, KpiCard } from "@/components/mockup-ui";
|
||||
import { FilterTabs } from "@/components/filter-tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { controlTitle, compareControl } from "@/lib/control-titles";
|
||||
import { 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 {
|
||||
Table,
|
||||
@@ -40,15 +41,40 @@ const STATUS_LABEL: Record<string, string> = {
|
||||
ARCHIVIERT: "Archiviert",
|
||||
};
|
||||
|
||||
const OBLIGATION_STYLE: Record<string, string> = {
|
||||
MUSS: "bg-[rgba(214,60,94,0.18)] text-[#e88aa0]",
|
||||
SOLL: "bg-[rgba(90,169,230,0.18)] text-[var(--info)]",
|
||||
HOCH: "bg-[rgba(226,128,46,0.2)] text-[#e2a06e]",
|
||||
"SEHR HOCH": "bg-[rgba(125,111,214,0.22)] text-[#c3bdec]",
|
||||
};
|
||||
function ObligationBadge({ type }: { type: string }) {
|
||||
const muss = type === "MUSS";
|
||||
return (
|
||||
<span className={`inline-block rounded px-1.5 py-0.5 text-[10px] font-bold ${muss ? "bg-[rgba(214,60,94,0.18)] text-[#e88aa0]" : "bg-[rgba(90,169,230,0.18)] text-[var(--info)]"}`}>
|
||||
<span className={`inline-block whitespace-nowrap rounded px-1.5 py-0.5 text-[10px] font-bold ${OBLIGATION_STYLE[type] ?? "bg-muted text-muted-foreground"}`}>
|
||||
{type}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** Zentrale Schutzbedarf-/TISAX-Level-Einstellung (AL2/AL3) — mandantenweit. */
|
||||
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 (
|
||||
<div className="shadow-card rounded-xl border bg-card p-4">
|
||||
<p className="text-[12px] text-muted-foreground">Schutzbedarf / TISAX-Level</p>
|
||||
<div className="mt-1.5 inline-flex rounded-lg border p-1">
|
||||
{opt("AL2", "AL2")}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
/** Gruppenblock „nach Dokument": Kopfzeile je Richtlinie + Anforderungszeilen. */
|
||||
function PolicyGroup({
|
||||
policy,
|
||||
@@ -104,7 +130,7 @@ export default async function PoliciesPage({
|
||||
db.policyVariable.findMany(),
|
||||
]);
|
||||
const titleOf = (code: string) => docs.find((d) => d.code === code)?.title ?? code;
|
||||
const ctx = buildContext(variables);
|
||||
const ctx = applyProtection(buildContext(variables));
|
||||
const clean = (s: string, n = 160) =>
|
||||
renderPolicyMarkdown(s, ctx, { readMode: true, stripBaseline: true })
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
@@ -234,8 +260,9 @@ export default async function PoliciesPage({
|
||||
);
|
||||
|
||||
const kpiControls = new Set(requirements.map((r) => r.control)).size;
|
||||
const kpiMust = requirements.filter((r) => r.obligation === "MUSS").length;
|
||||
const kpiShould = requirements.length - kpiMust;
|
||||
const count = (typ: string) => requirements.filter((r) => r.obligation === typ).length;
|
||||
const globalVeryHigh = variables.find((v) => v.key === "FLAG_VERY_HIGH_PROTECTION")?.value === "true";
|
||||
const globalLevel = globalVeryHigh ? "AL3" : "AL2";
|
||||
|
||||
const typeHref = (v: string | null) => `/policies${v ? `?type=${v}` : ""}`;
|
||||
|
||||
@@ -246,8 +273,8 @@ export default async function PoliciesPage({
|
||||
<div className="mt-4 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<KpiCard label={t("kpiDocs")} value={docs.length} trend={t("kpiDocsTrend")} />
|
||||
<KpiCard label={t("kpiControls")} value={kpiControls} trend={t("kpiControlsTrend")} />
|
||||
<KpiCard label={t("kpiMust")} value={kpiMust} trend="MUSS" />
|
||||
<KpiCard label={t("kpiShould")} value={kpiShould} trend="SOLL" />
|
||||
<KpiCard label={t("kpiRequirements")} value={requirements.length} trend={`${count("MUSS")} MUSS · ${count("SOLL")} SOLL · ${count("HOCH")} HOCH · ${count("SEHR HOCH")} SEHR HOCH`} />
|
||||
<TisaxLevelCard level={globalLevel} />
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
|
||||
@@ -2,7 +2,7 @@ import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import type { PolicyDocument, PolicyVariable } from "@prisma/client";
|
||||
import { Pill } from "@/components/mockup-ui";
|
||||
import { buildContext, renderPolicyHtml, splitPolicyDoc } from "@/lib/policy-render";
|
||||
import { applyProtection, buildContext, renderPolicyHtml, splitPolicyDoc } from "@/lib/policy-render";
|
||||
|
||||
export const POLICY_TYPE_LABEL: Record<string, string> = {
|
||||
LEITLINIE: "Leitlinie",
|
||||
@@ -48,7 +48,7 @@ export interface PolicyReadData {
|
||||
export async function PolicyReadView({ data }: { data: PolicyReadData }) {
|
||||
const t = await getTranslations("policies");
|
||||
const { doc, variables, controls, relatedVas } = data;
|
||||
const ctx = buildContext(variables);
|
||||
const ctx = applyProtection(buildContext(variables), doc.protectionOverride);
|
||||
const { infoMd, bodyMd } = splitPolicyDoc(doc.rawMarkdown);
|
||||
|
||||
const stripBaseline = doc.code !== "BASELINE";
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Pill } from "@/components/mockup-ui";
|
||||
import { isExpired, isExpiring } from "@/lib/supplier";
|
||||
import { buildContext, renderPolicyHtml, resolveLink } from "@/lib/policy-render";
|
||||
import { applyProtection, buildContext, renderPolicyHtml, resolveLink } from "@/lib/policy-render";
|
||||
import {
|
||||
addClassificationClass,
|
||||
addCryptoEntry,
|
||||
@@ -375,7 +375,7 @@ function RiskMatrix({
|
||||
/* ─────────────── Anwender-Handbuch ─────────────── */
|
||||
|
||||
function Handbook({ topics, variables }: { topics: HandbookTopic[]; variables: PolicyVariable[] }) {
|
||||
const ctx = buildContext(variables);
|
||||
const ctx = applyProtection(buildContext(variables));
|
||||
const cats = [...new Set(topics.map((t) => t.category))];
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
|
||||
@@ -22,6 +22,27 @@ export function buildContext(vars: { key: string; kind: string; value: string }[
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die effektiven Schutzbedarf-/TISAX-Flags (Delta-Update):
|
||||
* - HIGH ist im TISAX-Modell stets aktiv (AL2-Baseline schließt HOHER ein).
|
||||
* - VERY_HIGH = Dokument-Override (AL2/AL3) sonst globaler Wert (AL3-Schalter).
|
||||
* - ELEVATED = HIGH || VERY_HIGH (hier stets true) — wird abgeleitet, nie manuell.
|
||||
*/
|
||||
export function applyProtection(ctx: RenderContext, override?: string | null): RenderContext {
|
||||
const global = ctx.FLAG_VERY_HIGH_PROTECTION === true;
|
||||
const veryHigh = override === "AL3" ? true : override === "AL2" ? false : global;
|
||||
ctx.FLAG_HIGH_PROTECTION = true;
|
||||
ctx.FLAG_VERY_HIGH_PROTECTION = veryHigh;
|
||||
ctx.FLAG_ELEVATED_PROTECTION = true;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/** Effektiver TISAX-Level für Anzeige: AL3 wenn VERY_HIGH aktiv, sonst AL2. */
|
||||
export function effectiveLevel(globalVeryHigh: boolean, override?: string | null): "AL2" | "AL3" {
|
||||
const veryHigh = override === "AL3" ? true : override === "AL2" ? false : globalVeryHigh;
|
||||
return veryHigh ? "AL3" : "AL2";
|
||||
}
|
||||
|
||||
// Abschnitte, die das Tool zentral verwaltet und daher im Lese-/Exportmodus ausblendet (§7a.3)
|
||||
const SUPPRESS_SECTIONS = [
|
||||
"Verwandte Dokumente",
|
||||
|
||||
@@ -204,6 +204,31 @@ export async function updateScopedVariables(code: string, formData: FormData) {
|
||||
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) ── */
|
||||
|
||||
export async function submitForApproval(code: string) {
|
||||
|
||||
Reference in New Issue
Block a user