From ee4e554554e1a50d8a15dcad14c9f3b5e455645b Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 3 Jul 2026 16:31:58 +0200 Subject: [PATCH] =?UTF-8?q?IT-Service-Detail=20auf=20Asset-Grundger=C3=BCs?= =?UTF-8?q?t=20+=20Schutzbedarf-Layout=20gefixt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ServiceDetailModal spiegelt jetzt das normale Asset-Detail: links "■ Stammdaten" (violette Karte, CIA-Badge, Provider/Kritikalität, Notizen, zugeordnete Prozesse), rechts "◆ Abhängigkeiten" (Verknüpfte-Assets-Tabelle mit Typ + CIA + CiaLegend, Reverse-Liste, Link zum Abhängigkeitsgraph), darunter das Risiken-Band — und erst darunter die Verantwortungsmatrix (RACI) - SERVICE_INCLUDE um Typ/CIA der verknüpften Assets und Risk-Score erweitert, RACI nach controlRef sortiert - Schutzbedarf (C/I/A) in Lieferanten- und IT-Service-Bearbeiten-Formular: vom gequetschten flex-Layout auf das saubere Asset-Formular-Muster umgestellt (grid mit Einzellabels Vertraulichkeit/Integrität/Verfügbarkeit + Normal…Sehr hoch) Co-Authored-By: Claude Opus 4.8 --- src/app/(app)/suppliers/page.tsx | 6 +- src/components/service-modals.tsx | 191 +++++++++++++++++++++-------- src/components/supplier-modals.tsx | 25 +++- 3 files changed, 162 insertions(+), 60 deletions(-) diff --git a/src/app/(app)/suppliers/page.tsx b/src/app/(app)/suppliers/page.tsx index 102abfc..55ffd84 100644 --- a/src/app/(app)/suppliers/page.tsx +++ b/src/app/(app)/suppliers/page.tsx @@ -56,9 +56,9 @@ const SUPPLIER_INCLUDE = { const SERVICE_INCLUDE = { serviceProfile: { include: { provider: { select: { id: true, name: true } } } }, - raci: true, - riskAssets: { include: { risk: { select: { id: true, refNo: true, title: true } } } }, - relationsFrom: { include: { relatedAsset: { select: { id: true, name: true } } } }, + raci: { orderBy: { controlRef: "asc" } }, + riskAssets: { include: { risk: { select: { id: true, refNo: true, title: true, score: true } } } }, + relationsFrom: { include: { relatedAsset: { select: { id: true, name: true, type: true, confidentiality: true, integrity: true, availability: true } } } }, relationsTo: { include: { asset: { select: { id: true, name: true } } } }, processAssets: { include: { process: { select: { id: true, name: true } } } }, } as const; diff --git a/src/components/service-modals.tsx b/src/components/service-modals.tsx index ded158d..497d765 100644 --- a/src/components/service-modals.tsx +++ b/src/components/service-modals.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { getTranslations } from "next-intl/server"; -import { Pencil, Plus, Trash2 } from "lucide-react"; +import { Network, Pencil, Plus, Trash2 } from "lucide-react"; import type { Prisma } from "@prisma/client"; import { addRaci, @@ -16,16 +16,17 @@ 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 } from "@/components/mockup-ui"; +import { CiaBadge, CiaLegend, CriticalityPill, Pill, Tag } from "@/components/mockup-ui"; import { serviceRef } from "@/lib/supplier"; +import { riskLevel, riskRef, RISK_PILL_TONE } from "@/lib/risk"; import { ISA_CONTROLS } from "@/lib/isa-controls"; export type ServiceAssetDetail = Prisma.AssetGetPayload<{ include: { serviceProfile: { include: { provider: { select: { id: true; name: true } } } }; raci: true; - riskAssets: { include: { risk: { select: { id: true; refNo: true; title: true } } } }; - relationsFrom: { include: { relatedAsset: { select: { id: true; name: true } } } }; + riskAssets: { include: { risk: { select: { id: true; refNo: true; title: true; score: true } } } }; + relationsFrom: { include: { relatedAsset: { select: { id: true; name: true; type: true; confidentiality: true; integrity: true; availability: true } } } }; relationsTo: { include: { asset: { select: { id: true; name: true } } } }; processAssets: { include: { process: { select: { id: true; name: true } } } }; }; @@ -38,23 +39,29 @@ const RACI_TONE = { PROVIDER: "info", US: "violet", SHARED: "warn" } as const; export async function ServiceDetailModal({ service, canWrite }: { service: ServiceAssetDetail; canWrite: boolean }) { const t = await getTranslations("services"); + const ta = await getTranslations("assets"); const tParty = await getTranslations("raciParty"); const tCrit = await getTranslations("criticality"); + const tType = await getTranslations("assetType"); + const tLevel = await getTranslations("riskLevel"); + const tRisks = await getTranslations("risks"); + const tDep = await getTranslations("dependencies"); const tc = await getTranslations("common"); + const tp = await getTranslations("processes"); const p = service.serviceProfile!; return ( - + {tType(service.type)} } closeHref="/suppliers?tab=services" - closeLabel={t("close")} + closeLabel={tp("close")} footer={ <> {canWrite && ( @@ -62,54 +69,123 @@ export async function ServiceDetailModal({ service, canWrite }: { service: Servi {tc("edit")} )} - + } > -
- {/* Stammdaten + Verknüpfungen */} -
-
-
-
{t("provider")}
-
- {p.provider ? ( - {p.provider.name} - ) : ( - {p.internal ? t("internal") : t("noProvider")} - )} -
-
{t("linkedAssets")}
-
- {[...service.relationsFrom.map((r) => r.relatedAsset), ...service.relationsTo.map((r) => r.asset)].map((a) => ( - {a.name} - ))} - {service.relationsFrom.length + service.relationsTo.length === 0 && tc("none")} -
-
{t("linkedProcesses")}
-
- {service.processAssets.map((pa) => ( - {pa.process.name} - ))} - {service.processAssets.length === 0 && tc("none")} -
-
- {p.notes &&

{p.notes}

} +
+ {/* Stammdaten — violette Karte wie beim Asset-Detail */} +
+
+ {ta("masterPill")} + {ta("masterNote")}
-
-

{t("risks")}

- {service.riskAssets.length === 0 &&

{tc("none")}

} -
    - {service.riskAssets.map((ra) => ( -
  • - R-{String(ra.risk.refNo).padStart(3, "0")} · {ra.risk.title} +
    +
    + {serviceRef(p.refNo)} · {service.name} + +
    +
    + {t("provider")}:{" "} + {p.provider ? ( + {p.provider.name} + ) : ( + p.internal ? t("internal") : t("noProvider") + )} + {" · "} + {t("criticality")}: {tCrit(String(p.criticality))} +
    + {p.notes &&

    {p.notes}

    } +
    + +
    +

    {ta("processes")}

    + {service.processAssets.length === 0 &&

    {tc("none")}

    } +
      + {service.processAssets.map((pa) => ( +
    • + {pa.process.name}
    • ))}
- {/* RACI-Matrix */} + {/* Verknüpfte Assets — Tabelle wie beim Asset-Detail */} +
+
+ {ta("depPill")} + {ta("depNote")} +
+ {service.relationsFrom.length === 0 && service.relationsTo.length === 0 && ( +

{tc("none")}

+ )} + {service.relationsFrom.length > 0 && ( + + + + + + + + {service.relationsFrom.map((rel) => ( + + + + + + ))} + +
+ +
+ {rel.relatedAsset.name} + {tType(rel.relatedAsset.type)} + +
+ )} + {service.relationsTo.length > 0 && ( + <> +

{ta("relationReverseHint")}

+
    + {service.relationsTo.map((rel) => ( +
  • + {rel.asset.name} +
  • + ))} +
+ + )} + + {tDep("openGraph")} → + +
+
+ + {/* Zugeordnete Risiken — Band wie beim Asset-Detail */} +
+
+ {ta("linkedRisks")} + {canWrite && ( + + )} +
+ {service.riskAssets.length === 0 &&

{tc("none")}

} +
    + {service.riskAssets.map((ra) => ( +
  • + {riskRef(ra.risk.refNo)} + {ra.risk.title} + {ra.risk.score} · {tLevel(riskLevel(ra.risk.score))} +
  • + ))} +
+
+ + {/* Verantwortungsmatrix (RACI) — darunter, über die volle Breite */} +
@@ -189,6 +265,8 @@ export async function ServiceDetailModal({ service, canWrite }: { service: Servi async function ServiceFields({ service, providers, formId }: { service?: ServiceAssetDetail; providers: { id: string; name: string }[]; formId?: string }) { const t = await getTranslations("services"); + const tA = await getTranslations("assets"); + const tLevel = await getTranslations("protectionLevel"); const f = formId ? { form: formId } : {}; const p = service?.serviceProfile; return ( @@ -210,14 +288,25 @@ async function ServiceFields({ service, providers, formId }: { service?: Service
-
- -
- {(["confidentiality", "integrity", "availability"] as const).map((n) => ( - +
+ {t("protection")} (1–4) +
+ {( + [ + ["confidentiality", tA("confidentiality")], + ["integrity", tA("integrity")], + ["availability", tA("availability")], + ] as const + ).map(([n, label]) => ( +
+ +
+ +
+
))}
-
+ diff --git a/src/components/supplier-modals.tsx b/src/components/supplier-modals.tsx index a243c7c..e529ee3 100644 --- a/src/components/supplier-modals.tsx +++ b/src/components/supplier-modals.tsx @@ -221,6 +221,8 @@ export async function SupplierDetailModal({ supplier, canWrite }: { supplier: Su async function SupplierFields({ supplier, formId }: { supplier?: SupplierAssetDetail; formId?: string }) { const t = await getTranslations("suppliers"); const tLife = await getTranslations("supplierStatus"); + const tA = await getTranslations("assets"); + const tLevel = await getTranslations("protectionLevel"); const f = formId ? { form: formId } : {}; const p = supplier?.supplierProfile; return ( @@ -253,14 +255,25 @@ async function SupplierFields({ supplier, formId }: { supplier?: SupplierAssetDe
-
- -
- {(["confidentiality", "integrity", "availability"] as const).map((n) => ( - +
+ {t("protection")} (1–4) +
+ {( + [ + ["confidentiality", tA("confidentiality")], + ["integrity", tA("integrity")], + ["availability", tA("availability")], + ] as const + ).map(([n, label]) => ( +
+ +
+ +
+
))}
-
+