diff --git a/messages/de.json b/messages/de.json index 41c114a..fc80565 100644 --- a/messages/de.json +++ b/messages/de.json @@ -341,5 +341,122 @@ "legCrit": "Kritisch (K≥3)", "empty": "Noch keine Prozesse/Assets für einen Graphen vorhanden.", "openGraph": "Im Abhängigkeitsgraph anzeigen" + }, + "suppliers": { + "title": "Lieferanten & Dienstleister", + "sub": "VDA-ISA 2027 Kap. 6 · NIS2 Lieferkette (Art. 21(2)(d))", + "crumb": "Fachdaten", + "new": "Lieferant", + "newSupplier": "Neuer Lieferant", + "ref": "ID", + "kpiTotal": "Lieferanten", + "kpiNis2": "NIS2-relevant", + "kpiExpiring": "Ablaufend (90 T.)", + "kpiReviews": "Reviews fällig", + "name": "Name", + "sector": "Sektor", + "services": "Leistung / IT-Services", + "criticality": "Kritikalität", + "dataCategories": "Datenkategorien (kommagetrennt)", + "protection": "Schutzbedarf", + "nis2": "NIS2-relevant (Lieferkette)", + "status": "Status", + "contact": "Kontakt", + "nextReview": "Nächstes Review", + "notes": "Anmerkungen", + "empty": "Keine Lieferanten erfasst.", + "detailSub": "Bewertung, Verträge, Nachweise & Verantwortung", + "createTitle": "Lieferant anlegen", + "editTitle": "Lieferant bearbeiten", + "masterPill": "■ Stammdaten", + "catalog": "VDA-ISA 2027 · Kap. 6 Supplier Relationships", + "catalogNote": "Reifegrad je Prüfziel (Ziel 3)", + "target": "Ziel", + "maturity": "Reifegrad", + "references": "Referenzen", + "objective": "Zielbild", + "must": "Muss", + "should": "Soll", + "high": "Hoher Schutzbedarf", + "veryHigh": "Sehr hoher Schutzbedarf", + "sga": "Simplified Group Assessment", + "assessments": "Sicherheitsbewertungen", + "addAssessment": "Bewertung hinzufügen", + "score": "Score", + "result": "Ergebnis", + "type": "Typ", + "date": "Datum", + "contracts": "Verträge", + "addContract": "Vertrag hinzufügen", + "avDpa": "AV/DPA (Art. 28)", + "securityClauses": "Sicherheitsklauseln", + "flowdown": "Flow-down (Subunternehmer)", + "customerTransparency": "Kunden-Transparenz", + "validFrom": "Gültig ab", + "validTo": "Gültig bis", + "reference": "Referenz", + "ndas": "NDA / Geheimhaltung", + "addNda": "NDA hinzufügen", + "parties": "Parteien", + "infoScope": "Informationsart", + "subject": "Gegenstand", + "obligations": "Pflichten", + "extensionStatus": "Verlängerung", + "evidence": "Nachweise & Assurance", + "addEvidence": "Nachweis hinzufügen", + "kind": "Art", + "protectsCia": "Deckt (C/I/A)", + "adequacy": "Angemessenheit geprüft", + "expires": "läuft ab", + "raci": "Verantwortung (Shared Responsibility)", + "addRaci": "Zuordnung hinzufügen", + "itService": "IT-Service", + "requirement": "Anforderung", + "responsible": "Verantwortlich", + "isaApplicability": "ISA-Anwendbarkeit", + "localControls": "Lokale Schutzmaßnahmen", + "subcontractors": "Subunternehmer (4th Party)", + "addSub": "Subunternehmer hinzufügen", + "flowdownObl": "Flow-down-Pflicht", + "decision": "Risikobasierte Managemententscheidung", + "addDecision": "Entscheidung protokollieren", + "reasonNoAudit": "Grund (kein Audit/Label)", + "decisionText": "Entscheidung", + "decidedBy": "Entschieden von", + "recordRef": "Aktenzeichen", + "decisionNeeded": "Kein Third-Party-Audit/TISAX-Label mit geprüfter Angemessenheit vorhanden — eine dokumentierte risikobasierte Managemententscheidung ist erforderlich.", + "assets": "Betroffene Assets", + "close": "Schließen", + "none": "—", + "add": "Hinzufügen" + }, + "assessmentType": { + "QUESTIONNAIRE": "Fragebogen", + "SELF_ASSESSMENT": "Self-Assessment", + "AUDIT": "Audit" + }, + "assessmentStatus": { + "SENT": "Versendet", + "RECEIVED": "Eingegangen", + "EVALUATED": "Bewertet", + "OVERDUE": "Überfällig" + }, + "evidenceKind": { + "CERTIFICATE": "Zertifikat", + "TISAX_LABEL": "TISAX-Label", + "ATTESTATION": "Attestierung", + "AUDIT_REPORT": "Auditbericht", + "SELF_ASSESSMENT": "Self-Assessment" + }, + "responsibleParty": { + "CLIENT": "Kunde", + "SUPPLIER": "Lieferant", + "SHARED": "Geteilt" + }, + "supplierStatus": { + "ACTIVE": "Aktiv", + "ONBOARDING": "Onboarding", + "UNDER_REVIEW": "In Prüfung", + "OFFBOARDED": "Beendet" } } \ No newline at end of file diff --git a/messages/en.json b/messages/en.json index 6b52c6b..97e6d53 100644 --- a/messages/en.json +++ b/messages/en.json @@ -341,5 +341,122 @@ "legCrit": "Critical (K≥3)", "empty": "No processes/assets available for a graph yet.", "openGraph": "Show in dependency graph" + }, + "suppliers": { + "title": "Suppliers & service providers", + "sub": "VDA-ISA 2027 ch. 6 · NIS2 supply chain (Art. 21(2)(d))", + "crumb": "Core data", + "new": "Supplier", + "newSupplier": "New supplier", + "ref": "ID", + "kpiTotal": "Suppliers", + "kpiNis2": "NIS2-relevant", + "kpiExpiring": "Expiring (90 d)", + "kpiReviews": "Reviews due", + "name": "Name", + "sector": "Sector", + "services": "Service / IT services", + "criticality": "Criticality", + "dataCategories": "Data categories (comma-separated)", + "protection": "Protection needs", + "nis2": "NIS2-relevant (supply chain)", + "status": "Status", + "contact": "Contact", + "nextReview": "Next review", + "notes": "Notes", + "empty": "No suppliers recorded.", + "detailSub": "Assessment, contracts, evidence & responsibility", + "createTitle": "Create supplier", + "editTitle": "Edit supplier", + "masterPill": "■ Master data", + "catalog": "VDA-ISA 2027 · ch. 6 Supplier Relationships", + "catalogNote": "Maturity per objective (target 3)", + "target": "Target", + "maturity": "Maturity", + "references": "References", + "objective": "Objective", + "must": "Must", + "should": "Should", + "high": "High protection", + "veryHigh": "Very high protection", + "sga": "Simplified Group Assessment", + "assessments": "Security assessments", + "addAssessment": "Add assessment", + "score": "Score", + "result": "Result", + "type": "Type", + "date": "Date", + "contracts": "Contracts", + "addContract": "Add contract", + "avDpa": "DPA (Art. 28)", + "securityClauses": "Security clauses", + "flowdown": "Flow-down (subcontractors)", + "customerTransparency": "Customer transparency", + "validFrom": "Valid from", + "validTo": "Valid to", + "reference": "Reference", + "ndas": "NDA / non-disclosure", + "addNda": "Add NDA", + "parties": "Parties", + "infoScope": "Information type", + "subject": "Subject", + "obligations": "Obligations", + "extensionStatus": "Extension", + "evidence": "Evidence & assurance", + "addEvidence": "Add evidence", + "kind": "Kind", + "protectsCia": "Covers (C/I/A)", + "adequacy": "Adequacy checked", + "expires": "expires", + "raci": "Responsibility (shared responsibility)", + "addRaci": "Add assignment", + "itService": "IT service", + "requirement": "Requirement", + "responsible": "Responsible", + "isaApplicability": "ISA applicability", + "localControls": "Local controls", + "subcontractors": "Subcontractors (4th party)", + "addSub": "Add subcontractor", + "flowdownObl": "Flow-down obligation", + "decision": "Risk-based management decision", + "addDecision": "Record decision", + "reasonNoAudit": "Reason (no audit/label)", + "decisionText": "Decision", + "decidedBy": "Decided by", + "recordRef": "Record ref", + "decisionNeeded": "No third-party audit/TISAX label with checked adequacy present — a documented risk-based management decision is required.", + "assets": "Affected assets", + "close": "Close", + "none": "—", + "add": "Add" + }, + "assessmentType": { + "QUESTIONNAIRE": "Questionnaire", + "SELF_ASSESSMENT": "Self-assessment", + "AUDIT": "Audit" + }, + "assessmentStatus": { + "SENT": "Sent", + "RECEIVED": "Received", + "EVALUATED": "Evaluated", + "OVERDUE": "Overdue" + }, + "evidenceKind": { + "CERTIFICATE": "Certificate", + "TISAX_LABEL": "TISAX label", + "ATTESTATION": "Attestation", + "AUDIT_REPORT": "Audit report", + "SELF_ASSESSMENT": "Self-assessment" + }, + "responsibleParty": { + "CLIENT": "Client", + "SUPPLIER": "Supplier", + "SHARED": "Shared" + }, + "supplierStatus": { + "ACTIVE": "Active", + "ONBOARDING": "Onboarding", + "UNDER_REVIEW": "Under review", + "OFFBOARDED": "Offboarded" } } \ No newline at end of file diff --git a/prisma/migrations/20260703081059_suppliers/migration.sql b/prisma/migrations/20260703081059_suppliers/migration.sql new file mode 100644 index 0000000..1cc6df3 --- /dev/null +++ b/prisma/migrations/20260703081059_suppliers/migration.sql @@ -0,0 +1,260 @@ +-- CreateEnum +CREATE TYPE "SupplierStatus" AS ENUM ('ACTIVE', 'ONBOARDING', 'UNDER_REVIEW', 'OFFBOARDED'); + +-- CreateEnum +CREATE TYPE "AssessmentType" AS ENUM ('QUESTIONNAIRE', 'SELF_ASSESSMENT', 'AUDIT'); + +-- CreateEnum +CREATE TYPE "AssessmentStatus" AS ENUM ('SENT', 'RECEIVED', 'EVALUATED', 'OVERDUE'); + +-- CreateEnum +CREATE TYPE "EvidenceKind" AS ENUM ('CERTIFICATE', 'TISAX_LABEL', 'ATTESTATION', 'AUDIT_REPORT', 'SELF_ASSESSMENT'); + +-- CreateEnum +CREATE TYPE "ResponsibleParty" AS ENUM ('CLIENT', 'SUPPLIER', 'SHARED'); + +-- CreateTable +CREATE TABLE "suppliers" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "ref_no" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "sector" TEXT, + "services" TEXT, + "criticality" INTEGER NOT NULL DEFAULT 1, + "data_categories" TEXT[] DEFAULT ARRAY[]::TEXT[], + "confidentiality" INTEGER NOT NULL DEFAULT 1, + "integrity" INTEGER NOT NULL DEFAULT 1, + "availability" INTEGER NOT NULL DEFAULT 1, + "nis2_relevant" BOOLEAN NOT NULL DEFAULT false, + "status" "SupplierStatus" NOT NULL DEFAULT 'ACTIVE', + "contact" TEXT, + "next_review" TIMESTAMP(3), + "notes" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + "created_by" TEXT, + + CONSTRAINT "suppliers_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "supplier_assets" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "supplier_id" TEXT NOT NULL, + "asset_id" TEXT NOT NULL, + + CONSTRAINT "supplier_assets_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "supplier_assessments" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "supplier_id" TEXT NOT NULL, + "type" "AssessmentType" NOT NULL, + "status" "AssessmentStatus" NOT NULL DEFAULT 'SENT', + "score" INTEGER, + "date" TIMESTAMP(3), + "next_review" TIMESTAMP(3), + "result" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "supplier_assessments_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "contracts" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "supplier_id" TEXT NOT NULL, + "type" TEXT NOT NULL DEFAULT 'service', + "av_dpa" BOOLEAN NOT NULL DEFAULT false, + "security_clauses" BOOLEAN NOT NULL DEFAULT false, + "flowdown" BOOLEAN NOT NULL DEFAULT false, + "customer_transparency" BOOLEAN NOT NULL DEFAULT false, + "valid_from" TIMESTAMP(3), + "valid_to" TIMESTAMP(3), + "reference" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "contracts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ndas" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "supplier_id" TEXT NOT NULL, + "parties" TEXT, + "info_scope" TEXT, + "subject" TEXT, + "valid_from" TIMESTAMP(3), + "valid_to" TIMESTAMP(3), + "obligations" TEXT, + "extension_status" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ndas_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "supplier_evidence" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "supplier_id" TEXT NOT NULL, + "kind" "EvidenceKind" NOT NULL, + "name" TEXT, + "protects_cia" TEXT, + "valid_to" TIMESTAMP(3), + "adequacy_checked" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "supplier_evidence_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "service_responsibilities" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "supplier_id" TEXT NOT NULL, + "it_service" TEXT NOT NULL, + "requirement" TEXT NOT NULL, + "responsible_party" "ResponsibleParty" NOT NULL DEFAULT 'SHARED', + "isa_applicability" TEXT, + "evidence" TEXT, + "integrated_local_controls" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "service_responsibilities_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "subcontractors" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "supplier_id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "flowdown_obligation" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "subcontractors_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "management_decisions" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "supplier_id" TEXT NOT NULL, + "reason_no_audit" TEXT NOT NULL, + "decision" TEXT NOT NULL, + "decided_by" TEXT, + "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "record_ref" TEXT, + + CONSTRAINT "management_decisions_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "supplier_controls" ( + "id" TEXT NOT NULL, + "ref" TEXT NOT NULL, + "title" TEXT NOT NULL, + "objective" TEXT NOT NULL, + "must_req" TEXT, + "should_req" TEXT, + "high_req" TEXT, + "very_high_req" TEXT, + "simplified_group_assessment" BOOLEAN NOT NULL DEFAULT false, + "target_maturity" INTEGER NOT NULL DEFAULT 3, + "references" TEXT, + + CONSTRAINT "supplier_controls_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "supplier_control_maturity" ( + "id" TEXT NOT NULL, + "tenant_id" TEXT NOT NULL, + "supplier_id" TEXT NOT NULL, + "control_id" TEXT NOT NULL, + "maturity" INTEGER NOT NULL DEFAULT 0, + "notes" TEXT, + + CONSTRAINT "supplier_control_maturity_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "suppliers_tenant_id_idx" ON "suppliers"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "suppliers_tenant_id_ref_no_key" ON "suppliers"("tenant_id", "ref_no"); + +-- CreateIndex +CREATE INDEX "supplier_assets_tenant_id_idx" ON "supplier_assets"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "supplier_assets_supplier_id_asset_id_key" ON "supplier_assets"("supplier_id", "asset_id"); + +-- CreateIndex +CREATE INDEX "supplier_assessments_tenant_id_idx" ON "supplier_assessments"("tenant_id"); + +-- CreateIndex +CREATE INDEX "contracts_tenant_id_idx" ON "contracts"("tenant_id"); + +-- CreateIndex +CREATE INDEX "ndas_tenant_id_idx" ON "ndas"("tenant_id"); + +-- CreateIndex +CREATE INDEX "supplier_evidence_tenant_id_idx" ON "supplier_evidence"("tenant_id"); + +-- CreateIndex +CREATE INDEX "service_responsibilities_tenant_id_idx" ON "service_responsibilities"("tenant_id"); + +-- CreateIndex +CREATE INDEX "subcontractors_tenant_id_idx" ON "subcontractors"("tenant_id"); + +-- CreateIndex +CREATE INDEX "management_decisions_tenant_id_idx" ON "management_decisions"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "supplier_controls_ref_key" ON "supplier_controls"("ref"); + +-- CreateIndex +CREATE INDEX "supplier_control_maturity_tenant_id_idx" ON "supplier_control_maturity"("tenant_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "supplier_control_maturity_supplier_id_control_id_key" ON "supplier_control_maturity"("supplier_id", "control_id"); + +-- AddForeignKey +ALTER TABLE "supplier_assets" ADD CONSTRAINT "supplier_assets_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "supplier_assets" ADD CONSTRAINT "supplier_assets_asset_id_fkey" FOREIGN KEY ("asset_id") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "supplier_assessments" ADD CONSTRAINT "supplier_assessments_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "contracts" ADD CONSTRAINT "contracts_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ndas" ADD CONSTRAINT "ndas_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "supplier_evidence" ADD CONSTRAINT "supplier_evidence_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "service_responsibilities" ADD CONSTRAINT "service_responsibilities_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "subcontractors" ADD CONSTRAINT "subcontractors_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "management_decisions" ADD CONSTRAINT "management_decisions_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "supplier_control_maturity" ADD CONSTRAINT "supplier_control_maturity_supplier_id_fkey" FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "supplier_control_maturity" ADD CONSTRAINT "supplier_control_maturity_control_id_fkey" FOREIGN KEY ("control_id") REFERENCES "supplier_controls"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260703081120_rls_suppliers/migration.sql b/prisma/migrations/20260703081120_rls_suppliers/migration.sql new file mode 100644 index 0000000..8bef651 --- /dev/null +++ b/prisma/migrations/20260703081120_rls_suppliers/migration.sql @@ -0,0 +1,16 @@ +-- RLS-Policies für die Lieferanten-Tabellen (analog zu row_level_security) +DO $$ +DECLARE t text; +BEGIN + FOREACH t IN ARRAY ARRAY[ + 'suppliers','supplier_assets','supplier_assessments','contracts','ndas', + 'supplier_evidence','service_responsibilities','subcontractors', + 'management_decisions','supplier_control_maturity' + ] LOOP + EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', t); + EXECUTE format( + 'CREATE POLICY tenant_isolation ON %I USING (tenant_id = current_setting(''app.tenant_id'', true))', + t + ); + END LOOP; +END $$; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d432c8b..967db5c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -166,6 +166,7 @@ model Asset { relationsTo AssetRelation[] @relation("relationTo") processAssets ProcessAsset[] riskAssets RiskAsset[] + supplierLinks SupplierAsset[] @@index([tenantId]) @@index([tenantId, type]) @@ -396,6 +397,253 @@ model Vulnerability { @@map("vulnerabilities") } +// ── Lieferantenmanagement (SPEC §4.14, VDA-ISA 2027 Kap. 6, NIS2 Art. 21(2)(d)) ── + +enum SupplierStatus { + ACTIVE + ONBOARDING + UNDER_REVIEW + OFFBOARDED +} + +enum AssessmentType { + QUESTIONNAIRE + SELF_ASSESSMENT + AUDIT +} + +enum AssessmentStatus { + SENT + RECEIVED + EVALUATED + OVERDUE +} + +enum EvidenceKind { + CERTIFICATE + TISAX_LABEL + ATTESTATION + AUDIT_REPORT + SELF_ASSESSMENT +} + +enum ResponsibleParty { + CLIENT + SUPPLIER + SHARED +} + +model Supplier { + id String @id @default(cuid()) + tenantId String @map("tenant_id") + refNo Int @map("ref_no") // Anzeige "L-001" + + name String + sector String? + services String? // erbrachte Leistung / IT-Services + criticality Int @default(1) // 1–4 + dataCategories String[] @default([]) @map("data_categories") + // Schutzbedarf der verarbeiteten Informationen (C/I/A 1–4) + confidentiality Int @default(1) + integrity Int @default(1) + availability Int @default(1) + nis2Relevant Boolean @default(false) @map("nis2_relevant") // Teil der Lieferkette + status SupplierStatus @default(ACTIVE) + contact String? + nextReview DateTime? @map("next_review") + notes String? + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + createdBy String? @map("created_by") + + assessments SupplierAssessment[] + contracts Contract[] + ndas Nda[] + evidence SupplierEvidence[] + responsibilities ServiceResponsibility[] + subcontractors Subcontractor[] + decisions ManagementDecision[] + controlMaturity SupplierControlMaturity[] + assetLinks SupplierAsset[] + + @@unique([tenantId, refNo]) + @@index([tenantId]) + @@map("suppliers") +} + +// Lieferant ↔ Asset (welcher Dienstleister betrifft welche Assets) — speist den Graph +model SupplierAsset { + id String @id @default(cuid()) + tenantId String @map("tenant_id") + supplierId String @map("supplier_id") + assetId String @map("asset_id") + + supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade) + + @@unique([supplierId, assetId]) + @@index([tenantId]) + @@map("supplier_assets") +} + +model SupplierAssessment { + id String @id @default(cuid()) + tenantId String @map("tenant_id") + supplierId String @map("supplier_id") + type AssessmentType + status AssessmentStatus @default(SENT) + score Int? // 0–100 Scoring + date DateTime? + nextReview DateTime? @map("next_review") + result String? + createdAt DateTime @default(now()) @map("created_at") + + supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + + @@index([tenantId]) + @@map("supplier_assessments") +} + +model Contract { + id String @id @default(cuid()) + tenantId String @map("tenant_id") + supplierId String @map("supplier_id") + type String @default("service") // service | av_dpa | nda | sla … + avDpa Boolean @default(false) @map("av_dpa") // AV/DPA (DSGVO Art. 28) + securityClauses Boolean @default(false) @map("security_clauses") + flowdown Boolean @default(false) // Weitergabe an Subunternehmer + customerTransparency Boolean @default(false) @map("customer_transparency") + validFrom DateTime? @map("valid_from") + validTo DateTime? @map("valid_to") + reference String? + createdAt DateTime @default(now()) @map("created_at") + + supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + + @@index([tenantId]) + @@map("contracts") +} + +model Nda { + id String @id @default(cuid()) + tenantId String @map("tenant_id") + supplierId String @map("supplier_id") + parties String? + infoScope String? @map("info_scope") + subject String? + validFrom DateTime? @map("valid_from") + validTo DateTime? @map("valid_to") + obligations String? + extensionStatus String? @map("extension_status") // z. B. offen | verlängert | ausgelaufen + createdAt DateTime @default(now()) @map("created_at") + + supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + + @@index([tenantId]) + @@map("ndas") +} + +model SupplierEvidence { + id String @id @default(cuid()) + tenantId String @map("tenant_id") + supplierId String @map("supplier_id") + kind EvidenceKind + name String? + protectsCia String? @map("protects_cia") // z. B. "C,I,A" + validTo DateTime? @map("valid_to") + adequacyChecked Boolean @default(false) @map("adequacy_checked") + createdAt DateTime @default(now()) @map("created_at") + + supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + + @@index([tenantId]) + @@map("supplier_evidence") +} + +model ServiceResponsibility { + id String @id @default(cuid()) + tenantId String @map("tenant_id") + supplierId String @map("supplier_id") + itService String @map("it_service") + requirement String + responsibleParty ResponsibleParty @default(SHARED) @map("responsible_party") + isaApplicability String? @map("isa_applicability") + evidence String? + integratedLocalControls String? @map("integrated_local_controls") + createdAt DateTime @default(now()) @map("created_at") + + supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + + @@index([tenantId]) + @@map("service_responsibilities") +} + +model Subcontractor { + id String @id @default(cuid()) + tenantId String @map("tenant_id") + supplierId String @map("supplier_id") + name String + flowdownObligation Boolean @default(false) @map("flowdown_obligation") + + supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + + @@index([tenantId]) + @@map("subcontractors") +} + +model ManagementDecision { + id String @id @default(cuid()) + tenantId String @map("tenant_id") + supplierId String @map("supplier_id") + reasonNoAudit String @map("reason_no_audit") + decision String + decidedBy String? @map("decided_by") + date DateTime @default(now()) + recordRef String? @map("record_ref") + + supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + + @@index([tenantId]) + @@map("management_decisions") +} + +// VDA-ISA 2027 Kapitel 6 — Supplier Relationships (globaler Katalog, per Import versioniert) +model SupplierControl { + id String @id @default(cuid()) + ref String @unique // 6.1.1 / 6.1.2 / 6.1.3 + title String + objective String + + mustReq String? @map("must_req") + shouldReq String? @map("should_req") + highReq String? @map("high_req") // Additional for high protection + veryHighReq String? @map("very_high_req") // Additional for very high protection + simplifiedGroupAssessment Boolean @default(false) @map("simplified_group_assessment") + targetMaturity Int @default(3) @map("target_maturity") + references String? // ISO 27001, NIST CSF, BSI … + + maturity SupplierControlMaturity[] + + @@map("supplier_controls") +} + +model SupplierControlMaturity { + id String @id @default(cuid()) + tenantId String @map("tenant_id") + supplierId String @map("supplier_id") + controlId String @map("control_id") + maturity Int @default(0) // 0–5 + notes String? + + supplier Supplier @relation(fields: [supplierId], references: [id], onDelete: Cascade) + control SupplierControl @relation(fields: [controlId], references: [id], onDelete: Cascade) + + @@unique([supplierId, controlId]) + @@index([tenantId]) + @@map("supplier_control_maturity") +} + model AuditLog { id String @id @default(cuid()) tenantId String @map("tenant_id") diff --git a/prisma/seed.ts b/prisma/seed.ts index 6cee2b5..46d38a8 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -452,6 +452,170 @@ async function main() { } console.log(`✔ ${measures.length} Beispiel-Maßnahmen angelegt, Rest-Risiken berechnet`); } + + // 8. VDA-ISA 2027 Kapitel 6 — Supplier Relationships (globaler Katalog) + const controls = [ + { + ref: "6.1.1", + title: + "To what extent is information security ensured among contractors and cooperation partners?", + objective: + "Ein angemessenes Informationssicherheitsniveau wird auch bei der Zusammenarbeit mit Partnern und Auftragnehmern aufrechterhalten.", + mustReq: + "Auftragnehmer/Partner werden einer Sicherheits­risikobewertung unterzogen; angemessenes Niveau vertraglich sichergestellt; Kundenanforderungen ggf. weitergegeben.", + shouldReq: + "Vertragliche Verpflichtung zur Weitergabe an Subunternehmer; Prüfung von Service-Reports/Dokumenten.", + highReq: + "Nachweis eines angemessenen Sicherheitsniveaus (geprüfter Fragebogen/Self-Assessment, Attest, Zertifikat, Lieferantenaudit) (C,I,A); Compliance dokumentiert, regelmäßig & anlassbezogen überwacht.", + veryHighReq: + "Nachweis durch Third-Party-Audit (adäquates TISAX-Label o. Ä.) oder Lieferantenaudit; ohne Audit: risikobasierte Management­entscheidung mit Protokoll (C,I,A); Transparenzpflichten gegenüber Kunden erfüllt.", + targetMaturity: 3, + simplifiedGroupAssessment: true, + references: + "ISO 27001:2022 A.5.19–A.5.22 · NIST CSF 2.0 GV.SC-02..06 · BSI OPS.2.1/OPS.2.2/OPS.3.1/ORP.2 · NIST SP800-53r5 MA-4, CA-3, CA-6, PM-16, PM-30, SR-2, SR-3, SR-6, SR-7, SR-8", + }, + { + ref: "6.1.2", + title: + "To what extent is non-disclosure regarding the exchange of information contractually agreed?", + objective: + "Geheimhaltungsvereinbarungen schützen den Informationsaustausch über Organisationsgrenzen hinweg rechtlich.", + mustReq: + "Geheimhaltungs­anforderungen bestimmt und erfüllt; allen Beteiligten bekannt; gültige NDAs vor Weitergabe sensibler Infos; regelmäßige Überprüfung.", + shouldReq: + "Geprüfte NDA-Vorlagen; NDAs mit Parteien, Informationsart, Gegenstand, Gültigkeit, Pflichten; Nachweis-/Auditrechte; Prozess zur Fristenüberwachung und rechtzeitigen Verlängerung.", + highReq: null, + veryHighReq: null, + targetMaturity: 3, + simplifiedGroupAssessment: true, + references: "ISO 27001:2022 A.5.14, A.6.6 · BSI OPS.2.1/OPS.2.2/OPS.3.1/ORP.5/CON.2", + }, + { + ref: "6.1.3", + title: + "To what extent are the responsibilities between external IT service providers and the own organization defined?", + objective: + "Gemeinsames Verständnis der Verantwortungsteilung; alle Sicherheitsanforderungen umgesetzt und nachweislich dokumentiert.", + mustReq: + "Betroffene IT-Services identifiziert; Anforderungen bestimmt; verantwortliche Organisation je Anforderung definiert; Mechanismen für geteilte Verantwortung umgesetzt.", + shouldReq: + "Konfiguration konzeptioniert/umgesetzt/dokumentiert; zuständiges Personal geschult.", + highReq: + "Liste der IT-Services und Provider (C,I,A); ISA-Control-Anwendbarkeit bewertet; regelmäßige Security-Assessments; Nachweis der Pflichterfüllung; Integration in lokale Schutzmaßnahmen dokumentiert.", + veryHighReq: null, + targetMaturity: 3, + simplifiedGroupAssessment: false, + references: + "ISO 27001:2022 A.5.23, A.8.9 · ISO 27017 CLD.6.3.1 · IEC 62443-2-1 6.2.3 · NIST CSF 2.0 GV.OC-05, GV.SC-01/02 · BSI 200-2, OPS.2.1/OPS.2.2/OPS.3.1/ORP.2 · NIST SP800-53r5 MA-4, PT-1, PL-2", + }, + ]; + for (const c of controls) { + await prisma.supplierControl.upsert({ where: { ref: c.ref }, update: c, create: c }); + } + console.log(`✔ VDA-ISA 2027 Kap. 6: ${controls.length} Supplier-Controls`); + + // 9. Demo-Lieferant mit Nachweisen/Verträgen (nur wenn noch keiner existiert) + const supplierCount = await prisma.supplier.count({ where: { tenantId: tenant.id } }); + if (supplierCount === 0) { + const hoster = await prisma.asset.findFirst({ + where: { tenantId: tenant.id, name: "Cloud-Hoster (IaaS)" }, + }); + const sup = await prisma.supplier.create({ + data: { + tenantId: tenant.id, + refNo: 1, + name: "Cloud-Hoster GmbH", + sector: "IT-Dienstleistung / IaaS", + services: "Rechenzentrum, Virtualisierung, Backup für ERP & CRM", + criticality: 4, + dataCategories: ["Kundendaten", "Auftragsdaten"], + confidentiality: 3, + integrity: 3, + availability: 4, + nis2Relevant: true, + status: "ACTIVE", + contact: "security@cloud-hoster.example", + nextReview: new Date(Date.now() + 90 * 24 * 3600 * 1000), + }, + }); + if (hoster) { + await prisma.supplierAsset.create({ + data: { tenantId: tenant.id, supplierId: sup.id, assetId: hoster.id }, + }); + } + await prisma.supplierEvidence.create({ + data: { + tenantId: tenant.id, + supplierId: sup.id, + kind: "TISAX_LABEL", + name: "TISAX AL3 (info high)", + protectsCia: "C,I,A", + validTo: new Date(Date.now() + 200 * 24 * 3600 * 1000), + adequacyChecked: true, + }, + }); + await prisma.contract.create({ + data: { + tenantId: tenant.id, + supplierId: sup.id, + type: "av_dpa", + avDpa: true, + securityClauses: true, + flowdown: true, + customerTransparency: false, + validFrom: new Date(Date.now() - 300 * 24 * 3600 * 1000), + validTo: new Date(Date.now() + 400 * 24 * 3600 * 1000), + reference: "AV-2025-014", + }, + }); + await prisma.nda.create({ + data: { + tenantId: tenant.id, + supplierId: sup.id, + parties: "Demo GmbH ↔ Cloud-Hoster GmbH", + infoScope: "Betriebs- und Kundendaten", + subject: "Betrieb der ERP-/CRM-Infrastruktur", + validFrom: new Date(Date.now() - 300 * 24 * 3600 * 1000), + validTo: new Date(Date.now() + 60 * 24 * 3600 * 1000), + extensionStatus: "offen", + }, + }); + await prisma.supplierAssessment.create({ + data: { + tenantId: tenant.id, + supplierId: sup.id, + type: "SELF_ASSESSMENT", + status: "EVALUATED", + score: 82, + date: new Date(Date.now() - 120 * 24 * 3600 * 1000), + nextReview: new Date(Date.now() + 245 * 24 * 3600 * 1000), + result: "angemessen", + }, + }); + await prisma.serviceResponsibility.create({ + data: { + tenantId: tenant.id, + supplierId: sup.id, + itService: "IaaS-Plattform", + requirement: "Patch-Management der Hypervisor-Ebene", + responsibleParty: "SUPPLIER", + isaApplicability: "5.x IT/Cyber Security", + integratedLocalControls: "Sichere Authentisierung, Monitoring beim Kunden", + }, + }); + const dbControls = await prisma.supplierControl.findMany(); + for (const c of dbControls) { + await prisma.supplierControlMaturity.create({ + data: { + tenantId: tenant.id, + supplierId: sup.id, + controlId: c.id, + maturity: c.ref === "6.1.2" ? 2 : 3, + }, + }); + } + console.log("✔ Demo-Lieferant mit Nachweisen/Vertrag/NDA/Assessment angelegt"); + } } main() diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index f53ab62..1d206b2 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -43,7 +43,7 @@ export default async function AppLayout({ { href: "/chat", label: t("chat"), icon: MessagesSquare, enabled: false }, { href: "/dependencies", label: t("dependencies"), icon: Network, enabled: true }, { href: "/evidence", label: t("evidence"), icon: FolderCheck, enabled: false }, - { href: "/suppliers", label: t("suppliers"), icon: Truck, enabled: false }, + { href: "/suppliers", label: t("suppliers"), icon: Truck, enabled: true }, { href: "/review", label: t("review"), icon: LineChart, enabled: false }, ]; diff --git a/src/app/(app)/suppliers/page.tsx b/src/app/(app)/suppliers/page.tsx new file mode 100644 index 0000000..715c398 --- /dev/null +++ b/src/app/(app)/suppliers/page.tsx @@ -0,0 +1,146 @@ +import Link from "next/link"; +import { getFormatter, getTranslations } from "next-intl/server"; +import { Plus } from "lucide-react"; +import { requireSession } from "@/server/auth"; +import { dbForTenant, prisma } from "@/server/db"; +import { hasPermission, requirePermission } from "@/server/rbac"; +import { Button } from "@/components/ui/button"; +import { CriticalityPill, KpiCard, PageHead, Pill } from "@/components/mockup-ui"; +import { + SupplierCreateModal, + SupplierDetailModal, + SupplierEditModal, +} from "@/components/supplier-modals"; +import { supplierRef, SUPPLIER_STATUS_TONE, isExpiring, isReviewDue } from "@/lib/supplier"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +const SUPPLIER_INCLUDE = { + assessments: true, + contracts: true, + ndas: true, + evidence: true, + responsibilities: true, + subcontractors: true, + decisions: true, + controlMaturity: true, + assetLinks: { include: { asset: { select: { id: true, name: true } } } }, +} as const; + +export default async function SuppliersPage({ + searchParams, +}: { + searchParams: Promise<{ detail?: string; edit?: string; new?: string }>; +}) { + const session = await requireSession(); + requirePermission(session, "supplier:read"); + const t = await getTranslations("suppliers"); + const tStatus = await getTranslations("supplierStatus"); + const tCrit = await getTranslations("criticality"); + const tc = await getTranslations("common"); + const fmt = await getFormatter(); + + const params = await searchParams; + const db = dbForTenant(session.user.tenantId); + const canWrite = hasPermission(session, "supplier:write"); + + const suppliers = await db.supplier.findMany({ + include: { + contracts: { select: { validTo: true } }, + evidence: { select: { validTo: true } }, + _count: { select: { assessments: true, contracts: true } }, + }, + orderBy: { refNo: "asc" }, + take: 300, + }); + + const total = suppliers.length; + const nis2 = suppliers.filter((s) => s.nis2Relevant).length; + const expiring = suppliers.filter( + (s) => + s.contracts.some((c) => isExpiring(c.validTo)) || s.evidence.some((e) => isExpiring(e.validTo)) + ).length; + const reviewsDue = suppliers.filter((s) => isReviewDue(s.nextReview)).length; + + const controls = await prisma.supplierControl.findMany({ orderBy: { ref: "asc" } }); + const modalId = params.edit && canWrite ? params.edit : params.detail; + const modalSupplier = modalId + ? await db.supplier.findUnique({ where: { id: modalId }, include: SUPPLIER_INCLUDE }) + : null; + + return ( +
+ }> + {t("newSupplier")} + + ) + } + /> + +
+ + + 0 ? t("kpiExpiring") : undefined} /> + 0 ? t("kpiReviews") : undefined} /> +
+ +
+ + + + {t("ref")} + {t("name")} + {t("sector")} + {t("criticality")} + NIS2 + {t("nextReview")} + {t("status")} + + + + {suppliers.length === 0 && ( + + {t("empty")} + + )} + {suppliers.map((s) => ( + + {supplierRef(s.refNo)} + + {s.name} + + {s.sector ?? tc("none")} + + {s.nis2Relevant ? NIS2 : {tc("none")}} + + {s.nextReview ? fmt.dateTime(s.nextReview, { dateStyle: "medium" }) : tc("none")} + + {tStatus(s.status)} + + ))} + +
+
+ + {modalSupplier && params.edit && canWrite ? ( + + ) : modalSupplier ? ( + + ) : params.new && canWrite ? ( + + ) : null} +
+ ); +} diff --git a/src/components/supplier-modals.tsx b/src/components/supplier-modals.tsx new file mode 100644 index 0000000..bd3b5e5 --- /dev/null +++ b/src/components/supplier-modals.tsx @@ -0,0 +1,651 @@ +import Link from "next/link"; +import { getFormatter, getTranslations } from "next-intl/server"; +import { Pencil, Plus, Trash2, X, AlertTriangle } from "lucide-react"; +import type { Prisma, SupplierControl } from "@prisma/client"; +import { + addAssessment, + addContract, + addDecision, + addEvidence, + addNda, + addResponsibility, + addSubcontractor, + createSupplier, + deleteChild, + deleteSupplier, + saveMaturity, + updateSupplier, +} from "@/server/actions/suppliers"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Modal } from "@/components/modal"; +import { SegmentedRating } from "@/components/segmented-rating"; +import { CiaBadge, CriticalityPill, Pill } from "@/components/mockup-ui"; +import { supplierRef, SUPPLIER_STATUS_TONE, isExpired, isExpiring, needsManagementDecision } from "@/lib/supplier"; + +export type SupplierWithDetail = Prisma.SupplierGetPayload<{ + include: { + assessments: true; + contracts: true; + ndas: true; + evidence: true; + responsibilities: true; + subcontractors: true; + decisions: true; + controlMaturity: true; + assetLinks: { include: { asset: { select: { id: true; name: true } } } }; + }; +}>; + +const STATUSES = ["ACTIVE", "ONBOARDING", "UNDER_REVIEW", "OFFBOARDED"] as const; +const inputCls = "h-9 w-full rounded-md border border-input bg-transparent px-3 text-sm"; + +/* ─────────────────────────── Detail (read-only) ─────────────────────────── */ + +export async function SupplierDetailModal({ + supplier, + controls, + canWrite, +}: { + supplier: SupplierWithDetail; + controls: SupplierControl[]; + canWrite: boolean; +}) { + const t = await getTranslations("suppliers"); + const tStatus = await getTranslations("supplierStatus"); + const tCrit = await getTranslations("criticality"); + const tAType = await getTranslations("assessmentType"); + const tEKind = await getTranslations("evidenceKind"); + const tParty = await getTranslations("responsibleParty"); + const tc = await getTranslations("common"); + const fmt = await getFormatter(); + const date = (d: Date | null) => (d ? fmt.dateTime(d, { dateStyle: "medium" }) : tc("none")); + + const decisionNeeded = needsManagementDecision(supplier.evidence); + const matById = new Map(supplier.controlMaturity.map((m) => [m.controlId, m.maturity])); + + return ( + + {supplier.nis2Relevant && NIS2} + + {tStatus(supplier.status)} + + } + closeHref="/suppliers" + closeLabel={t("close")} + footer={ + <> + {canWrite && ( + + )} + + + } + > +
+ {/* Stammdaten + Assets */} +
+
+
+
{t("sector")}
+
{supplier.sector ?? tc("none")}
+
{t("services")}
+
{supplier.services ?? tc("none")}
+
{t("contact")}
+
{supplier.contact ?? tc("none")}
+
{t("dataCategories")}
+
{supplier.dataCategories.join(", ") || tc("none")}
+
{t("protection")}
+
+
{t("nextReview")}
+
{date(supplier.nextReview)}
+
+
+
+

{t("assets")}

+ {supplier.assetLinks.length === 0 &&

{tc("none")}

} +
    + {supplier.assetLinks.map((l) => ( +
  • + {l.asset.name} +
  • + ))} +
+ {supplier.notes &&

{supplier.notes}

} +
+
+ + {/* Managemententscheidung nötig? */} + {decisionNeeded && ( +
+ + {t("decisionNeeded")} +
+ )} + + {/* VDA-ISA Kap. 6 Reifegrade */} +
+

{t("catalog")}

+

{t("catalogNote")}

+
+ {controls.map((c) => { + const m = matById.get(c.id) ?? 0; + return ( +
+
+ + {c.ref} · {c.objective} + + + {t("maturity")} {m}/5 · {t("target")} {c.targetMaturity} + +
+
+ {[1, 2, 3, 4, 5].map((s) => ( + = c.targetMaturity + ? "var(--ok)" + : "var(--warn)" + : "rgba(120,135,180,0.2)", + }} + /> + ))} +
+ {c.references && ( +

{t("references")}: {c.references}

+ )} +
+ ); + })} +
+
+ + {/* Nachweise */} + + {supplier.evidence.length === 0 ? ( + + ) : ( + supplier.evidence.map((e) => ( +
  • + {tEKind(e.kind)} + {e.name ?? tEKind(e.kind)} + {e.protectsCia && ({e.protectsCia})} + {e.adequacyChecked && {t("adequacy")}} + {e.validTo && ( + + {t("expires")} {date(e.validTo)} + + )} +
  • + )) + )} +
    + + {/* Bewertungen */} + + {supplier.assessments.length === 0 ? ( + + ) : ( + supplier.assessments.map((a) => ( +
  • + {tAType(a.type)} + {a.score != null && {a.score}/100} + {a.result && {a.result}} + {date(a.date)} +
  • + )) + )} +
    + + {/* Verträge */} + + {supplier.contracts.length === 0 ? ( + + ) : ( + supplier.contracts.map((k) => ( +
  • + {k.reference ?? k.type} + {k.avDpa && {t("avDpa")}} + {k.flowdown && {t("flowdown")}} + {k.validTo && ( + + {t("validTo")}: {date(k.validTo)} + + )} +
  • + )) + )} +
    + + {/* NDAs */} + + {supplier.ndas.length === 0 ? ( + + ) : ( + supplier.ndas.map((n) => ( +
  • + {n.subject ?? n.parties ?? "NDA"} + {n.validTo && ( + + {t("validTo")}: {date(n.validTo)} + + )} + {n.extensionStatus && · {n.extensionStatus}} +
  • + )) + )} +
    + + {/* RACI */} + + {supplier.responsibilities.length === 0 ? ( + + ) : ( + supplier.responsibilities.map((r) => ( +
  • + {r.itService} + · {r.requirement} + + {tParty(r.responsibleParty)} + +
  • + )) + )} +
    + + {/* Subunternehmer */} + {supplier.subcontractors.length > 0 && ( + + {supplier.subcontractors.map((s) => ( +
  • + {s.name} {s.flowdownObligation && {t("flowdownObl")}} +
  • + ))} +
    + )} + + {/* Entscheidungen */} + {supplier.decisions.length > 0 && ( + + {supplier.decisions.map((d) => ( +
  • + {d.decision} + — {d.reasonNoAudit} + · {d.decidedBy} · {date(d.date)} +
  • + ))} +
    + )} +
    +
    + ); +} + +function ListSection({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
    +

    {title}

    + +
    + ); +} +function Empty({ t }: { t: string }) { + return
  • {t}
  • ; +} + +/* ─────────────────────────── Stammdaten-Formular ─────────────────────────── */ + +async function SupplierFields({ supplier, formId }: { supplier?: SupplierWithDetail; formId?: string }) { + const t = await getTranslations("suppliers"); + const tStatus = await getTranslations("supplierStatus"); + const f = formId ? { form: formId } : {}; + return ( +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +