diff --git a/messages/de.json b/messages/de.json
index 7c574e5..8e99698 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -175,7 +175,10 @@
"impactShort": "Schadenshöhe (C/I/A)",
"close": "Schließen",
"noAssets": "Noch keine Assets zugeordnet.",
- "category": "Prozesskategorie"
+ "category": "Prozesskategorie",
+ "secMaster": "Grunddaten",
+ "secAssets": "Zugeordnete Assets",
+ "deleteProcess": "Prozess löschen"
},
"processRole": {
"PRIMARY": "Primär",
diff --git a/messages/en.json b/messages/en.json
index 0494dfb..b89a7d2 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -175,7 +175,10 @@
"impactShort": "Damage level (C/I/A)",
"close": "Close",
"noAssets": "No assets assigned yet.",
- "category": "Process category"
+ "category": "Process category",
+ "secMaster": "Basic data",
+ "secAssets": "Assigned assets",
+ "deleteProcess": "Delete process"
},
"processRole": {
"PRIMARY": "Primary",
diff --git a/src/components/asset-form.tsx b/src/components/asset-form.tsx
index 50d186b..a2422b5 100644
--- a/src/components/asset-form.tsx
+++ b/src/components/asset-form.tsx
@@ -5,10 +5,10 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
+import { SegmentedRating } from "@/components/segmented-rating";
const ASSET_TYPES = ["INFORMATION", "SYSTEM", "APPLICATION", "LOCATION", "SUPPLIER", "PERSON", "DATA"] as const;
const ASSET_STATUS = ["ACTIVE", "PLANNED", "RETIRED"] as const;
-const LEVELS = [1, 2, 3, 4] as const;
/** Formular für Anlegen/Bearbeiten eines Assets (Server-Action wird übergeben). */
export async function AssetForm({
@@ -96,14 +96,15 @@ export async function AssetForm({
] as const
).map(([name, label, value]) => (
-
-
+
+
+
+
))}
diff --git a/src/components/asset-modals.tsx b/src/components/asset-modals.tsx
index 1dead85..b36dd59 100644
--- a/src/components/asset-modals.tsx
+++ b/src/components/asset-modals.tsx
@@ -98,7 +98,7 @@ export async function AssetDetailModal({
{asset.name}
-
+
{t("owner")}: {asset.owner?.name ?? tc("none")} · {t("type")}: {tType(asset.type)}
diff --git a/src/components/mockup-ui.tsx b/src/components/mockup-ui.tsx
index fd41e74..f4c3a57 100644
--- a/src/components/mockup-ui.tsx
+++ b/src/components/mockup-ui.tsx
@@ -55,28 +55,60 @@ export function KpiCard({
);
}
-/** Schutzbedarf/Schadenshöhe als drei Quadrate (C/I/A) wie im Mockup. */
-const CIA_COLORS: Record = {
+// Segmentbalken-Farben je Stufe 1–4 (Referenz docs/ISMS-GUI-Verbesserungen-GEFIM.html)
+const CIA_BAR_COLORS: Record = {
1: "bg-[#9db2c9]",
2: "bg-[#5d8fbf]",
3: "bg-[#e0982e]",
4: "bg-[#d64c4c]",
};
-export function CiaBadge({ c, i, a }: { c: number; i: number; a: number }) {
+/** Ein Schutzziel als 4er-Segmentbalken, gefüllt bis `level`. */
+function CiaSegment({ level, label }: { level: number; label?: string }) {
+ const color = CIA_BAR_COLORS[level] ?? CIA_BAR_COLORS[1];
return (
-
- {[c, i, a].map((v, idx) => (
-
- {v}
-
- ))}
+
+
+ {[1, 2, 3, 4].map((seg) => (
+
+ ))}
+
+ {label && (
+
+ {label}
+
+ )}
+
+ );
+}
+
+/**
+ * Schutzbedarf / Schadenshöhe als drei Segmentbalken (C/I/A). Mit `labels`
+ * werden VER/INT/VFB unter den Balken gezeigt (Detailkarten); ohne für
+ * kompakte Tabellenzellen.
+ */
+export function CiaBadge({
+ c,
+ i,
+ a,
+ labels = false,
+}: {
+ c: number;
+ i: number;
+ a: number;
+ labels?: boolean;
+}) {
+ return (
+
+
+
+
);
}
diff --git a/src/components/process-modals.tsx b/src/components/process-modals.tsx
index 6fcd4b6..09c3cf8 100644
--- a/src/components/process-modals.tsx
+++ b/src/components/process-modals.tsx
@@ -1,14 +1,13 @@
import Link from "next/link";
import { getTranslations } from "next-intl/server";
-import { Pencil, Trash2, X } from "lucide-react";
+import { MoreHorizontal, Pencil, Trash2, X } from "lucide-react";
import type { Prisma } from "@prisma/client";
import {
assignAsset,
createProcess,
deleteProcess,
- saveBia,
+ saveProcessAll,
unassignAsset,
- updateProcess,
} from "@/server/actions/processes";
import { ProcessForm } from "@/components/process-form";
import { Button } from "@/components/ui/button";
@@ -16,6 +15,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Modal } from "@/components/modal";
+import { SegmentedRating } from "@/components/segmented-rating";
import { CiaBadge, CriticalityPill, Pill, Tag } from "@/components/mockup-ui";
import { riskLevel, riskRef, RISK_PILL_TONE } from "@/lib/risk";
@@ -48,7 +48,6 @@ export type ProcessWithDetail = Prisma.ProcessGetPayload<{
};
}>;
-const LEVELS = [1, 2, 3, 4] as const;
/** Anlegen als Popup (Stammdaten; Assets/BIA folgen im Bearbeiten-Popup). */
export async function ProcessCreateModal({
@@ -141,6 +140,7 @@ export async function ProcessDetailModal({
c={pa.asset.confidentiality}
i={pa.asset.integrity}
a={pa.asset.availability}
+ labels
/>
@@ -234,7 +234,12 @@ export async function ProcessDetailModal({
);
}
-/** Bearbeiten im selben Popup: Stammdaten, Asset-Zuordnung, BIA, Löschen. */
+/**
+ * Bearbeiten im selben Popup — aufgeräumt (Referenz docs/ISMS-GUI-Verbesserungen):
+ * Sprungnavigation, genau EIN Speichern-Button (Stammdaten + BIA über die
+ * HTML-`form`-Association verbunden), Löschen im ⋯-Überlaufmenü. Die
+ * Asset-Zuordnung bleibt inkrementell (Chips add/remove).
+ */
export async function ProcessEditModal({
process,
users,
@@ -247,20 +252,23 @@ export async function ProcessEditModal({
const t = await getTranslations("processes");
const tAssets = await getTranslations("assets");
const tRole = await getTranslations("processRole");
- const tCrit = await getTranslations("criticality");
const tCat = await getTranslations("processCategory");
const tc = await getTranslations("common");
const bia = process.bia;
+ const FORM_ID = "process-edit-form";
const selectClass = "h-9 rounded-md border border-input bg-transparent px-3 text-sm";
const primary = process.processAssets.filter((pa) => pa.role === "PRIMARY");
const secondary = process.processAssets.filter((pa) => pa.role === "SECONDARY");
- const assetRow = (pa: ProcessWithDetail["processAssets"][number]) => (
-
- {pa.asset.name}
+ const assetChip = (pa: ProcessWithDetail["processAssets"][number]) => (
+
+ {pa.asset.name}
-
-
+
);
+ const jump = [
+ { href: "#p-grunddaten", label: t("secMaster") },
+ { href: "#p-assets", label: t("secAssets") },
+ { href: "#p-bia", label: t("bia") },
+ ];
+
return (
+
+
+
+
+
+
+
+ }
closeHref={`/processes?detail=${process.id}`}
closeLabel={t("close")}
+ footer={
+ <>
+ }
+ >
+ {tc("cancel")}
+
+
+ >
+ }
>
-
- {/* Stammdaten */}
-
+ {/* Sprungnavigation */}
+
- {/* Asset-Zuordnung */}
-
-
+ {/* Die eigentliche
+
+
+ {/* Grunddaten */}
+
+ {t("secMaster")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Assets (inkrementell) */}
+
+ {t("secAssets")}
+
{t("primaryAssets")}
-
{t("primaryHint")}
- {primary.length === 0 ?
{tc("none")}
:
}
+
{t("primaryHint")}
+ {primary.length === 0 ? (
+
{tc("none")}
+ ) : (
+
{primary.map(assetChip)}
+ )}
{t("secondaryAssets")}
-
{t("secondaryHint")}
- {secondary.length === 0 ?
{tc("none")}
:
{secondary.map(assetRow)}
}
+
{t("secondaryHint")}
+ {secondary.length === 0 ? (
+
{tc("none")}
+ ) : (
+
{secondary.map(assetChip)}
+ )}
{availableAssets.length > 0 && (
-
)}
-
-
+
- {/* BIA */}
-
-
{t("bia")}
- {/* key erzwingt Remount nach dem Speichern — sonst behalten uncontrolled Inputs alte Werte */}
-
-