- 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 <noreply@anthropic.com>
195 lines
6.3 KiB
TypeScript
195 lines
6.3 KiB
TypeScript
import Link from "next/link";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { Plus } from "lucide-react";
|
|
import { requireSession } from "@/server/auth";
|
|
import { dbForTenant } from "@/server/db";
|
|
import { hasPermission, requirePermission } from "@/server/rbac";
|
|
import { Button } from "@/components/ui/button";
|
|
import { CriticalityPill, PageHead, SectTitle, Tag } from "@/components/mockup-ui";
|
|
import {
|
|
ProcessCreateModal,
|
|
ProcessDetailModal,
|
|
ProcessEditModal,
|
|
} from "@/components/process-modals";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
|
|
const PROCESS_INCLUDE = {
|
|
owner: { select: { name: true } },
|
|
bia: true,
|
|
processAssets: {
|
|
include: {
|
|
asset: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
type: true,
|
|
confidentiality: true,
|
|
integrity: true,
|
|
availability: true,
|
|
owner: { select: { name: true } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} as const;
|
|
|
|
export default async function ProcessesPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<{ detail?: string; edit?: string; new?: string }>;
|
|
}) {
|
|
const session = await requireSession();
|
|
requirePermission(session, "bia:read");
|
|
const t = await getTranslations("processes");
|
|
const tAssets = await getTranslations("assets");
|
|
const tCrit = await getTranslations("criticality");
|
|
const tCat = await getTranslations("processCategory");
|
|
const tc = await getTranslations("common");
|
|
|
|
const params = await searchParams;
|
|
const db = dbForTenant(session.user.tenantId);
|
|
const canWrite = hasPermission(session, "bia:write");
|
|
|
|
const processes = await db.process.findMany({
|
|
include: {
|
|
owner: { select: { name: true } },
|
|
bia: { select: { criticality: true, rtoHours: true, rpoHours: true, mtdHours: true } },
|
|
},
|
|
orderBy: { name: "asc" },
|
|
take: 200,
|
|
});
|
|
|
|
// Popup-Zustand aus searchParams: ?detail= (read-only), ?edit=, ?new=1
|
|
const modalId = params.edit && canWrite ? params.edit : params.detail;
|
|
const modalProcess = modalId
|
|
? await db.process.findUnique({ where: { id: modalId }, include: PROCESS_INCLUDE })
|
|
: null;
|
|
|
|
const users =
|
|
canWrite && (params.edit || params.new)
|
|
? await db.user.findMany({
|
|
where: { status: "ACTIVE" },
|
|
select: { id: true, name: true },
|
|
orderBy: { name: "asc" },
|
|
})
|
|
: [];
|
|
// Zugeordnete Risiken: direkt am Prozess ODER an einem seiner Assets
|
|
const modalRisks = modalProcess
|
|
? await db.risk.findMany({
|
|
where: {
|
|
OR: [
|
|
{ processId: modalProcess.id },
|
|
{ riskAssets: { some: { assetId: { in: modalProcess.processAssets.map((pa) => pa.assetId) } } } },
|
|
],
|
|
},
|
|
select: { id: true, refNo: true, title: true, score: true },
|
|
orderBy: { score: "desc" },
|
|
})
|
|
: [];
|
|
|
|
const availableAssets =
|
|
canWrite && params.edit && modalProcess
|
|
? await db.asset.findMany({
|
|
where: { id: { notIn: modalProcess.processAssets.map((pa) => pa.assetId) } },
|
|
select: { id: true, name: true },
|
|
orderBy: { name: "asc" },
|
|
})
|
|
: [];
|
|
|
|
const hours = (v: number | null | undefined) => (v != null ? `${v} h` : tc("none"));
|
|
|
|
return (
|
|
<main className="flex-1 p-6">
|
|
<PageHead
|
|
crumb={tAssets("crumb")}
|
|
title={t("biaTitle")}
|
|
sub={t("biaSub")}
|
|
actions={
|
|
<>
|
|
<Button variant="outline" disabled title={tc("comingSoon")}>
|
|
{t("biaReport")}
|
|
</Button>
|
|
{canWrite && (
|
|
<Button nativeButton={false} render={<Link href="/processes?new=1" />}>
|
|
<Plus className="size-4" /> {t("newProcess")}
|
|
</Button>
|
|
)}
|
|
</>
|
|
}
|
|
/>
|
|
|
|
|
|
<div className="shadow-card mt-4 rounded-xl border bg-card">
|
|
<div className="p-4 pb-0">
|
|
<SectTitle title={t("critTable")} />
|
|
</div>
|
|
<Table className="mt-2">
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>{t("process")}</TableHead>
|
|
<TableHead>{t("category")}</TableHead>
|
|
<TableHead>{t("owner")}</TableHead>
|
|
<TableHead>RTO</TableHead>
|
|
<TableHead>RPO</TableHead>
|
|
<TableHead>MTD</TableHead>
|
|
<TableHead>{t("criticality")}</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{processes.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="py-8 text-center text-muted-foreground">
|
|
{t("empty")}
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
{processes.map((process) => (
|
|
<TableRow key={process.id}>
|
|
<TableCell>
|
|
<Link href={`/processes?detail=${process.id}`} className="font-bold hover:underline">
|
|
{process.name}
|
|
</Link>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Tag>{tCat(process.category)}</Tag>
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground">
|
|
{process.owner?.name ?? tc("none")}
|
|
</TableCell>
|
|
<TableCell>{hours(process.bia?.rtoHours)}</TableCell>
|
|
<TableCell>{hours(process.bia?.rpoHours)}</TableCell>
|
|
<TableCell>{hours(process.bia?.mtdHours)}</TableCell>
|
|
<TableCell>
|
|
{process.bia ? (
|
|
<CriticalityPill
|
|
level={process.bia.criticality}
|
|
label={tCrit(String(process.bia.criticality))}
|
|
/>
|
|
) : (
|
|
<span className="text-muted-foreground">{t("noBia")}</span>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{modalProcess && params.edit && canWrite ? (
|
|
<ProcessEditModal process={modalProcess} users={users} availableAssets={availableAssets} />
|
|
) : modalProcess ? (
|
|
<ProcessDetailModal process={modalProcess} risks={modalRisks} canWrite={canWrite} />
|
|
) : params.new && canWrite ? (
|
|
<ProcessCreateModal users={users} />
|
|
) : null}
|
|
</main>
|
|
);
|
|
}
|