All 5 files reduced below 500 LOC (hard cap) by extracting sub-components: - training/page.tsx: 780→278 LOC — imports existing _components/, adds BlocksSection - control-provenance/page.tsx: 739→145 LOC — extracts provenance-data.ts, ProvenanceHelpers, LicenseMatrix, SourceRegistry - iace/[projectId]/verification/page.tsx: 673→164 LOC — extracts VerificationForm, CompleteModal, SuggestEvidenceModal, VerificationTable - training/learner/page.tsx: 560→216 LOC — extracts AssignmentsList, ContentView, QuizView, CertificatesView - ControlDetail.tsx: 878→311 LOC — adds ControlSourceCitation, ControlTraceability, ControlRegulatorySection, ControlSimilarControls, ControlReviewActions siblings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
86 lines
3.7 KiB
TypeScript
86 lines
3.7 KiB
TypeScript
'use client'
|
|
|
|
import { Scale } from 'lucide-react'
|
|
import type { CanonicalControl } from './helpers'
|
|
|
|
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<string, string> | null
|
|
similarity_score: number
|
|
match_rank: number
|
|
match_method: string
|
|
}
|
|
|
|
interface ControlRegulatorySectionProps {
|
|
ctrl: CanonicalControl
|
|
v1Matches: V1Match[]
|
|
loadingV1: boolean
|
|
onNavigateToControl?: (controlId: string) => void
|
|
onCompare?: (ctrl: CanonicalControl, matches: V1Match[]) => void
|
|
}
|
|
|
|
export function ControlRegulatorySection({
|
|
ctrl, v1Matches, loadingV1, onNavigateToControl, onCompare,
|
|
}: ControlRegulatorySectionProps) {
|
|
return (
|
|
<section className="bg-orange-50 border border-orange-200 rounded-lg p-4">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Scale className="w-4 h-4 text-orange-600" />
|
|
<h3 className="text-sm font-semibold text-orange-900">Regulatorische Abdeckung</h3>
|
|
{loadingV1 && <span className="text-xs text-orange-400">Laden...</span>}
|
|
</div>
|
|
{v1Matches.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{v1Matches.map((match, i) => (
|
|
<div key={i} className="bg-white/60 border border-orange-100 rounded-lg p-3">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap mb-1">
|
|
{match.matched_source && (
|
|
<span className="text-xs font-semibold text-blue-800 bg-blue-100 px-1.5 py-0.5 rounded">{match.matched_source}</span>
|
|
)}
|
|
{match.matched_article && (
|
|
<span className="text-xs text-blue-700 bg-blue-50 px-1.5 py-0.5 rounded">{match.matched_article}</span>
|
|
)}
|
|
<span className={`text-xs font-medium px-1.5 py-0.5 rounded ${
|
|
match.similarity_score >= 0.85 ? 'bg-green-100 text-green-700' :
|
|
match.similarity_score >= 0.80 ? 'bg-yellow-100 text-yellow-700' : 'bg-gray-100 text-gray-600'
|
|
}`}>
|
|
{(match.similarity_score * 100).toFixed(0)}%
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-gray-800">
|
|
{onNavigateToControl ? (
|
|
<button onClick={() => onNavigateToControl(match.matched_control_id)}
|
|
className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded hover:bg-purple-100 hover:underline mr-1.5">
|
|
{match.matched_control_id}
|
|
</button>
|
|
) : (
|
|
<span className="font-mono text-xs text-purple-600 bg-purple-50 px-1.5 py-0.5 rounded mr-1.5">{match.matched_control_id}</span>
|
|
)}
|
|
{match.matched_title}
|
|
</p>
|
|
</div>
|
|
{onCompare && (
|
|
<button onClick={() => onCompare(ctrl, v1Matches)}
|
|
className="text-xs text-orange-600 border border-orange-300 rounded px-2 py-1 hover:bg-orange-100 whitespace-nowrap flex-shrink-0">
|
|
Vergleichen
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : !loadingV1 ? (
|
|
<p className="text-sm text-orange-600">Keine regulatorische Abdeckung gefunden. Dieses Control ist eine reine Eigenentwicklung.</p>
|
|
) : null}
|
|
</section>
|
|
)
|
|
}
|