diff --git a/messages/de.json b/messages/de.json
index 52c081e..7a4b727 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -87,7 +87,24 @@
"editTitle": "Asset bearbeiten",
"createTitle": "Asset anlegen",
"deleted": "Asset gelöscht",
- "inheritedNote": "Vererbter Schutzbedarf aus Prozessen (Max-Prinzip) wird bei der BIA berücksichtigt."
+ "inheritedNote": "Vererbter Schutzbedarf aus Prozessen (Max-Prinzip) wird bei der BIA berücksichtigt.",
+ "crumb": "Fachdaten",
+ "invTitle": "Asset-Inventar",
+ "invSub": "{count} Werte · Schutzbedarf nach C/I/A · verknüpft mit BIA und Risiko",
+ "excelImport": "Excel-Import",
+ "export": "Export",
+ "kpiTotal": "Assets gesamt",
+ "kpiTotalTrend": "{count} Typen",
+ "kpiHigh": "Hoher Schutzbedarf",
+ "kpiHighTrend": "C/I/A ≥ 3",
+ "kpiNoOwner": "Ohne Owner",
+ "kpiNoOwnerTrend": "Zuweisung nötig",
+ "kpiSuppliers": "Lieferanten",
+ "kpiSuppliersTrend": "extern",
+ "all": "Alle",
+ "filter": "Filtern …",
+ "noOwner": "kein Owner",
+ "dependencies": "Abhängigkeiten"
},
"assetType": {
"INFORMATION": "Information",
@@ -132,7 +149,24 @@
"impact": "Schadenshöhe je Schutzziel (1–4)",
"notes": "Anmerkungen / Schadensszenarien",
"biaSaved": "BIA gespeichert",
- "noBia": "Noch keine BIA erfasst."
+ "noBia": "Noch keine BIA erfasst.",
+ "biaTitle": "Business Impact Analyse",
+ "biaSub": "Kritikalität der Geschäftsprozesse · RTO / RPO / MTD",
+ "biaReport": "BIA-Report (PDF)",
+ "critTable": "Kritikalität der Prozesse",
+ "process": "Geschäftsprozess",
+ "detailHeading": "Prozess-Detail · {name}",
+ "detailSub": "Zugeordnete Assets nach Rolle",
+ "critLabel": "Kritikalität: {label}",
+ "primaryPill": "★ Primäres Asset",
+ "primaryNote": "Ergebnis, das im Prozess erzeugt wird",
+ "secondaryPill": "◆ Sekundäre Assets",
+ "secondaryNote": "Werden zur Bearbeitung benötigt",
+ "inherits": "vererbt Schutzbedarf an sekundäre Assets",
+ "showDependencies": "Abhängigkeiten anzeigen →",
+ "impactShort": "Schadenshöhe (C/I/A)",
+ "close": "Schließen",
+ "noAssets": "Noch keine Assets zugeordnet."
},
"processRole": {
"PRIMARY": "Primär",
diff --git a/messages/en.json b/messages/en.json
index 744a4ad..14cf612 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -87,7 +87,24 @@
"editTitle": "Edit asset",
"createTitle": "Create asset",
"deleted": "Asset deleted",
- "inheritedNote": "Inherited protection needs from processes (max principle) are considered in the BIA."
+ "inheritedNote": "Inherited protection needs from processes (max principle) are considered in the BIA.",
+ "crumb": "Core data",
+ "invTitle": "Asset inventory",
+ "invSub": "{count} assets · protection needs by C/I/A · linked to BIA and risk",
+ "excelImport": "Excel import",
+ "export": "Export",
+ "kpiTotal": "Total assets",
+ "kpiTotalTrend": "{count} types",
+ "kpiHigh": "High protection needs",
+ "kpiHighTrend": "C/I/A ≥ 3",
+ "kpiNoOwner": "Without owner",
+ "kpiNoOwnerTrend": "assignment needed",
+ "kpiSuppliers": "Suppliers",
+ "kpiSuppliersTrend": "external",
+ "all": "All",
+ "filter": "Filter …",
+ "noOwner": "no owner",
+ "dependencies": "Dependencies"
},
"assetType": {
"INFORMATION": "Information",
@@ -132,7 +149,24 @@
"impact": "Damage level per protection goal (1–4)",
"notes": "Notes / damage scenarios",
"biaSaved": "BIA saved",
- "noBia": "No BIA recorded yet."
+ "noBia": "No BIA recorded yet.",
+ "biaTitle": "Business impact analysis",
+ "biaSub": "Criticality of business processes · RTO / RPO / MTD",
+ "biaReport": "BIA report (PDF)",
+ "critTable": "Process criticality",
+ "process": "Business process",
+ "detailHeading": "Process detail · {name}",
+ "detailSub": "Assigned assets by role",
+ "critLabel": "Criticality: {label}",
+ "primaryPill": "★ Primary asset",
+ "primaryNote": "Result produced by the process",
+ "secondaryPill": "◆ Secondary assets",
+ "secondaryNote": "Required for processing",
+ "inherits": "inherits protection needs to secondary assets",
+ "showDependencies": "Show dependencies →",
+ "impactShort": "Damage level (C/I/A)",
+ "close": "Close",
+ "noAssets": "No assets assigned yet."
},
"processRole": {
"PRIMARY": "Primary",
diff --git a/src/app/(app)/assets/[id]/page.tsx b/src/app/(app)/assets/[id]/page.tsx
index dd9cc42..8db6f03 100644
--- a/src/app/(app)/assets/[id]/page.tsx
+++ b/src/app/(app)/assets/[id]/page.tsx
@@ -11,9 +11,8 @@ import {
removeAssetRelation,
} from "@/server/actions/assets";
import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { LevelBadge } from "@/components/level-badge";
+import { CiaBadge, Pill, Tag } from "@/components/mockup-ui";
export default async function AssetDetailPage({
params,
@@ -62,8 +61,10 @@ export default async function AssetDetailPage({
{asset.name}
- {tType(asset.type)}
- {tStatus(asset.status)}
+ {tType(asset.type)}
+
+ {tStatus(asset.status)}
+
{canWrite && (
@@ -93,20 +94,14 @@ export default async function AssetDetailPage({
{t("location")}
{asset.location ?? tc("none")}
{t("protection")}
-
-
-
-
+
+
{t("tags")}
{asset.tags.length === 0
? tc("none")
- : asset.tags.map((tag) => (
-
- {tag}
-
- ))}
+ : asset.tags.map((tag) => {tag})}
@@ -196,9 +191,7 @@ export default async function AssetDetailPage({
{pa.process.name}
-
- {tRole(pa.role)}
-
+
{tRole(pa.role)}
))}
diff --git a/src/app/(app)/assets/page.tsx b/src/app/(app)/assets/page.tsx
index a52532f..ee77420 100644
--- a/src/app/(app)/assets/page.tsx
+++ b/src/app/(app)/assets/page.tsx
@@ -1,15 +1,21 @@
import Link from "next/link";
import { getTranslations } from "next-intl/server";
-import { Plus } from "lucide-react";
-import type { AssetStatus, AssetType } from "@prisma/client";
+import { Plus, Search } from "lucide-react";
+import type { AssetType } 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 { Input } from "@/components/ui/input";
-import { LevelBadge } from "@/components/level-badge";
import { ModuleTabs } from "@/components/module-tabs";
-import { Badge } from "@/components/ui/badge";
+import { FilterTabs } from "@/components/filter-tabs";
+import {
+ CiaBadge,
+ KpiCard,
+ OwnerChip,
+ PageHead,
+ Pill,
+ Tag,
+} from "@/components/mockup-ui";
import {
Table,
TableBody,
@@ -20,12 +26,13 @@ import {
} from "@/components/ui/table";
const ASSET_TYPES = ["INFORMATION", "SYSTEM", "APPLICATION", "LOCATION", "SUPPLIER", "PERSON", "DATA"] as const;
-const ASSET_STATUS = ["ACTIVE", "PLANNED", "RETIRED"] as const;
+
+const STATUS_TONE = { ACTIVE: "ok", PLANNED: "info", RETIRED: "mut" } as const;
export default async function AssetsPage({
searchParams,
}: {
- searchParams: Promise<{ q?: string; type?: string; status?: string }>;
+ searchParams: Promise<{ q?: string; type?: string }>;
}) {
const session = await requireSession();
requirePermission(session, "asset:read");
@@ -34,96 +41,125 @@ export default async function AssetsPage({
const tStatus = await getTranslations("assetStatus");
const tc = await getTranslations("common");
- const { q, type, status } = await searchParams;
+ const { q, type } = await searchParams;
+ const activeType = ASSET_TYPES.includes(type as AssetType) ? (type as AssetType) : null;
const db = dbForTenant(session.user.tenantId);
- const assets = await db.asset.findMany({
- where: {
- ...(q ? { name: { contains: q, mode: "insensitive" } } : {}),
- ...(type && ASSET_TYPES.includes(type as AssetType) ? { type: type as AssetType } : {}),
- ...(status && ASSET_STATUS.includes(status as AssetStatus)
- ? { status: status as AssetStatus }
- : {}),
- },
- include: { owner: { select: { name: true } } },
- orderBy: { name: "asc" },
- take: 200,
- });
+ const [assets, allAssets] = await Promise.all([
+ db.asset.findMany({
+ where: {
+ ...(q ? { name: { contains: q, mode: "insensitive" } } : {}),
+ ...(activeType ? { type: activeType } : {}),
+ },
+ include: {
+ owner: { select: { name: true } },
+ relationsFrom: {
+ include: { relatedAsset: { select: { name: true } } },
+ take: 4,
+ },
+ },
+ orderBy: { name: "asc" },
+ take: 200,
+ }),
+ db.asset.findMany({
+ select: { type: true, ownerId: true, confidentiality: true, integrity: true, availability: true },
+ }),
+ ]);
+
+ const total = allAssets.length;
+ const typeCount = new Set(allAssets.map((a) => a.type)).size;
+ const highProtection = allAssets.filter(
+ (a) => Math.max(a.confidentiality, a.integrity, a.availability) >= 3
+ ).length;
+ const withoutOwner = allAssets.filter((a) => !a.ownerId).length;
+ const suppliers = allAssets.filter((a) => a.type === "SUPPLIER").length;
const canWrite = hasPermission(session, "asset:write");
+ const typeHref = (v: string | null) =>
+ `/assets${v ? `?type=${v}` : ""}${q ? `${v ? "&" : "?"}q=${encodeURIComponent(q)}` : ""}`;
+
return (
-
-
{t("title")}
- {canWrite && (
-
}>
-
{t("newAsset")}
-
- )}
+
+
+
+ {canWrite && (
+ }>
+ {t("newAsset")}
+
+ )}
+ >
+ }
+ />
+
+
+
+
+
+
+ 0 ? t("kpiNoOwnerTrend") : undefined}
+ trendColor="warn"
+ />
+
-
-
-
-
-
-
-
+
+
+ ({
+ href: typeHref(v),
+ label: tType(v),
+ active: activeType === v,
+ })),
+ ]}
+ />
+
+
- {t("name")}
+ Asset
{t("type")}
- {t("protection")} (C/I/A)
{t("owner")}
+ {t("protection")} (C/I/A)
+ {t("dependencies")}
{t("status")}
{assets.length === 0 && (
-
+
{t("empty")}
@@ -131,25 +167,26 @@ export default async function AssetsPage({
{assets.map((asset) => (
-
+
{asset.name}
- {tType(asset.type)}
+ {tType(asset.type)}
-
-
-
-
-
+
+
+
+
- {asset.owner?.name ?? tc("none")}
+ {asset.relationsFrom.length
+ ? asset.relationsFrom.map((r) => r.relatedAsset.name).join(", ")
+ : tc("none")}
-
- {tStatus(asset.status)}
+
+ {tStatus(asset.status)}
))}
diff --git a/src/app/(app)/processes/[id]/page.tsx b/src/app/(app)/processes/[id]/page.tsx
index 49e47e9..746e34d 100644
--- a/src/app/(app)/processes/[id]/page.tsx
+++ b/src/app/(app)/processes/[id]/page.tsx
@@ -13,10 +13,10 @@ import {
} 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 { LevelBadge } from "@/components/level-badge";
const LEVELS = [1, 2, 3, 4] as const;
@@ -84,11 +84,7 @@ export default async function ProcessDetailPage({
{pa.asset.name}
-
-
-
-
-
+
{canWrite && (