From df634e356d98ed769a0447ac36cb1de900efded9 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 2 Jul 2026 15:43:25 +0200 Subject: [PATCH] =?UTF-8?q?Iteration=203:=20Risikoanalyse=20+=20Men=C3=BC-?= =?UTF-8?q?Trennung=20+=20Prozesskategorie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Risiko-Modul (SPEC §4.2): Risk/RiskAsset-Schema mit RLS, laufende Nummer je Mandant (R-001), Brutto-/Rest-Bewertung (5×5), Behandlung, Status, Owner- und Prozess-Bezug, n:m betroffene Assets - /risks im Mockup-Layout: 5×5-Heatmap (Farb-Bänder + R-Chips, Legende, Achsen) und Risikoregister mit Score-Pillen, Behandlungs-Tags, Status - Risiko-Popups nach App-Muster: Read-only-Detail (Bewertungs-Band Brutto vs. Rest, betroffene Assets, Maßnahmen-Platzhalter für Iteration 4), Bearbeiten im Popup (Bewertung, Assets verknüpfen, Löschen), Anlegen; Server-Actions mit Zod, RBAC und Audit-Log - Menü aufgetrennt: eigene Punkte "Asset-Inventar", "Business Impact Analyse" und "Risikoanalyse" (Modul-Tabs entfernt) - Prozesskategorie (Kernprozess/Managementprozess/Unterstützender Prozess): Schema-Feld, Formulare, Listen-Spalte, Popup-Tag - Asset-Detail-Popup optisch ans Prozess-Popup angeglichen (Stammdaten- Karte mit violettem Rand, Abhängigkeiten-Tabelle, Risiken-Band) - "Zugeordnete Risiken" jetzt echt: im Asset-Popup (verknüpfte Risiken) und im Prozess-Popup (direkt + über Assets aggregiert); Dashboard-KPI "Offene Risiken" mit echten Zahlen; Seed mit 4 Beispiel-Risiken Co-Authored-By: Claude Fable 5 --- messages/de.json | 86 ++- messages/en.json | 86 ++- .../migration.sql | 74 +++ .../20260702133003_rls_risks/migration.sql | 9 + prisma/schema.prisma | 86 ++- prisma/seed.ts | 108 ++++ src/app/(app)/assets/page.tsx | 27 +- src/app/(app)/dashboard/page.tsx | 6 +- src/app/(app)/layout.tsx | 8 +- src/app/(app)/processes/page.tsx | 33 +- src/app/(app)/risks/page.tsx | 252 +++++++++ src/components/asset-modals.tsx | 131 +++-- src/components/module-tabs.tsx | 30 -- src/components/process-form.tsx | 16 + src/components/process-modals.tsx | 65 ++- src/components/risk-modals.tsx | 494 ++++++++++++++++++ src/lib/risk.ts | 33 ++ src/server/actions/processes.ts | 2 + src/server/actions/risks.ts | 191 +++++++ src/server/db.ts | 2 + 20 files changed, 1633 insertions(+), 106 deletions(-) create mode 100644 prisma/migrations/20260702132947_risks_and_process_category/migration.sql create mode 100644 prisma/migrations/20260702133003_rls_risks/migration.sql create mode 100644 src/app/(app)/risks/page.tsx delete mode 100644 src/components/module-tabs.tsx create mode 100644 src/components/risk-modals.tsx create mode 100644 src/lib/risk.ts create mode 100644 src/server/actions/risks.ts diff --git a/messages/de.json b/messages/de.json index 7a4b727..e4607e4 100644 --- a/messages/de.json +++ b/messages/de.json @@ -28,7 +28,9 @@ "dependencies": "Abhängigkeiten", "evidence": "Nachweise", "suppliers": "Lieferanten", - "review": "Management-Review" + "review": "Management-Review", + "assets": "Asset-Inventar", + "bia": "Business Impact Analyse" }, "login": { "title": "Anmelden", @@ -104,7 +106,13 @@ "all": "Alle", "filter": "Filtern …", "noOwner": "kein Owner", - "dependencies": "Abhängigkeiten" + "dependencies": "Abhängigkeiten", + "detailSub": "Stammdaten, Abhängigkeiten & Risiken", + "masterPill": "■ Stammdaten", + "masterNote": "Klassifizierung & Verantwortung", + "depPill": "◆ Abhängigkeiten", + "depNote": "Verknüpfte Assets", + "riskCount": "{count} Risiken" }, "assetType": { "INFORMATION": "Information", @@ -166,7 +174,8 @@ "showDependencies": "Abhängigkeiten anzeigen →", "impactShort": "Schadenshöhe (C/I/A)", "close": "Schließen", - "noAssets": "Noch keine Assets zugeordnet." + "noAssets": "Noch keine Assets zugeordnet.", + "category": "Prozesskategorie" }, "processRole": { "PRIMARY": "Primär", @@ -183,5 +192,76 @@ "2": "Erhöht", "3": "Hoch", "4": "Sehr hoch" + }, + "processCategory": { + "CORE": "Kernprozess", + "MANAGEMENT": "Managementprozess", + "SUPPORT": "Unterstützender Prozess" + }, + "risks": { + "title": "Risikoanalyse", + "sub": "5×5-Matrix · Eintrittswahrscheinlichkeit × Auswirkung", + "export": "Risikoregister exportieren", + "newRisk": "Risiko", + "heatmap": "Risiko-Heatmap", + "heatmapSub": "Auswirkung (Y) × Eintrittswahrscheinlichkeit (X)", + "register": "Risikoregister", + "registerSub": "Risiken nach Bewertung", + "id": "ID", + "risk": "Risiko", + "rating": "Bewertung", + "treatment": "Behandlung", + "status": "Status", + "owner": "Owner", + "empty": "Noch keine Risiken erfasst.", + "detailHeading": "Risiko-Detail · {ref}", + "detailSub": "Bewertung, betroffene Assets & Behandlung", + "editTitle": "Risiko bearbeiten", + "createTitle": "Risiko anlegen", + "titleField": "Titel", + "description": "Beschreibung", + "threat": "Bedrohung", + "vulnerability": "Schwachstelle", + "likelihood": "Eintrittswahrscheinlichkeit (1–5)", + "impact": "Auswirkung (1–5)", + "gross": "Brutto-Risiko", + "residual": "Rest-Risiko", + "residualHint": "Nach Umsetzung der Maßnahmen", + "noResidual": "Noch nicht bewertet", + "process": "Geschäftsprozess", + "affectedAssets": "Betroffene Assets", + "affectedNote": "Assets, auf die dieses Risiko wirkt", + "addAsset": "Asset verknüpfen", + "noAssets": "Keine Assets verknüpft.", + "measures": "Notwendige Maßnahmen", + "measuresPlaceholder": "Maßnahmen werden mit dem Maßnahmen-Modul (Iteration 4) verknüpft und erscheinen dann hier.", + "close": "Schließen", + "scoreLabel": "{score} · {level}", + "legendLow": "Gering", + "legendMedium": "Mittel", + "legendElevated": "Erhöht", + "legendHigh": "Hoch", + "legendCritical": "Kritisch", + "axisX": "Eintrittswahrscheinlichkeit →", + "axisY": "Auswirkung ↑" + }, + "riskTreatment": { + "AVOID": "Vermeiden", + "MITIGATE": "Vermindern", + "TRANSFER": "Übertragen", + "ACCEPT": "Akzeptieren" + }, + "riskStatus": { + "OPEN": "Offen", + "IN_TREATMENT": "In Behandlung", + "ACCEPTED": "Akzeptiert", + "CLOSED": "Geschlossen" + }, + "riskLevel": { + "low": "Gering", + "medium": "Mittel", + "elevated": "Erhöht", + "high": "Hoch", + "critical": "Kritisch" } } \ No newline at end of file diff --git a/messages/en.json b/messages/en.json index 14cf612..fb20931 100644 --- a/messages/en.json +++ b/messages/en.json @@ -28,7 +28,9 @@ "dependencies": "Dependencies", "evidence": "Evidence", "suppliers": "Suppliers", - "review": "Management review" + "review": "Management review", + "assets": "Asset inventory", + "bia": "Business impact analysis" }, "login": { "title": "Sign in", @@ -104,7 +106,13 @@ "all": "All", "filter": "Filter …", "noOwner": "no owner", - "dependencies": "Dependencies" + "dependencies": "Dependencies", + "detailSub": "Master data, dependencies & risks", + "masterPill": "■ Master data", + "masterNote": "Classification & responsibility", + "depPill": "◆ Dependencies", + "depNote": "Linked assets", + "riskCount": "{count} risks" }, "assetType": { "INFORMATION": "Information", @@ -166,7 +174,8 @@ "showDependencies": "Show dependencies →", "impactShort": "Damage level (C/I/A)", "close": "Close", - "noAssets": "No assets assigned yet." + "noAssets": "No assets assigned yet.", + "category": "Process category" }, "processRole": { "PRIMARY": "Primary", @@ -183,5 +192,76 @@ "2": "Elevated", "3": "High", "4": "Very high" + }, + "processCategory": { + "CORE": "Core process", + "MANAGEMENT": "Management process", + "SUPPORT": "Supporting process" + }, + "risks": { + "title": "Risk analysis", + "sub": "5×5 matrix · likelihood × impact", + "export": "Export risk register", + "newRisk": "Risk", + "heatmap": "Risk heatmap", + "heatmapSub": "Impact (Y) × likelihood (X)", + "register": "Risk register", + "registerSub": "Risks by rating", + "id": "ID", + "risk": "Risk", + "rating": "Rating", + "treatment": "Treatment", + "status": "Status", + "owner": "Owner", + "empty": "No risks recorded yet.", + "detailHeading": "Risk detail · {ref}", + "detailSub": "Rating, affected assets & treatment", + "editTitle": "Edit risk", + "createTitle": "Create risk", + "titleField": "Title", + "description": "Description", + "threat": "Threat", + "vulnerability": "Vulnerability", + "likelihood": "Likelihood (1–5)", + "impact": "Impact (1–5)", + "gross": "Gross risk", + "residual": "Residual risk", + "residualHint": "After implementing measures", + "noResidual": "Not yet rated", + "process": "Business process", + "affectedAssets": "Affected assets", + "affectedNote": "Assets this risk applies to", + "addAsset": "Link asset", + "noAssets": "No assets linked.", + "measures": "Required measures", + "measuresPlaceholder": "Measures will be linked with the measures module (iteration 4) and appear here.", + "close": "Close", + "scoreLabel": "{score} · {level}", + "legendLow": "Low", + "legendMedium": "Medium", + "legendElevated": "Elevated", + "legendHigh": "High", + "legendCritical": "Critical", + "axisX": "Likelihood →", + "axisY": "Impact ↑" + }, + "riskTreatment": { + "AVOID": "Avoid", + "MITIGATE": "Mitigate", + "TRANSFER": "Transfer", + "ACCEPT": "Accept" + }, + "riskStatus": { + "OPEN": "Open", + "IN_TREATMENT": "In treatment", + "ACCEPTED": "Accepted", + "CLOSED": "Closed" + }, + "riskLevel": { + "low": "Low", + "medium": "Medium", + "elevated": "Elevated", + "high": "High", + "critical": "Critical" } } \ No newline at end of file diff --git a/prisma/migrations/20260702132947_risks_and_process_category/migration.sql b/prisma/migrations/20260702132947_risks_and_process_category/migration.sql new file mode 100644 index 0000000..9ed3855 --- /dev/null +++ b/prisma/migrations/20260702132947_risks_and_process_category/migration.sql @@ -0,0 +1,74 @@ +-- CreateEnum +CREATE TYPE "ProcessCategory" AS ENUM ('CORE', 'MANAGEMENT', 'SUPPORT'); + +-- CreateEnum +CREATE TYPE "RiskTreatment" AS ENUM ('AVOID', 'MITIGATE', 'TRANSFER', 'ACCEPT'); + +-- CreateEnum +CREATE TYPE "RiskStatus" AS ENUM ('OPEN', 'IN_TREATMENT', 'ACCEPTED', 'CLOSED'); + +-- AlterTable +ALTER TABLE "processes" ADD COLUMN "category" "ProcessCategory" NOT NULL DEFAULT 'CORE'; + +-- CreateTable +CREATE TABLE "risks" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "ref_no" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "threat" TEXT, + "vulnerability" TEXT, + "likelihood" INTEGER NOT NULL DEFAULT 1, + "impact" INTEGER NOT NULL DEFAULT 1, + "score" INTEGER NOT NULL DEFAULT 1, + "residual_likelihood" INTEGER, + "residual_impact" INTEGER, + "residual_score" INTEGER, + "treatment" "RiskTreatment" NOT NULL DEFAULT 'MITIGATE', + "status" "RiskStatus" NOT NULL DEFAULT 'OPEN', + "owner_id" TEXT, + "process_id" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "created_by" TEXT, + + CONSTRAINT "risks_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "risk_assets" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "risk_id" TEXT NOT NULL, + "asset_id" TEXT NOT NULL, + + CONSTRAINT "risk_assets_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "risks_tenant_id_idx" ON "risks"("tenant_id"); + +-- CreateIndex +CREATE INDEX "risks_tenant_id_status_idx" ON "risks"("tenant_id", "status"); + +-- CreateIndex +CREATE UNIQUE INDEX "risks_tenant_id_ref_no_key" ON "risks"("tenant_id", "ref_no"); + +-- CreateIndex +CREATE INDEX "risk_assets_tenant_id_idx" ON "risk_assets"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "risk_assets_risk_id_asset_id_key" ON "risk_assets"("risk_id", "asset_id"); + +-- AddForeignKey +ALTER TABLE "risks" ADD CONSTRAINT "risks_owner_id_fkey" FOREIGN KEY ("owner_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "risks" ADD CONSTRAINT "risks_process_id_fkey" FOREIGN KEY ("process_id") REFERENCES "processes"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "risk_assets" ADD CONSTRAINT "risk_assets_risk_id_fkey" FOREIGN KEY ("risk_id") REFERENCES "risks"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "risk_assets" ADD CONSTRAINT "risk_assets_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260702133003_rls_risks/migration.sql b/prisma/migrations/20260702133003_rls_risks/migration.sql new file mode 100644 index 0000000..f542644 --- /dev/null +++ b/prisma/migrations/20260702133003_rls_risks/migration.sql @@ -0,0 +1,9 @@ +-- RLS-Policies für die Risiko-Tabellen (analog zu den vorherigen RLS-Migrationen) + +ALTER TABLE "risks" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "risk_assets" ENABLE ROW LEVEL SECURITY; + +CREATE POLICY tenant_isolation ON "risks" + USING ("tenant_id" = current_setting('app.tenant_id', true)); +CREATE POLICY tenant_isolation ON "risk_assets" + USING ("tenant_id" = current_setting('app.tenant_id', true)); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6a318a0..76d88ca 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -60,6 +60,7 @@ model User { userRoles UserRole[] ownedAssets Asset[] @relation("assetOwner") ownedProcesses Process[] @relation("processOwner") + ownedRisks Risk[] @relation("riskOwner") @@unique([tenantId, email]) @@index([tenantId]) @@ -135,6 +136,12 @@ enum ProcessAssetRole { SECONDARY } +enum ProcessCategory { + CORE // Kernprozess + MANAGEMENT // Managementprozess + SUPPORT // Unterstützender Prozess +} + model Asset { id String @id @default(cuid()) tenantId String @map("tenant_id") @@ -157,6 +164,7 @@ model Asset { relationsFrom AssetRelation[] @relation("relationFrom") relationsTo AssetRelation[] @relation("relationTo") processAssets ProcessAsset[] + riskAssets RiskAsset[] @@index([tenantId]) @@index([tenantId, type]) @@ -180,11 +188,12 @@ model AssetRelation { } model Process { - id String @id @default(cuid()) - tenantId String @map("tenant_id") + id String @id @default(cuid()) + tenantId String @map("tenant_id") name String description String? - ownerId String? @map("owner_id") + category ProcessCategory @default(CORE) + ownerId String? @map("owner_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") createdBy String? @map("created_by") @@ -192,6 +201,7 @@ model Process { owner User? @relation("processOwner", fields: [ownerId], references: [id]) processAssets ProcessAsset[] bia BiaEntry? + risks Risk[] @@index([tenantId]) @@map("processes") @@ -236,6 +246,76 @@ model BiaEntry { @@map("bia_entries") } +// ── Risikoanalyse (SPEC §4.2) ─────────────────────────────────────────────── + +enum RiskTreatment { + AVOID // Vermeiden + MITIGATE // Vermindern + TRANSFER // Übertragen + ACCEPT // Akzeptieren +} + +enum RiskStatus { + OPEN + IN_TREATMENT + ACCEPTED + CLOSED +} + +model Risk { + id String @id @default(cuid()) + tenantId String @map("tenant_id") + // Laufende Nummer je Mandant → Anzeige als "R-001" + refNo Int @map("ref_no") + + title String + description String? + threat String? // Bedrohung (Katalog folgt, SPEC §5) + vulnerability String? // Schwachstelle + + // Brutto-Bewertung: Eintrittswahrscheinlichkeit × Auswirkung (je 1–5) + likelihood Int @default(1) + impact Int @default(1) + score Int @default(1) // redundant für Listen/Heatmap + + // Rest-Risiko nach Maßnahmen + residualLikelihood Int? @map("residual_likelihood") + residualImpact Int? @map("residual_impact") + residualScore Int? @map("residual_score") + + treatment RiskTreatment @default(MITIGATE) + status RiskStatus @default(OPEN) + ownerId String? @map("owner_id") + processId String? @map("process_id") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + createdBy String? @map("created_by") + + owner User? @relation("riskOwner", fields: [ownerId], references: [id]) + process Process? @relation(fields: [processId], references: [id]) + riskAssets RiskAsset[] + + @@unique([tenantId, refNo]) + @@index([tenantId]) + @@index([tenantId, status]) + @@map("risks") +} + +model RiskAsset { + id String @id @default(cuid()) + tenantId String @map("tenant_id") + riskId String @map("risk_id") + assetId String @map("asset_id") + + risk Risk @relation(fields: [riskId], references: [id], onDelete: Cascade) + asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade) + + @@unique([riskId, assetId]) + @@index([tenantId]) + @@map("risk_assets") +} + model AuditLog { id String @id @default(cuid()) tenantId String @map("tenant_id") diff --git a/prisma/seed.ts b/prisma/seed.ts index fa2e681..e48ad77 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -138,6 +138,7 @@ async function main() { tenantId: tenant.id, name: "Auftragsabwicklung", description: "Vom Kundenauftrag bis zur Auslieferung.", + category: "CORE", ownerId: owner.id, }, }); @@ -169,6 +170,7 @@ async function main() { tenantId: tenant.id, name: "Kundenbetreuung", description: "Anfragen, Angebote, Support.", + category: "SUPPORT", ownerId: isb.id, }, }); @@ -195,6 +197,112 @@ async function main() { console.log("✔ Beispiel-Assets, -Prozesse und BIA angelegt"); } + + // 6. Beispiel-Risiken (nur wenn noch keine existieren) + const riskCount = await prisma.risk.count({ where: { tenantId: tenant.id } }); + if (riskCount === 0) { + const isb = await prisma.user.findUniqueOrThrow({ + where: { tenantId_email: { tenantId: tenant.id, email: "admin@demo.example" } }, + }); + const byName = async (name: string) => + prisma.asset.findFirstOrThrow({ where: { tenantId: tenant.id, name } }); + const erp = await byName("ERP-System"); + const crm = await byName("Kundendatenbank"); + const hoster = await byName("Cloud-Hoster (IaaS)"); + const auftrag = await prisma.process.findFirstOrThrow({ + where: { tenantId: tenant.id, name: "Auftragsabwicklung" }, + }); + + const risks: { + title: string; + threat: string; + vulnerability: string; + likelihood: number; + impact: number; + residual?: [number, number]; + treatment: "AVOID" | "MITIGATE" | "TRANSFER" | "ACCEPT"; + status: "OPEN" | "IN_TREATMENT" | "ACCEPTED" | "CLOSED"; + assets: string[]; + processId?: string; + description?: string; + }[] = [ + { + title: "Ransomware auf ERP", + threat: "Schadsoftware / Verschlüsselungstrojaner", + vulnerability: "Fehlende Offline-Backups, Makro-Ausführung erlaubt", + likelihood: 4, + impact: 5, + residual: [2, 4], + treatment: "MITIGATE", + status: "IN_TREATMENT", + assets: [erp.id, crm.id], + processId: auftrag.id, + description: "Verschlüsselung der ERP-Datenbank würde die Auftragsabwicklung stoppen.", + }, + { + title: "Ausfall Cloud-Hoster", + threat: "Ausfall des Rechenzentrums / Insolvenz Dienstleister", + vulnerability: "Kein Ausweich-Standort, Single Provider", + likelihood: 4, + impact: 4, + treatment: "TRANSFER", + status: "OPEN", + assets: [hoster.id, erp.id], + processId: auftrag.id, + }, + { + title: "Phishing / Social Engineering", + threat: "Gezielte Phishing-Kampagnen", + vulnerability: "Fehlende Awareness-Schulungen", + likelihood: 3, + impact: 3, + residual: [2, 3], + treatment: "MITIGATE", + status: "IN_TREATMENT", + assets: [crm.id], + }, + { + title: "Verlust mobiler Geräte", + threat: "Diebstahl/Verlust von Notebooks", + vulnerability: "Unvollständige Festplattenverschlüsselung", + likelihood: 2, + impact: 3, + treatment: "ACCEPT", + status: "ACCEPTED", + assets: [], + }, + ]; + + let refNo = 1; + for (const r of risks) { + const risk = await prisma.risk.create({ + data: { + tenantId: tenant.id, + refNo: refNo++, + title: r.title, + description: r.description, + threat: r.threat, + vulnerability: r.vulnerability, + likelihood: r.likelihood, + impact: r.impact, + score: r.likelihood * r.impact, + residualLikelihood: r.residual?.[0], + residualImpact: r.residual?.[1], + residualScore: r.residual ? r.residual[0] * r.residual[1] : undefined, + treatment: r.treatment, + status: r.status, + ownerId: isb.id, + processId: r.processId, + }, + }); + for (const assetId of r.assets) { + await prisma.riskAsset.create({ + data: { tenantId: tenant.id, riskId: risk.id, assetId }, + }); + } + } + console.log(`✔ ${risks.length} Beispiel-Risiken angelegt`); + } } main() diff --git a/src/app/(app)/assets/page.tsx b/src/app/(app)/assets/page.tsx index 507f54c..1029fa4 100644 --- a/src/app/(app)/assets/page.tsx +++ b/src/app/(app)/assets/page.tsx @@ -6,7 +6,6 @@ import { requireSession } from "@/server/auth"; import { dbForTenant } from "@/server/db"; import { hasPermission, requirePermission } from "@/server/rbac"; import { Button } from "@/components/ui/button"; -import { ModuleTabs } from "@/components/module-tabs"; import { FilterTabs } from "@/components/filter-tabs"; import { CiaBadge, @@ -89,9 +88,26 @@ export default async function AssetsPage({ where: { id: modalId }, include: { owner: { select: { name: true } }, - relationsFrom: { include: { relatedAsset: { select: { id: true, name: 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 } } } }, + riskAssets: { + include: { risk: { select: { id: true, refNo: true, title: true, score: true } } }, + orderBy: { risk: { score: "desc" } }, + }, }, }) : null; @@ -138,13 +154,6 @@ export default async function AssetsPage({ } /> -
diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index 5277f25..cd2c12c 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -6,14 +6,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com export default async function DashboardPage() { const session = await requireSession(); const t = await getTranslations("dashboard"); - const tc = await getTranslations("common"); const format = await getFormatter(); const db = dbForTenant(session.user.tenantId); - const [assetCount, processCount, criticalCount, activities] = await Promise.all([ + const [assetCount, processCount, criticalCount, openRisks, activities] = await Promise.all([ db.asset.count(), db.process.count(), db.biaEntry.count({ where: { criticality: { gte: 3 } } }), + db.risk.count({ where: { status: { in: ["OPEN", "IN_TREATMENT"] } } }), db.auditLog.findMany({ orderBy: { createdAt: "desc" }, take: 6, @@ -34,7 +34,7 @@ export default async function DashboardPage() { { label: t("kpiAssets"), value: String(assetCount), hint: null }, { label: t("kpiProcesses"), value: String(processCount), hint: null }, { label: t("kpiCritical"), value: String(criticalCount), hint: t("kpiCriticalHint") }, - { label: t("kpiRisks"), value: tc("none"), hint: t("kpiRisksHint") }, + { label: t("kpiRisks"), value: String(openRisks), hint: null }, ]; return ( diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index 7935ffa..9d808f4 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -5,6 +5,7 @@ import { getTranslations } from "next-intl/server"; import { LayoutDashboard, Boxes, + GitBranch, ShieldAlert, ClipboardCheck, KanbanSquare, @@ -32,8 +33,9 @@ export default async function AppLayout({ const nav = [ { href: "/dashboard", label: t("dashboard"), icon: LayoutDashboard, enabled: true }, - { href: "/assets", label: t("assetsBia"), icon: Boxes, enabled: true, match: ["/assets", "/processes"] }, - { href: "/risks", label: t("risks"), icon: ShieldAlert, enabled: false }, + { href: "/assets", label: t("assets"), icon: Boxes, enabled: true }, + { href: "/processes", label: t("bia"), icon: GitBranch, enabled: true }, + { href: "/risks", label: t("risks"), icon: ShieldAlert, enabled: true }, { href: "/soa", label: t("soa"), icon: ClipboardCheck, enabled: false }, { href: "/measures", label: t("measures"), icon: KanbanSquare, enabled: false }, { href: "/incidents", label: t("incidents"), icon: Siren, enabled: false }, @@ -74,7 +76,7 @@ export default async function AppLayout({
+ + ); +} + +/** Formularfelder (Anlegen + Bearbeiten teilen sich das Markup). */ +async function RiskFields({ + risk, + users, + processes, + selectClass, +}: { + risk?: RiskWithDetail; + users: { id: string; name: string }[]; + processes: { id: string; name: string }[]; + selectClass: string; +}) { + const t = await getTranslations("risks"); + const tTreat = await getTranslations("riskTreatment"); + const tStatus = await getTranslations("riskStatus"); + const tc = await getTranslations("common"); + + return ( + <> +
+ + +
+
+ +