'use client' import { useState, useEffect } from 'react' import { ArrowLeft, ExternalLink, BookOpen, Scale, FileText, Eye, CheckCircle2, Trash2, Pencil, Clock, ChevronLeft, SkipForward, GitMerge, Search, } from 'lucide-react' import { CanonicalControl, EFFORT_LABELS, BACKEND_URL, SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, TargetAudienceBadge, VERIFICATION_METHODS, CATEGORY_OPTIONS, } from './helpers' 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 ControlDetailProps { ctrl: CanonicalControl onBack: () => void onEdit: () => void onDelete: (controlId: string) => void onReview: (controlId: string, action: string) => void onRefresh?: () => void // Review mode navigation reviewMode?: boolean reviewIndex?: number reviewTotal?: number onReviewPrev?: () => void onReviewNext?: () => void } export function ControlDetail({ ctrl, onBack, onEdit, onDelete, onReview, onRefresh, 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) useEffect(() => { loadSimilarControls() setSelectedDuplicates(new Set()) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ctrl.control_id]) 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) } } const toggleDuplicate = (controlId: string) => { setSelectedDuplicates(prev => { const next = new Set(prev) if (next.has(controlId)) next.delete(controlId) else next.add(controlId) return next }) } 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 each duplicate: mark as deprecated 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' }), }) } // Refresh to show updated state 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 */}
{/* Objective */}

Ziel

{ctrl.objective}

{/* Rationale */}

Begruendung

{ctrl.rationale}

{/* Gesetzliche Grundlage (Rule 1 + 2) */} {ctrl.source_citation && (

Gesetzliche Grundlage

{ctrl.license_rule === 1 && ( Direkte gesetzliche Pflicht )} {ctrl.license_rule === 2 && ( Standard mit Zitationspflicht )}
{ctrl.source_citation.source && (

{ctrl.source_citation.source}

)} {ctrl.source_citation.license && (

Lizenz: {ctrl.source_citation.license}

)} {ctrl.source_citation.license_notice && (

{ctrl.source_citation.license_notice}

)}
{ctrl.source_citation.url && ( Quelle )}
{ctrl.source_original_text && (
Originaltext anzeigen

{ctrl.source_original_text}

)}
)} {/* Impliziter Gesetzesbezug (Rule 3 — kein Originaltext, aber ggf. Gesetzesbezug ueber Anchors) */} {!ctrl.source_citation && ctrl.open_anchors.length > 0 && (

Dieser Control setzt implizit gesetzliche Anforderungen um (z.B. DSGVO Art. 32, NIS2 Art. 21). Die konkreten Massnahmen leiten sich aus den Open-Source-Referenzen unten ab.

)} {/* Scope */} {(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} {/* Requirements */} {ctrl.requirements.length > 0 && (

Anforderungen

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

Pruefverfahren

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

Nachweise

{ctrl.evidence.map((ev, i) => (
{ev.type}: {ev.description}
))}
)} {/* Meta */}
{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 Anchors */}

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.

)}
{/* Generation Metadata (internal) */} {ctrl.generation_metadata && (

Generierungsdetails (intern)

Pfad: {String(ctrl.generation_metadata.processing_path || '-')}

{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)})

))}
)}
)} {/* Similar Controls (Dedup) */}

Aehnliche Controls

{loadingSimilar && Laden...}
{similarControls.length > 0 ? ( <>
{ctrl.control_id} — {ctrl.title} Behalten (Haupt-Control)
{similarControls.map(sim => (
toggleDuplicate(sim.control_id)} className="text-red-600" /> {sim.control_id} {sim.title} {(sim.similarity * 100).toFixed(1)}%
))}
{selectedDuplicates.size > 0 && ( )} ) : (

{loadingSimilar ? 'Suche aehnliche Controls...' : 'Keine aehnlichen Controls gefunden.'}

)}
{/* Review Actions */} {['needs_review', 'too_close', 'duplicate'].includes(ctrl.release_state) && (

Review erforderlich

{reviewMode && ( Review-Modus aktiv )}
)}
) }