Feinschliff Risiko/Bewertung: Ampelfarben, Titel, Skala, Legende

- Detail-Popups einheitlich: Name/Bezeichner steht immer im Titel
  (Risiko "R-001 · <Name>", Maßnahme "M-001 · <Name>", Prozess <Name>)
- Bewertungsbalken (Schutzbedarf/Schadenshöhe) + Score-Pillen als
  einheitliche Ampel: 1 grün, 2 gelb, 3 orange, 4 rot; Risiko-Pillen
  folgen exakt der Heatmap-Skala (medium = gelb statt blau, elevated =
  orange) — Restrisiko-Pille jetzt korrekt gelb
- Risiko-Detailkarte: Farbskala von der Mitte nach oben verschoben;
  alle Detail-/Bearbeiten-Popups etwas breiter (max-w-4xl)
- CiaGauge-Beschriftung vereinheitlicht: VER/INT/VFB erscheint einmal
  pro Tabelle als Spaltenüberschrift (Asset-Liste + Popup-Tabellen),
  keine Labels mehr pro Zeile; Standalone-Gauges in Detailkarten
  behalten die Labels

Verifiziert: alle vier Ampelfarben im DOM, Restrisiko-Pille gelb,
Legende in Tabellenköpfen, Titel mit Name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Martin
2026-07-02 20:28:48 +02:00
co-authored by Claude Opus 4.8
parent 430aaab024
commit 1b13af46db
11 changed files with 76 additions and 29 deletions
+3 -3
View File
@@ -163,7 +163,7 @@
"biaReport": "BIA-Report (PDF)", "biaReport": "BIA-Report (PDF)",
"critTable": "Kritikalität der Prozesse", "critTable": "Kritikalität der Prozesse",
"process": "Geschäftsprozess", "process": "Geschäftsprozess",
"detailHeading": "Prozess-Detail · {name}", "detailHeading": "{name}",
"detailSub": "Zugeordnete Assets nach Rolle", "detailSub": "Zugeordnete Assets nach Rolle",
"critLabel": "Kritikalität: {label}", "critLabel": "Kritikalität: {label}",
"primaryPill": "★ Primäres Asset", "primaryPill": "★ Primäres Asset",
@@ -217,7 +217,7 @@
"status": "Status", "status": "Status",
"owner": "Owner", "owner": "Owner",
"empty": "Noch keine Risiken erfasst.", "empty": "Noch keine Risiken erfasst.",
"detailHeading": "Risiko-Detail · {ref}", "detailHeading": "{ref} · {name}",
"detailSub": "Bewertung, betroffene Assets & Behandlung", "detailSub": "Bewertung, betroffene Assets & Behandlung",
"editTitle": "Risiko bearbeiten", "editTitle": "Risiko bearbeiten",
"createTitle": "Risiko anlegen", "createTitle": "Risiko anlegen",
@@ -291,7 +291,7 @@
"newMeasure": "Maßnahme", "newMeasure": "Maßnahme",
"board": "Maßnahmen-Board", "board": "Maßnahmen-Board",
"empty": "Keine Maßnahmen.", "empty": "Keine Maßnahmen.",
"detailHeading": "Maßnahme · {ref}", "detailHeading": "{ref} · {name}",
"detailSub": "Status, Verantwortung & verknüpfte Risiken", "detailSub": "Status, Verantwortung & verknüpfte Risiken",
"editTitle": "Maßnahme bearbeiten", "editTitle": "Maßnahme bearbeiten",
"createTitle": "Maßnahme anlegen", "createTitle": "Maßnahme anlegen",
+3 -3
View File
@@ -163,7 +163,7 @@
"biaReport": "BIA report (PDF)", "biaReport": "BIA report (PDF)",
"critTable": "Process criticality", "critTable": "Process criticality",
"process": "Business process", "process": "Business process",
"detailHeading": "Process detail · {name}", "detailHeading": "{name}",
"detailSub": "Assigned assets by role", "detailSub": "Assigned assets by role",
"critLabel": "Criticality: {label}", "critLabel": "Criticality: {label}",
"primaryPill": "★ Primary asset", "primaryPill": "★ Primary asset",
@@ -217,7 +217,7 @@
"status": "Status", "status": "Status",
"owner": "Owner", "owner": "Owner",
"empty": "No risks recorded yet.", "empty": "No risks recorded yet.",
"detailHeading": "Risk detail · {ref}", "detailHeading": "{ref} · {name}",
"detailSub": "Rating, affected assets & treatment", "detailSub": "Rating, affected assets & treatment",
"editTitle": "Edit risk", "editTitle": "Edit risk",
"createTitle": "Create risk", "createTitle": "Create risk",
@@ -291,7 +291,7 @@
"newMeasure": "Measure", "newMeasure": "Measure",
"board": "Measures board", "board": "Measures board",
"empty": "No measures.", "empty": "No measures.",
"detailHeading": "Measure · {ref}", "detailHeading": "{ref} · {name}",
"detailSub": "Status, responsibility & linked risks", "detailSub": "Status, responsibility & linked risks",
"editTitle": "Edit measure", "editTitle": "Edit measure",
"createTitle": "Create measure", "createTitle": "Create measure",
+7 -1
View File
@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button";
import { FilterTabs } from "@/components/filter-tabs"; import { FilterTabs } from "@/components/filter-tabs";
import { import {
CiaBadge, CiaBadge,
CiaLegend,
KpiCard, KpiCard,
OwnerChip, OwnerChip,
PageHead, PageHead,
@@ -196,7 +197,12 @@ export default async function AssetsPage({
<TableHead>Asset</TableHead> <TableHead>Asset</TableHead>
<TableHead>{t("type")}</TableHead> <TableHead>{t("type")}</TableHead>
<TableHead>{t("owner")}</TableHead> <TableHead>{t("owner")}</TableHead>
<TableHead>{t("protection")} (C/I/A)</TableHead> <TableHead>
<span className="flex flex-col gap-1">
{t("protection")}
<CiaLegend />
</span>
</TableHead>
<TableHead>{t("dependencies")}</TableHead> <TableHead>{t("dependencies")}</TableHead>
<TableHead>{t("status")}</TableHead> <TableHead>{t("status")}</TableHead>
</TableRow> </TableRow>
+10 -1
View File
@@ -12,7 +12,7 @@ import {
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Modal } from "@/components/modal"; import { Modal } from "@/components/modal";
import { AssetForm } from "@/components/asset-form"; import { AssetForm } from "@/components/asset-form";
import { CiaBadge, Pill, Tag } from "@/components/mockup-ui"; import { CiaBadge, CiaLegend, Pill, Tag } from "@/components/mockup-ui";
import { riskLevel, riskRef, RISK_PILL_TONE } from "@/lib/risk"; import { riskLevel, riskRef, RISK_PILL_TONE } from "@/lib/risk";
export type AssetWithRelations = Prisma.AssetGetPayload<{ export type AssetWithRelations = Prisma.AssetGetPayload<{
@@ -145,6 +145,15 @@ export async function AssetDetailModal({
<p className="text-sm text-muted-foreground">{tc("none")}</p> <p className="text-sm text-muted-foreground">{tc("none")}</p>
)} )}
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead>
<tr>
<th />
<th />
<th className="pb-1 text-right font-normal">
<CiaLegend />
</th>
</tr>
</thead>
<tbody> <tbody>
{asset.relationsFrom.map((rel) => ( {asset.relationsFrom.map((rel) => (
<tr key={rel.id} className="border-b last:border-0"> <tr key={rel.id} className="border-b last:border-0">
+1 -1
View File
@@ -42,7 +42,7 @@ export async function MeasureDetailModal({
return ( return (
<Modal <Modal
title={t("detailHeading", { ref: measureRef(measure.refNo) })} title={t("detailHeading", { ref: measureRef(measure.refNo), name: measure.title })}
sub={t("detailSub")} sub={t("detailSub")}
headerExtra={ headerExtra={
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
+18 -4
View File
@@ -55,11 +55,11 @@ export function KpiCard({
); );
} }
// Segmentbalken-Farben je Stufe 14 (Referenz docs/ISMS-GUI-Verbesserungen-GEFIM.html) // Segmentbalken-Farben je Stufe 14 als Ampel: Grün → Gelb → Orange → Rot
const CIA_BAR_COLORS: Record<number, string> = { const CIA_BAR_COLORS: Record<number, string> = {
1: "bg-[#9db2c9]", 1: "bg-[#2e9e6b]",
2: "bg-[#5d8fbf]", 2: "bg-[#e5b000]",
3: "bg-[#e0982e]", 3: "bg-[#e07d2e]",
4: "bg-[#d64c4c]", 4: "bg-[#d64c4c]",
}; };
@@ -113,6 +113,19 @@ export function CiaBadge({
); );
} }
/** VER/INT/VFB-Beschriftung passend zur Balkenbreite — einmal pro Tabelle als Überschrift. */
export function CiaLegend() {
return (
<span className="inline-flex items-center gap-2.5 text-[9.5px] font-bold tracking-[.04em] text-muted-foreground">
{["VER", "INT", "VFB"].map((l) => (
<span key={l} className="w-[34px] text-center">
{l}
</span>
))}
</span>
);
}
export function Tag({ children }: { children: React.ReactNode }) { export function Tag({ children }: { children: React.ReactNode }) {
return ( return (
<span className="inline-block rounded-md bg-[#eef0f4] px-2 py-0.5 text-[11px] font-bold text-[#555]"> <span className="inline-block rounded-md bg-[#eef0f4] px-2 py-0.5 text-[11px] font-bold text-[#555]">
@@ -142,6 +155,7 @@ export function OwnerChip({ name, noOwnerLabel }: { name?: string | null; noOwne
const PILL_TONES = { const PILL_TONES = {
ok: "bg-[#e7f6ee] text-[#2e9e6b]", ok: "bg-[#e7f6ee] text-[#2e9e6b]",
warn: "bg-[#fbf1de] text-[#a9760f]", warn: "bg-[#fbf1de] text-[#a9760f]",
orange: "bg-[#fbdcc0] text-[#c2681a]",
risk: "bg-[#fbe7e7] text-[#d64c4c]", risk: "bg-[#fbe7e7] text-[#d64c4c]",
info: "bg-[#e7f1fa] text-[#3a86c8]", info: "bg-[#e7f1fa] text-[#3a86c8]",
mut: "bg-[#eef0f4] text-muted-foreground", mut: "bg-[#eef0f4] text-muted-foreground",
+1 -1
View File
@@ -26,7 +26,7 @@ export function Modal({
}) { }) {
return ( return (
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/30 p-6 backdrop-blur-[2px]"> <div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/30 p-6 backdrop-blur-[2px]">
<div className="shadow-card my-auto w-full max-w-3xl rounded-2xl border bg-card"> <div className="shadow-card my-auto w-full max-w-4xl rounded-2xl border bg-card">
<div className="flex items-start justify-between gap-3 border-b p-5"> <div className="flex items-start justify-between gap-3 border-b p-5">
<SectTitle title={title} sub={sub} /> <SectTitle title={title} sub={sub} />
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
+10 -1
View File
@@ -16,7 +16,7 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Modal } from "@/components/modal"; import { Modal } from "@/components/modal";
import { SegmentedRating } from "@/components/segmented-rating"; import { SegmentedRating } from "@/components/segmented-rating";
import { CiaBadge, CriticalityPill, Pill, Tag } from "@/components/mockup-ui"; import { CiaBadge, CiaLegend, CriticalityPill, Pill, Tag } from "@/components/mockup-ui";
import { riskLevel, riskRef, RISK_PILL_TONE } from "@/lib/risk"; import { riskLevel, riskRef, RISK_PILL_TONE } from "@/lib/risk";
export type LinkedRisk = { export type LinkedRisk = {
@@ -158,6 +158,15 @@ export async function ProcessDetailModal({
</div> </div>
{secondary.length === 0 && <p className="text-sm text-muted-foreground">{t("noAssets")}</p>} {secondary.length === 0 && <p className="text-sm text-muted-foreground">{t("noAssets")}</p>}
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead>
<tr>
<th />
<th />
<th className="pb-1 text-right font-normal">
<CiaLegend />
</th>
</tr>
</thead>
<tbody> <tbody>
{secondary.map((pa) => ( {secondary.map((pa) => (
<tr key={pa.id} className="border-b last:border-0"> <tr key={pa.id} className="border-b last:border-0">
+15 -6
View File
@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Modal } from "@/components/modal"; import { Modal } from "@/components/modal";
import { CiaBadge, Pill, Tag } from "@/components/mockup-ui"; import { CiaBadge, CiaLegend, Pill, Tag } from "@/components/mockup-ui";
import { riskLevel, riskRef, RISK_PILL_TONE } from "@/lib/risk"; import { riskLevel, riskRef, RISK_PILL_TONE } from "@/lib/risk";
import { measureRef, MEASURE_STATUS_TONE } from "@/lib/measure"; import { measureRef, MEASURE_STATUS_TONE } from "@/lib/measure";
@@ -123,7 +123,7 @@ function RiskScoreScale({ gross, residual }: { gross: number; residual: number |
// 5×5 → Score 1..25; Position linear auf der Skala // 5×5 → Score 1..25; Position linear auf der Skala
const pos = (v: number) => `${((Math.min(25, Math.max(1, v)) - 1) / 24) * 100}%`; const pos = (v: number) => `${((Math.min(25, Math.max(1, v)) - 1) / 24) * 100}%`;
return ( return (
<div className="mt-4"> <div className="pt-1">
<div <div
className="relative h-3 rounded-lg" className="relative h-3 rounded-lg"
style={{ style={{
@@ -173,7 +173,7 @@ export async function RiskDetailModal({
return ( return (
<Modal <Modal
title={t("detailHeading", { ref: riskRef(risk.refNo) })} title={t("detailHeading", { ref: riskRef(risk.refNo), name: risk.title })}
sub={t("detailSub")} sub={t("detailSub")}
headerExtra={ headerExtra={
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
@@ -241,6 +241,15 @@ export async function RiskDetailModal({
<p className="text-sm text-muted-foreground">{t("noAssets")}</p> <p className="text-sm text-muted-foreground">{t("noAssets")}</p>
)} )}
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead>
<tr>
<th />
<th />
<th className="pb-1 text-right font-normal">
<CiaLegend />
</th>
</tr>
</thead>
<tbody> <tbody>
{risk.riskAssets.map((ra) => ( {risk.riskAssets.map((ra) => (
<tr key={ra.id} className="border-b last:border-0"> <tr key={ra.id} className="border-b last:border-0">
@@ -266,9 +275,10 @@ export async function RiskDetailModal({
</div> </div>
</div> </div>
{/* Brutto → Rest-Risiko: zwei Karten mit Pfeil + Farbskala */} {/* Brutto → Rest-Risiko: Farbskala oben, darunter zwei Karten mit Pfeil */}
<div className="mx-5 rounded-xl border border-[#e6e2f3] bg-[#f2f0f9] p-4"> <div className="mx-5 rounded-xl border border-[#e6e2f3] bg-[#f2f0f9] p-4">
<div className="grid items-center gap-3 sm:grid-cols-[1fr_auto_1fr]"> <RiskScoreScale gross={risk.score} residual={risk.residualScore} />
<div className="mt-4 grid items-center gap-3 sm:grid-cols-[1fr_auto_1fr]">
<RatingCard <RatingCard
title={t("gross")} title={t("gross")}
likelihood={risk.likelihood} likelihood={risk.likelihood}
@@ -285,7 +295,6 @@ export async function RiskDetailModal({
accent="residual" accent="residual"
/> />
</div> </div>
<RiskScoreScale gross={risk.score} residual={risk.residualScore} />
</div> </div>
{/* Notwendige Maßnahmen — echt verknüpft */} {/* Notwendige Maßnahmen — echt verknüpft */}
+4 -4
View File
@@ -3,11 +3,11 @@
import { useState } from "react"; import { useState } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
// Farblogik Stufe 14 (Referenz docs/ISMS-GUI-Verbesserungen-GEFIM.html) // Ampel-Farblogik Stufe 14: Grün → Gelb → Orange → Rot
const LEVEL_STYLE: Record<number, { on: string; border: string }> = { const LEVEL_STYLE: Record<number, { on: string; border: string }> = {
1: { on: "bg-[#9db2c9] text-white border-transparent", border: "border-[#9db2c9]" }, 1: { on: "bg-[#2e9e6b] text-white border-transparent", border: "border-[#2e9e6b]" },
2: { on: "bg-[#5d8fbf] text-white border-transparent", border: "border-[#5d8fbf]" }, 2: { on: "bg-[#e5b000] text-[#3b3b3a] border-transparent", border: "border-[#e5b000]" },
3: { on: "bg-[#e0982e] text-white border-transparent", border: "border-[#e0982e]" }, 3: { on: "bg-[#e07d2e] text-white border-transparent", border: "border-[#e07d2e]" },
4: { on: "bg-[#d64c4c] text-white border-transparent", border: "border-[#d64c4c]" }, 4: { on: "bg-[#d64c4c] text-white border-transparent", border: "border-[#d64c4c]" },
}; };
+4 -4
View File
@@ -19,11 +19,11 @@ export const RISK_CELL_COLORS: Record<RiskLevelKey, string> = {
critical: "#ef9a98", critical: "#ef9a98",
}; };
/** Pillen-Ton je Stufe (mockup-ui Pill). */ /** Pillen-Ton je Stufe — gemäß Heatmap-Skala: Grün → Gelb → Orange → Rot. */
export const RISK_PILL_TONE: Record<RiskLevelKey, "ok" | "info" | "warn" | "risk"> = { export const RISK_PILL_TONE: Record<RiskLevelKey, "ok" | "warn" | "orange" | "risk"> = {
low: "ok", low: "ok",
medium: "info", medium: "warn",
elevated: "warn", elevated: "orange",
high: "risk", high: "risk",
critical: "risk", critical: "risk",
}; };