Assets & Prozesse: Detail/Bearbeiten/Anlegen als Popups
- Wiederverwendbares, serverseitig gerendertes Modal (searchParams- gesteuert: ?detail= / ?edit= / ?new=1) — Browser-Zurück schließt, kein Client-State nötig - Assets: Read-only-Detail-Popup (Stammdaten, C/I/A, Abhängigkeiten, Prozesse, Risiken-Platzhalter), Bearbeiten-Popup (Formular + Abhängigkeiten verwalten + Löschen), Anlegen-Popup - Prozesse: "Bearbeiten" wechselt jetzt innerhalb des Popups in den Editiermodus (Stammdaten, Asset-Zuordnung, BIA, Löschen) statt auf die alte Detailseite zu springen; Anlegen ebenfalls als Popup - Server-Actions leiten auf die Popup-URLs zurück (Speichern im Bearbeiten-Popup → Detail-Popup; BIA-/Zuordnungs-Änderungen lassen das Popup offen); BIA-Formular remountet nach dem Speichern (key) - Alte Routen (/assets/[id], /assets/new, /processes/[id], …) leiten auf die Popup-URLs um Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,17 @@
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Plus, X, Pencil } from "lucide-react";
|
||||
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 { ModuleTabs } from "@/components/module-tabs";
|
||||
import { CriticalityPill, PageHead, SectTitle } from "@/components/mockup-ui";
|
||||
import {
|
||||
CiaBadge,
|
||||
CriticalityPill,
|
||||
PageHead,
|
||||
Pill,
|
||||
SectTitle,
|
||||
Tag,
|
||||
} from "@/components/mockup-ui";
|
||||
ProcessCreateModal,
|
||||
ProcessDetailModal,
|
||||
ProcessEditModal,
|
||||
} from "@/components/process-modals";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -23,21 +21,41 @@ import {
|
||||
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 }>;
|
||||
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 tType = await getTranslations("assetType");
|
||||
const tCrit = await getTranslations("criticality");
|
||||
const tc = await getTranslations("common");
|
||||
|
||||
const { detail } = await searchParams;
|
||||
const params = await searchParams;
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const canWrite = hasPermission(session, "bia:write");
|
||||
|
||||
const processes = await db.process.findMany({
|
||||
include: {
|
||||
@@ -48,37 +66,30 @@ export default async function ProcessesPage({
|
||||
take: 200,
|
||||
});
|
||||
|
||||
// Read-only-Detail fürs Popup (?detail=<id>)
|
||||
const detailProcess = detail
|
||||
? await db.process.findUnique({
|
||||
where: { id: detail },
|
||||
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 } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
// 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 canWrite = hasPermission(session, "bia:write");
|
||||
const hours = (v: number | null | undefined) => (v != null ? `${v} h` : tc("none"));
|
||||
const users =
|
||||
canWrite && (params.edit || params.new)
|
||||
? await db.user.findMany({
|
||||
where: { status: "ACTIVE" },
|
||||
select: { id: true, name: true },
|
||||
orderBy: { name: "asc" },
|
||||
})
|
||||
: [];
|
||||
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 primary = detailProcess?.processAssets.filter((pa) => pa.role === "PRIMARY") ?? [];
|
||||
const secondary = detailProcess?.processAssets.filter((pa) => pa.role === "SECONDARY") ?? [];
|
||||
const hours = (v: number | null | undefined) => (v != null ? `${v} h` : tc("none"));
|
||||
|
||||
return (
|
||||
<main className="flex-1 p-6">
|
||||
@@ -92,7 +103,7 @@ export default async function ProcessesPage({
|
||||
{t("biaReport")}
|
||||
</Button>
|
||||
{canWrite && (
|
||||
<Button nativeButton={false} render={<Link href="/processes/new" />}>
|
||||
<Button nativeButton={false} render={<Link href="/processes?new=1" />}>
|
||||
<Plus className="size-4" /> {t("newProcess")}
|
||||
</Button>
|
||||
)}
|
||||
@@ -160,144 +171,13 @@ export default async function ProcessesPage({
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{detailProcess && (
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/30 p-6 backdrop-blur-[2px]">
|
||||
<div className="shadow-card w-full max-w-3xl rounded-2xl border bg-card">
|
||||
<div className="flex items-start justify-between gap-3 border-b p-5">
|
||||
<SectTitle
|
||||
title={t("detailHeading", { name: detailProcess.name })}
|
||||
sub={t("detailSub")}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
{detailProcess.bia && (
|
||||
<CriticalityPill
|
||||
level={detailProcess.bia.criticality}
|
||||
label={t("critLabel", {
|
||||
label: tCrit(String(detailProcess.bia.criticality)),
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<Link
|
||||
href="/processes"
|
||||
title={t("close")}
|
||||
className="rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<X className="size-4.5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 p-5 md:grid-cols-2">
|
||||
<div>
|
||||
<div className="mb-2.5 flex items-center gap-2.5">
|
||||
<Pill tone="violet">{t("primaryPill")}</Pill>
|
||||
<span className="text-[12.5px] text-muted-foreground">{t("primaryNote")}</span>
|
||||
</div>
|
||||
{primary.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">{t("noAssets")}</p>
|
||||
)}
|
||||
{primary.map((pa) => (
|
||||
<div
|
||||
key={pa.id}
|
||||
className="mb-2 rounded-xl border border-l-[3px] border-l-[#5d52a3] bg-[#faf9fd] p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<b>{pa.asset.name}</b>
|
||||
<CiaBadge
|
||||
c={pa.asset.confidentiality}
|
||||
i={pa.asset.integrity}
|
||||
a={pa.asset.availability}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-1.5 text-[12.5px] leading-relaxed text-muted-foreground">
|
||||
{t("owner")}: {pa.asset.owner?.name ?? tc("none")} · {tAssets("type")}:{" "}
|
||||
{tType(pa.asset.type)} · {t("inherits")}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-2.5 flex items-center gap-2.5">
|
||||
<Pill tone="info">{t("secondaryPill")}</Pill>
|
||||
<span className="text-[12.5px] text-muted-foreground">{t("secondaryNote")}</span>
|
||||
</div>
|
||||
{secondary.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">{t("noAssets")}</p>
|
||||
)}
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
{secondary.map((pa) => (
|
||||
<tr key={pa.id} className="border-b last:border-0">
|
||||
<td className="py-2.5 pr-2 font-bold">{pa.asset.name}</td>
|
||||
<td className="py-2.5 pr-2">
|
||||
<Tag>{tType(pa.asset.type)}</Tag>
|
||||
</td>
|
||||
<td className="py-2.5 text-right">
|
||||
<CiaBadge
|
||||
c={pa.asset.confidentiality}
|
||||
i={pa.asset.integrity}
|
||||
a={pa.asset.availability}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-5 rounded-xl border border-[#e6e2f3] bg-[#f2f0f9] p-4 text-[12.5px] text-[#5a4e86]">
|
||||
<div className="grid gap-2 sm:grid-cols-4">
|
||||
<div>
|
||||
<b>RTO</b>
|
||||
<div className="mt-0.5">{hours(detailProcess.bia?.rtoHours)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<b>RPO</b>
|
||||
<div className="mt-0.5">{hours(detailProcess.bia?.rpoHours)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<b>MTD</b>
|
||||
<div className="mt-0.5">{hours(detailProcess.bia?.mtdHours)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<b>{t("impactShort")}</b>
|
||||
<div className="mt-1">
|
||||
{detailProcess.bia ? (
|
||||
<CiaBadge
|
||||
c={detailProcess.bia.impactC}
|
||||
i={detailProcess.bia.impactI}
|
||||
a={detailProcess.bia.impactA}
|
||||
/>
|
||||
) : (
|
||||
t("noBia")
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{detailProcess.bia?.notes && (
|
||||
<p className="mt-3 border-t border-[#e6e2f3] pt-2.5">{detailProcess.bia.notes}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 p-5">
|
||||
{canWrite && (
|
||||
<Button
|
||||
variant="outline"
|
||||
nativeButton={false}
|
||||
render={<Link href={`/processes/${detailProcess.id}`} />}
|
||||
>
|
||||
<Pencil className="size-4" /> {tc("edit")}
|
||||
</Button>
|
||||
)}
|
||||
<Button nativeButton={false} render={<Link href="/processes" />}>
|
||||
{t("close")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{modalProcess && params.edit && canWrite ? (
|
||||
<ProcessEditModal process={modalProcess} users={users} availableAssets={availableAssets} />
|
||||
) : modalProcess ? (
|
||||
<ProcessDetailModal process={modalProcess} canWrite={canWrite} />
|
||||
) : params.new && canWrite ? (
|
||||
<ProcessCreateModal users={users} />
|
||||
) : null}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user