GAP WP3.0: Generisches, editierbares Register-Datenmodell + Editor

Verwaltete Register (GAP-Report §5.2) sind nicht mehr nur benannte Dokumente,
sondern editierbare Register mit Pflichtspalten und Cross-Links.

- Schema: ManagedRegister (code, title, columns JSON, reviewCycle, responsible,
  supplierLink/assetLink) + RegisterRow (values JSON, supplierRef, assetRef).
  Migration + RLS; in TENANT_MODELS.
- import-managed.ts: 7 generische Register (REG-PROJECTS, -SENS-ROLES, -AUDIT-PLAN,
  -EXT-SERVICES, -SW-WHITELIST, -CRIT-SERVICES, -NET) mit definierten Pflichtspalten
  + Cross-Link-Flags; legt Register-Dokument (Bibliothek/Link) UND ManagedRegister-
  Definition idempotent an (RegisterRow bleibt bei Re-Import erhalten).
- actions/register.ts: addRegisterRow/updateRegisterRow/deleteRegisterRow
  (Modul policies, policy:write; Werte gegen die Pflichtspalten, optional Lieferant/Asset).
- components/generic-register.tsx: editierbare Zeilen-Tabelle + Cross-Link-Selects
  (Lieferant = Asset SUPPLIER/IT_SERVICE, Asset = alle); in /policies/[code] eingebunden.

Browser-verifiziert: REG-EXT-SERVICES rendert 7 Spalten + Lieferant/Asset-Selects;
Eintrag anlegen persistiert alle Werte. tsc + lint + build + Guard-Check grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 11:07:08 +02:00
co-authored by Claude Opus 4.8
parent b9784e55f4
commit bd56a478fc
8 changed files with 326 additions and 8 deletions
+25 -2
View File
@@ -14,6 +14,7 @@ import {
POLICY_TYPE_LABEL,
} from "@/components/policy-modals";
import { PolicyRegisterView } from "@/components/policy-registers";
import { GenericRegisterView, type RegisterColumn } from "@/components/generic-register";
const REGISTER_CODES = new Set(["CRYPTO", "RISKMATRIX", "CLASSIFICATION", "HANDBUCH"]);
// Aus Vorlagen-Markdown gerenderte Dokumente (bearbeitbar über den Editor)
@@ -35,9 +36,31 @@ export default async function PolicyDetailPage({
const canWrite = hasPermission(session, "policy:write");
const isRegister = REGISTER_CODES.has(doc.code);
// Generisches verwaltetes Register (WP3.0): editierbare Zeilen nach Pflichtspalten.
const genericReg = doc.type === "REGISTER" && !isRegister
? await db.managedRegister.findFirst({ where: { code: doc.code } })
: null;
let content: React.ReactNode;
if (isRegister) {
if (genericReg) {
const [rows, allAssets] = await Promise.all([
db.registerRow.findMany({ where: { registerId: genericReg.id }, orderBy: { orderIdx: "asc" } }),
db.asset.findMany({ select: { id: true, name: true, type: true }, orderBy: { name: "asc" } }),
]);
content = (
<GenericRegisterView
register={{
code: genericReg.code, title: genericReg.title, description: genericReg.description,
columns: (genericReg.columns as unknown as RegisterColumn[]) ?? [],
supplierLink: genericReg.supplierLink, assetLink: genericReg.assetLink,
}}
rows={rows.map((r) => ({ id: r.id, values: (r.values as Record<string, string>) ?? {}, supplierRef: r.supplierRef, assetRef: r.assetRef }))}
canWrite={canWrite}
suppliers={allAssets.filter((a) => a.type === "SUPPLIER" || a.type === "IT_SERVICE").map((a) => ({ id: a.id, name: a.name }))}
assets={allAssets.map((a) => ({ id: a.id, name: a.name }))}
/>
);
} else if (isRegister) {
const data =
doc.code === "CRYPTO"
? { crypto: await db.cryptoEntry.findMany({ orderBy: { orderIdx: "asc" } }) }
@@ -81,7 +104,7 @@ export default async function PolicyDetailPage({
</div>
<p className="mt-0.5 text-[12.5px] text-muted-foreground">
{POLICY_TYPE_LABEL[doc.type]} · v{doc.version}
{isRegister && canWrite ? ` · ${t("editableInline")}` : ""}
{(isRegister || genericReg) && canWrite ? ` · ${t("editableInline")}` : ""}
</p>
</div>
{showEdit && (