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:
@@ -0,0 +1,69 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { moduleGuard } from "@/server/action-guard";
|
||||
import { writeAuditLog } from "@/server/audit";
|
||||
|
||||
/**
|
||||
* Editierbare verwaltete Register (WP3.0). Zeilen werden gegen die Pflichtspalten
|
||||
* der ManagedRegister-Definition gepflegt; optionale Cross-Links an Lieferant/Asset.
|
||||
* Modul „policies", Recht policy:write.
|
||||
*/
|
||||
const guard = moduleGuard("policies");
|
||||
|
||||
interface Column { key: string; label: string }
|
||||
|
||||
function readValues(columns: Column[], formData: FormData): Record<string, string> {
|
||||
const values: Record<string, string> = {};
|
||||
for (const c of columns) values[c.key] = String(formData.get(`col_${c.key}`) ?? "").trim();
|
||||
return values;
|
||||
}
|
||||
const optRef = (v: FormDataEntryValue | null) => {
|
||||
const s = v ? String(v).trim() : "";
|
||||
return s || null;
|
||||
};
|
||||
|
||||
export async function addRegisterRow(code: string, formData: FormData) {
|
||||
const { session, db } = await guard("policy:write");
|
||||
const reg = await db.managedRegister.findFirst({ where: { code } });
|
||||
if (!reg) throw new Error("Register nicht gefunden");
|
||||
const columns = reg.columns as unknown as Column[];
|
||||
const last = await db.registerRow.aggregate({ where: { registerId: reg.id }, _max: { orderIdx: true } });
|
||||
await db.registerRow.create({
|
||||
data: {
|
||||
tenantId: session.user.tenantId, registerId: reg.id,
|
||||
values: readValues(columns, formData),
|
||||
supplierRef: reg.supplierLink ? optRef(formData.get("supplierRef")) : null,
|
||||
assetRef: reg.assetLink ? optRef(formData.get("assetRef")) : null,
|
||||
orderIdx: (last._max.orderIdx ?? 0) + 1,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "register_row", entityId: reg.code });
|
||||
revalidatePath(`/policies/${code}`);
|
||||
}
|
||||
|
||||
export async function updateRegisterRow(rowId: string, formData: FormData) {
|
||||
const { session, db } = await guard("policy:write");
|
||||
const row = await db.registerRow.findUnique({ where: { id: rowId }, include: { register: true } });
|
||||
if (!row) throw new Error("Zeile nicht gefunden");
|
||||
const columns = row.register.columns as unknown as Column[];
|
||||
await db.registerRow.update({
|
||||
where: { id: rowId },
|
||||
data: {
|
||||
values: readValues(columns, formData),
|
||||
supplierRef: row.register.supplierLink ? optRef(formData.get("supplierRef")) : row.supplierRef,
|
||||
assetRef: row.register.assetLink ? optRef(formData.get("assetRef")) : row.assetRef,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "register_row", entityId: rowId });
|
||||
revalidatePath(`/policies/${row.register.code}`);
|
||||
}
|
||||
|
||||
export async function deleteRegisterRow(rowId: string) {
|
||||
const { session, db } = await guard("policy:write");
|
||||
const row = await db.registerRow.findUnique({ where: { id: rowId }, include: { register: { select: { code: true } } } });
|
||||
if (!row) return;
|
||||
await db.registerRow.delete({ where: { id: rowId } });
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "delete", entity: "register_row", entityId: rowId });
|
||||
revalidatePath(`/policies/${row.register.code}`);
|
||||
}
|
||||
@@ -30,6 +30,8 @@ const TENANT_MODELS = new Set<string>([
|
||||
"AuditLog",
|
||||
"Task",
|
||||
"TaskComment",
|
||||
"ManagedRegister",
|
||||
"RegisterRow",
|
||||
"TenantSettings",
|
||||
"TenantModule",
|
||||
"Asset",
|
||||
|
||||
Reference in New Issue
Block a user