'use client' import { useState, useEffect, useCallback } from 'react' import { ArrowLeft, BookOpen, ExternalLink, FileText, Eye, Clock, ChevronLeft, SkipForward, Pencil, Trash2, Scale, GitMerge, } from 'lucide-react' import { CanonicalControl, EFFORT_LABELS, BACKEND_URL, SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, EvidenceTypeBadge, TargetAudienceBadge, GenerationStrategyBadge, ObligationTypeBadge, isEigenentwicklung, ObligationInfo, DocumentReference, MergedDuplicate, RegulationSummary, } from './helpers' import { ControlSourceCitation } from './ControlSourceCitation' import { ControlTraceability } from './ControlTraceability' import { ControlRegulatorySection } from './ControlRegulatorySection' import { ControlSimilarControls } from './ControlSimilarControls' import { ControlReviewActions } from './ControlReviewActions' interface SimilarControl { control_id: string; title: string; severity: string; release_state: string; tags: string[]; license_rule: number | null; verification_method: string | null; category: string | null; similarity: number; } interface ParentLink { parent_control_id: string; parent_title: string; link_type: string; confidence: number; source_regulation: string | null; source_article: string | null; parent_citation: Record | null; obligation: { text: string; action: string; object: string; normative_strength: string } | null; } interface TraceabilityData { control_id: string; title: string; is_atomic: boolean; parent_links: ParentLink[]; children: Array<{ control_id: string; title: string; category: string; severity: string; decomposition_method: string }>; source_count: number; obligations?: ObligationInfo[]; obligation_count?: number; document_references?: DocumentReference[]; merged_duplicates?: MergedDuplicate[]; merged_duplicates_count?: number; regulations_summary?: RegulationSummary[]; } interface V1Match { matched_control_id: string; matched_title: string; matched_objective: string; matched_severity: string; matched_category: string; matched_source: string | null; matched_article: string | null; matched_source_citation: Record | null; similarity_score: number; match_rank: number; match_method: string; } interface ControlDetailProps { ctrl: CanonicalControl onBack: () => void onEdit: () => void onDelete: (controlId: string) => void onReview: (controlId: string, action: string) => void onRefresh?: () => void onNavigateToControl?: (controlId: string) => void onCompare?: (ctrl: CanonicalControl, matches: V1Match[]) => void reviewMode?: boolean reviewIndex?: number reviewTotal?: number onReviewPrev?: () => void onReviewNext?: () => void } export function ControlDetail({ ctrl, onBack, onEdit, onDelete, onReview, onRefresh, onNavigateToControl, onCompare, reviewMode, reviewIndex = 0, reviewTotal = 0, onReviewPrev, onReviewNext, }: ControlDetailProps) { const [similarControls, setSimilarControls] = useState([]) const [loadingSimilar, setLoadingSimilar] = useState(false) const [selectedDuplicates, setSelectedDuplicates] = useState>(new Set()) const [merging, setMerging] = useState(false) const [traceability, setTraceability] = useState(null) const [loadingTrace, setLoadingTrace] = useState(false) const [v1Matches, setV1Matches] = useState([]) const [loadingV1, setLoadingV1] = useState(false) const eigenentwicklung = isEigenentwicklung(ctrl) const loadTraceability = useCallback(async () => { setLoadingTrace(true) try { let res = await fetch(`${BACKEND_URL}?endpoint=provenance&id=${ctrl.control_id}`) if (!res.ok) res = await fetch(`${BACKEND_URL}?endpoint=traceability&id=${ctrl.control_id}`) if (res.ok) setTraceability(await res.json()) } catch { /* ignore */ } finally { setLoadingTrace(false) } }, [ctrl.control_id]) const loadV1Matches = useCallback(async () => { if (!eigenentwicklung) { setV1Matches([]); return } setLoadingV1(true) try { const res = await fetch(`${BACKEND_URL}?endpoint=v1-matches&id=${ctrl.control_id}`) if (res.ok) setV1Matches(await res.json()); else setV1Matches([]) } catch { setV1Matches([]) } finally { setLoadingV1(false) } }, [ctrl.control_id, eigenentwicklung]) const loadSimilarControls = async () => { setLoadingSimilar(true) try { const res = await fetch(`${BACKEND_URL}?endpoint=similar&id=${ctrl.control_id}`) if (res.ok) setSimilarControls(await res.json()) } catch { /* ignore */ } finally { setLoadingSimilar(false) } } useEffect(() => { loadSimilarControls(); loadTraceability(); loadV1Matches() setSelectedDuplicates(new Set()) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ctrl.control_id]) const toggleDuplicate = (controlId: string) => { setSelectedDuplicates(prev => { const n = new Set(prev); if (n.has(controlId)) n.delete(controlId); else n.add(controlId); return n }) } const handleMergeDuplicates = async () => { if (selectedDuplicates.size === 0) return if (!confirm(`${selectedDuplicates.size} Controls als Duplikate markieren und Tags/Anchors in ${ctrl.control_id} zusammenfuehren?`)) return setMerging(true) try { for (const dupId of selectedDuplicates) { await fetch(`${BACKEND_URL}?endpoint=update-control&id=${dupId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ release_state: 'deprecated' }), }) } if (onRefresh) onRefresh() setSelectedDuplicates(new Set()); loadSimilarControls() } catch { alert('Fehler beim Zusammenfuehren') } finally { setMerging(false) } } return (
{/* Header */}
{ctrl.control_id}

{ctrl.title}

{reviewMode && (
{reviewIndex + 1} / {reviewTotal}
)}
{/* Content */}

Ziel

{ctrl.objective}

Begruendung

{ctrl.rationale}

{eigenentwicklung && ( )} {!ctrl.source_citation && ctrl.open_anchors.length > 0 && (

Abgeleitet aus regulatorischen Anforderungen

Dieser Control wurde aus geschuetzten Quellen reformuliert (z.B. BSI Grundschutz, ISO 27001). Die konkreten Massnahmen leiten sich aus den Open-Source-Referenzen unten ab.

)} {(ctrl.scope.platforms?.length || ctrl.scope.components?.length || ctrl.scope.data_classes?.length) ? (

Geltungsbereich

{ctrl.scope.platforms?.length ?
Plattformen: {ctrl.scope.platforms.join(', ')}
: null} {ctrl.scope.components?.length ?
Komponenten: {ctrl.scope.components.join(', ')}
: null} {ctrl.scope.data_classes?.length ?
Datenklassen: {ctrl.scope.data_classes.join(', ')}
: null}
) : null} {ctrl.requirements.length > 0 && (

Anforderungen

    {ctrl.requirements.map((r, i) =>
  1. {r}
  2. )}
)} {ctrl.test_procedure.length > 0 && (

Pruefverfahren

    {ctrl.test_procedure.map((s, i) =>
  1. {s}
  2. )}
)} {ctrl.evidence.length > 0 && (

Nachweise

{ctrl.evidence.map((ev, i) => (
{typeof ev === 'string' ?
{ev}
:
{ev.type}: {ev.description}
}
))}
)}
{ctrl.risk_score !== null &&
Risiko-Score: {ctrl.risk_score}
} {ctrl.implementation_effort &&
Aufwand: {EFFORT_LABELS[ctrl.implementation_effort] || ctrl.implementation_effort}
} {ctrl.tags.length > 0 && (
{ctrl.tags.map(t => {t})}
)}

Open-Source-Referenzen ({ctrl.open_anchors.length})

{ctrl.open_anchors.length > 0 ? (
{ctrl.open_anchors.map((anchor, i) => (
{anchor.framework} {anchor.ref} {anchor.url && Link}
))}
) : (

Keine Referenzen vorhanden.

)}
{ctrl.generation_metadata && (

Generierungsdetails (intern)

{ctrl.generation_metadata.processing_path &&

Pfad: {String(ctrl.generation_metadata.processing_path)}

} {ctrl.generation_metadata.decomposition_method &&

Methode: {String(ctrl.generation_metadata.decomposition_method)}

} {ctrl.generation_metadata.pass0b_model &&

LLM: {String(ctrl.generation_metadata.pass0b_model)}

} {ctrl.generation_metadata.obligation_type &&

Obligation-Typ: {String(ctrl.generation_metadata.obligation_type)}

} {ctrl.generation_metadata.similarity_status &&

Similarity: {String(ctrl.generation_metadata.similarity_status)}

} {Array.isArray(ctrl.generation_metadata.similar_controls) && (

Aehnliche Controls:

{(ctrl.generation_metadata.similar_controls as Array>).map((s, i) => (

{String(s.control_id)} — {String(s.title)} ({String(s.similarity)})

))}
)}
)}
) }