+ {/* Schutzbedarf-Banner mit Simulation */}
+
+
+
+
Schutzbedarf steuert die Anforderungen
+
+ Verknüpfte Assets:{" "}
+ {linkedAssets.length
+ ? linkedAssets.map((a) => `${a.name} (${a.c}/${a.i}/${a.a})`).join(" · ")
+ : "—"}{" "}
+ · abgeleitet: {PROTECTION_LABEL[actualLevel]} (max C/I/A {maxCia})
+
+
+
+ Simulieren:
+ {([1, 2, 3] as ProtectionLevel[]).map((lvl) => (
+
+ ))}
+
+
+
+
+ {/* Flache Anforderungsliste, nach Themen gruppiert */}
+
+
Anforderungen
+
+ Nur die tatsächlich relevanten Anforderungen — mit Referenz und Herkunfts-Control.
+
+ {themes.map((theme) => {
+ const rows = reqs.filter((r) => r.theme === theme);
+ if (rows.length === 0) return null;
+ return (
+
+
+ {THEME_LABEL[theme]}
+
+
+ {rows.map((r) => {
+ const st = status(r);
+ return (
+ -
+ {st === "met" ? (
+
+ ) : st === "open" ? (
+
+ ) : (
+
+ )}
+ {r.label}
+ {st === "open" && (
+
+ + verknüpfen
+
+ )}
+ {r.control}
+
+ );
+ })}
+
+
+ );
+ })}
+
+ {/* Gate-Panel */}
+ {gateActive && (
+
+
+ Sehr hoher Schutzbedarf — Kompensation erforderlich
+
+
+ Kein gültiges Third-Party-Audit / TISAX-Label. Nachweis über eine der Optionen:
+
+
+
+
Option A · Audit-Nachweis
+
+ TISAX-Label / Audit erfassen
+
+
oder selbst durchgeführtes Lieferanten-Audit
+
+
+
Option B · Kompensation (beides nötig)
+
+
+ Managemententscheidung
+
+ Dokumentieren
+
+
+
+ Risiko im Risikomodul
+
+
+
+
+
+
+ )}
+
+
+ {/* Reifegrad-/Konformitätskarte */}
+
+
+
+
+
Reifegrad
+
+ {maturity.toFixed(1)} / {targetMaturity.toFixed(1)}
+
+
+
+
+ {isbValue != null ? "ISB-freigegeben" : `berechnet: ${computedMaturity.toFixed(1)}`}
+
+
+
+ );
+}
diff --git a/src/components/supplier-modals.tsx b/src/components/supplier-modals.tsx
index bd3b5e5..a243c7c 100644
--- a/src/components/supplier-modals.tsx
+++ b/src/components/supplier-modals.tsx
@@ -1,19 +1,18 @@
import Link from "next/link";
import { getFormatter, getTranslations } from "next-intl/server";
-import { Pencil, Plus, Trash2, X, AlertTriangle } from "lucide-react";
-import type { Prisma, SupplierControl } from "@prisma/client";
+import { Pencil, Plus, Trash2, X } from "lucide-react";
+import type { Prisma } from "@prisma/client";
import {
addAssessment,
addContract,
addDecision,
addEvidence,
addNda,
- addResponsibility,
- addSubcontractor,
+ approveMaturity,
+ createGateRisk,
createSupplier,
deleteChild,
deleteSupplier,
- saveMaturity,
updateSupplier,
} from "@/server/actions/suppliers";
import { Button } from "@/components/ui/button";
@@ -22,59 +21,91 @@ 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 { supplierRef, SUPPLIER_STATUS_TONE, isExpired, isExpiring, needsManagementDecision } from "@/lib/supplier";
+import { Pill } from "@/components/mockup-ui";
+import { SupplierRequirements } from "@/components/supplier-cockpit";
+import {
+ supplierRef,
+ SUPPLIER_LIFECYCLE_TONE,
+ protectionLevel,
+ PROTECTION_LABEL,
+ buildReqContext,
+ REQUIREMENTS,
+ conformity,
+ computedMaturity,
+ CONFORMITY_TONE,
+ CONFORMITY_LABEL,
+ TARGET_MATURITY,
+ isExpired,
+ isExpiring,
+} from "@/lib/supplier";
-export type SupplierWithDetail = Prisma.SupplierGetPayload<{
+export type SupplierAssetDetail = Prisma.AssetGetPayload<{
include: {
- assessments: true;
+ supplierProfile: true;
contracts: true;
ndas: true;
evidence: true;
- responsibilities: true;
+ assessments: true;
subcontractors: true;
decisions: true;
- controlMaturity: true;
- assetLinks: { include: { asset: { select: { id: true; name: true } } } };
+ maturity: true;
+ riskAssets: { include: { risk: { select: { id: true; refNo: true; title: true; score: true } } } };
+ relationsFrom: { include: { relatedAsset: { select: { name: true; confidentiality: true; integrity: true; availability: true } } } };
+ relationsTo: { include: { asset: { select: { name: true; confidentiality: true; integrity: true; availability: true } } } };
};
}>;
-const STATUSES = ["ACTIVE", "ONBOARDING", "UNDER_REVIEW", "OFFBOARDED"] as const;
const inputCls = "h-9 w-full rounded-md border border-input bg-transparent px-3 text-sm";
-/* ─────────────────────────── Detail (read-only) ─────────────────────────── */
+function contextOf(s: SupplierAssetDetail) {
+ const linkedAssets = [
+ { name: s.name, c: s.confidentiality, i: s.integrity, a: s.availability },
+ ...s.relationsFrom.map((r) => ({ name: r.relatedAsset.name, c: r.relatedAsset.confidentiality, i: r.relatedAsset.integrity, a: r.relatedAsset.availability })),
+ ...s.relationsTo.map((r) => ({ name: r.asset.name, c: r.asset.confidentiality, i: r.asset.integrity, a: r.asset.availability })),
+ ];
+ const maxCia = Math.max(...linkedAssets.map((a) => Math.max(a.c, a.i, a.a)));
+ const level = protectionLevel(maxCia);
+ const ctx = buildReqContext({
+ contracts: s.contracts,
+ ndas: s.ndas,
+ evidence: s.evidence,
+ assessments: s.assessments,
+ subcontractors: s.subcontractors,
+ nextReview: s.supplierProfile?.nextReview ?? null,
+ linkedRiskCount: s.riskAssets.length,
+ decisionCount: s.decisions.length,
+ });
+ return { linkedAssets: linkedAssets.slice(1), maxCia, level, ctx };
+}
-export async function SupplierDetailModal({
- supplier,
- controls,
- canWrite,
-}: {
- supplier: SupplierWithDetail;
- controls: SupplierControl[];
- canWrite: boolean;
-}) {
+/* ─────────────────────── Cockpit (Detail) ─────────────────────── */
+
+export async function SupplierDetailModal({ supplier, canWrite }: { supplier: SupplierAssetDetail; canWrite: boolean }) {
const t = await getTranslations("suppliers");
- const tStatus = await getTranslations("supplierStatus");
- const tCrit = await getTranslations("criticality");
- const tAType = await getTranslations("assessmentType");
+ const tLife = await getTranslations("supplierStatus");
const tEKind = await getTranslations("evidenceKind");
- const tParty = await getTranslations("responsibleParty");
const tc = await getTranslations("common");
const fmt = await getFormatter();
const date = (d: Date | null) => (d ? fmt.dateTime(d, { dateStyle: "medium" }) : tc("none"));
- const decisionNeeded = needsManagementDecision(supplier.evidence);
- const matById = new Map(supplier.controlMaturity.map((m) => [m.controlId, m.maturity]));
+ const p = supplier.supplierProfile!;
+ const { linkedAssets, maxCia, level, ctx } = contextOf(supplier);
+ const conf = conformity(level, ctx);
+ const computed = computedMaturity(level, ctx);
+ const reqs = REQUIREMENTS.map((r) => ({ key: r.key, label: r.label, control: r.control, tier: r.tier, theme: r.theme, met: r.met(ctx) }));
+
+ const gateEvidenceHref = `/suppliers?edit=${supplier.id}`;
+ const gateDecisionHref = `/suppliers?edit=${supplier.id}`;
return (
- {supplier.nis2Relevant && NIS2}
-
- {tStatus(supplier.status)}
+ {p.nis2Relevant && NIS2}
+ Schutzbedarf {PROTECTION_LABEL[level]}
+ {CONFORMITY_LABEL[conf]}
}
closeHref="/suppliers"
@@ -86,234 +117,112 @@ export async function SupplierDetailModal({
{tc("edit")}
)}
- }>
- {t("close")}
-
+ }>{t("close")}
>
}
>
- {/* Stammdaten + Assets */}
-
-
-
- - {t("sector")}
- - {supplier.sector ?? tc("none")}
- - {t("services")}
- - {supplier.services ?? tc("none")}
- - {t("contact")}
- - {supplier.contact ?? tc("none")}
- - {t("dataCategories")}
- - {supplier.dataCategories.join(", ") || tc("none")}
- - {t("protection")}
-
- - {t("nextReview")}
- - {date(supplier.nextReview)}
-
-
-
-
{t("assets")}
- {supplier.assetLinks.length === 0 &&
{tc("none")}
}
-
- {supplier.assetLinks.map((l) => (
- -
- {l.asset.name}
-
- ))}
-
- {supplier.notes &&
{supplier.notes}
}
-
-
+
- {/* Managemententscheidung nötig? */}
- {decisionNeeded && (
-
-
-
{t("decisionNeeded")}
-
- )}
-
- {/* VDA-ISA Kap. 6 Reifegrade */}
+ {/* Risiken */}
- {t("catalog")}
- {t("catalogNote")}
-
- {controls.map((c) => {
- const m = matById.get(c.id) ?? 0;
- return (
-
-
-
- {c.ref} · {c.objective}
-
-
- {t("maturity")} {m}/5 · {t("target")} {c.targetMaturity}
-
-
-
- {[1, 2, 3, 4, 5].map((s) => (
- = c.targetMaturity
- ? "var(--ok)"
- : "var(--warn)"
- : "rgba(120,135,180,0.2)",
- }}
- />
- ))}
-
- {c.references && (
-
{t("references")}: {c.references}
- )}
-
- );
- })}
-
+ {t("risks")}
+ {supplier.riskAssets.length === 0 && {tc("none")}
}
+
+ {supplier.riskAssets.map((ra) => (
+ -
+ R-{String(ra.risk.refNo).padStart(3, "0")}
+ {ra.risk.title}
+
+ ))}
+
- {/* Nachweise */}
-
- {supplier.evidence.length === 0 ? (
-
- ) : (
- supplier.evidence.map((e) => (
-
- {tEKind(e.kind)}
- {e.name ?? tEKind(e.kind)}
- {e.protectsCia && ({e.protectsCia})}
- {e.adequacyChecked && {t("adequacy")}}
- {e.validTo && (
-
- {t("expires")} {date(e.validTo)}
-
- )}
-
- ))
- )}
-
+ {/* Verträge & NDA & Nachweise (read) */}
+
+
+ {t("contracts")} & {t("ndas")}
+
+ {supplier.contracts.map((c) => (
+ -
+ {c.reference ?? c.type}
+ {c.avDpa && AV/DPA}
+ {c.validTo && {t("validTo")}: {date(c.validTo)}}
+
+ ))}
+ {supplier.ndas.map((n) => (
+ -
+ NDA: {n.subject ?? n.parties ?? ""}
+ {n.validTo && {t("validTo")}: {date(n.validTo)}}
+
+ ))}
+ {supplier.contracts.length + supplier.ndas.length === 0 && - {tc("none")}
}
+
+
+
+ {t("evidence")}
+
+ {supplier.evidence.map((e) => (
+ -
+ {tEKind(e.kind)}
+ {e.name ?? tEKind(e.kind)}
+ {e.adequacyChecked && {t("adequacy")}}
+ {e.validTo && {t("expires")} {date(e.validTo)}}
+
+ ))}
+ {supplier.evidence.length === 0 && - {tc("none")}
}
+
+
+
- {/* Bewertungen */}
-
- {supplier.assessments.length === 0 ? (
-
- ) : (
- supplier.assessments.map((a) => (
-
- {tAType(a.type)}
- {a.score != null && {a.score}/100}
- {a.result && {a.result}}
- {date(a.date)}
-
- ))
- )}
-
-
- {/* Verträge */}
-
- {supplier.contracts.length === 0 ? (
-
- ) : (
- supplier.contracts.map((k) => (
-
- {k.reference ?? k.type}
- {k.avDpa && {t("avDpa")}}
- {k.flowdown && {t("flowdown")}}
- {k.validTo && (
-
- {t("validTo")}: {date(k.validTo)}
-
- )}
-
- ))
- )}
-
-
- {/* NDAs */}
-
- {supplier.ndas.length === 0 ? (
-
- ) : (
- supplier.ndas.map((n) => (
-
- {n.subject ?? n.parties ?? "NDA"}
- {n.validTo && (
-
- {t("validTo")}: {date(n.validTo)}
-
- )}
- {n.extensionStatus && · {n.extensionStatus}}
-
- ))
- )}
-
-
- {/* RACI */}
-
- {supplier.responsibilities.length === 0 ? (
-
- ) : (
- supplier.responsibilities.map((r) => (
-
- {r.itService}
- · {r.requirement}
-
- {tParty(r.responsibleParty)}
-
-
- ))
- )}
-
-
- {/* Subunternehmer */}
- {supplier.subcontractors.length > 0 && (
-
- {supplier.subcontractors.map((s) => (
-
- {s.name} {s.flowdownObligation && {t("flowdownObl")}}
-
- ))}
-
+ {/* ISB-Freigabe */}
+ {canWrite && (
+
+ {t("isbApproval")}
+
+ {t("approvalNote")}
+
)}
- {/* Entscheidungen */}
- {supplier.decisions.length > 0 && (
-
- {supplier.decisions.map((d) => (
-
- {d.decision}
- — {d.reasonNoAudit}
- · {d.decidedBy} · {date(d.date)}
-
- ))}
-
- )}
+ {/* Lifecycle + Datenkategorien */}
+
+ {t("status")}:
{tLife(p.lifecycle)}
+ {p.dataCategories.length > 0 && ` · ${t("dataCategories")}: ${p.dataCategories.join(", ")}`}
+
);
}
-function ListSection({ title, children }: { title: string; children: React.ReactNode }) {
- return (
-
);
@@ -389,40 +296,27 @@ export async function SupplierCreateModal() {
);
}
-/* ─────────────────────────── Bearbeiten ─────────────────────────── */
+/* ─────────────────────── Bearbeiten ─────────────────────── */
-export async function SupplierEditModal({
- supplier,
- controls,
-}: {
- supplier: SupplierWithDetail;
- controls: SupplierControl[];
-}) {
+export async function SupplierEditModal({ supplier }: { supplier: SupplierAssetDetail }) {
const t = await getTranslations("suppliers");
const tc = await getTranslations("common");
const tEKind = await getTranslations("evidenceKind");
const tAType = await getTranslations("assessmentType");
- const tParty = await getTranslations("responsibleParty");
const FORM = "supplier-edit";
- const matById = new Map(supplier.controlMaturity.map((m) => [m.controlId, m.maturity]));
-
const disc =
"cursor-pointer list-none rounded-lg border border-[var(--panel-brd)] bg-[var(--elevated)] px-3 py-1.5 text-[12.5px] font-semibold text-muted-foreground hover:text-foreground [&::-webkit-details-marker]:hidden";
return (