Files
certvia/src/lib/supplier.ts
T
msolarczekandClaude Opus 4.8 2706424c08 Lieferanten-/IT-Service-Cockpit direkt auf der Asset-Seite öffnen
Statt beim Klick auf ein Lieferanten-/IT-Service-Asset zur Lieferanten-Seite zu
wechseln, wird das Fach-Cockpit jetzt als Popup auf /assets gerendert; Schließen,
Bearbeiten, Speichern und Löschen bleiben auf der Asset-Seite.

- backHref-Prop an SupplierDetail/Edit- und ServiceDetail/Edit-Modal: alle
  Schließen-/Bearbeiten-/Abbrechen-Links leiten sich davon ab (Default /suppliers)
- withQuery-Helper (lib/supplier) für korrekte ?/&-Verkettung
- updateSupplier/updateService lesen returnTo (verstecktes Feld) und leiten dorthin
  zurück; deleteSupplier/deleteService bekommen returnTo-Parameter
- Kind-Actions revalidieren zusätzlich /assets, damit das Popup dort aktuell bleibt
- SUPPLIER_INCLUDE/SERVICE_INCLUDE in lib/supplier-include ausgelagert und von
  beiden Seiten genutzt
- assets/page.tsx bestimmt die Popup-Art vorab (supplier/service/asset) und rendert
  das passende Cockpit mit backHref="/assets"; profillose Alt-Assets bleiben Assets

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 13:55:46 +02:00

188 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Anforderungs-Engine für Lieferanten (VDA-ISA 2027 Kap. 6, NIS2 Art. 21(2)(d)).
* Schutzbedarf → Anforderungsstufe; flacher Anforderungskatalog mit
* Status-Ableitung aus den hinterlegten Objekten; Gate-Logik & Konformität.
*/
export function supplierRef(refNo: number): string {
return `L-${String(refNo).padStart(3, "0")}`;
}
export function serviceRef(refNo: number): string {
return `S-${String(refNo).padStart(3, "0")}`;
}
/**
* Hängt einen Query-Parameter an einen Basis-Pfad an und wählt automatisch
* das richtige Trennzeichen (`?` oder `&`). Erlaubt, Detail-/Bearbeiten-Popups
* sowohl auf /suppliers als auch auf /assets zu öffnen (backHref-Muster).
*/
export function withQuery(base: string, key: string, val: string): string {
return `${base}${base.includes("?") ? "&" : "?"}${key}=${val}`;
}
export const SUPPLIER_LIFECYCLE_TONE = {
ACTIVE: "ok",
ONBOARDING: "info",
UNDER_REVIEW: "warn",
OFFBOARDED: "mut",
} as const;
const DAY = 24 * 3600 * 1000;
export function isExpired(d: Date | null | undefined): boolean {
return !!d && d.getTime() < Date.now();
}
export function isExpiring(d: Date | null | undefined, days = 90): boolean {
return !!d && d.getTime() - Date.now() < days * DAY;
}
export function isReviewDue(d: Date | null | undefined, days = 30): boolean {
return !!d && d.getTime() < Date.now() + days * DAY;
}
/** Schutzbedarf 1(normal)/2(hoch)/3(sehr hoch) aus der höchsten C/I/A-Stufe. */
export type ProtectionLevel = 1 | 2 | 3;
export function protectionLevel(maxCia: number): ProtectionLevel {
if (maxCia >= 4) return 3;
if (maxCia >= 3) return 2;
return 1;
}
export const PROTECTION_LABEL: Record<ProtectionLevel, string> = {
1: "normal",
2: "hoch",
3: "sehr hoch",
};
/** Kontext für die Statusableitung — aus dem Lieferanten-Datensatz berechnet. */
export type ReqContext = {
hasValidNda: boolean;
ndaMonitored: boolean;
hasSecurityContract: boolean;
customerReqPassed: boolean;
hasFlowdown: boolean;
hasAssessment: boolean;
hasReviewedAssessment: boolean;
hasAdequateEvidence: boolean;
complianceMonitored: boolean;
hasLinkedRisk: boolean;
hasManagementDecision: boolean;
hasAudit: boolean;
};
export type Tier = "must" | "should" | "high" | "veryhigh";
export type ReqStatus = "met" | "partial" | "open" | "na";
export type ReqTheme = "contract" | "assessment" | "transparency";
export type Requirement = {
key: string;
label: string;
control: "6.1.1" | "6.1.2" | "6.1.3";
tier: Tier;
theme: ReqTheme;
met: (c: ReqContext) => boolean;
refLabel?: (c: ReqContext) => string | null;
};
export const REQUIREMENTS: Requirement[] = [
// Vertrag & Geheimhaltung
{ key: "nda_valid", label: "NDA abgeschlossen und gültig", control: "6.1.2", tier: "must", theme: "contract", met: (c) => c.hasValidNda },
{ key: "infosec_contract", label: "Vertragliche InfoSec-Vereinbarung geschlossen", control: "6.1.1", tier: "must", theme: "contract", met: (c) => c.hasSecurityContract },
{ key: "customer_req_passed", label: "Kundenanforderungen an Partner weitergegeben", control: "6.1.1", tier: "must", theme: "contract", met: (c) => c.customerReqPassed },
{ key: "flowdown", label: "Flow-down an Subunternehmer verpflichtet", control: "6.1.1", tier: "should", theme: "contract", met: (c) => c.hasFlowdown },
{ key: "nda_monitoring", label: "NDA-Fristen-Monitoring aktiv", control: "6.1.2", tier: "should", theme: "contract", met: (c) => c.ndaMonitored },
// Bewertung & Nachweise
{ key: "risk_assessment", label: "Sicherheits-Risikobewertung durchgeführt", control: "6.1.1", tier: "must", theme: "assessment", met: (c) => c.hasAssessment || c.hasLinkedRisk },
{ key: "reports_reviewed", label: "Service-Reports & Dokumente geprüft", control: "6.1.1", tier: "should", theme: "assessment", met: (c) => c.hasReviewedAssessment },
{ key: "adequate_evidence", label: "Nachweis angemessenes Sicherheitsniveau (Zertifikat/Audit)", control: "6.1.1", tier: "high", theme: "assessment", met: (c) => c.hasAdequateEvidence },
{ key: "compliance_monitored", label: "Compliance-Grad dokumentiert & anlassbezogen überwacht", control: "6.1.1", tier: "high", theme: "assessment", met: (c) => c.complianceMonitored },
// Transparenz / sehr hoch
{ key: "customer_transparency", label: "Kundentransparenz zu Lieferkettenrisiken erfüllt", control: "6.1.1", tier: "veryhigh", theme: "transparency", met: (c) => c.customerReqPassed },
];
const TIER_MIN_LEVEL: Record<Tier, ProtectionLevel> = { must: 1, should: 1, high: 2, veryhigh: 3 };
export function tierActive(tier: Tier, level: ProtectionLevel): boolean {
return level >= TIER_MIN_LEVEL[tier];
}
export function requirementStatus(req: Requirement, ctx: ReqContext, level: ProtectionLevel): ReqStatus {
if (!tierActive(req.tier, level)) return "na";
return req.met(ctx) ? "met" : "open";
}
/** Gate: sehr hoher Schutzbedarf ohne gültiges Audit/Label → Kompensation nötig. */
export function gateActive(level: ProtectionLevel, ctx: ReqContext): boolean {
return level === 3 && !ctx.hasAudit;
}
export function gateSatisfied(ctx: ReqContext): boolean {
return ctx.hasAudit || (ctx.hasManagementDecision && ctx.hasLinkedRisk);
}
export type Conformity = "conform" | "partial" | "gaps" | "action";
export function conformity(level: ProtectionLevel, ctx: ReqContext): Conformity {
if (gateActive(level, ctx) && !gateSatisfied(ctx)) return "action";
const active = REQUIREMENTS.filter((r) => tierActive(r.tier, level));
const met = active.filter((r) => r.met(ctx)).length;
if (met === active.length) return "conform";
if (met >= Math.ceil(active.length * 0.6)) return "partial";
return "gaps";
}
export const CONFORMITY_TONE: Record<Conformity, "ok" | "warn" | "risk"> = {
conform: "ok",
partial: "warn",
gaps: "risk",
action: "risk",
};
export const CONFORMITY_LABEL: Record<Conformity, string> = {
conform: "Konform",
partial: "Teilweise",
gaps: "Lücken offen",
action: "Handlung nötig",
};
/** Berechneter Reifegrad (03) aus Erfüllungsgrad der aktiven Anforderungen. */
export function computedMaturity(level: ProtectionLevel, ctx: ReqContext): number {
const active = REQUIREMENTS.filter((r) => tierActive(r.tier, level));
if (active.length === 0) return 0;
const met = active.filter((r) => r.met(ctx)).length;
return Math.round((met / active.length) * 3 * 10) / 10;
}
export const TARGET_MATURITY = 3.0;
type CtxInput = {
contracts: { securityClauses: boolean; flowdown: boolean; customerRequirementsPassed: boolean }[];
ndas: { validTo: Date | null }[];
evidence: { kind: string; adequacyChecked: boolean; validTo: Date | null }[];
assessments: { result: string | null; nextReview: Date | null }[];
subcontractors: { flowdownObligation: boolean }[];
nextReview: Date | null;
linkedRiskCount: number;
decisionCount: number;
};
/** Baut den ReqContext aus den hinterlegten Lieferanten-Objekten. */
export function buildReqContext(d: CtxInput): ReqContext {
const hasAudit = d.evidence.some(
(e) =>
(e.kind === "AUDIT_REPORT" || e.kind === "TISAX_LABEL") &&
e.adequacyChecked &&
!isExpired(e.validTo)
);
const hasAdequateEvidence = d.evidence.some((e) => e.adequacyChecked && !isExpired(e.validTo));
return {
hasValidNda: d.ndas.some((n) => !isExpired(n.validTo)),
ndaMonitored: d.ndas.length > 0 && !!d.nextReview,
hasSecurityContract: d.contracts.some((c) => c.securityClauses),
customerReqPassed: d.contracts.some((c) => c.customerRequirementsPassed),
hasFlowdown: d.contracts.some((c) => c.flowdown) || d.subcontractors.some((s) => s.flowdownObligation),
hasAssessment: d.assessments.length > 0,
hasReviewedAssessment: d.assessments.some((a) => !!a.result),
hasAdequateEvidence,
complianceMonitored: d.assessments.some((a) => !!a.nextReview),
hasLinkedRisk: d.linkedRiskCount > 0,
hasManagementDecision: d.decisionCount > 0,
hasAudit,
};
}