'use client' /** * DependencyMap Component * * Visualizes the relationship between Controls and Requirements. * Shows which controls satisfy which regulatory requirements. * * Sprint 5: PDF Reports & Erweiterte Visualisierungen */ import { useState, useMemo } from 'react' import { Language, getTerm } from '@/lib/compliance-i18n' interface Requirement { id: string article: string title: string regulation_code: string } interface Control { id: string control_id: string title: string domain: string status: string } interface Mapping { requirement_id: string control_id: string coverage_level: 'full' | 'partial' | 'planned' } interface DependencyMapProps { requirements: Requirement[] controls: Control[] mappings: Mapping[] lang?: Language onControlClick?: (control: Control) => void onRequirementClick?: (requirement: Requirement) => void } const DOMAIN_COLORS: Record = { gov: '#64748b', priv: '#3b82f6', iam: '#a855f7', crypto: '#eab308', sdlc: '#22c55e', ops: '#f97316', ai: '#ec4899', cra: '#06b6d4', aud: '#6366f1', } const DOMAIN_LABELS: Record = { gov: 'Governance', priv: 'Datenschutz', iam: 'Identity & Access', crypto: 'Kryptografie', sdlc: 'Secure Dev', ops: 'Operations', ai: 'KI-spezifisch', cra: 'Supply Chain', aud: 'Audit', } const COVERAGE_COLORS: Record = { full: { bg: 'bg-green-100', border: 'border-green-500', text: 'text-green-700' }, partial: { bg: 'bg-yellow-100', border: 'border-yellow-500', text: 'text-yellow-700' }, planned: { bg: 'bg-slate-100', border: 'border-slate-400', text: 'text-slate-600' }, } export default function DependencyMap({ requirements, controls, mappings, lang = 'de', onControlClick, onRequirementClick, }: DependencyMapProps) { const [selectedControl, setSelectedControl] = useState(null) const [selectedRequirement, setSelectedRequirement] = useState(null) const [filterRegulation, setFilterRegulation] = useState('') const [filterDomain, setFilterDomain] = useState('') const [viewMode, setViewMode] = useState<'matrix' | 'sankey'>('matrix') // Get unique regulations const regulations = useMemo(() => { const regs = new Set(requirements.map((r) => r.regulation_code)) return Array.from(regs).sort() }, [requirements]) // Get unique domains const domains = useMemo(() => { const doms = new Set(controls.map((c) => c.domain)) return Array.from(doms).sort() }, [controls]) // Filter requirements and controls const filteredRequirements = useMemo(() => { return requirements.filter((r) => { if (filterRegulation && r.regulation_code !== filterRegulation) return false return true }) }, [requirements, filterRegulation]) const filteredControls = useMemo(() => { return controls.filter((c) => { if (filterDomain && c.domain !== filterDomain) return false return true }) }, [controls, filterDomain]) // Build mapping lookup const mappingLookup = useMemo(() => { const lookup: Record> = {} mappings.forEach((m) => { if (!lookup[m.control_id]) lookup[m.control_id] = {} lookup[m.control_id][m.requirement_id] = m }) return lookup }, [mappings]) // Get connected requirements for a control const getConnectedRequirements = (controlId: string) => { return Object.keys(mappingLookup[controlId] || {}) } // Get connected controls for a requirement const getConnectedControls = (requirementId: string) => { return Object.keys(mappingLookup) .filter((controlId) => mappingLookup[controlId][requirementId]) .map((controlId) => ({ controlId, coverage: mappingLookup[controlId][requirementId].coverage_level, })) } // Handle control selection const handleControlClick = (control: Control) => { if (selectedControl === control.control_id) { setSelectedControl(null) } else { setSelectedControl(control.control_id) setSelectedRequirement(null) } onControlClick?.(control) } // Handle requirement selection const handleRequirementClick = (requirement: Requirement) => { if (selectedRequirement === requirement.id) { setSelectedRequirement(null) } else { setSelectedRequirement(requirement.id) setSelectedControl(null) } onRequirementClick?.(requirement) } // Calculate statistics const stats = useMemo(() => { const totalMappings = mappings.length const fullMappings = mappings.filter((m) => m.coverage_level === 'full').length const partialMappings = mappings.filter((m) => m.coverage_level === 'partial').length const plannedMappings = mappings.filter((m) => m.coverage_level === 'planned').length const coveredRequirements = new Set(mappings.map((m) => m.requirement_id)).size const usedControls = new Set(mappings.map((m) => m.control_id)).size return { totalMappings, fullMappings, partialMappings, plannedMappings, coveredRequirements, totalRequirements: requirements.length, usedControls, totalControls: controls.length, coveragePercent: ((coveredRequirements / requirements.length) * 100).toFixed(1), } }, [mappings, requirements, controls]) if (requirements.length === 0 || controls.length === 0) { return (

{lang === 'de' ? 'Keine Mappings vorhanden' : 'No mappings available'}

) } return (
{/* Statistics Header */}

{lang === 'de' ? 'Abdeckung' : 'Coverage'}

{stats.coveragePercent}%

{stats.coveredRequirements}/{stats.totalRequirements} {lang === 'de' ? 'Anforderungen' : 'Requirements'}

{lang === 'de' ? 'Vollstaendig' : 'Full'}

{stats.fullMappings}

{lang === 'de' ? 'Mappings' : 'Mappings'}

{lang === 'de' ? 'Teilweise' : 'Partial'}

{stats.partialMappings}

{lang === 'de' ? 'Mappings' : 'Mappings'}

{lang === 'de' ? 'Geplant' : 'Planned'}

{stats.plannedMappings}

{lang === 'de' ? 'Mappings' : 'Mappings'}

{/* Filters */}
{/* Main Visualization */} {viewMode === 'matrix' ? (
{/* Matrix Header */}
{filteredControls.map((control) => (
handleControlClick(control)} className={` w-20 flex-shrink-0 text-center p-2 cursor-pointer transition-colors ${selectedControl === control.control_id ? 'bg-primary-100' : 'hover:bg-slate-50'} `} >

{control.control_id}

))}
{/* Matrix Body */} {filteredRequirements.map((req) => { const connectedControls = getConnectedControls(req.id) const isHighlighted = selectedRequirement === req.id || (selectedControl && connectedControls.some((c) => c.controlId === selectedControl)) return (
handleRequirementClick(req)} className={` w-48 flex-shrink-0 p-2 cursor-pointer transition-colors ${selectedRequirement === req.id ? 'bg-primary-100' : 'hover:bg-slate-50'} `} >

{req.regulation_code} {req.article}

{req.title}

{filteredControls.map((control) => { const mapping = mappingLookup[control.control_id]?.[req.id] const isControlHighlighted = selectedControl === control.control_id const isConnected = selectedControl && mapping return (
{mapping && (
{mapping.coverage_level === 'full' && ( )} {mapping.coverage_level === 'partial' && ( )} {mapping.coverage_level === 'planned' && ( )}
)}
) })}
) })}
) : ( /* Sankey/Connection View */
{/* Controls Column */}

Controls ({filteredControls.length})

{filteredControls.map((control) => { const connectedReqs = getConnectedRequirements(control.control_id) const isSelected = selectedControl === control.control_id return ( ) })}
{/* Connection Lines (simplified) */}
{selectedControl && (
{getConnectedRequirements(selectedControl).slice(0, 10).map((reqId, idx) => { const req = requirements.find((r) => r.id === reqId) const mapping = mappingLookup[selectedControl][reqId] if (!req) return null return (
{req.regulation_code} {req.article}
) })} {getConnectedRequirements(selectedControl).length > 10 && ( +{getConnectedRequirements(selectedControl).length - 10} {lang === 'de' ? 'weitere' : 'more'} )}
)} {selectedRequirement && (
{getConnectedControls(selectedRequirement).slice(0, 10).map(({ controlId, coverage }) => { const control = controls.find((c) => c.control_id === controlId) if (!control) return null return (
{control.control_id}
) })}
)} {!selectedControl && !selectedRequirement && (

{lang === 'de' ? 'Waehlen Sie ein Control oder eine Anforderung aus' : 'Select a control or requirement'}

)}
{/* Requirements Column */}

{lang === 'de' ? 'Anforderungen' : 'Requirements'} ({filteredRequirements.length})

{filteredRequirements.slice(0, 15).map((req) => { const connectedCtrls = getConnectedControls(req.id) const isSelected = selectedRequirement === req.id const isHighlighted = selectedControl && connectedCtrls.some((c) => c.controlId === selectedControl) return ( ) })} {filteredRequirements.length > 15 && (

+{filteredRequirements.length - 15} {lang === 'de' ? 'weitere' : 'more'}

)}
)} {/* Legend */}
{lang === 'de' ? 'Vollstaendig abgedeckt' : 'Fully covered'}
{lang === 'de' ? 'Teilweise abgedeckt' : 'Partially covered'}
{lang === 'de' ? 'Geplant' : 'Planned'}
) }