// Cockpit (M3, 1.4): Kapitel/Control → Bereich (Domain) + Default-RACI. // // Diese Datei ist die *kanonische Quelle* der globalen Default-Zuordnung. Sie wird // (a) vom Seed (`prisma/seed.ts`) in die Tabelle `ControlDomainMap` geschrieben und // (b) als In-App-Fallback verwendet, falls für ein Control keine DB-Zeile existiert. // // Client-safe: nur Daten + reine Funktionen, keine Server-/Prisma-Importe. // Fachliche Korrektur ggü. erstem Entwurf: BCM ist aus Kapitel 3 gelöst und ein // eigener Bereich (Kontinuität/Krise). Prototyp/Datenschutz sind label-getrieben. import type { Domain, RaciKind } from "@prisma/client"; /** Deutsche Anzeigelabels der Bereiche (Cockpit-Board). */ export const DOMAIN_LABELS: Record = { GOVERNANCE: "Governance", HR: "Personal", PHYSICAL: "Physische Sicherheit", BCM: "Notfallmanagement (BCM)", IT: "IT", PROCUREMENT: "Beschaffung", COMPLIANCE: "Compliance", DATA_PROTECTION: "Datenschutz", PROTOTYPE: "Prototypenschutz", }; /** Stabile Reihenfolge der Bereiche für Filter/Lanes. */ export const DOMAIN_ORDER: Domain[] = [ "GOVERNANCE", "HR", "PHYSICAL", "BCM", "IT", "PROCUREMENT", "COMPLIANCE", "DATA_PROTECTION", "PROTOTYPE", ]; export interface ControlDomainRule { /** Control-ID ("5.2.9") ODER Kapitel-Präfix ("5"). Exaktere Regel gewinnt. */ control: string; domain: Domain; raci: RaciKind; /** Default-verantwortliche/mitwirkende Funktion (ISB, IT_LEAD, …). */ functionKey?: string; } // Globale Default-Zuordnung. Erste Zeile je control mit raci=RESPONSIBLE = primärer // Bereich; weitere Zeilen (CONSULTED/INFORMED) sind Default-Mitwirkende. export const CONTROL_DOMAIN_DEFAULTS: ControlDomainRule[] = [ // K1 — Governance (ISB + Leitung) { control: "1", domain: "GOVERNANCE", raci: "RESPONSIBLE", functionKey: "ISB" }, // K1.6 Krisenmanagement zählt fachlich zu BCM (aus K3 „BCM" gelöst → eigener Bereich) { control: "1.6.3", domain: "BCM", raci: "RESPONSIBLE", functionKey: "BCM" }, // K2 — HR / Personal { control: "2", domain: "HR", raci: "RESPONSIBLE", functionKey: "HR_LEAD" }, // K3 — physische Sicherheit { control: "3", domain: "PHYSICAL", raci: "RESPONSIBLE", functionKey: "FACILITY_LEAD" }, // K4 IAM — IT verantwortlich, HR mitwirkend (Joiner-Mover-Leaver) { control: "4", domain: "IT", raci: "RESPONSIBLE", functionKey: "IT_LEAD" }, { control: "4", domain: "HR", raci: "CONSULTED", functionKey: "HR_LEAD" }, // K5 — IT-Betrieb { control: "5", domain: "IT", raci: "RESPONSIBLE", functionKey: "IT_LEAD" }, // K5.2.8 IT-Kontinuitätsplanung zählt fachlich zu BCM { control: "5.2.8", domain: "BCM", raci: "RESPONSIBLE", functionKey: "BCM" }, { control: "5.2.8", domain: "IT", raci: "CONSULTED", functionKey: "IT_LEAD" }, // K6 — Beschaffung/Lieferanten, ISB mitwirkend (Recht/ISB CONSULTED) { control: "6", domain: "PROCUREMENT", raci: "RESPONSIBLE", functionKey: "PROCUREMENT_LEAD" }, { control: "6", domain: "GOVERNANCE", raci: "CONSULTED", functionKey: "ISB" }, // K7 — Compliance, DPO + IT mitwirkend { control: "7", domain: "COMPLIANCE", raci: "RESPONSIBLE", functionKey: "COMPLIANCE_LEAD" }, { control: "7", domain: "DATA_PROTECTION", raci: "CONSULTED", functionKey: "DPO" }, { control: "7", domain: "IT", raci: "CONSULTED", functionKey: "IT_LEAD" }, // K7.1.2 Schutz personenbezogener Daten → Datenschutz verantwortlich { control: "7.1.2", domain: "DATA_PROTECTION", raci: "RESPONSIBLE", functionKey: "DPO" }, // Label-getriebene Sonderbereiche (nicht kapitelgebunden, s. 1.1/1.4) { control: "prototype", domain: "PROTOTYPE", raci: "RESPONSIBLE", functionKey: "PROTOTYPE_LEAD" }, { control: "datenschutz", domain: "DATA_PROTECTION", raci: "RESPONSIBLE", functionKey: "DPO" }, ]; /** Erste Zahl-Segment eines Control-Refs als Kapitel-Präfix ("5.2.9" → "5"). */ export function controlChapter(control: string): string { return String(control ?? "").trim().split(".")[0] ?? ""; } /** * Passende Regeln für ein Control aus einer Regelliste ermitteln. Priorität: * exakter Control-Match vor Kapitel-Präfix-Match. Gibt alle Regeln der besten * Genauigkeitsstufe zurück (RESPONSIBLE zuerst, dann CONSULTED/INFORMED). */ export function matchControlRules( control: string, rules: ControlDomainRule[] = CONTROL_DOMAIN_DEFAULTS, ): ControlDomainRule[] { const ref = String(control ?? "").trim().toLowerCase(); if (!ref) return []; const exact = rules.filter((r) => r.control.toLowerCase() === ref); if (exact.length) return sortByRaci(exact); const chapter = controlChapter(ref); const byChapter = rules.filter((r) => r.control.toLowerCase() === chapter); return sortByRaci(byChapter); } const RACI_ORDER: Record = { RESPONSIBLE: 0, ACCOUNTABLE: 1, CONSULTED: 2, INFORMED: 3, }; function sortByRaci(rules: ControlDomainRule[]): ControlDomainRule[] { return [...rules].sort((a, b) => RACI_ORDER[a.raci] - RACI_ORDER[b.raci]); } /** Primärer Bereich (RESPONSIBLE) eines Controls aus den globalen Defaults. */ export function defaultDomainForControl(control: string): Domain | null { const rules = matchControlRules(control); const primary = rules.find((r) => r.raci === "RESPONSIBLE") ?? rules[0]; return primary?.domain ?? null; } /** Default-verantwortliche Funktion (functionKey) je Bereich — erste RESPONSIBLE-Regel. */ export const DOMAIN_FUNCTION: Partial> = (() => { const map: Partial> = {}; for (const r of CONTROL_DOMAIN_DEFAULTS) { if (r.raci === "RESPONSIBLE" && r.functionKey && !map[r.domain]) map[r.domain] = r.functionKey; } return map; })();