All 4 page.tsx files reduced well below 500 LOC (235/181/158/262) by extracting components and hooks into colocated _components/ and _hooks/ subdirectories. Zero behavior changes — logic relocated verbatim. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
236 lines
12 KiB
TypeScript
236 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { Scale, Landmark, GitMerge, FileText } from 'lucide-react'
|
|
import {
|
|
SeverityBadge, ObligationTypeBadge, ExtractionMethodBadge,
|
|
type CanonicalControl, type ObligationInfo, type DocumentReference, type MergedDuplicate, type RegulationSummary,
|
|
} from './helpers'
|
|
|
|
interface TraceabilityData {
|
|
control_id: string
|
|
title: string
|
|
is_atomic: boolean
|
|
parent_links: Array<{
|
|
parent_control_id: string
|
|
parent_title: string
|
|
link_type: string
|
|
confidence: number
|
|
source_regulation: string | null
|
|
source_article: string | null
|
|
parent_citation: Record<string, string> | null
|
|
obligation: {
|
|
text: string; action: string; object: string; normative_strength: string
|
|
} | null
|
|
}>
|
|
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 ControlTraceabilityProps {
|
|
ctrl: CanonicalControl
|
|
traceability: TraceabilityData | null
|
|
loadingTrace: boolean
|
|
onNavigateToControl?: (controlId: string) => void
|
|
}
|
|
|
|
export function ControlTraceability({ ctrl, traceability, loadingTrace, onNavigateToControl }: ControlTraceabilityProps) {
|
|
const ControlLink = ({ controlId }: { controlId: string }) => {
|
|
if (onNavigateToControl) {
|
|
return (
|
|
<button onClick={() => onNavigateToControl(controlId)}
|
|
className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded hover:bg-purple-100 hover:underline">
|
|
{controlId}
|
|
</button>
|
|
)
|
|
}
|
|
return <span className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded">{controlId}</span>
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{/* Regulatorische Abdeckung (Eigenentwicklung) covered by parent */}
|
|
|
|
{/* Rechtsgrundlagen / Traceability */}
|
|
{traceability && traceability.parent_links.length > 0 && (
|
|
<section className="bg-violet-50 border border-violet-200 rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Landmark className="w-4 h-4 text-violet-600" />
|
|
<h3 className="text-sm font-semibold text-violet-900">
|
|
Rechtsgrundlagen ({traceability.source_count} {traceability.source_count === 1 ? 'Quelle' : 'Quellen'})
|
|
</h3>
|
|
<ObligationTypeBadge type={ctrl.generation_metadata?.obligation_type as string} />
|
|
{traceability.regulations_summary?.map(rs => (
|
|
<span key={rs.regulation_code} className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-violet-200 text-violet-800">
|
|
{rs.regulation_code}
|
|
</span>
|
|
))}
|
|
{loadingTrace && <span className="text-xs text-violet-400">Laden...</span>}
|
|
</div>
|
|
<div className="space-y-3">
|
|
{traceability.parent_links.map((link, i) => (
|
|
<div key={i} className="bg-white/60 border border-violet-100 rounded-lg p-3">
|
|
<div className="flex items-start gap-2">
|
|
<Scale className="w-4 h-4 text-violet-500 mt-0.5 flex-shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
{link.source_regulation && <span className="text-sm font-semibold text-violet-900">{link.source_regulation}</span>}
|
|
{link.source_article && <span className="text-sm text-violet-700">{link.source_article}</span>}
|
|
{!link.source_regulation && link.parent_citation?.source && (
|
|
<span className="text-sm font-semibold text-violet-900">
|
|
{link.parent_citation.source}{link.parent_citation.article && ` — ${link.parent_citation.article}`}
|
|
</span>
|
|
)}
|
|
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
|
link.link_type === 'decomposition' ? 'bg-violet-100 text-violet-600' :
|
|
link.link_type === 'dedup_merge' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-600'
|
|
}`}>
|
|
{link.link_type === 'decomposition' ? 'Ableitung' : link.link_type === 'dedup_merge' ? 'Dedup' : link.link_type}
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-violet-600 mt-1">
|
|
via <ControlLink controlId={link.parent_control_id} />
|
|
{link.parent_title && <span className="text-violet-500 ml-1">— {link.parent_title}</span>}
|
|
</p>
|
|
{link.obligation && (
|
|
<p className="text-xs text-violet-500 mt-1.5 bg-violet-50 rounded p-2">
|
|
<span className={`inline-block mr-1.5 px-1.5 py-0.5 rounded text-xs font-medium ${
|
|
link.obligation.normative_strength === 'must' ? 'bg-red-100 text-red-700' :
|
|
link.obligation.normative_strength === 'should' ? 'bg-amber-100 text-amber-700' : 'bg-green-100 text-green-700'
|
|
}`}>
|
|
{link.obligation.normative_strength === 'must' ? 'MUSS' : link.obligation.normative_strength === 'should' ? 'SOLL' : 'KANN'}
|
|
</span>
|
|
{link.obligation.text.slice(0, 200)}{link.obligation.text.length > 200 ? '...' : ''}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Fallback: simple parent display */}
|
|
{ctrl.parent_control_uuid && (!traceability || traceability.parent_links.length === 0) && !loadingTrace && (
|
|
<section className="bg-violet-50 border border-violet-200 rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<GitMerge className="w-4 h-4 text-violet-600" />
|
|
<h3 className="text-sm font-semibold text-violet-900">Atomares Control</h3>
|
|
<ObligationTypeBadge type={ctrl.generation_metadata?.obligation_type as string} />
|
|
</div>
|
|
<p className="text-sm text-violet-800">
|
|
Abgeleitet aus Eltern-Control{' '}
|
|
<span className="font-mono font-semibold text-purple-700 bg-purple-100 px-1.5 py-0.5 rounded">
|
|
{ctrl.parent_control_id || ctrl.parent_control_uuid}
|
|
</span>
|
|
{ctrl.parent_control_title && <span className="text-violet-700 ml-1">— {ctrl.parent_control_title}</span>}
|
|
</p>
|
|
</section>
|
|
)}
|
|
|
|
{/* Document References */}
|
|
{traceability?.is_atomic && traceability.document_references && traceability.document_references.length > 0 && (
|
|
<section className="bg-indigo-50 border border-indigo-200 rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<FileText className="w-4 h-4 text-indigo-600" />
|
|
<h3 className="text-sm font-semibold text-indigo-900">Original-Dokumente ({traceability.document_references.length})</h3>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{traceability.document_references.map((dr, i) => (
|
|
<div key={i} className="flex items-center gap-2 text-sm bg-white/60 border border-indigo-100 rounded-lg p-2">
|
|
<span className="font-semibold text-indigo-900">{dr.regulation_code}</span>
|
|
{dr.article && <span className="text-indigo-700">{dr.article}</span>}
|
|
{dr.paragraph && <span className="text-indigo-600 text-xs">{dr.paragraph}</span>}
|
|
<span className="ml-auto flex items-center gap-1.5">
|
|
<ExtractionMethodBadge method={dr.extraction_method} />
|
|
{dr.confidence !== null && <span className="text-xs text-gray-500">{(dr.confidence * 100).toFixed(0)}%</span>}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Obligations */}
|
|
{traceability && !traceability.is_atomic && traceability.obligations && traceability.obligations.length > 0 && (
|
|
<section className="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Scale className="w-4 h-4 text-amber-600" />
|
|
<h3 className="text-sm font-semibold text-amber-900">
|
|
Abgeleitete Pflichten ({traceability.obligation_count ?? traceability.obligations.length})
|
|
</h3>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{traceability.obligations.map((ob) => (
|
|
<div key={ob.candidate_id} className="bg-white/60 border border-amber-100 rounded-lg p-3">
|
|
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
|
<span className="font-mono text-xs text-amber-700 bg-amber-100 px-1.5 py-0.5 rounded">{ob.candidate_id}</span>
|
|
<span className={`inline-block px-1.5 py-0.5 rounded text-xs font-medium ${
|
|
ob.normative_strength === 'must' ? 'bg-red-100 text-red-700' :
|
|
ob.normative_strength === 'should' ? 'bg-amber-100 text-amber-700' : 'bg-green-100 text-green-700'
|
|
}`}>
|
|
{ob.normative_strength === 'must' ? 'MUSS' : ob.normative_strength === 'should' ? 'SOLL' : 'KANN'}
|
|
</span>
|
|
{ob.action && <span className="text-xs text-amber-600">{ob.action}</span>}
|
|
{ob.object && <span className="text-xs text-amber-500">→ {ob.object}</span>}
|
|
</div>
|
|
<p className="text-xs text-gray-700 leading-relaxed">
|
|
{ob.obligation_text.slice(0, 300)}{ob.obligation_text.length > 300 ? '...' : ''}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Merged Duplicates */}
|
|
{traceability?.merged_duplicates && traceability.merged_duplicates.length > 0 && (
|
|
<section className="bg-slate-50 border border-slate-200 rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<GitMerge className="w-4 h-4 text-slate-600" />
|
|
<h3 className="text-sm font-semibold text-slate-900">
|
|
Zusammengefuehrte Duplikate ({traceability.merged_duplicates_count ?? traceability.merged_duplicates.length})
|
|
</h3>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
{traceability.merged_duplicates.map((dup) => (
|
|
<div key={dup.control_id} className="flex items-center gap-2 text-sm">
|
|
<ControlLink controlId={dup.control_id} />
|
|
<span className="text-gray-700 flex-1 truncate">{dup.title}</span>
|
|
{dup.source_regulation && <span className="text-xs text-slate-500 bg-slate-100 px-1.5 py-0.5 rounded">{dup.source_regulation}</span>}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{/* Child controls */}
|
|
{traceability && traceability.children.length > 0 && (
|
|
<section className="bg-emerald-50 border border-emerald-200 rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<GitMerge className="w-4 h-4 text-emerald-600" />
|
|
<h3 className="text-sm font-semibold text-emerald-900">Abgeleitete Controls ({traceability.children.length})</h3>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
{traceability.children.map((child) => (
|
|
<div key={child.control_id} className="flex items-center gap-2 text-sm">
|
|
<ControlLink controlId={child.control_id} />
|
|
<span className="text-gray-700 flex-1 truncate">{child.title}</span>
|
|
<SeverityBadge severity={child.severity} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</>
|
|
)
|
|
}
|