import Link from "next/link"; import { getTranslations } from "next-intl/server"; import { Plus } from "lucide-react"; import type { Prisma } from "@prisma/client"; import { requireSession } from "@/server/auth"; import { dbForTenant } from "@/server/db"; import { hasPermission, requirePermission } from "@/server/rbac"; import { Button } from "@/components/ui/button"; import { PageHead, Pill, SectTitle, Tag } from "@/components/mockup-ui"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { INCIDENT_INCLUDE, IncidentCreateModal, IncidentDetailModal, IncidentEditModal, } from "@/components/incident-modals"; import { INCIDENT_CATEGORIES, INCIDENT_STATUSES, SEVERITY_TONE, STATUS_TONE, canSeeRestricted, type IncidentStatus, } from "@/lib/incident"; import { INCIDENT_SEVERITIES, type IncidentSeverity } from "@/lib/incident-severity"; export default async function IncidentsPage({ searchParams, }: { searchParams: Promise<{ detail?: string; edit?: string; new?: string; status?: string; severity?: string; category?: string; }>; }) { const session = await requireSession(); requirePermission(session, "incident:read"); const t = await getTranslations("incidents"); const tCat = await getTranslations("incidentCategory"); const tStatus = await getTranslations("incidentStatus"); const tSev = await getTranslations("incidentSeverity"); const params = await searchParams; const db = dbForTenant(session.user.tenantId); const canManage = hasPermission(session, "incident:manage"); const canClose = hasPermission(session, "incident:close"); const canReport = hasPermission(session, "incident:report"); // §11 Vertraulichkeit: wer nicht manage/close hat, sieht nur unrestricted + // seine eigenen (owner) Vorfälle — serverseitiger Filter. const restrictedWhere: Prisma.IncidentWhereInput = canSeeRestricted(session) ? {} : { OR: [{ restricted: false }, { ownerId: session.user.id }] }; const filterWhere: Prisma.IncidentWhereInput = { ...(params.status && INCIDENT_STATUSES.includes(params.status as IncidentStatus) ? { status: params.status as IncidentStatus } : {}), ...(params.severity && INCIDENT_SEVERITIES.includes(params.severity as IncidentSeverity) ? { severity: params.severity } : {}), ...(params.category && INCIDENT_CATEGORIES.includes(params.category as (typeof INCIDENT_CATEGORIES)[number]) ? { category: params.category as (typeof INCIDENT_CATEGORIES)[number] } : {}), }; const incidents = await db.incident.findMany({ where: { AND: [restrictedWhere, filterWhere] }, include: { owner: { select: { name: true } } }, orderBy: { createdAt: "desc" }, take: 200, }); // Detail/Bearbeiten-Popup const modalId = params.edit && canManage ? params.edit : params.detail; const modalIncident = modalId ? await db.incident.findFirst({ where: { AND: [{ id: modalId }, restrictedWhere] }, include: INCIDENT_INCLUDE, }) : null; // Für die Detail-Timeline: Audit-Einträge dieses Vorfalls + Akteursnamen. let auditRows: { id: string; createdAt: Date; actorId: string | null; action: string; after: unknown; }[] = []; let actorNames: Record = {}; if (modalIncident && params.detail) { const rows = await db.auditLog.findMany({ where: { entity: "incident", entityId: modalIncident.id }, orderBy: { createdAt: "asc" }, take: 200, }); auditRows = rows.map((r) => ({ id: r.id, createdAt: r.createdAt, actorId: r.actorId, action: r.action, after: r.after, })); const actorIds = new Set(); rows.forEach((r) => r.actorId && actorIds.add(r.actorId)); modalIncident.comments.forEach((c) => c.authorId && actorIds.add(c.authorId)); const actors = await db.user.findMany({ where: { id: { in: [...actorIds] } }, select: { id: true, name: true }, }); actorNames = Object.fromEntries(actors.map((a) => [a.id, a.name])); } // Für das Bearbeiten-Popup: Auswahl-Listen (nur noch nicht verknüpfte). const needsEdit = canManage && params.edit && modalIncident; const [availableAssets, availableProcesses, availableRisks, availableMeasures, availableEvidence] = needsEdit ? await Promise.all([ db.asset.findMany({ where: { id: { notIn: modalIncident.incidentAssets.map((x) => x.assetId) } }, select: { id: true, name: true }, orderBy: { name: "asc" }, }), db.process.findMany({ where: { id: { notIn: modalIncident.incidentProcesses.map((x) => x.processId) } }, select: { id: true, name: true }, orderBy: { name: "asc" }, }), db.risk.findMany({ where: { id: { notIn: modalIncident.incidentRisks.map((x) => x.riskId) } }, select: { id: true, refNo: true, title: true }, orderBy: { refNo: "asc" }, }), db.measure.findMany({ where: { id: { notIn: modalIncident.incidentMeasures.map((x) => x.measureId) } }, select: { id: true, refNo: true, title: true }, orderBy: { refNo: "asc" }, }), db.evidence.findMany({ where: { id: { notIn: modalIncident.incidentEvidence.map((x) => x.evidenceId) } }, select: { id: true, title: true }, orderBy: { createdAt: "desc" }, take: 200, }), ]) : [[], [], [], [], []]; // Aktive Nutzer für Owner-/Bearbeiter-/Maßnahmen-Auswahl (Detail + Bearbeiten). const users = modalIncident && canManage ? await db.user.findMany({ where: { status: "ACTIVE" }, select: { id: true, name: true }, orderBy: { name: "asc" } }) : []; // NIS2-Betroffenheit des Mandanten steuert die Anzeige/Timer (§6) im Detail. const nis2Category = modalIncident && params.detail ? (await db.tenantSettings.findUnique({ where: { tenantId: session.user.tenantId }, select: { nis2Category: true } }))?.nis2Category ?? "keine" : "keine"; const filterLink = (patch: Record) => { const sp = new URLSearchParams(); const merged = { status: params.status, severity: params.severity, category: params.category, ...patch }; for (const [k, v] of Object.entries(merged)) if (v) sp.set(k, v); const qs = sp.toString(); return `/incidents${qs ? `?${qs}` : ""}`; }; return (
{t("exportRegisterCsv")} {t("exportRegisterXlsx")} {canReport ? ( ) : null} } />
{/* Filterleiste */}
{t("filter")}: {t("filterAll")} {INCIDENT_STATUSES.map((s) => ( {tStatus(s)} ))} {INCIDENT_SEVERITIES.map((s) => ( {tSev(s)} ))}
{/* Register */}
{t("id")} {t("incident")} {t("category")} {t("severity")} {t("status")} {t("owner")} {incidents.length === 0 && ( {t("empty")} )} {incidents.map((inc) => ( {inc.refNo} {inc.restricted ? "🔒 " : ""}{inc.title} {tCat(inc.category)} {tSev(inc.severity)} {tStatus(inc.status)} {inc.owner?.name ?? "—"} ))}
{modalIncident && params.edit && canManage ? ( ) : modalIncident ? ( ) : params.new && canReport ? ( ) : null}
); }