From 9b479abb0f55c52365f45e4b67cfc652d58eb84d Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 24 Jul 2026 10:13:19 +0200 Subject: [PATCH] Software und Projekte als Assets + Auto-Sicht kritische Dienste MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Software (Lieferantenmodul): - Neuer Tab „Software" mit Popups (Detail/Anlegen/Bearbeiten) analog IT-Services, Anbieter-Verknüpfung, Freigabestatus, Version, Review; Server-Actions createSoftware/updateSoftware/deleteSoftware. Projekte (Assetinventar): - AssetType PROJECT als Filter, Popup mit Kritikalität (C/I/A), Projektleitung, IS-Klassifizierung, Status, ISB-Einbindung; Risiko-Verknüpfung über die bestehende RiskAsset-Logik; Server-Actions createProject/updateProject/ deleteProject. Kontextabhängiger „Neu"-Button je Filter. Kritische IT-Dienste: - Schreibgeschützte Auto-Sicht (/assets?view=critical), abgeleitet aus Verfügbarkeit und BIA; RTO/RPO aus den verknüpften Prozessen (strengster Wert), Abhängigkeiten aus Asset-Relationen. Software-/Projekt-Cockpits werden auch auf der Asset-Seite gerendert (backHref=/assets). i18n (de/en) und Modul-Guard-Registrierung ergänzt; alle Editierfunktionen als Popup. Co-Authored-By: Claude Opus 4.8 --- messages/de.json | 80 ++++++++- messages/en.json | 80 ++++++++- scripts/check-module-guards.ts | 2 + src/app/(app)/assets/page.tsx | 150 +++++++++++++++- src/app/(app)/suppliers/page.tsx | 92 +++++++++- src/components/project-modals.tsx | 277 ++++++++++++++++++++++++++++ src/components/software-modals.tsx | 280 +++++++++++++++++++++++++++++ src/lib/supplier-include.ts | 16 ++ src/lib/supplier.ts | 6 + src/server/actions/projects.ts | 92 ++++++++++ src/server/actions/software.ts | 96 ++++++++++ 11 files changed, 1148 insertions(+), 23 deletions(-) create mode 100644 src/components/project-modals.tsx create mode 100644 src/components/software-modals.tsx create mode 100644 src/server/actions/projects.ts create mode 100644 src/server/actions/software.ts diff --git a/messages/de.json b/messages/de.json index 1a66bfc..f30e7a7 100644 --- a/messages/de.json +++ b/messages/de.json @@ -14,7 +14,9 @@ "remove": "Entfernen", "none": "—", "comingSoon": "Bald verfügbar", - "confirmDelete": "Wirklich löschen?" + "confirmDelete": "Wirklich löschen?", + "yes": "Ja", + "no": "Nein" }, "nav": { "dashboard": "Dashboard", @@ -116,7 +118,15 @@ "masterNote": "Klassifizierung & Verantwortung", "depPill": "◆ Abhängigkeiten", "depNote": "Verknüpfte Assets", - "riskCount": "{count} Risiken" + "riskCount": "{count} Risiken", + "criticalTitle": "Kritische IT-Dienste", + "criticalSub": "Automatisch aus Assetinventar und BIA abgeleitet – schreibgeschützt.", + "criticalService": "Kritischer IT-Dienst", + "biaCriticality": "BIA-Kritikalität", + "supportedProcesses": "Gestützte Prozesse", + "criticalEmpty": "Keine kritischen IT-Dienste ermittelt (hohe Verfügbarkeit oder BIA-kritischer Prozess).", + "criticalNote": "Diese Sicht wird laufend aus dem Assetinventar und der Business-Impact-Analyse abgeleitet. RTO/RPO stammen aus den verknüpften Prozessen (strengster Wert). Pflege erfolgt am jeweiligen Asset bzw. Prozess.", + "backToInventory": "Zum Assetinventar" }, "assetType": { "INFORMATION": "Information", @@ -126,7 +136,9 @@ "SUPPLIER": "Lieferant", "PERSON": "Person/Rolle", "DATA": "Datenkategorie", - "IT_SERVICE": "IT-Service" + "IT_SERVICE": "IT-Service", + "SOFTWARE": "Software", + "PROJECT": "Projekt" }, "assetStatus": { "ACTIVE": "Aktiv", @@ -505,7 +517,8 @@ "evidence": "Nachweis", "raciEmpty": "Noch keine Controls zugeordnet.", "close": "Schließen", - "none": "—" + "none": "—", + "tabSoftware": "Software" }, "raciParty": { "PROVIDER": "Provider", @@ -583,5 +596,62 @@ "levelGlobal": "Global übernehmen", "levelAl2": "MUSS·SOLL·HOCH", "levelAl3": "+ SEHR HOCH" + }, + "software": { + "title": "Software-Freigaben", + "tabTitle": "Software", + "newSoftware": "Software erfassen", + "ref": "Kürzel", + "name": "Software", + "provider": "Anbieter/Lieferant", + "noProvider": "kein Anbieter", + "version": "Version/Patch-Stand", + "approvalStatus": "Freigabestatus", + "approvedBy": "Freigeber", + "criticality": "Kritikalität", + "protection": "Schutzbedarf", + "nextReview": "Nächstes Review", + "review": "Review", + "notes": "Notizen", + "createTitle": "Software erfassen", + "editTitle": "Software bearbeiten", + "detailSub": "Freigegebene Software (Whitelist) mit Anbieter und Review", + "masterPill": "Stammdaten", + "linkedRisks": "Zugeordnete Risiken", + "empty": "Noch keine Software erfasst.", + "close": "Schließen", + "none": "—" + }, + "softwareStatus": { + "BEANTRAGT": "Beantragt", + "FREIGEGEBEN": "Freigegeben", + "GESPERRT": "Gesperrt" + }, + "projects": { + "title": "Projekte", + "newProject": "Projekt anlegen", + "ref": "Kürzel", + "name": "Projektname", + "owner": "Projektleitung", + "classification": "IS-Klassifizierung", + "status": "Status", + "isbInvolved": "ISB eingebunden", + "criticality": "Kritikalität", + "protection": "Schutzbedarf", + "notes": "Notizen", + "createTitle": "Projekt anlegen", + "editTitle": "Projekt bearbeiten", + "detailSub": "Informationssicherheit in Projekten (R01 / VA-19)", + "masterPill": "Stammdaten", + "linkedRisks": "Zugeordnete Risiken", + "empty": "Noch keine Projekte erfasst.", + "close": "Schließen", + "none": "—" + }, + "projectStatus": { + "GEPLANT": "Geplant", + "LAUFEND": "Laufend", + "ABGESCHLOSSEN": "Abgeschlossen", + "ABGEBROCHEN": "Abgebrochen" } -} \ No newline at end of file +} diff --git a/messages/en.json b/messages/en.json index b288d10..75cec69 100644 --- a/messages/en.json +++ b/messages/en.json @@ -14,7 +14,9 @@ "remove": "Remove", "none": "—", "comingSoon": "Coming soon", - "confirmDelete": "Really delete?" + "confirmDelete": "Really delete?", + "yes": "Yes", + "no": "No" }, "nav": { "dashboard": "Dashboard", @@ -116,7 +118,15 @@ "masterNote": "Classification & responsibility", "depPill": "◆ Dependencies", "depNote": "Linked assets", - "riskCount": "{count} risks" + "riskCount": "{count} risks", + "criticalTitle": "Critical IT services", + "criticalSub": "Automatically derived from asset inventory and BIA – read-only.", + "criticalService": "Critical IT service", + "biaCriticality": "BIA criticality", + "supportedProcesses": "Supported processes", + "criticalEmpty": "No critical IT services identified (high availability or BIA-critical process).", + "criticalNote": "This view is derived continuously from the asset inventory and the business impact analysis. RTO/RPO come from the linked processes (strictest value). Maintain data on the respective asset or process.", + "backToInventory": "Back to inventory" }, "assetType": { "INFORMATION": "Information", @@ -126,7 +136,9 @@ "SUPPLIER": "Supplier", "PERSON": "Person/role", "DATA": "Data category", - "IT_SERVICE": "IT service" + "IT_SERVICE": "IT service", + "SOFTWARE": "Software", + "PROJECT": "Project" }, "assetStatus": { "ACTIVE": "Active", @@ -505,7 +517,8 @@ "evidence": "Evidence", "raciEmpty": "No controls assigned yet.", "close": "Close", - "none": "—" + "none": "—", + "tabSoftware": "Software" }, "raciParty": { "PROVIDER": "Provider", @@ -583,5 +596,62 @@ "levelGlobal": "Inherit global", "levelAl2": "MUST·SHOULD·HIGH", "levelAl3": "+ VERY HIGH" + }, + "software": { + "title": "Software approvals", + "tabTitle": "Software", + "newSoftware": "Add software", + "ref": "Ref", + "name": "Software", + "provider": "Provider/Supplier", + "noProvider": "no provider", + "version": "Version/patch level", + "approvalStatus": "Approval status", + "approvedBy": "Approved by", + "criticality": "Criticality", + "protection": "Protection need", + "nextReview": "Next review", + "review": "Review", + "notes": "Notes", + "createTitle": "Add software", + "editTitle": "Edit software", + "detailSub": "Approved software (whitelist) with provider and review", + "masterPill": "Master data", + "linkedRisks": "Linked risks", + "empty": "No software recorded yet.", + "close": "Close", + "none": "—" + }, + "softwareStatus": { + "BEANTRAGT": "Requested", + "FREIGEGEBEN": "Approved", + "GESPERRT": "Blocked" + }, + "projects": { + "title": "Projects", + "newProject": "Add project", + "ref": "Ref", + "name": "Project name", + "owner": "Project lead", + "classification": "IS classification", + "status": "Status", + "isbInvolved": "ISB involved", + "criticality": "Criticality", + "protection": "Protection need", + "notes": "Notes", + "createTitle": "Add project", + "editTitle": "Edit project", + "detailSub": "Information security in projects (R01 / VA-19)", + "masterPill": "Master data", + "linkedRisks": "Linked risks", + "empty": "No projects recorded yet.", + "close": "Close", + "none": "—" + }, + "projectStatus": { + "GEPLANT": "Planned", + "LAUFEND": "Running", + "ABGESCHLOSSEN": "Completed", + "ABGEBROCHEN": "Cancelled" } -} \ No newline at end of file +} diff --git a/scripts/check-module-guards.ts b/scripts/check-module-guards.ts index fa1f534..ce65de0 100644 --- a/scripts/check-module-guards.ts +++ b/scripts/check-module-guards.ts @@ -29,6 +29,8 @@ const ACTION_MODULE: Record = { "tasks.ts": "tasks", "suppliers.ts": "suppliers", "services.ts": "suppliers", + "software.ts": "suppliers", + "projects.ts": "assets", "policies.ts": "policies", "register.ts": "policies", // Plattform-Betrieb (eigene Auth) und Kunden-Einstellungen (tenant:manage) sind diff --git a/src/app/(app)/assets/page.tsx b/src/app/(app)/assets/page.tsx index 737e1a4..479b758 100644 --- a/src/app/(app)/assets/page.tsx +++ b/src/app/(app)/assets/page.tsx @@ -10,6 +10,7 @@ import { FilterTabs } from "@/components/filter-tabs"; import { CiaBadge, CiaLegend, + CriticalityPill, KpiCard, OwnerChip, PageHead, @@ -31,7 +32,18 @@ import { ServiceEditModal, type ServiceAssetDetail, } from "@/components/service-modals"; -import { SUPPLIER_INCLUDE, SERVICE_INCLUDE } from "@/lib/supplier-include"; +import { + SoftwareDetailModal, + SoftwareEditModal, + type SoftwareAssetDetail, +} from "@/components/software-modals"; +import { + ProjectCreateModal, + ProjectDetailModal, + ProjectEditModal, + type ProjectAssetDetail, +} from "@/components/project-modals"; +import { SUPPLIER_INCLUDE, SERVICE_INCLUDE, SOFTWARE_INCLUDE, PROJECT_INCLUDE } from "@/lib/supplier-include"; import { Table, TableBody, @@ -41,20 +53,22 @@ import { TableRow, } from "@/components/ui/table"; -const ASSET_TYPES = ["INFORMATION", "SYSTEM", "APPLICATION", "LOCATION", "SUPPLIER", "IT_SERVICE", "PERSON", "DATA"] as const; +const ASSET_TYPES = ["INFORMATION", "SYSTEM", "APPLICATION", "LOCATION", "SUPPLIER", "IT_SERVICE", "SOFTWARE", "PROJECT", "PERSON", "DATA"] as const; const STATUS_TONE = { ACTIVE: "ok", PLANNED: "info", RETIRED: "mut" } as const; export default async function AssetsPage({ searchParams, }: { - searchParams: Promise<{ q?: string; type?: string; detail?: string; edit?: string; new?: string }>; + searchParams: Promise<{ q?: string; type?: string; view?: string; detail?: string; edit?: string; new?: string }>; }) { const session = await requireSession(); requirePermission(session, "asset:read"); const t = await getTranslations("assets"); const tType = await getTranslations("assetType"); const tStatus = await getTranslations("assetStatus"); + const tProj = await getTranslations("projects"); + const tSw = await getTranslations("software"); const tc = await getTranslations("common"); const params = await searchParams; @@ -62,6 +76,90 @@ export default async function AssetsPage({ const activeType = ASSET_TYPES.includes(type as AssetType) ? (type as AssetType) : null; const db = dbForTenant(session.user.tenantId); + /* ── Auto-Sicht „Kritische IT-Dienste" (schreibgeschützt, aus Assetinventar/BIA) ── + Ziel des früheren Registers REG-CRIT-SERVICES: keine gepflegte Liste, sondern eine + abgeleitete Sicht. Kritisch = hohe Verfügbarkeit (≥3) ODER stützt einen BIA-kritischen + Prozess. RTO/RPO stammen aus den verknüpften Prozessen (BiaEntry, strengster Wert). */ + if (params.view === "critical") { + const tCrit = await getTranslations("criticality"); + const critical = await db.asset.findMany({ + where: { + type: { in: ["IT_SERVICE", "SYSTEM", "APPLICATION", "SOFTWARE"] }, + OR: [ + { availability: { gte: 3 } }, + { processAssets: { some: { process: { bia: { criticality: { gte: 3 } } } } } }, + ], + }, + include: { + processAssets: { include: { process: { select: { id: true, name: true, bia: { select: { criticality: true, rtoHours: true, rpoHours: true } } } } } }, + relationsFrom: { include: { relatedAsset: { select: { name: true } } } }, + }, + orderBy: [{ availability: "desc" }, { name: "asc" }], + take: 300, + }); + const min = (xs: (number | null | undefined)[]) => { + const v = xs.filter((x): x is number => typeof x === "number"); + return v.length ? Math.min(...v) : null; + }; + const fmtH = (h: number | null) => (h == null ? tc("none") : h >= 24 && h % 24 === 0 ? `${h / 24} d` : `${h} h`); + const rows = critical.map((a) => { + const bias = a.processAssets.map((pa) => pa.process.bia).filter(Boolean); + const biaCrit = Math.max(a.availability, ...bias.map((b) => b!.criticality)); + return { + a, + biaCrit, + rto: min(bias.map((b) => b!.rtoHours)), + rpo: min(bias.map((b) => b!.rpoHours)), + processes: a.processAssets.map((pa) => pa.process.name), + deps: a.relationsFrom.map((r) => r.relatedAsset.name), + }; + }); + + return ( +
+ }>{t("backToInventory")} + } /> +
+ + + + {t("criticalService")} + {t("type")} + {t("biaCriticality")} + RTO + RPO + {t("supportedProcesses")} + {t("dependencies")} + + + + {rows.length === 0 && ( + + {t("criticalEmpty")} + + )} + {rows.map(({ a, biaCrit, rto, rpo, processes, deps }) => ( + + + {a.name} + + {tType(a.type)} + + {fmtH(rto)} + {fmtH(rpo)} + {processes.join(", ") || tc("none")} + {deps.join(", ") || tc("none")} + + ))} + +
+
+

{t("criticalNote")}

+
+ ); + } + const [assets, allAssets] = await Promise.all([ db.asset.findMany({ where: { @@ -109,16 +207,22 @@ export default async function AssetsPage({ type: true, supplierProfile: { select: { id: true } }, serviceProfile: { select: { id: true } }, + softwareProfile: { select: { id: true } }, + projectProfile: { select: { id: true } }, }, }) : null; - const modalKind: "supplier" | "service" | "asset" | null = !modalMeta + const modalKind: "supplier" | "service" | "software" | "project" | "asset" | null = !modalMeta ? null : modalMeta.type === "SUPPLIER" && modalMeta.supplierProfile ? "supplier" : modalMeta.type === "IT_SERVICE" && modalMeta.serviceProfile ? "service" - : "asset"; + : modalMeta.type === "SOFTWARE" && modalMeta.softwareProfile + ? "software" + : modalMeta.type === "PROJECT" && modalMeta.projectProfile + ? "project" + : "asset"; const supplierModal = modalKind === "supplier" @@ -128,8 +232,16 @@ export default async function AssetsPage({ modalKind === "service" ? ((await db.asset.findUnique({ where: { id: modalId! }, include: SERVICE_INCLUDE })) as ServiceAssetDetail | null) : null; + const softwareModal = + modalKind === "software" + ? ((await db.asset.findUnique({ where: { id: modalId! }, include: SOFTWARE_INCLUDE })) as SoftwareAssetDetail | null) + : null; + const projectModal = + modalKind === "project" + ? ((await db.asset.findUnique({ where: { id: modalId! }, include: PROJECT_INCLUDE })) as ProjectAssetDetail | null) + : null; const providers = - modalKind === "service" && isEdit + (modalKind === "service" || modalKind === "software") && isEdit ? await db.asset.findMany({ where: { type: "SUPPLIER", supplierProfile: { isNot: null } }, select: { id: true, name: true }, @@ -202,9 +314,19 @@ export default async function AssetsPage({ {t("export")} {canWrite && ( - + activeType === "PROJECT" ? ( + + ) : activeType === "SOFTWARE" ? ( + + ) : ( + + ) )} } @@ -310,6 +432,16 @@ export default async function AssetsPage({ ) : serviceModal ? ( + ) : softwareModal && isEdit ? ( + + ) : softwareModal ? ( + + ) : projectModal && isEdit ? ( + + ) : projectModal ? ( + + ) : params.new && canWrite && activeType === "PROJECT" ? ( + ) : modalAsset && isEdit ? ( ) : modalAsset ? ( diff --git a/src/app/(app)/suppliers/page.tsx b/src/app/(app)/suppliers/page.tsx index d28ddac..7040772 100644 --- a/src/app/(app)/suppliers/page.tsx +++ b/src/app/(app)/suppliers/page.tsx @@ -19,9 +19,16 @@ import { ServiceEditModal, type ServiceAssetDetail, } from "@/components/service-modals"; +import { + SoftwareCreateModal, + SoftwareDetailModal, + SoftwareEditModal, + type SoftwareAssetDetail, +} from "@/components/software-modals"; import { supplierRef, serviceRef, + softwareRef, protectionLevel, PROTECTION_LABEL, buildReqContext, @@ -31,7 +38,7 @@ import { CONFORMITY_LABEL, TARGET_MATURITY, } from "@/lib/supplier"; -import { SUPPLIER_INCLUDE, SERVICE_INCLUDE } from "@/lib/supplier-include"; +import { SUPPLIER_INCLUDE, SERVICE_INCLUDE, SOFTWARE_INCLUDE } from "@/lib/supplier-include"; import { Table, TableBody, @@ -50,19 +57,25 @@ export default async function SuppliersPage({ requirePermission(session, "supplier:read"); const t = await getTranslations("suppliers"); const ts = await getTranslations("services"); + const tsw = await getTranslations("software"); + const tSwStatus = await getTranslations("softwareStatus"); const tCrit = await getTranslations("criticality"); const tc = await getTranslations("common"); const params = await searchParams; const isServices = params.tab === "services"; + const isSoftware = params.tab === "software"; const db = dbForTenant(session.user.tenantId); const canWrite = hasPermission(session, "supplier:write"); const tabs = [ - { href: "/suppliers", label: ts("tabSuppliers"), active: !isServices }, + { href: "/suppliers", label: ts("tabSuppliers"), active: !isServices && !isSoftware }, { href: "/suppliers?tab=services", label: ts("tabServices"), active: isServices }, + { href: "/suppliers?tab=software", label: ts("tabSoftware"), active: isSoftware }, ]; + const newHref = isServices ? "/suppliers?tab=services&new=1" : isSoftware ? "/suppliers?tab=software&new=1" : "/suppliers?new=1"; + const newLabel = isServices ? ts("newService") : isSoftware ? tsw("newSoftware") : t("newSupplier"); const head = ( }> - {isServices ? ts("newService") : t("newSupplier")} + ) } @@ -147,6 +160,77 @@ export default async function SuppliersPage({ ); } + /* ── Software-Whitelist ── */ + if (isSoftware) { + const software = (await db.asset.findMany({ + where: { type: "SOFTWARE", softwareProfile: { isNot: null } }, + include: SOFTWARE_INCLUDE, + orderBy: { softwareProfile: { refNo: "asc" } }, + take: 300, + })) as SoftwareAssetDetail[]; + + const providers = await db.asset.findMany({ + where: { type: "SUPPLIER", supplierProfile: { isNot: null } }, + select: { id: true, name: true }, + orderBy: { name: "asc" }, + }); + + const modalId = params.edit && canWrite ? params.edit : params.detail; + const modalSoftware = modalId + ? ((await db.asset.findUnique({ where: { id: modalId }, include: SOFTWARE_INCLUDE })) as SoftwareAssetDetail | null) + : null; + + const statusTone = { BEANTRAGT: "warn", FREIGEGEBEN: "ok", GESPERRT: "risk" } as const; + + return ( +
+ {head} +
+
+ + + + {tsw("ref")} + {tsw("name")} + {tsw("provider")} + {tsw("version")} + {tsw("approvalStatus")} + {tsw("criticality")} + + + + {software.length === 0 && ( + + {tsw("empty")} + + )} + {software.map((s) => ( + + {softwareRef(s.softwareProfile!.refNo)} + + {s.name} + + {s.softwareProfile?.provider?.name ?? tc("none")} + {s.softwareProfile?.version ?? tc("none")} + {tSwStatus(s.softwareProfile!.approvalStatus)} + + + ))} + +
+
+ + {modalSoftware && params.edit && canWrite ? ( + + ) : modalSoftware ? ( + + ) : params.new && canWrite ? ( + + ) : null} +
+ ); + } + /* ── Lieferanten ── */ const suppliers = await db.asset.findMany({ where: { type: "SUPPLIER", supplierProfile: { isNot: null } }, diff --git a/src/components/project-modals.tsx b/src/components/project-modals.tsx new file mode 100644 index 0000000..3c86d1e --- /dev/null +++ b/src/components/project-modals.tsx @@ -0,0 +1,277 @@ +import Link from "next/link"; +import { getTranslations } from "next-intl/server"; +import { Network, Pencil, Plus, Trash2 } from "lucide-react"; +import type { Prisma } from "@prisma/client"; +import { createProject, deleteProject, updateProject } from "@/server/actions/projects"; +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 { Modal } from "@/components/modal"; +import { SegmentedRating } from "@/components/segmented-rating"; +import { CiaBadge, CiaLegend, Pill, Tag } from "@/components/mockup-ui"; +import { projectRef, withQuery } from "@/lib/supplier"; +import { riskLevel, riskRef, RISK_PILL_TONE } from "@/lib/risk"; + +export type ProjectAssetDetail = Prisma.AssetGetPayload<{ + include: { + projectProfile: true; + owner: { 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 } } } }; + }; +}>; + +const inputCls = "h-9 w-full rounded-md border border-input bg-transparent px-3 text-sm"; +const STATUS_TONE = { GEPLANT: "info", LAUFEND: "warn", ABGESCHLOSSEN: "ok", ABGEBROCHEN: "risk" } as const; +const STATUS_VALUES = ["GEPLANT", "LAUFEND", "ABGESCHLOSSEN", "ABGEBROCHEN"] as const; + +/* ─────────────────────── Detail-Cockpit ─────────────────────── */ + +export async function ProjectDetailModal({ project, canWrite, backHref = "/assets?type=PROJECT" }: { project: ProjectAssetDetail; canWrite: boolean; backHref?: string }) { + const t = await getTranslations("projects"); + const ta = await getTranslations("assets"); + const tType = await getTranslations("assetType"); + const tStatus = await getTranslations("projectStatus"); + const tLevel = await getTranslations("riskLevel"); + const tRisks = await getTranslations("risks"); + const tDep = await getTranslations("dependencies"); + const tc = await getTranslations("common"); + const p = project.projectProfile!; + + return ( + + {tType(project.type)} + {tStatus(p.status)} + + } + closeHref={backHref} + closeLabel={t("close")} + footer={ + <> + {canWrite && ( + + )} + + + } + > +
+ {/* Stammdaten */} +
+
+ {t("masterPill")} +
+
+
+ {projectRef(p.refNo)} · {project.name} + +
+
+
{t("owner")}: {project.owner?.name ?? tc("none")}
+ {p.classification &&
{t("classification")}: {p.classification}
} +
{t("status")}: {tStatus(p.status)}
+
{t("isbInvolved")}: {p.isbInvolved ? tc("yes") : tc("no")}
+
+ {p.notes &&

{p.notes}

} +
+ +
+

{ta("processes")}

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

{tc("none")}

} +
    + {project.processAssets.map((pa) => ( +
  • + {pa.process.name} +
  • + ))} +
+
+
+ + {/* Verknüpfte Assets */} +
+
+ {ta("depPill")} +
+ {project.relationsFrom.length === 0 && project.relationsTo.length === 0 && ( +

{tc("none")}

+ )} + {project.relationsFrom.length > 0 && ( + + + + + + {project.relationsFrom.map((rel) => ( + + + + + + ))} + +
+ {rel.relatedAsset.name} + {tType(rel.relatedAsset.type)} + +
+ )} + + {tDep("openGraph")} → + +
+
+ + {/* Zugeordnete Risiken */} +
+
+ {t("linkedRisks")} + {canWrite && ( + + )} +
+ {project.riskAssets.length === 0 &&

{tc("none")}

} +
    + {project.riskAssets.map((ra) => ( +
  • + {riskRef(ra.risk.refNo)} + {ra.risk.title} + {ra.risk.score} · {tLevel(riskLevel(ra.risk.score))} +
  • + ))} +
+
+
+ ); +} + +/* ─────────────────────── Formular ─────────────────────── */ + +async function ProjectFields({ project, users, formId }: { project?: ProjectAssetDetail; users: { id: string; name: string }[]; formId?: string }) { + const t = await getTranslations("projects"); + const tA = await getTranslations("assets"); + const tLevel = await getTranslations("protectionLevel"); + const tStatus = await getTranslations("projectStatus"); + const f = formId ? { form: formId } : {}; + const p = project?.projectProfile; + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ {t("protection")} (1–4) +
+ {( + [ + ["confidentiality", tA("confidentiality")], + ["integrity", tA("integrity")], + ["availability", tA("availability")], + ] as const + ).map(([n, label]) => ( +
+ +
+ +
+
+ ))} +
+
+ +
+ +