From c5a1f0095befad6c3320a75bfb84237ff6769ab9 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 2 Jul 2026 14:59:21 +0200 Subject: [PATCH] Assets & Prozesse: Detail/Bearbeiten/Anlegen als Popups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/app/(app)/assets/[id]/edit/page.tsx | 36 +- src/app/(app)/assets/[id]/page.tsx | 201 +---------- src/app/(app)/assets/new/page.tsx | 30 +- src/app/(app)/assets/page.tsx | 52 ++- src/app/(app)/processes/[id]/edit/page.tsx | 41 +-- src/app/(app)/processes/[id]/page.tsx | 282 +-------------- src/app/(app)/processes/new/page.tsx | 30 +- src/app/(app)/processes/page.tsx | 236 ++++--------- src/components/asset-modals.tsx | 243 +++++++++++++ src/components/modal.tsx | 48 +++ src/components/process-modals.tsx | 382 +++++++++++++++++++++ src/server/actions/assets.ts | 9 +- src/server/actions/processes.ts | 10 +- 13 files changed, 811 insertions(+), 789 deletions(-) create mode 100644 src/components/asset-modals.tsx create mode 100644 src/components/modal.tsx create mode 100644 src/components/process-modals.tsx diff --git a/src/app/(app)/assets/[id]/edit/page.tsx b/src/app/(app)/assets/[id]/edit/page.tsx index 5458e5b..639c5b6 100644 --- a/src/app/(app)/assets/[id]/edit/page.tsx +++ b/src/app/(app)/assets/[id]/edit/page.tsx @@ -1,39 +1,11 @@ -import { notFound } from "next/navigation"; -import { getTranslations } from "next-intl/server"; -import { requireSession } from "@/server/auth"; -import { dbForTenant } from "@/server/db"; -import { requirePermission } from "@/server/rbac"; -import { updateAsset } from "@/server/actions/assets"; -import { AssetForm } from "@/components/asset-form"; +import { redirect } from "next/navigation"; -export default async function EditAssetPage({ +// Alte Route — Detail/Bearbeiten laufen jetzt als Popup über die Listen-Seite. +export default async function Page({ params, }: { params: Promise<{ id: string }>; }) { - const session = await requireSession(); - requirePermission(session, "asset:write"); - const t = await getTranslations("assets"); - const { id } = await params; - const db = dbForTenant(session.user.tenantId); - const asset = await db.asset.findUnique({ where: { id } }); - if (!asset) notFound(); - - const users = await db.user.findMany({ - where: { status: "ACTIVE" }, - select: { id: true, name: true }, - orderBy: { name: "asc" }, - }); - - const action = updateAsset.bind(null, asset.id); - - return ( -
-

{t("editTitle")}

-
- -
-
- ); + redirect(`/assets?edit=${id}`); } diff --git a/src/app/(app)/assets/[id]/page.tsx b/src/app/(app)/assets/[id]/page.tsx index 8db6f03..f4c2e7b 100644 --- a/src/app/(app)/assets/[id]/page.tsx +++ b/src/app/(app)/assets/[id]/page.tsx @@ -1,204 +1,11 @@ -import Link from "next/link"; -import { notFound } from "next/navigation"; -import { getTranslations } from "next-intl/server"; -import { ArrowLeft, Pencil, Trash2, X } from "lucide-react"; -import { requireSession } from "@/server/auth"; -import { dbForTenant } from "@/server/db"; -import { hasPermission, requirePermission } from "@/server/rbac"; -import { - addAssetRelation, - deleteAsset, - removeAssetRelation, -} from "@/server/actions/assets"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { CiaBadge, Pill, Tag } from "@/components/mockup-ui"; +import { redirect } from "next/navigation"; -export default async function AssetDetailPage({ +// Alte Route — Detail/Bearbeiten laufen jetzt als Popup über die Listen-Seite. +export default async function Page({ params, }: { params: Promise<{ id: 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 tRole = await getTranslations("processRole"); - const tc = await getTranslations("common"); - const { id } = await params; - const db = dbForTenant(session.user.tenantId); - - const asset = await db.asset.findUnique({ - where: { id }, - include: { - owner: { select: { name: true } }, - relationsFrom: { include: { relatedAsset: { select: { id: true, name: true } } } }, - relationsTo: { include: { asset: { select: { id: true, name: true } } } }, - processAssets: { include: { process: { select: { id: true, name: true } } } }, - }, - }); - if (!asset) notFound(); - - const canWrite = hasPermission(session, "asset:write"); - const otherAssets = canWrite - ? await db.asset.findMany({ - where: { id: { not: asset.id } }, - select: { id: true, name: true }, - orderBy: { name: "asc" }, - }) - : []; - - const removeAction = deleteAsset.bind(null, asset.id); - const addRelationAction = addAssetRelation.bind(null, asset.id); - - return ( -
-
-
- -

{asset.name}

- {tType(asset.type)} - - {tStatus(asset.status)} - -
- {canWrite && ( -
- -
- -
-
- )} -
- -
- - - {t("detailTitle")} - - -
-
{t("description")}
-
{asset.description ?? tc("none")}
-
{t("owner")}
-
{asset.owner?.name ?? tc("none")}
-
{t("location")}
-
{asset.location ?? tc("none")}
-
{t("protection")}
-
- -
-
{t("tags")}
-
- {asset.tags.length === 0 - ? tc("none") - : asset.tags.map((tag) => {tag})} -
-
-
-
- - - - {t("linkedRisks")} - - -

{t("linkedRisksPlaceholder")}

-
-
- - - - {t("relations")} - - -
-

{t("relationHint")}

- {asset.relationsFrom.length === 0 &&

{tc("none")}

} -
    - {asset.relationsFrom.map((rel) => ( -
  • - - {rel.relatedAsset.name} - - {canWrite && ( -
    - -
    - )} -
  • - ))} -
-
-
-

{t("relationReverseHint")}

- {asset.relationsTo.length === 0 &&

{tc("none")}

} -
    - {asset.relationsTo.map((rel) => ( -
  • - - {rel.asset.name} - -
  • - ))} -
-
- {canWrite && otherAssets.length > 0 && ( -
- - -
- )} -
-
- - - - {t("processes")} - - - {asset.processAssets.length === 0 &&

{tc("none")}

} -
    - {asset.processAssets.map((pa) => ( -
  • - - {pa.process.name} - - {tRole(pa.role)} -
  • - ))} -
-

{t("inheritedNote")}

-
-
-
-
- ); + redirect(`/assets?detail=${id}`); } diff --git a/src/app/(app)/assets/new/page.tsx b/src/app/(app)/assets/new/page.tsx index 0cda9ff..6ee04af 100644 --- a/src/app/(app)/assets/new/page.tsx +++ b/src/app/(app)/assets/new/page.tsx @@ -1,28 +1,6 @@ -import { getTranslations } from "next-intl/server"; -import { requireSession } from "@/server/auth"; -import { dbForTenant } from "@/server/db"; -import { requirePermission } from "@/server/rbac"; -import { createAsset } from "@/server/actions/assets"; -import { AssetForm } from "@/components/asset-form"; +import { redirect } from "next/navigation"; -export default async function NewAssetPage() { - const session = await requireSession(); - requirePermission(session, "asset:write"); - const t = await getTranslations("assets"); - - const db = dbForTenant(session.user.tenantId); - const users = await db.user.findMany({ - where: { status: "ACTIVE" }, - select: { id: true, name: true }, - orderBy: { name: "asc" }, - }); - - return ( -
-

{t("createTitle")}

-
- -
-
- ); +// Alte Route — Anlegen läuft jetzt als Popup über die Listen-Seite. +export default function Page() { + redirect("/assets?new=1"); } diff --git a/src/app/(app)/assets/page.tsx b/src/app/(app)/assets/page.tsx index ee77420..507f54c 100644 --- a/src/app/(app)/assets/page.tsx +++ b/src/app/(app)/assets/page.tsx @@ -16,6 +16,11 @@ import { Pill, Tag, } from "@/components/mockup-ui"; +import { + AssetCreateModal, + AssetDetailModal, + AssetEditModal, +} from "@/components/asset-modals"; import { Table, TableBody, @@ -32,7 +37,7 @@ const STATUS_TONE = { ACTIVE: "ok", PLANNED: "info", RETIRED: "mut" } as const; export default async function AssetsPage({ searchParams, }: { - searchParams: Promise<{ q?: string; type?: string }>; + searchParams: Promise<{ q?: string; type?: string; detail?: string; edit?: string; new?: string }>; }) { const session = await requireSession(); requirePermission(session, "asset:read"); @@ -41,7 +46,8 @@ export default async function AssetsPage({ const tStatus = await getTranslations("assetStatus"); const tc = await getTranslations("common"); - const { q, type } = await searchParams; + const params = await searchParams; + const { q, type } = params; const activeType = ASSET_TYPES.includes(type as AssetType) ? (type as AssetType) : null; const db = dbForTenant(session.user.tenantId); @@ -76,6 +82,36 @@ export default async function AssetsPage({ const canWrite = hasPermission(session, "asset:write"); + // Popup-Zustand aus searchParams: ?detail= (read-only), ?edit=, ?new=1 + const modalId = params.edit && canWrite ? params.edit : params.detail; + const modalAsset = modalId + ? await db.asset.findUnique({ + where: { id: modalId }, + include: { + owner: { select: { name: true } }, + relationsFrom: { include: { relatedAsset: { select: { id: true, name: true } } } }, + relationsTo: { include: { asset: { select: { id: true, name: true } } } }, + processAssets: { include: { process: { select: { id: true, name: true } } } }, + }, + }) + : null; + const users = + canWrite && (params.edit || params.new) + ? await db.user.findMany({ + where: { status: "ACTIVE" }, + select: { id: true, name: true }, + orderBy: { name: "asc" }, + }) + : []; + const otherAssets = + canWrite && params.edit && modalAsset + ? await db.asset.findMany({ + where: { id: { not: modalAsset.id } }, + select: { id: true, name: true }, + orderBy: { name: "asc" }, + }) + : []; + const typeHref = (v: string | null) => `/assets${v ? `?type=${v}` : ""}${q ? `${v ? "&" : "?"}q=${encodeURIComponent(q)}` : ""}`; @@ -94,7 +130,7 @@ export default async function AssetsPage({ {t("export")} {canWrite && ( - )} @@ -167,7 +203,7 @@ export default async function AssetsPage({ {assets.map((asset) => ( - + {asset.name} @@ -193,6 +229,14 @@ export default async function AssetsPage({ + + {modalAsset && params.edit && canWrite ? ( + + ) : modalAsset ? ( + + ) : params.new && canWrite ? ( + + ) : null} ); } diff --git a/src/app/(app)/processes/[id]/edit/page.tsx b/src/app/(app)/processes/[id]/edit/page.tsx index a639285..a15b85c 100644 --- a/src/app/(app)/processes/[id]/edit/page.tsx +++ b/src/app/(app)/processes/[id]/edit/page.tsx @@ -1,44 +1,11 @@ -import { notFound } from "next/navigation"; -import { getTranslations } from "next-intl/server"; -import { requireSession } from "@/server/auth"; -import { dbForTenant } from "@/server/db"; -import { requirePermission } from "@/server/rbac"; -import { updateProcess } from "@/server/actions/processes"; -import { ProcessForm } from "@/components/process-form"; +import { redirect } from "next/navigation"; -export default async function EditProcessPage({ +// Alte Route — Detail/Bearbeiten laufen jetzt als Popup über die Listen-Seite. +export default async function Page({ params, }: { params: Promise<{ id: string }>; }) { - const session = await requireSession(); - requirePermission(session, "bia:write"); - const t = await getTranslations("processes"); - const { id } = await params; - const db = dbForTenant(session.user.tenantId); - const process = await db.process.findUnique({ where: { id } }); - if (!process) notFound(); - - const users = await db.user.findMany({ - where: { status: "ACTIVE" }, - select: { id: true, name: true }, - orderBy: { name: "asc" }, - }); - - const action = updateProcess.bind(null, process.id); - - return ( -
-

{t("editTitle")}

-
- -
-
- ); + redirect(`/processes?edit=${id}`); } diff --git a/src/app/(app)/processes/[id]/page.tsx b/src/app/(app)/processes/[id]/page.tsx index 746e34d..8e79b75 100644 --- a/src/app/(app)/processes/[id]/page.tsx +++ b/src/app/(app)/processes/[id]/page.tsx @@ -1,285 +1,11 @@ -import Link from "next/link"; -import { notFound } from "next/navigation"; -import { getTranslations } from "next-intl/server"; -import { ArrowLeft, Pencil, Trash2, X } from "lucide-react"; -import { requireSession } from "@/server/auth"; -import { dbForTenant } from "@/server/db"; -import { hasPermission, requirePermission } from "@/server/rbac"; -import { - assignAsset, - deleteProcess, - saveBia, - unassignAsset, -} from "@/server/actions/processes"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { CiaBadge, CriticalityPill } from "@/components/mockup-ui"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; +import { redirect } from "next/navigation"; -const LEVELS = [1, 2, 3, 4] as const; - -export default async function ProcessDetailPage({ +// Alte Route — Detail/Bearbeiten laufen jetzt als Popup über die Listen-Seite. +export default async function Page({ params, }: { params: Promise<{ id: string }>; }) { - const session = await requireSession(); - requirePermission(session, "bia:read"); - const t = await getTranslations("processes"); - const tAssets = await getTranslations("assets"); - const tRole = await getTranslations("processRole"); - const tCrit = await getTranslations("criticality"); - const tc = await getTranslations("common"); - const { id } = await params; - const db = dbForTenant(session.user.tenantId); - - const process = await db.process.findUnique({ - where: { id }, - include: { - owner: { select: { name: true } }, - bia: true, - processAssets: { - include: { - asset: { - select: { - id: true, - name: true, - confidentiality: true, - integrity: true, - availability: true, - }, - }, - }, - orderBy: { role: "asc" }, - }, - }, - }); - if (!process) notFound(); - - const canWrite = hasPermission(session, "bia:write"); - const assignedIds = process.processAssets.map((pa) => pa.assetId); - const availableAssets = canWrite - ? await db.asset.findMany({ - where: { id: { notIn: assignedIds } }, - select: { id: true, name: true }, - orderBy: { name: "asc" }, - }) - : []; - - const primary = process.processAssets.filter((pa) => pa.role === "PRIMARY"); - const secondary = process.processAssets.filter((pa) => pa.role === "SECONDARY"); - - const selectClass = - "h-9 rounded-md border border-input bg-transparent px-3 text-sm"; - const bia = process.bia; - - const assetList = (items: typeof primary) => { - return ( -
    - {items.map((pa) => ( -
  • - - {pa.asset.name} - - - {canWrite && ( -
    - -
    - )} -
  • - ))} -
- ); - }; - - return ( -
-
-
- -

{process.name}

- {bia && } -
- {canWrite && ( -
- -
- -
-
- )} -
- -
- - - {t("detailTitle")} - - -
-
{t("description")}
-
{process.description ?? tc("none")}
-
{t("owner")}
-
{process.owner?.name ?? tc("none")}
-
-
-
- - - - {t("linkedRisks")} - - -

{t("linkedRisksPlaceholder")}

-
-
- - - - {t("assets")} - - -
-

{t("primaryAssets")}

-

{t("primaryHint")}

- {primary.length === 0 ?

{tc("none")}

: assetList(primary)} -
-
-

{t("secondaryAssets")}

-

{t("secondaryHint")}

- {secondary.length === 0 ?

{tc("none")}

: assetList(secondary)} -
- {canWrite && availableAssets.length > 0 && ( -
- - - -
- )} -
-
- - - - {t("bia")} - - - {!canWrite && !bia && ( -

{t("noBia")}

- )} - {!canWrite && bia && ( -
-
RTO
-
{bia.rtoHours != null ? `${bia.rtoHours} h` : tc("none")}
-
RPO
-
{bia.rpoHours != null ? `${bia.rpoHours} h` : tc("none")}
-
MTD
-
{bia.mtdHours != null ? `${bia.mtdHours} h` : tc("none")}
-
{t("impact")}
-
- -
-
- )} - {canWrite && ( -
-
- {( - [ - ["rtoHours", t("rto"), t("rtoLong"), bia?.rtoHours], - ["rpoHours", t("rpo"), t("rpoLong"), bia?.rpoHours], - ["mtdHours", t("mtd"), t("mtdLong"), bia?.mtdHours], - ] as const - ).map(([name, label, hint, value]) => ( -
- - -
- ))} -
-
- {t("impact")} -
- {( - [ - ["impactC", tAssets("confidentiality"), bia?.impactC], - ["impactI", tAssets("integrity"), bia?.impactI], - ["impactA", tAssets("availability"), bia?.impactA], - ] as const - ).map(([name, label, value]) => ( -
- - -
- ))} -
-
-
- -