Referenzmatrix nach Mockup + Risiko-Bewertungsmatrix editierbar

Referenz-/Coverage-Matrix (§9.3) wie im GEFIM-Mockup aufgebaut:
- Zwei Richtungen „nach Control" und „nach Dokument" (Umschalter) + Assessment-
  Export-Button (Platzhalter).
- nach Control: Control-Nr + -Titel, je Anforderung MUSS/SOLL-Badge + Text,
  Richtlinie-Link, Verfahren-Chips.
- nach Dokument: gruppiert je Richtlinie (Kopfzeile), Zeilen Control · Anforderung
  · Umsetzung (Variablen aufgelöst, BL entfernt) · Verfahren.
- 44 Control-Titel aus dem Mockup übernommen (lib/control-titles.ts), numerische
  Control-Sortierung.

Risiko-Bewertungsmatrix jetzt editierbar (war nur anzeigend / Edit-Popover wurde
vom overflow-Container abgeschnitten):
- Risikoklassen, Eintrittswahrscheinlichkeit UND Schadenskategorien inline
  bearbeitbar (Klick auf Zeile klappt Formular auf — kein abgeschnittenes Popover).
- Neue Actions updateEwLevel/updateDamageDimension; Krypto-Register-Clipping
  (overflow-x-auto) ebenfalls entfernt.

Verifiziert: beide Referenz-Richtungen gerendert; Risikoklasse-Edit end-to-end
gespeichert (persistiert).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 07:20:12 +02:00
co-authored by Claude Opus 4.8
parent 92c184dfc6
commit 222046a362
6 changed files with 320 additions and 112 deletions
+8 -1
View File
@@ -564,6 +564,13 @@
"saveVariables": "Variablen speichern", "saveVariables": "Variablen speichern",
"modeStandard": "Standard (Variablen)", "modeStandard": "Standard (Variablen)",
"modeExpert": "Experten-Modus", "modeExpert": "Experten-Modus",
"expertHint": "Voller Vorlagen-Editor: Text, Formatierung, Variablen, Deep-Links & Referenzen. Neue Variablen werden beim Speichern angelegt und sind danach im Standard-Modus pflegbar." "expertHint": "Voller Vorlagen-Editor: Text, Formatierung, Variablen, Deep-Links & Referenzen. Neue Variablen werden beim Speichern angelegt und sind danach im Standard-Modus pflegbar.",
"byControl": "nach Control",
"byDocument": "nach Dokument",
"assessmentExport": "Assessment-Export",
"requirements": "Anforderung(en)",
"requirement": "Anforderung",
"implementation": "Umsetzung",
"comingSoon": "Bald verfügbar"
} }
} }
+8 -1
View File
@@ -564,6 +564,13 @@
"saveVariables": "Save variables", "saveVariables": "Save variables",
"modeStandard": "Standard (variables)", "modeStandard": "Standard (variables)",
"modeExpert": "Expert mode", "modeExpert": "Expert mode",
"expertHint": "Full template editor: text, formatting, variables, deep links & references. New variables are created on save and can then be maintained in standard mode." "expertHint": "Full template editor: text, formatting, variables, deep links & references. New variables are created on save and can then be maintained in standard mode.",
"byControl": "by control",
"byDocument": "by document",
"assessmentExport": "Assessment export",
"requirements": "Requirement(s)",
"requirement": "Requirement",
"implementation": "Implementation",
"comingSoon": "Coming soon"
} }
} }
+137 -52
View File
@@ -5,6 +5,10 @@ import { dbForTenant } from "@/server/db";
import { requirePermission } from "@/server/rbac"; import { requirePermission } from "@/server/rbac";
import { PageHead, Pill, Tag, KpiCard } from "@/components/mockup-ui"; import { PageHead, Pill, Tag, KpiCard } from "@/components/mockup-ui";
import { FilterTabs } from "@/components/filter-tabs"; 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 type { PolicyRequirement } from "@prisma/client";
import { import {
Table, Table,
TableBody, TableBody,
@@ -36,10 +40,57 @@ const STATUS_LABEL: Record<string, string> = {
ARCHIVIERT: "Archiviert", ARCHIVIERT: "Archiviert",
}; };
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)]"}`}>
{type}
</span>
);
}
/** Gruppenblock „nach Dokument": Kopfzeile je Richtlinie + Anforderungszeilen. */
function PolicyGroup({
policy,
title,
rows,
clean,
}: {
policy: string;
title: string;
rows: PolicyRequirement[];
clean: (s: string, n?: number) => string;
}) {
return (
<>
<tr className="border-b bg-[var(--surface-soft)]">
<td colSpan={4} className="p-2.5">
<Link href={`/policies/${policy}`} className="font-heading font-bold hover:underline">{title}</Link>{" "}
<span className="text-[11px] text-muted-foreground">{policy}</span>
</td>
</tr>
{rows.map((r) => (
<tr key={r.id} className="border-b align-top last:border-0">
<td className="p-3"><b className="font-heading">{r.control}</b></td>
<td className="p-3"><ObligationBadge type={r.obligation} /> <span title={clean(r.requirement, 400)}>{clean(r.requirement, 110)}</span></td>
<td className="p-3 text-muted-foreground">{clean(r.implementation, 130)}</td>
<td className="p-3">
<span className="flex flex-wrap gap-1">
{r.vaCodes.length === 0 ? <span className="text-muted-foreground"></span> : [...r.vaCodes].sort().map((va) => (
<Link key={va} href={`/policies/${va}`}><Pill tone="info">{va}</Pill></Link>
))}
</span>
</td>
</tr>
))}
</>
);
}
export default async function PoliciesPage({ export default async function PoliciesPage({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ type?: string; q?: string; doc?: string; view?: string }>; searchParams: Promise<{ type?: string; q?: string; doc?: string; view?: string; dir?: string }>;
}) { }) {
const session = await requireSession(); const session = await requireSession();
requirePermission(session, "policy:read"); requirePermission(session, "policy:read");
@@ -47,10 +98,19 @@ export default async function PoliciesPage({
const db = dbForTenant(session.user.tenantId); const db = dbForTenant(session.user.tenantId);
const params = await searchParams; const params = await searchParams;
const [docs, requirements] = await Promise.all([ const [docs, requirements, variables] = await Promise.all([
db.policyDocument.findMany({ orderBy: { orderIdx: "asc" } }), db.policyDocument.findMany({ orderBy: { orderIdx: "asc" } }),
db.policyRequirement.findMany({ orderBy: [{ control: "asc" }, { reqId: "asc" }] }), db.policyRequirement.findMany({ orderBy: [{ control: "asc" }, { reqId: "asc" }] }),
db.policyVariable.findMany(),
]); ]);
const titleOf = (code: string) => docs.find((d) => d.code === code)?.title ?? code;
const ctx = buildContext(variables);
const clean = (s: string, n = 160) =>
renderPolicyMarkdown(s, ctx, { readMode: true, stripBaseline: true })
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/\s+/g, " ")
.trim()
.slice(0, n);
// Ableitung: je Richtlinie ihre Controls + operationalisierende Verfahren // Ableitung: je Richtlinie ihre Controls + operationalisierende Verfahren
const byPolicy = new Map<string, { controls: Set<string>; vas: Set<string> }>(); const byPolicy = new Map<string, { controls: Set<string>; vas: Set<string> }>();
@@ -79,61 +139,86 @@ export default async function PoliciesPage({
/> />
); );
/* ── Coverage-Matrix (Control → Richtlinie → Verfahren) ── */ /* ── Referenz-/Coverage-Matrix (§9.3, nach Mockup) ── */
if (isCoverage) { if (isCoverage) {
const byControl = new Map<string, { policy: string; must: number; should: number; ids: string[]; vas: Set<string> }>(); const dir = params.dir === "doc" ? "doc" : "ctrl";
for (const r of requirements) { const controlCount = new Set(requirements.map((r) => r.control)).size;
if (!byControl.has(r.control)) byControl.set(r.control, { policy: r.policyCode, must: 0, should: 0, ids: [], vas: new Set() }); const vasOf = (ids: string[]) => [...new Set(ids)].sort();
const e = byControl.get(r.control)!;
if (r.obligation === "MUSS") e.must++;
else e.should++;
e.ids.push(r.reqId.split("-").slice(1).join("-"));
r.vaCodes.forEach((v) => e.vas.add(v));
}
const rows = [...byControl.entries()];
return ( return (
<main className="flex-1 p-6"> <main className="flex-1 p-6">
{head} {head}
<p className="mt-3 text-[12.5px] text-muted-foreground">{t("coverageHint", { controls: rows.length, reqs: requirements.length })}</p> <div className="mt-3 flex flex-wrap items-center justify-between gap-3">
<div className="shadow-card mt-3 rounded-xl border bg-card"> <p className="text-[12.5px] text-muted-foreground">{t("coverageHint", { controls: controlCount, reqs: requirements.length })}</p>
<Table> <div className="flex items-center gap-3">
<TableHeader> <div className="inline-flex rounded-lg border bg-card p-1 text-[12px] font-semibold">
<TableRow> <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>
<TableHead>{t("control")}</TableHead> <Link href="/policies?view=coverage&dir=doc" className={dir === "doc" ? "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("byDocument")}</Link>
<TableHead>{t("policy")}</TableHead> </div>
<TableHead>MUSS</TableHead> <Button variant="outline" size="sm" disabled title={t("comingSoon")}>{t("assessmentExport")}</Button>
<TableHead>SOLL</TableHead> </div>
<TableHead>{t("procedures")}</TableHead> </div>
<TableHead>{t("reqIds")}</TableHead>
</TableRow> <div className="shadow-card mt-3 overflow-x-auto rounded-xl border bg-card">
</TableHeader> {dir === "ctrl" ? (
<TableBody> <table className="w-full text-[12.5px]">
{rows.map(([control, e]) => ( <thead>
<TableRow key={control}> <tr className="border-b bg-[var(--elevated)] text-[10.5px] tracking-[.04em] text-muted-foreground uppercase">
<TableCell className="font-bold">{control}</TableCell> <th className="p-3 text-left">{t("control")}</th>
<TableCell> <th className="p-3 text-left">{t("requirements")}</th>
<Link href={`/policies/${e.policy}`} className="font-semibold hover:underline">{e.policy}</Link> <th className="p-3 text-left">{t("policy")}</th>
</TableCell> <th className="p-3 text-left">{t("procedures")}</th>
<TableCell>{e.must}</TableCell> </tr>
<TableCell className="text-muted-foreground">{e.should || ""}</TableCell> </thead>
<TableCell> <tbody>
<span className="flex flex-wrap gap-1"> {[...new Set(requirements.map((r) => r.control))].sort(compareControl).map((control) => {
{e.vas.size === 0 ? ( const list = requirements.filter((r) => r.control === control);
<span className="text-muted-foreground"></span> const vas = vasOf(list.flatMap((r) => r.vaCodes));
) : ( return (
[...e.vas].sort().map((va) => ( <tr key={control} className="border-b align-top last:border-0">
<Link key={va} href={`/policies/${va}`}> <td className="p-3">
<Pill tone="info">{va}</Pill> <b className="font-heading">{control}</b>
</Link> <div className="text-[11px] text-muted-foreground">{controlTitle(control)}</div>
)) </td>
)} <td className="p-3">
</span> <ul className="space-y-1.5">
</TableCell> {list.map((r) => (
<TableCell className="max-w-64 truncate text-[12px] text-muted-foreground">{e.ids.join(", ")}</TableCell> <li key={r.id}>
</TableRow> <ObligationBadge type={r.obligation} /> <span title={clean(r.requirement, 400)}>{clean(r.requirement, 120)}</span>
</li>
))} ))}
</TableBody> </ul>
</Table> </td>
<td className="p-3"><Link href={`/policies/${list[0].policyCode}`} className="font-semibold hover:underline">{list[0].policyCode}</Link></td>
<td className="p-3">
<span className="flex flex-wrap gap-1">
{vas.length === 0 ? <span className="text-muted-foreground"></span> : vas.map((va) => (
<Link key={va} href={`/policies/${va}`}><Pill tone="info">{va}</Pill></Link>
))}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
) : (
<table className="w-full text-[12.5px]">
<thead>
<tr className="border-b bg-[var(--elevated)] text-[10.5px] tracking-[.04em] text-muted-foreground uppercase">
<th className="p-3 text-left">{t("control")}</th>
<th className="p-3 text-left">{t("requirement")}</th>
<th className="p-3 text-left">{t("implementation")}</th>
<th className="p-3 text-left">{t("procedures")}</th>
</tr>
</thead>
<tbody>
{[...new Set(requirements.map((r) => r.policyCode))].map((policy) => (
<PolicyGroup key={policy} policy={policy} title={titleOf(policy)} rows={requirements.filter((r) => r.policyCode === policy)} clean={clean} />
))}
</tbody>
</table>
)}
</div> </div>
</main> </main>
); );
+71 -48
View File
@@ -24,6 +24,8 @@ import {
addHandlingAspect, addHandlingAspect,
deleteCryptoEntry, deleteCryptoEntry,
updateCryptoEntry, updateCryptoEntry,
updateDamageDimension,
updateEwLevel,
updateHandlingRule, updateHandlingRule,
updateRiskClass, updateRiskClass,
} from "@/server/actions/policies"; } from "@/server/actions/policies";
@@ -98,7 +100,7 @@ async function CryptoRegister({ entries, canWrite }: { entries: CryptoEntry[]; c
</details> </details>
)} )}
</div> </div>
<div className="overflow-x-auto rounded-xl border"> <div className="rounded-xl border">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b bg-[var(--elevated)] text-[10.5px] tracking-[.04em] text-muted-foreground uppercase"> <tr className="border-b bg-[var(--elevated)] text-[10.5px] tracking-[.04em] text-muted-foreground uppercase">
@@ -276,73 +278,94 @@ function RiskMatrix({
</div> </div>
</section> </section>
{/* Risikoklassen + Akzeptanzinstanz */} {/* Risikoklassen + Akzeptanzinstanz — inline editierbar */}
<section> <section>
<p className="mb-2 font-heading text-sm font-semibold">Risikoklassen & Akzeptanz</p> <p className="mb-2 font-heading text-sm font-semibold">Risikoklassen & Akzeptanz {canWrite && <span className="text-[11px] font-normal text-muted-foreground">· zum Bearbeiten auf eine Zeile klicken</span>}</p>
<div className="overflow-hidden rounded-xl border"> <div className="rounded-xl border">
<table className="w-full text-sm"> <div className="grid grid-cols-[8rem_7rem_1fr] gap-x-3 border-b bg-[var(--elevated)] px-3 py-2 text-[10.5px] tracking-[.04em] text-muted-foreground uppercase">
<thead><tr className="border-b bg-[var(--elevated)] text-[10.5px] tracking-[.04em] text-muted-foreground uppercase"> <span>Klasse</span><span>Bis Risikowert</span><span>Akzeptanzinstanz</span>
<th className="p-2.5 text-left">Klasse</th><th className="p-2.5 text-left">Bis Risikowert</th><th className="p-2.5 text-left">Akzeptanzinstanz</th>{canWrite && <th className="p-2.5" />} </div>
</tr></thead>
<tbody>
{sorted.map((c) => ( {sorted.map((c) => (
<tr key={c.id} className="border-b last:border-0"> canWrite ? (
<td className="p-2.5"><Pill tone={(c.tone as "ok" | "warn" | "orange" | "risk")}>{c.name}</Pill></td> <details key={c.id} className="border-b last:border-0">
<td className="p-2.5 font-medium"> {c.maxScore}</td> <summary className="grid cursor-pointer grid-cols-[8rem_7rem_1fr] items-center gap-x-3 px-3 py-2.5 text-sm select-none hover:bg-muted/40 [&::-webkit-details-marker]:hidden">
<td className="p-2.5 text-muted-foreground">{c.acceptance}</td> <span><Pill tone={c.tone as "ok" | "warn" | "orange" | "risk"}>{c.name}</Pill></span>
{canWrite && ( <span className="font-medium"> {c.maxScore}</span>
<td className="p-2.5 text-right"> <span className="flex items-center justify-between gap-2 text-muted-foreground">{c.acceptance}<Pencil className="size-3.5 shrink-0 opacity-60" /></span>
<details className="relative"> </summary>
<summary className="grid size-7 cursor-pointer list-none place-items-center rounded-md text-muted-foreground hover:bg-muted [&::-webkit-details-marker]:hidden"><Pencil className="size-3.5" /></summary> <form action={updateRiskClass.bind(null, c.id)} className="grid gap-2 border-t bg-[var(--surface-soft)] p-3 sm:grid-cols-[8rem_7rem_1fr_auto]">
<form action={updateRiskClass.bind(null, c.id)} className="shadow-card absolute right-0 z-10 mt-2 w-72 space-y-2 rounded-xl border bg-card p-3 text-sm"> <Input name="name" required defaultValue={c.name} placeholder="Klasse" className="h-8" />
<Input name="name" required defaultValue={c.name} /> <Input name="maxScore" type="number" required defaultValue={c.maxScore} placeholder="≤ Wert" className="h-8" />
<Input name="maxScore" type="number" required defaultValue={c.maxScore} placeholder="Bis Risikowert" /> <Input name="acceptance" defaultValue={c.acceptance} placeholder="Akzeptanzinstanz" className="h-8" />
<Input name="acceptance" defaultValue={c.acceptance} placeholder="Akzeptanzinstanz" />
<Button type="submit" variant="secondary" size="sm">Speichern</Button> <Button type="submit" variant="secondary" size="sm">Speichern</Button>
</form> </form>
</details> </details>
</td> ) : (
)} <div key={c.id} className="grid grid-cols-[8rem_7rem_1fr] items-center gap-x-3 border-b px-3 py-2.5 text-sm last:border-0">
</tr> <span><Pill tone={c.tone as "ok" | "warn" | "orange" | "risk"}>{c.name}</Pill></span>
<span className="font-medium"> {c.maxScore}</span>
<span className="text-muted-foreground">{c.acceptance}</span>
</div>
)
))} ))}
</tbody>
</table>
</div> </div>
</section> </section>
{/* Eintrittswahrscheinlichkeit */} {/* Eintrittswahrscheinlichkeit — inline editierbar */}
<section> <section>
<p className="mb-2 font-heading text-sm font-semibold">Eintrittswahrscheinlichkeit</p> <p className="mb-2 font-heading text-sm font-semibold">Eintrittswahrscheinlichkeit</p>
<ul className="space-y-1 text-[12.5px]"> <div className="rounded-xl border">
{ew.sort((a, b) => a.level - b.level).map((l) => ( {[...ew].sort((a, b) => a.level - b.level).map((l) => (
<li key={l.id}><b>{l.level} · {l.label}</b> <span className="text-muted-foreground"> {l.definition}</span></li> canWrite ? (
<details key={l.id} className="border-b last:border-0">
<summary className="flex cursor-pointer items-center justify-between gap-2 px-3 py-2 text-[12.5px] select-none hover:bg-muted/40 [&::-webkit-details-marker]:hidden">
<span><b>{l.level} · {l.label}</b> <span className="text-muted-foreground"> {l.definition}</span></span>
<Pencil className="size-3.5 shrink-0 opacity-60" />
</summary>
<form action={updateEwLevel.bind(null, l.id)} className="grid gap-2 border-t bg-[var(--surface-soft)] p-3 sm:grid-cols-[10rem_1fr_auto]">
<Input name="label" required defaultValue={l.label} placeholder="Bezeichnung" className="h-8" />
<Input name="definition" defaultValue={l.definition} placeholder="Definition" className="h-8" />
<Button type="submit" variant="secondary" size="sm">Speichern</Button>
</form>
</details>
) : (
<div key={l.id} className="border-b px-3 py-2 text-[12.5px] last:border-0"><b>{l.level} · {l.label}</b> <span className="text-muted-foreground"> {l.definition}</span></div>
)
))} ))}
</ul> </div>
</section> </section>
{/* Schadenskategorien */} {/* Schadenskategorien — inline editierbar */}
<section> <section>
<p className="mb-2 font-heading text-sm font-semibold">Schadenskategorien</p> <p className="mb-2 font-heading text-sm font-semibold">Schadenskategorien</p>
<div className="overflow-x-auto rounded-xl border"> <div className="rounded-xl border">
<table className="w-full text-[12px]"> <div className="hidden grid-cols-[12rem_1fr_1fr_1fr_1fr] gap-x-3 border-b bg-[var(--elevated)] px-3 py-2 text-[10.5px] tracking-[.04em] text-muted-foreground uppercase sm:grid">
<thead><tr className="border-b bg-[var(--elevated)] text-[10.5px] tracking-[.04em] text-muted-foreground uppercase"> <span>Dimension</span><span>1 Niedrig</span><span>2 Normal</span><span>3 Hoch</span><span>4 Sehr hoch</span>
<th className="p-2.5 text-left">Dimension</th><th className="p-2.5 text-left">1 Niedrig</th><th className="p-2.5 text-left">2 Normal</th><th className="p-2.5 text-left">3 Hoch</th><th className="p-2.5 text-left">4 Sehr hoch</th> </div>
</tr></thead>
<tbody>
{damage.map((d) => { {damage.map((d) => {
const lv = d.levels as Record<string, string>; const lv = d.levels as Record<string, string>;
return ( return canWrite ? (
<tr key={d.id} className="border-b last:border-0 align-top"> <details key={d.id} className="border-b last:border-0">
<td className="p-2.5 font-medium">{d.name}</td> <summary className="grid cursor-pointer grid-cols-1 items-center gap-x-3 px-3 py-2.5 text-[12px] select-none hover:bg-muted/40 sm:grid-cols-[12rem_1fr_1fr_1fr_1fr] [&::-webkit-details-marker]:hidden">
<td className="p-2.5 text-muted-foreground">{lv["1"]}</td> <span className="flex items-center justify-between gap-2 font-medium">{d.name}<Pencil className="size-3.5 shrink-0 opacity-60 sm:hidden" /></span>
<td className="p-2.5 text-muted-foreground">{lv["2"]}</td> <span className="text-muted-foreground">{lv["1"]}</span><span className="text-muted-foreground">{lv["2"]}</span><span className="text-muted-foreground">{lv["3"]}</span><span className="text-muted-foreground">{lv["4"]}</span>
<td className="p-2.5 text-muted-foreground">{lv["3"]}</td> </summary>
<td className="p-2.5 text-muted-foreground">{lv["4"]}</td> <form action={updateDamageDimension.bind(null, d.id)} className="grid gap-2 border-t bg-[var(--surface-soft)] p-3 sm:grid-cols-2">
</tr> <Input name="name" required defaultValue={d.name} placeholder="Dimension" className="h-8 sm:col-span-2" />
<Input name="l1" defaultValue={lv["1"]} placeholder="1 Niedrig" className="h-8" />
<Input name="l2" defaultValue={lv["2"]} placeholder="2 Normal" className="h-8" />
<Input name="l3" defaultValue={lv["3"]} placeholder="3 Hoch" className="h-8" />
<Input name="l4" defaultValue={lv["4"]} placeholder="4 Sehr hoch" className="h-8" />
<Button type="submit" variant="secondary" size="sm" className="sm:col-span-2">Speichern</Button>
</form>
</details>
) : (
<div key={d.id} className="grid grid-cols-1 gap-x-3 border-b px-3 py-2.5 text-[12px] last:border-0 sm:grid-cols-[12rem_1fr_1fr_1fr_1fr]">
<span className="font-medium">{d.name}</span>
<span className="text-muted-foreground">{lv["1"]}</span><span className="text-muted-foreground">{lv["2"]}</span><span className="text-muted-foreground">{lv["3"]}</span><span className="text-muted-foreground">{lv["4"]}</span>
</div>
); );
})} })}
</tbody>
</table>
</div> </div>
</section> </section>
</div> </div>
+63
View File
@@ -0,0 +1,63 @@
/**
* VDA-ISA-2027 Control-Titel (aus dem GEFIM-Mockup übernommen) für die
* Referenz-/Coverage-Matrix. Fallback: „ISA <ref>“ wenn nicht enthalten.
*/
export const CONTROL_TITLES: Record<string, string> = {
"1.2.1": "Steuerung der Informationssicherheit",
"1.2.2": "Organisation der Verantwortlichkeiten",
"1.2.3": "Informationssicherheit in Projekten",
"1.3.1": "Identifikation von Assets",
"1.3.2": "Klassifizierung",
"1.3.3": "Zugelassene Hardware",
"1.3.4": "Zugelassene Software",
"1.4.1": "Risikomanagement",
"1.5.1": "Compliance-Prüfung im IS-Betrieb",
"1.5.2": "Unabhängige Überprüfung",
"1.6.1": "Meldung von Ereignissen",
"1.6.2": "Behandlung von Sicherheitsereignissen",
"1.6.3": "Krisenmanagement",
"2.1.1": "Qualifikation für sensible Tätigkeiten",
"2.1.2": "Vertragliche Verpflichtung",
"2.1.3": "Sensibilisierung und Schulung",
"2.1.4": "Mobiles Arbeiten",
"3.1.1": "Sicherheitszonen und Zutritt",
"3.1.3": "Umgang mit unterstützenden Betriebsmitteln",
"3.1.4": "Mobile Geräte und Datenträger",
"4.1.1": "Identifikationsmittel",
"4.1.2": "Sichere Anmeldung",
"4.1.3": "Konten und Anmeldeinformationen",
"4.2.1": "Zugriffsrechte",
"5.1.1": "Einsatz kryptografischer Verfahren",
"5.1.2": "Schutz bei Uebertragung",
"5.2.1": "Change-Management",
"5.2.2": "Trennung der Umgebungen",
"5.2.3": "Schutz vor Malware",
"5.2.4": "Protokollierung und Auswertung",
"5.2.5": "Schwachstellen- und Patch-Management",
"5.2.6": "Technische Überprüfung",
"5.2.7": "Netzwerksicherheit",
"5.2.8": "Kontinuitätsplanung IT",
"5.2.9": "Backup und Wiederherstellung",
"5.3.1": "Sicherheit bei Beschaffung/Entwicklung",
"5.3.2": "Anforderungen an Netzdienste",
"5.3.3": "Rückgabe und sichere Löschung",
"5.3.4": "Externe/geteilte IT-Dienste und Cloud",
"6.1.1": "Informationssicherheit bei Lieferanten",
"6.1.2": "Vertraulichkeit",
"6.1.3": "Abgrenzung der Verantwortlichkeiten",
"7.1.1": "Regulatorische und vertragliche Compliance",
"7.1.2": "Schutz personenbezogener Daten",
};
export function controlTitle(ref: string): string {
return CONTROL_TITLES[ref] ?? `ISA ${ref}`;
}
/** Numerischer Vergleich gepunkteter Control-Refs (1.2.10 nach 1.2.2). */
export function compareControl(a: string, b: string): number {
const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
}
return 0;
}
+23
View File
@@ -124,6 +124,29 @@ export async function updateRiskClass(id: string, formData: FormData) {
revalidatePath("/policies", "layout"); revalidatePath("/policies", "layout");
} }
export async function updateEwLevel(id: string, formData: FormData) {
const { session, db } = await guard();
await db.riskEwLevel.update({
where: { id },
data: { label: z.string().trim().min(1).parse(formData.get("label")), definition: str(formData.get("definition")) },
});
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "risk_ew_level", entityId: id });
revalidatePath("/policies", "layout");
}
export async function updateDamageDimension(id: string, formData: FormData) {
const { session, db } = await guard();
await db.riskDamageDimension.update({
where: { id },
data: {
name: z.string().trim().min(1).parse(formData.get("name")),
levels: { "1": str(formData.get("l1")), "2": str(formData.get("l2")), "3": str(formData.get("l3")), "4": str(formData.get("l4")) },
},
});
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "risk_damage_dimension", entityId: id });
revalidatePath("/policies", "layout");
}
/* ── Bearbeitungsmodus Richtlinien/Verfahren ── */ /* ── Bearbeitungsmodus Richtlinien/Verfahren ── */
/** Vorlagen-Markdown eines Dokuments speichern (Experten-Modus). */ /** Vorlagen-Markdown eines Dokuments speichern (Experten-Modus). */