Lieferantenmodul neu: Asset-basiert + Anforderungs-Engine (Phase 1)
Referenz: docs/ISMS-Lieferantenmanagement-GEFIM.html Architektur: Lieferanten (und IT-Services) sind jetzt Assets. - AssetType += IT_SERVICE; SupplierProfile/ITServiceProfile als 1:1- Erweiterung eines Asset; alle Kind-Entitäten (Contract, Nda, SupplierEvidence, SupplierAssessment, ManagementDecision, Subcontractor, ServiceControlResponsibility, MaturityAssessment) hängen am Asset (keine parallele Datenhaltung). Migration + RLS; Demo neu geseedet. - Lieferanten erscheinen im Asset-Inventar (Filter „Lieferant"/„IT-Service"), tragen C/I/A, nutzbar in Graph/BIA/Risiko. Anforderungs-Engine (Herzstück, src/lib/supplier.ts): - Schutzbedarf aus max C/I/A der verknüpften Assets → Stufe normal/hoch/ sehr hoch; schaltet Anforderungen gestuft scharf (must/should/high/veryhigh) - Flache Anforderungsliste (nach Themen gruppiert) mit Status-Ableitung aus den hinterlegten Objekten, Referenz + Herkunfts-Control (6.1.1–6.1.3) - Gate-Logik: sehr hoch ohne Audit/Label → Kompensation (Management- entscheidung UND verknüpftes Risiko); „Risiko anlegen" erzeugt echten Risk - Konformität (Konform/Teilweise/Lücken/Handlung nötig) + berechneter Reifegrad; ISB-Freigabe (Abweichung nur mit Begründung → Audit-Log) - Cockpit-Detail mit Schutzbedarf-Banner + Live-Simulation der Stufe Verifiziert: Register mit abgeleiteter Stufe/Reifegrad/Konformität, Cockpit-Simulation (Tiers werden inaktiv), Gate bei sehr hoch ohne Audit, „Risiko anlegen" erzeugt R-005 im Risikomodul. Offen (Phase 2): Fragebogen-Builder, Self-Service-Portal, IT-Service- Detail mit RACI-Matrix, Klick im Asset-Inventar öffnet Cockpit, Scheduler-Automatisierung. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
32ab91efbf
commit
dc884062b2
@@ -13,7 +13,7 @@ const level = z.coerce.number().int().min(1).max(4);
|
||||
const assetSchema = z.object({
|
||||
name: z.string().trim().min(1).max(200),
|
||||
description: z.string().trim().max(5000).optional(),
|
||||
type: z.enum(["INFORMATION", "SYSTEM", "APPLICATION", "LOCATION", "SUPPLIER", "PERSON", "DATA"]),
|
||||
type: z.enum(["INFORMATION", "SYSTEM", "APPLICATION", "LOCATION", "SUPPLIER", "IT_SERVICE", "PERSON", "DATA"]),
|
||||
status: z.enum(["ACTIVE", "PLANNED", "RETIRED"]),
|
||||
ownerId: z.string().optional(),
|
||||
location: z.string().trim().max(200).optional(),
|
||||
|
||||
+136
-165
@@ -13,51 +13,55 @@ const level = z.coerce.number().int().min(1).max(4);
|
||||
const supplierSchema = z.object({
|
||||
name: z.string().trim().min(1).max(200),
|
||||
sector: z.string().trim().max(200).optional(),
|
||||
services: z.string().trim().max(2000).optional(),
|
||||
serviceDesc: z.string().trim().max(2000).optional(),
|
||||
criticality: level,
|
||||
dataCategories: z.string().optional(),
|
||||
confidentiality: level,
|
||||
integrity: level,
|
||||
availability: level,
|
||||
nis2Relevant: z.union([z.literal("on"), z.literal(null), z.string()]).optional(),
|
||||
status: z.enum(["ACTIVE", "ONBOARDING", "UNDER_REVIEW", "OFFBOARDED"]),
|
||||
nis2Relevant: z.string().optional(),
|
||||
lifecycle: z.enum(["ACTIVE", "ONBOARDING", "UNDER_REVIEW", "OFFBOARDED"]),
|
||||
contact: z.string().trim().max(200).optional(),
|
||||
nextReview: z.string().optional(),
|
||||
notes: z.string().trim().max(5000).optional(),
|
||||
});
|
||||
|
||||
function parseSupplier(formData: FormData) {
|
||||
function parse(formData: FormData) {
|
||||
const p = supplierSchema.parse({
|
||||
name: formData.get("name"),
|
||||
sector: formData.get("sector") || undefined,
|
||||
services: formData.get("services") || undefined,
|
||||
serviceDesc: formData.get("serviceDesc") || undefined,
|
||||
criticality: formData.get("criticality"),
|
||||
dataCategories: formData.get("dataCategories") || undefined,
|
||||
confidentiality: formData.get("confidentiality"),
|
||||
integrity: formData.get("integrity"),
|
||||
availability: formData.get("availability"),
|
||||
nis2Relevant: formData.get("nis2Relevant"),
|
||||
status: formData.get("status"),
|
||||
nis2Relevant: formData.get("nis2Relevant") || undefined,
|
||||
lifecycle: formData.get("lifecycle"),
|
||||
contact: formData.get("contact") || undefined,
|
||||
nextReview: formData.get("nextReview") || undefined,
|
||||
notes: formData.get("notes") || undefined,
|
||||
});
|
||||
return {
|
||||
name: p.name,
|
||||
sector: p.sector || null,
|
||||
services: p.services || null,
|
||||
criticality: p.criticality,
|
||||
dataCategories: p.dataCategories
|
||||
? p.dataCategories.split(",").map((s) => s.trim()).filter(Boolean)
|
||||
: [],
|
||||
confidentiality: p.confidentiality,
|
||||
integrity: p.integrity,
|
||||
availability: p.availability,
|
||||
nis2Relevant: p.nis2Relevant === "on",
|
||||
status: p.status,
|
||||
contact: p.contact || null,
|
||||
nextReview: p.nextReview ? new Date(p.nextReview) : null,
|
||||
notes: p.notes || null,
|
||||
asset: {
|
||||
name: p.name,
|
||||
confidentiality: p.confidentiality,
|
||||
integrity: p.integrity,
|
||||
availability: p.availability,
|
||||
},
|
||||
profile: {
|
||||
sector: p.sector || null,
|
||||
serviceDesc: p.serviceDesc || null,
|
||||
criticality: p.criticality,
|
||||
dataCategories: p.dataCategories
|
||||
? p.dataCategories.split(",").map((s) => s.trim()).filter(Boolean)
|
||||
: [],
|
||||
nis2Relevant: p.nis2Relevant === "on",
|
||||
lifecycle: p.lifecycle,
|
||||
contact: p.contact || null,
|
||||
nextReview: p.nextReview ? new Date(p.nextReview) : null,
|
||||
notes: p.notes || null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,216 +69,158 @@ export async function createSupplier(formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const { asset, profile } = parse(formData);
|
||||
|
||||
const data = parseSupplier(formData);
|
||||
const last = await prisma.supplier.aggregate({
|
||||
const created = await db.asset.create({
|
||||
data: {
|
||||
...asset,
|
||||
type: "SUPPLIER",
|
||||
tenantId: session.user.tenantId,
|
||||
createdBy: session.user.id,
|
||||
},
|
||||
});
|
||||
const last = await prisma.supplierProfile.aggregate({
|
||||
where: { tenantId: session.user.tenantId },
|
||||
_max: { refNo: true },
|
||||
});
|
||||
const supplier = await db.supplier.create({
|
||||
await db.supplierProfile.create({
|
||||
data: {
|
||||
...data,
|
||||
...profile,
|
||||
assetId: created.id,
|
||||
refNo: (last._max.refNo ?? 0) + 1,
|
||||
tenantId: session.user.tenantId,
|
||||
createdBy: session.user.id,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({
|
||||
tenantId: session.user.tenantId,
|
||||
actorId: session.user.id,
|
||||
action: "create",
|
||||
entity: "supplier",
|
||||
entityId: supplier.id,
|
||||
after: data,
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "supplier", entityId: created.id, after: { ...asset, ...profile } });
|
||||
revalidatePath("/suppliers");
|
||||
redirect(`/suppliers?edit=${supplier.id}`);
|
||||
revalidatePath("/assets");
|
||||
redirect(`/suppliers?edit=${created.id}`);
|
||||
}
|
||||
|
||||
export async function updateSupplier(supplierId: string, formData: FormData) {
|
||||
export async function updateSupplier(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
|
||||
const before = await db.supplier.findUnique({ where: { id: supplierId } });
|
||||
const before = await db.asset.findUnique({ where: { id: assetId }, include: { supplierProfile: true } });
|
||||
if (!before) throw new Error("Lieferant nicht gefunden");
|
||||
const data = parseSupplier(formData);
|
||||
await db.supplier.update({ where: { id: supplierId }, data });
|
||||
await writeAuditLog({
|
||||
tenantId: session.user.tenantId,
|
||||
actorId: session.user.id,
|
||||
action: "update",
|
||||
entity: "supplier",
|
||||
entityId: supplierId,
|
||||
before,
|
||||
after: data,
|
||||
});
|
||||
const { asset, profile } = parse(formData);
|
||||
await db.asset.update({ where: { id: assetId }, data: asset });
|
||||
await db.supplierProfile.update({ where: { assetId }, data: profile });
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "supplier", entityId: assetId, before, after: { ...asset, ...profile } });
|
||||
revalidatePath("/suppliers");
|
||||
redirect(`/suppliers?detail=${supplierId}`);
|
||||
revalidatePath("/assets");
|
||||
redirect(`/suppliers?detail=${assetId}`);
|
||||
}
|
||||
|
||||
export async function deleteSupplier(supplierId: string) {
|
||||
export async function deleteSupplier(assetId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const before = await db.supplier.findUnique({ where: { id: supplierId } });
|
||||
const before = await db.asset.findUnique({ where: { id: assetId } });
|
||||
if (!before) throw new Error("Lieferant nicht gefunden");
|
||||
await db.supplier.delete({ where: { id: supplierId } });
|
||||
await writeAuditLog({
|
||||
tenantId: session.user.tenantId,
|
||||
actorId: session.user.id,
|
||||
action: "delete",
|
||||
entity: "supplier",
|
||||
entityId: supplierId,
|
||||
before,
|
||||
});
|
||||
await db.asset.delete({ where: { id: assetId } }); // Profil + Kinder kaskadieren
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "delete", entity: "supplier", entityId: assetId, before });
|
||||
revalidatePath("/suppliers");
|
||||
revalidatePath("/assets");
|
||||
redirect("/suppliers");
|
||||
}
|
||||
|
||||
/** Generischer Helper zum Anlegen einer Kind-Entität (mit Tenant/Owner-Prüfung). */
|
||||
async function ensureSupplier(session: Awaited<ReturnType<typeof requireSession>>, supplierId: string) {
|
||||
async function ensure(session: Awaited<ReturnType<typeof requireSession>>, assetId: string) {
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const count = await db.supplier.count({ where: { id: supplierId } });
|
||||
if (count !== 1) throw new Error("Lieferant nicht gefunden");
|
||||
if ((await db.asset.count({ where: { id: assetId } })) !== 1) throw new Error("Nicht gefunden");
|
||||
return db;
|
||||
}
|
||||
const optDate = (v: FormDataEntryValue | null) => (v && String(v) ? new Date(String(v)) : null);
|
||||
|
||||
const optDate = (v: FormDataEntryValue | null) =>
|
||||
v && String(v) ? new Date(String(v)) : null;
|
||||
|
||||
export async function addAssessment(supplierId: string, formData: FormData) {
|
||||
export async function addContract(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensureSupplier(session, supplierId);
|
||||
await db.supplierAssessment.create({
|
||||
data: {
|
||||
supplierId,
|
||||
tenantId: session.user.tenantId,
|
||||
type: z.enum(["QUESTIONNAIRE", "SELF_ASSESSMENT", "AUDIT"]).parse(formData.get("type")),
|
||||
status: z
|
||||
.enum(["SENT", "RECEIVED", "EVALUATED", "OVERDUE"])
|
||||
.parse(formData.get("status") ?? "SENT"),
|
||||
score: formData.get("score") ? Number(formData.get("score")) : null,
|
||||
date: optDate(formData.get("date")),
|
||||
nextReview: optDate(formData.get("nextReview")),
|
||||
result: (formData.get("result") as string)?.trim() || null,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "supplier_assessment", entityId: supplierId });
|
||||
revalidatePath("/suppliers");
|
||||
}
|
||||
|
||||
export async function addContract(supplierId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensureSupplier(session, supplierId);
|
||||
const db = await ensure(session, assetId);
|
||||
const bool = (n: string) => formData.get(n) === "on";
|
||||
await db.contract.create({
|
||||
data: {
|
||||
supplierId,
|
||||
assetId,
|
||||
tenantId: session.user.tenantId,
|
||||
type: (formData.get("type") as string) || "service",
|
||||
avDpa: bool("avDpa"),
|
||||
securityClauses: bool("securityClauses"),
|
||||
flowdown: bool("flowdown"),
|
||||
customerTransparency: bool("customerTransparency"),
|
||||
customerRequirementsPassed: bool("customerRequirementsPassed"),
|
||||
validFrom: optDate(formData.get("validFrom")),
|
||||
validTo: optDate(formData.get("validTo")),
|
||||
reference: (formData.get("reference") as string)?.trim() || null,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "contract", entityId: supplierId });
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "contract", entityId: assetId });
|
||||
revalidatePath("/suppliers");
|
||||
}
|
||||
|
||||
export async function addNda(supplierId: string, formData: FormData) {
|
||||
export async function addNda(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensureSupplier(session, supplierId);
|
||||
const db = await ensure(session, assetId);
|
||||
await db.nda.create({
|
||||
data: {
|
||||
supplierId,
|
||||
assetId,
|
||||
tenantId: session.user.tenantId,
|
||||
parties: (formData.get("parties") as string)?.trim() || null,
|
||||
infoScope: (formData.get("infoScope") as string)?.trim() || null,
|
||||
subject: (formData.get("subject") as string)?.trim() || null,
|
||||
validFrom: optDate(formData.get("validFrom")),
|
||||
validTo: optDate(formData.get("validTo")),
|
||||
obligations: (formData.get("obligations") as string)?.trim() || null,
|
||||
extensionStatus: (formData.get("extensionStatus") as string)?.trim() || null,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "nda", entityId: supplierId });
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "nda", entityId: assetId });
|
||||
revalidatePath("/suppliers");
|
||||
}
|
||||
|
||||
export async function addEvidence(supplierId: string, formData: FormData) {
|
||||
export async function addEvidence(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensureSupplier(session, supplierId);
|
||||
const db = await ensure(session, assetId);
|
||||
await db.supplierEvidence.create({
|
||||
data: {
|
||||
supplierId,
|
||||
assetId,
|
||||
tenantId: session.user.tenantId,
|
||||
kind: z
|
||||
.enum(["CERTIFICATE", "TISAX_LABEL", "ATTESTATION", "AUDIT_REPORT", "SELF_ASSESSMENT"])
|
||||
.parse(formData.get("kind")),
|
||||
kind: z.enum(["CERTIFICATE", "TISAX_LABEL", "ATTESTATION", "AUDIT_REPORT", "SELF_ASSESSMENT"]).parse(formData.get("kind")),
|
||||
name: (formData.get("name") as string)?.trim() || null,
|
||||
protectsCia: (formData.get("protectsCia") as string)?.trim() || null,
|
||||
validTo: optDate(formData.get("validTo")),
|
||||
adequacyChecked: formData.get("adequacyChecked") === "on",
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "supplier_evidence", entityId: supplierId });
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "supplier_evidence", entityId: assetId });
|
||||
revalidatePath("/suppliers");
|
||||
}
|
||||
|
||||
export async function addResponsibility(supplierId: string, formData: FormData) {
|
||||
export async function addAssessment(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensureSupplier(session, supplierId);
|
||||
await db.serviceResponsibility.create({
|
||||
const db = await ensure(session, assetId);
|
||||
await db.supplierAssessment.create({
|
||||
data: {
|
||||
supplierId,
|
||||
assetId,
|
||||
tenantId: session.user.tenantId,
|
||||
itService: z.string().trim().min(1).parse(formData.get("itService")),
|
||||
requirement: z.string().trim().min(1).parse(formData.get("requirement")),
|
||||
responsibleParty: z
|
||||
.enum(["CLIENT", "SUPPLIER", "SHARED"])
|
||||
.parse(formData.get("responsibleParty") ?? "SHARED"),
|
||||
isaApplicability: (formData.get("isaApplicability") as string)?.trim() || null,
|
||||
evidence: (formData.get("evidence") as string)?.trim() || null,
|
||||
integratedLocalControls: (formData.get("integratedLocalControls") as string)?.trim() || null,
|
||||
type: z.enum(["QUESTIONNAIRE", "SELF_ASSESSMENT", "AUDIT"]).parse(formData.get("type")),
|
||||
score: formData.get("score") ? Number(formData.get("score")) : null,
|
||||
date: optDate(formData.get("date")),
|
||||
nextReview: optDate(formData.get("nextReview")),
|
||||
result: (formData.get("result") as string)?.trim() || null,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "service_responsibility", entityId: supplierId });
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "supplier_assessment", entityId: assetId });
|
||||
revalidatePath("/suppliers");
|
||||
}
|
||||
|
||||
export async function addSubcontractor(supplierId: string, formData: FormData) {
|
||||
export async function addDecision(assetId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensureSupplier(session, supplierId);
|
||||
await db.subcontractor.create({
|
||||
data: {
|
||||
supplierId,
|
||||
tenantId: session.user.tenantId,
|
||||
name: z.string().trim().min(1).parse(formData.get("name")),
|
||||
flowdownObligation: formData.get("flowdownObligation") === "on",
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "subcontractor", entityId: supplierId });
|
||||
revalidatePath("/suppliers");
|
||||
}
|
||||
|
||||
export async function addDecision(supplierId: string, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensureSupplier(session, supplierId);
|
||||
const db = await ensure(session, assetId);
|
||||
await db.managementDecision.create({
|
||||
data: {
|
||||
supplierId,
|
||||
assetId,
|
||||
tenantId: session.user.tenantId,
|
||||
reasonNoAudit: z.string().trim().min(1).parse(formData.get("reasonNoAudit")),
|
||||
decision: z.string().trim().min(1).parse(formData.get("decision")),
|
||||
@@ -282,53 +228,78 @@ export async function addDecision(supplierId: string, formData: FormData) {
|
||||
recordRef: (formData.get("recordRef") as string)?.trim() || null,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "management_decision", entityId: supplierId });
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "management_decision", entityId: assetId });
|
||||
revalidatePath("/suppliers");
|
||||
}
|
||||
|
||||
export async function saveMaturity(supplierId: string, controlId: string, formData: FormData) {
|
||||
await setControlMaturity(supplierId, controlId, Number(formData.get("maturity")));
|
||||
}
|
||||
|
||||
export async function setControlMaturity(supplierId: string, controlId: string, maturity: number) {
|
||||
/** Gate-Kompensation: echtes Risiko im Risikomodul anlegen und mit dem Lieferanten verknüpfen. */
|
||||
export async function createGateRisk(assetId: string) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensureSupplier(session, supplierId);
|
||||
const m = z.coerce.number().int().min(0).max(5).parse(maturity);
|
||||
await db.supplierControlMaturity.upsert({
|
||||
where: { supplierId_controlId: { supplierId, controlId } },
|
||||
update: { maturity: m },
|
||||
create: { supplierId, controlId, maturity: m, tenantId: session.user.tenantId },
|
||||
requirePermission(session, "risk:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const asset = await db.asset.findUnique({ where: { id: assetId } });
|
||||
if (!asset) throw new Error("Lieferant nicht gefunden");
|
||||
|
||||
const last = await prisma.risk.aggregate({ where: { tenantId: session.user.tenantId }, _max: { refNo: true } });
|
||||
const risk = await db.risk.create({
|
||||
data: {
|
||||
tenantId: session.user.tenantId,
|
||||
refNo: (last._max.refNo ?? 0) + 1,
|
||||
title: `Third-Party-Risiko: ${asset.name}`,
|
||||
description: "Sehr hoher Schutzbedarf ohne gültiges Third-Party-Audit/TISAX-Label — Kompensationsrisiko (VDA-ISA 6.1.1).",
|
||||
threat: "Ausfall/Kompromittierung eines kritischen Lieferanten",
|
||||
vulnerability: "Fehlender unabhängiger Sicherheitsnachweis",
|
||||
likelihood: 3,
|
||||
impact: 4,
|
||||
score: 12,
|
||||
treatment: "MITIGATE",
|
||||
status: "IN_TREATMENT",
|
||||
createdBy: session.user.id,
|
||||
},
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "supplier_control_maturity", entityId: supplierId, after: { controlId, maturity: m } });
|
||||
await db.riskAsset.create({ data: { tenantId: session.user.tenantId, riskId: risk.id, assetId } });
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "create", entity: "risk", entityId: risk.id, after: { gateRiskFor: assetId } });
|
||||
revalidatePath("/suppliers");
|
||||
revalidatePath("/risks");
|
||||
redirect(`/risks?detail=${risk.id}`);
|
||||
}
|
||||
|
||||
/** ISB-Freigabe des Reifegrads (Abweichung nur mit Begründung → Audit-Log). */
|
||||
export async function approveMaturity(assetId: string, computed: number, formData: FormData) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = await ensure(session, assetId);
|
||||
const isbValue = z.coerce.number().min(0).max(3).parse(formData.get("isbValue"));
|
||||
const justification = (formData.get("justification") as string)?.trim() || null;
|
||||
if (Math.abs(isbValue - computed) > 0.01 && !justification) {
|
||||
throw new Error("Abweichung vom berechneten Wert erfordert eine Begründung.");
|
||||
}
|
||||
await db.maturityAssessment.upsert({
|
||||
where: { assetId },
|
||||
update: { computedValue: computed, isbValue, isbJustification: justification, approvedBy: session.user.name, approvedAt: new Date() },
|
||||
create: { assetId, tenantId: session.user.tenantId, computedValue: computed, isbValue, isbJustification: justification, approvedBy: session.user.name, approvedAt: new Date() },
|
||||
});
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "update", entity: "maturity_assessment", entityId: assetId, after: { computed, isbValue, justification } });
|
||||
revalidatePath("/suppliers");
|
||||
}
|
||||
|
||||
/** Generisches Löschen einer Kind-Entität. */
|
||||
export async function deleteChild(
|
||||
kind:
|
||||
| "assessment"
|
||||
| "contract"
|
||||
| "nda"
|
||||
| "evidence"
|
||||
| "responsibility"
|
||||
| "subcontractor"
|
||||
| "decision",
|
||||
kind: "contract" | "nda" | "evidence" | "assessment" | "decision" | "subcontractor",
|
||||
id: string
|
||||
) {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "supplier:write");
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const map = {
|
||||
assessment: db.supplierAssessment,
|
||||
contract: db.contract,
|
||||
nda: db.nda,
|
||||
evidence: db.supplierEvidence,
|
||||
responsibility: db.serviceResponsibility,
|
||||
subcontractor: db.subcontractor,
|
||||
assessment: db.supplierAssessment,
|
||||
decision: db.managementDecision,
|
||||
subcontractor: db.subcontractor,
|
||||
} as const;
|
||||
// @ts-expect-error dynamischer Delegate-Zugriff
|
||||
// @ts-expect-error dynamischer Delegate
|
||||
await map[kind].delete({ where: { id } });
|
||||
await writeAuditLog({ tenantId: session.user.tenantId, actorId: session.user.id, action: "delete", entity: `supplier_${kind}`, entityId: id });
|
||||
revalidatePath("/suppliers");
|
||||
|
||||
+4
-4
@@ -37,16 +37,16 @@ const TENANT_MODELS = new Set<string>([
|
||||
"RiskAsset",
|
||||
"Measure",
|
||||
"RiskMeasure",
|
||||
"Supplier",
|
||||
"SupplierAsset",
|
||||
"SupplierProfile",
|
||||
"ITServiceProfile",
|
||||
"SupplierAssessment",
|
||||
"Contract",
|
||||
"Nda",
|
||||
"SupplierEvidence",
|
||||
"ServiceResponsibility",
|
||||
"ServiceControlResponsibility",
|
||||
"Subcontractor",
|
||||
"ManagementDecision",
|
||||
"SupplierControlMaturity",
|
||||
"MaturityAssessment",
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@ export type GraphNodeKind =
|
||||
| "APPLICATION"
|
||||
| "LOCATION"
|
||||
| "SUPPLIER"
|
||||
| "IT_SERVICE"
|
||||
| "PERSON"
|
||||
| "DATA";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user