refactor(admin): split compliance-hub, obligations, document-generator pages
Each page.tsx was >1000 LOC; extract components to _components/ and hooks to _hooks/ so page files stay under 500 LOC (164 / 255 / 243 respectively). Zero behavior changes — logic relocated verbatim. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
|
||||
import type { ModuleStatusData } from './types'
|
||||
import { MODULE_ICONS } from './types'
|
||||
|
||||
interface ModulesTabProps {
|
||||
moduleStatus: ModuleStatusData | null
|
||||
}
|
||||
|
||||
export function ModulesTab({ moduleStatus }: ModulesTabProps) {
|
||||
if (!moduleStatus) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-48">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Summary */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6 text-center">
|
||||
<p className="text-sm text-slate-500">Gesamt-Fortschritt</p>
|
||||
<p className="text-3xl font-bold text-purple-600">{moduleStatus.overall_progress.toFixed(0)}%</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6 text-center">
|
||||
<p className="text-sm text-slate-500">Module gestartet</p>
|
||||
<p className="text-3xl font-bold text-blue-600">{moduleStatus.started}/{moduleStatus.total}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6 text-center">
|
||||
<p className="text-sm text-slate-500">Module abgeschlossen</p>
|
||||
<p className="text-3xl font-bold text-green-600">{moduleStatus.complete}/{moduleStatus.total}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Module Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{moduleStatus.modules.map(mod => (
|
||||
<div key={mod.key} className="bg-white rounded-xl shadow-sm border p-5">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="text-2xl">{MODULE_ICONS[mod.key] || '📦'}</span>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-900">{mod.label}</h4>
|
||||
<p className="text-xs text-slate-500">{mod.count} Eintraege</p>
|
||||
</div>
|
||||
<span className={`ml-auto px-2 py-0.5 text-xs rounded-full font-medium ${
|
||||
mod.status === 'complete' ? 'bg-green-100 text-green-700' :
|
||||
mod.status === 'in_progress' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-slate-100 text-slate-500'
|
||||
}`}>
|
||||
{mod.status === 'complete' ? 'Fertig' :
|
||||
mod.status === 'in_progress' ? 'In Arbeit' : 'Offen'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-slate-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all duration-500 ${
|
||||
mod.status === 'complete' ? 'bg-green-500' :
|
||||
mod.status === 'in_progress' ? 'bg-yellow-500' : 'bg-slate-300'
|
||||
}`}
|
||||
style={{ width: `${mod.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import type { DashboardData, MappingsData, FindingsData, NextAction } from './types'
|
||||
import { DOMAIN_LABELS } from './types'
|
||||
|
||||
interface OverviewTabProps {
|
||||
dashboard: DashboardData | null
|
||||
mappings: MappingsData | null
|
||||
findings: FindingsData | null
|
||||
nextActions: NextAction[]
|
||||
evidenceDistribution: {
|
||||
by_confidence: Record<string, number>
|
||||
four_eyes_pending: number
|
||||
total: number
|
||||
} | null
|
||||
score: number
|
||||
scoreColor: string
|
||||
scoreBgColor: string
|
||||
loadData: () => void
|
||||
regulations: Array<{ id: string; code: string; name: string; regulation_type: string; requirement_count: number }>
|
||||
}
|
||||
|
||||
export function OverviewTab({
|
||||
dashboard, mappings, findings, nextActions, evidenceDistribution,
|
||||
score, scoreColor, scoreBgColor, loadData, regulations,
|
||||
}: OverviewTabProps) {
|
||||
return (
|
||||
<>
|
||||
{/* Quick Actions */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Schnellzugriff</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4">
|
||||
{[
|
||||
{ href: '/sdk/audit-checklist', icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01', label: 'Audit Checkliste', sub: `${dashboard?.total_requirements || '...'} Anforderungen`, color: 'purple' },
|
||||
{ href: '/sdk/controls', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z', label: 'Controls', sub: `${dashboard?.total_controls || '...'} Massnahmen`, color: 'green' },
|
||||
{ href: '/sdk/evidence', icon: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z', label: 'Evidence', sub: 'Nachweise', color: 'blue' },
|
||||
{ href: '/sdk/risks', icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z', label: 'Risk Matrix', sub: '5x5 Risiken', color: 'red' },
|
||||
{ href: '/sdk/process-tasks', icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4', label: 'Prozesse', sub: 'Aufgaben', color: 'indigo' },
|
||||
{ href: '/sdk/audit-report', icon: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', label: 'Audit Report', sub: 'PDF Export', color: 'orange' },
|
||||
].map(item => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`p-4 rounded-lg border border-slate-200 hover:border-${item.color}-500 hover:bg-${item.color}-50 transition-colors text-center`}
|
||||
>
|
||||
<div className={`text-${item.color}-600 mb-2 flex justify-center`}>
|
||||
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={item.icon} />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="font-medium text-slate-900 text-sm">{item.label}</p>
|
||||
<p className="text-xs text-slate-500 mt-1">{item.sub}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Score and Stats Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-4">
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-sm font-medium text-slate-500 mb-4">Compliance Score</h3>
|
||||
<div className={`text-5xl font-bold ${scoreColor}`}>
|
||||
{score.toFixed(0)}%
|
||||
</div>
|
||||
<div className="mt-4 h-2 bg-slate-200 rounded-full overflow-hidden">
|
||||
<div className={`h-full transition-all duration-500 ${scoreBgColor}`} style={{ width: `${score}%` }} />
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-slate-500">
|
||||
{dashboard?.controls_by_status?.pass || 0} von {dashboard?.total_controls || 0} Controls bestanden
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{[
|
||||
{ label: 'Verordnungen', value: dashboard?.total_regulations || 0, sub: `${dashboard?.total_requirements || 0} Anforderungen`, iconColor: 'blue', icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' },
|
||||
{ label: 'Controls', value: dashboard?.total_controls || 0, sub: `${dashboard?.controls_by_status?.pass || 0} bestanden`, iconColor: 'green', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' },
|
||||
{ label: 'Nachweise', value: dashboard?.total_evidence || 0, sub: `${dashboard?.evidence_by_status?.valid || 0} aktiv`, iconColor: 'purple', icon: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z' },
|
||||
{ label: 'Risiken', value: dashboard?.total_risks || 0, sub: `${(dashboard?.risks_by_level?.high || 0) + (dashboard?.risks_by_level?.critical || 0)} kritisch`, iconColor: 'red', icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z' },
|
||||
].map(stat => (
|
||||
<div key={stat.label} className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">{stat.label}</p>
|
||||
<p className="text-2xl font-bold text-slate-900">{stat.value}</p>
|
||||
</div>
|
||||
<div className={`w-10 h-10 bg-${stat.iconColor}-100 rounded-lg flex items-center justify-center`}>
|
||||
<svg className={`w-5 h-5 text-${stat.iconColor}-600`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={stat.icon} />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-slate-500">{stat.sub}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Anti-Fake-Evidence Section (Phase 3) */}
|
||||
{dashboard && (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Anti-Fake-Evidence Status</h3>
|
||||
|
||||
{/* Confidence Distribution Bar */}
|
||||
{evidenceDistribution && evidenceDistribution.total > 0 && (
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-slate-500 mb-2">Confidence-Verteilung ({evidenceDistribution.total} Nachweise)</p>
|
||||
<div className="flex h-6 rounded-full overflow-hidden">
|
||||
{(['E0', 'E1', 'E2', 'E3', 'E4'] as const).map(level => {
|
||||
const count = evidenceDistribution.by_confidence[level] || 0
|
||||
const pct = (count / evidenceDistribution.total) * 100
|
||||
if (pct === 0) return null
|
||||
const colors: Record<string, string> = {
|
||||
E0: 'bg-red-400', E1: 'bg-yellow-400', E2: 'bg-blue-400', E3: 'bg-green-400', E4: 'bg-emerald-400'
|
||||
}
|
||||
return (
|
||||
<div key={level} className={`${colors[level]} flex items-center justify-center text-xs text-white font-medium`}
|
||||
style={{ width: `${pct}%` }} title={`${level}: ${count}`}>
|
||||
{pct >= 10 ? `${level} (${count})` : ''}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-slate-500">
|
||||
{(['E0', 'E1', 'E2', 'E3', 'E4'] as const).map(level => {
|
||||
const count = evidenceDistribution.by_confidence[level] || 0
|
||||
const dotColors: Record<string, string> = {
|
||||
E0: 'bg-red-400', E1: 'bg-yellow-400', E2: 'bg-blue-400', E3: 'bg-green-400', E4: 'bg-emerald-400'
|
||||
}
|
||||
return (
|
||||
<span key={level} className="flex items-center gap-1">
|
||||
<span className={`w-2 h-2 rounded-full ${dotColors[level]}`} />
|
||||
{level}: {count}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Multi-Score Dimensions */}
|
||||
{dashboard.multi_score && (
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-slate-500 mb-2">Multi-dimensionaler Score</p>
|
||||
<div className="space-y-2">
|
||||
{([
|
||||
{ key: 'requirement_coverage', label: 'Anforderungsabdeckung', color: 'bg-blue-500' },
|
||||
{ key: 'evidence_strength', label: 'Evidence-Staerke', color: 'bg-green-500' },
|
||||
{ key: 'validation_quality', label: 'Validierungsqualitaet', color: 'bg-purple-500' },
|
||||
{ key: 'evidence_freshness', label: 'Aktualitaet', color: 'bg-yellow-500' },
|
||||
{ key: 'control_effectiveness', label: 'Control-Wirksamkeit', color: 'bg-indigo-500' },
|
||||
] as const).map(dim => {
|
||||
const value = (dashboard.multi_score as Record<string, number>)[dim.key] || 0
|
||||
return (
|
||||
<div key={dim.key} className="flex items-center gap-3">
|
||||
<span className="text-xs text-slate-600 w-44 truncate">{dim.label}</span>
|
||||
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
|
||||
<div className={`h-full ${dim.color} rounded-full transition-all`} style={{ width: `${value}%` }} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-600 w-12 text-right">{typeof value === 'number' ? value.toFixed(0) : value}%</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="flex items-center gap-3 pt-2 border-t border-slate-100">
|
||||
<span className="text-xs font-semibold text-slate-700 w-44">Audit-Readiness</span>
|
||||
<div className="flex-1 h-3 bg-slate-200 rounded-full overflow-hidden">
|
||||
<div className={`h-full rounded-full transition-all ${
|
||||
(dashboard.multi_score.overall_readiness || 0) >= 80 ? 'bg-green-500' :
|
||||
(dashboard.multi_score.overall_readiness || 0) >= 60 ? 'bg-yellow-500' : 'bg-red-500'
|
||||
}`} style={{ width: `${dashboard.multi_score.overall_readiness || 0}%` }} />
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-slate-700 w-12 text-right">
|
||||
{typeof dashboard.multi_score.overall_readiness === 'number' ? dashboard.multi_score.overall_readiness.toFixed(0) : 0}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom row: Four-Eyes + Hard Blocks */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="text-center p-3 rounded-lg bg-yellow-50">
|
||||
<div className="text-2xl font-bold text-yellow-700">{evidenceDistribution?.four_eyes_pending || 0}</div>
|
||||
<div className="text-xs text-yellow-600 mt-1">Four-Eyes Reviews ausstehend</div>
|
||||
</div>
|
||||
{dashboard.multi_score?.hard_blocks && dashboard.multi_score.hard_blocks.length > 0 ? (
|
||||
<div className="p-3 rounded-lg bg-red-50">
|
||||
<div className="text-xs font-medium text-red-700 mb-1">Hard Blocks ({dashboard.multi_score.hard_blocks.length})</div>
|
||||
<ul className="space-y-1">
|
||||
{dashboard.multi_score.hard_blocks.slice(0, 3).map((block: string, i: number) => (
|
||||
<li key={i} className="text-xs text-red-600 flex items-start gap-1">
|
||||
<span className="text-red-400 mt-0.5">•</span>
|
||||
<span>{block}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center p-3 rounded-lg bg-green-50">
|
||||
<div className="text-2xl font-bold text-green-700">0</div>
|
||||
<div className="text-xs text-green-600 mt-1">Keine Hard Blocks</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Next Actions + Findings */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Next Actions */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Naechste Aktionen</h3>
|
||||
{nextActions.length === 0 ? (
|
||||
<p className="text-sm text-slate-500">Keine offenen Aktionen.</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{nextActions.map(action => (
|
||||
<div key={action.id} className="flex items-center gap-3 p-3 rounded-lg bg-slate-50">
|
||||
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${
|
||||
action.days_overdue > 0 ? 'bg-red-500' : 'bg-yellow-500'
|
||||
}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-slate-900 truncate">{action.title}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{action.control_id} · {DOMAIN_LABELS[action.domain] || action.domain}
|
||||
{action.days_overdue > 0 && <span className="text-red-600 ml-2">{action.days_overdue}d ueberfaellig</span>}
|
||||
</p>
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full ${
|
||||
action.status === 'partial' ? 'bg-yellow-100 text-yellow-700' : 'bg-slate-100 text-slate-600'
|
||||
}`}>
|
||||
{action.status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Audit Findings */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Audit Findings</h3>
|
||||
<Link href="/sdk/audit-checklist" className="text-sm text-purple-600 hover:text-purple-700">
|
||||
Audit Checkliste →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="w-3 h-3 bg-red-500 rounded-full" />
|
||||
<span className="text-sm font-medium text-red-800">Hauptabweichungen</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-red-600">{findings?.open_majors || 0}</p>
|
||||
<p className="text-xs text-red-600">offen (blockiert Zertifizierung)</p>
|
||||
</div>
|
||||
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="w-3 h-3 bg-yellow-500 rounded-full" />
|
||||
<span className="text-sm font-medium text-yellow-800">Nebenabweichungen</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-yellow-600">{findings?.open_minors || 0}</p>
|
||||
<p className="text-xs text-yellow-600">offen (erfordert CAPA)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-slate-500">
|
||||
Gesamt: {findings?.total || 0} Findings ({findings?.major_count || 0} Major, {findings?.minor_count || 0} Minor, {findings?.ofi_count || 0} OFI)
|
||||
</span>
|
||||
{(findings?.open_majors || 0) === 0 ? (
|
||||
<span className="px-2 py-1 bg-green-100 text-green-700 rounded-full text-xs font-medium">
|
||||
Zertifizierung moeglich
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 bg-red-100 text-red-700 rounded-full text-xs font-medium">
|
||||
Zertifizierung blockiert
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Control-Mappings & Domain Chart */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Control-Mappings</h3>
|
||||
<Link href="/sdk/controls" className="text-sm text-purple-600 hover:text-purple-700">
|
||||
Alle anzeigen →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center gap-6 mb-4">
|
||||
<div>
|
||||
<p className="text-4xl font-bold text-purple-600">{mappings?.total || 0}</p>
|
||||
<p className="text-sm text-slate-500">Mappings gesamt</p>
|
||||
</div>
|
||||
<div className="flex-1 h-16 bg-slate-50 rounded-lg p-3">
|
||||
<p className="text-xs text-slate-500 mb-1">Nach Verordnung</p>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{mappings?.by_regulation && Object.entries(mappings.by_regulation).slice(0, 5).map(([reg, count]) => (
|
||||
<span key={reg} className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs">
|
||||
{reg}: {count}
|
||||
</span>
|
||||
))}
|
||||
{!mappings?.by_regulation && (
|
||||
<span className="px-2 py-0.5 bg-slate-100 text-slate-500 rounded text-xs">Keine Mappings vorhanden</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Controls nach Domain</h3>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(dashboard?.controls_by_domain || {}).slice(0, 6).map(([domain, stats]) => {
|
||||
const total = stats.total || 0
|
||||
const pass = stats.pass || 0
|
||||
const partial = stats.partial || 0
|
||||
const passPercent = total > 0 ? ((pass + partial * 0.5) / total) * 100 : 0
|
||||
|
||||
return (
|
||||
<div key={domain} className="flex items-center gap-3">
|
||||
<span className="text-xs font-medium text-slate-600 w-24 truncate">
|
||||
{DOMAIN_LABELS[domain] || domain}
|
||||
</span>
|
||||
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden flex">
|
||||
<div className="bg-green-500 h-full" style={{ width: `${(pass / (total || 1)) * 100}%` }} />
|
||||
<div className="bg-yellow-500 h-full" style={{ width: `${(partial / (total || 1)) * 100}%` }} />
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 w-16 text-right">{passPercent.toFixed(0)}%</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Regulations Table */}
|
||||
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||
<div className="p-4 border-b flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Verordnungen & Standards ({regulations.length})</h3>
|
||||
<button onClick={loadData} className="text-sm text-purple-600 hover:text-purple-700">
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Code</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Name</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Typ</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase">Anforderungen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{regulations.slice(0, 15).map((reg) => (
|
||||
<tr key={reg.id} className="hover:bg-slate-50">
|
||||
<td className="px-4 py-3">
|
||||
<span className="font-mono font-medium text-purple-600">{reg.code}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<p className="font-medium text-slate-900">{reg.name}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
reg.regulation_type === 'eu_regulation' ? 'bg-blue-100 text-blue-700' :
|
||||
reg.regulation_type === 'eu_directive' ? 'bg-purple-100 text-purple-700' :
|
||||
reg.regulation_type === 'bsi_standard' ? 'bg-green-100 text-green-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
{reg.regulation_type === 'eu_regulation' ? 'EU-VO' :
|
||||
reg.regulation_type === 'eu_directive' ? 'EU-RL' :
|
||||
reg.regulation_type === 'bsi_standard' ? 'BSI' :
|
||||
reg.regulation_type === 'de_law' ? 'DE' : reg.regulation_type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className="font-medium">{reg.requirement_count}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import type { RoadmapData } from './types'
|
||||
import { BUCKET_LABELS, DOMAIN_LABELS } from './types'
|
||||
|
||||
interface RoadmapTabProps {
|
||||
roadmap: RoadmapData | null
|
||||
}
|
||||
|
||||
export function RoadmapTab({ roadmap }: RoadmapTabProps) {
|
||||
if (!roadmap) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-48">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
{(['quick_wins', 'must_have', 'should_have', 'nice_to_have'] as const).map(bucketKey => {
|
||||
const meta = BUCKET_LABELS[bucketKey]
|
||||
const items = roadmap.buckets[bucketKey] || []
|
||||
|
||||
return (
|
||||
<div key={bucketKey} className={`rounded-xl border p-4 ${meta.bg}`}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className={`font-semibold ${meta.color}`}>{meta.label}</h3>
|
||||
<span className={`text-xs font-medium px-2 py-0.5 rounded-full bg-white ${meta.color}`}>
|
||||
{items.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{items.length === 0 ? (
|
||||
<p className="text-sm text-slate-400 text-center py-4">Keine Eintraege</p>
|
||||
) : (
|
||||
items.map(item => (
|
||||
<div key={item.id} className="bg-white rounded-lg p-3 shadow-sm">
|
||||
<p className="text-sm font-medium text-slate-900 truncate">{item.title}</p>
|
||||
<div className="mt-1 flex items-center gap-2 text-xs text-slate-500">
|
||||
<span className="font-mono">{item.control_id}</span>
|
||||
<span>·</span>
|
||||
<span>{DOMAIN_LABELS[item.domain] || item.domain}</span>
|
||||
</div>
|
||||
{item.days_overdue > 0 && (
|
||||
<p className="mt-1 text-xs text-red-600">{item.days_overdue}d ueberfaellig</p>
|
||||
)}
|
||||
{item.owner && (
|
||||
<p className="mt-1 text-xs text-slate-400">{item.owner}</p>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
'use client'
|
||||
|
||||
import { ConfidenceLevelBadge } from '../../evidence/components/anti-fake-badges'
|
||||
import type { TraceabilityMatrixData } from './types'
|
||||
import { DOMAIN_LABELS } from './types'
|
||||
|
||||
interface TraceabilityTabProps {
|
||||
traceabilityMatrix: TraceabilityMatrixData | null
|
||||
traceabilityLoading: boolean
|
||||
traceabilityFilter: 'all' | 'covered' | 'uncovered' | 'fully_verified'
|
||||
setTraceabilityFilter: (f: 'all' | 'covered' | 'uncovered' | 'fully_verified') => void
|
||||
traceabilityDomainFilter: string
|
||||
setTraceabilityDomainFilter: (d: string) => void
|
||||
expandedControls: Set<string>
|
||||
expandedEvidence: Set<string>
|
||||
toggleControlExpanded: (id: string) => void
|
||||
toggleEvidenceExpanded: (id: string) => void
|
||||
}
|
||||
|
||||
export function TraceabilityTab({
|
||||
traceabilityMatrix, traceabilityLoading,
|
||||
traceabilityFilter, setTraceabilityFilter,
|
||||
traceabilityDomainFilter, setTraceabilityDomainFilter,
|
||||
expandedControls, expandedEvidence,
|
||||
toggleControlExpanded, toggleEvidenceExpanded,
|
||||
}: TraceabilityTabProps) {
|
||||
if (traceabilityLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
||||
<span className="ml-3 text-slate-500">Traceability Matrix wird geladen...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!traceabilityMatrix) {
|
||||
return (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
Keine Daten verfuegbar. Stellen Sie sicher, dass Controls und Evidence vorhanden sind.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const summary = traceabilityMatrix.summary
|
||||
const totalControls = summary.total_controls || 0
|
||||
const covered = summary.covered || 0
|
||||
const fullyVerified = summary.fully_verified || 0
|
||||
const uncovered = summary.uncovered || 0
|
||||
|
||||
const filteredControls = (traceabilityMatrix.controls || []).filter(ctrl => {
|
||||
if (traceabilityFilter === 'covered' && !ctrl.coverage.has_evidence) return false
|
||||
if (traceabilityFilter === 'uncovered' && ctrl.coverage.has_evidence) return false
|
||||
if (traceabilityFilter === 'fully_verified' && !ctrl.coverage.all_assertions_verified) return false
|
||||
if (traceabilityDomainFilter !== 'all' && ctrl.domain !== traceabilityDomainFilter) return false
|
||||
return true
|
||||
})
|
||||
|
||||
const domains = [...new Set(traceabilityMatrix.controls.map(c => c.domain))].sort()
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-purple-700">{totalControls}</div>
|
||||
<div className="text-sm text-purple-600">Total Controls</div>
|
||||
</div>
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-blue-700">{covered}</div>
|
||||
<div className="text-sm text-blue-600">Abgedeckt</div>
|
||||
</div>
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-green-700">{fullyVerified}</div>
|
||||
<div className="text-sm text-green-600">Vollst. verifiziert</div>
|
||||
</div>
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-red-700">{uncovered}</div>
|
||||
<div className="text-sm text-red-600">Unabgedeckt</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Bar */}
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<div className="flex gap-1">
|
||||
{([
|
||||
{ key: 'all', label: 'Alle' },
|
||||
{ key: 'covered', label: 'Abgedeckt' },
|
||||
{ key: 'uncovered', label: 'Nicht abgedeckt' },
|
||||
{ key: 'fully_verified', label: 'Vollst. verifiziert' },
|
||||
] as const).map(f => (
|
||||
<button
|
||||
key={f.key}
|
||||
onClick={() => setTraceabilityFilter(f.key)}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
traceabilityFilter === f.key
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{f.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="h-4 w-px bg-slate-300" />
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
<button
|
||||
onClick={() => setTraceabilityDomainFilter('all')}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
traceabilityDomainFilter === 'all'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
Alle Domains
|
||||
</button>
|
||||
{domains.map(d => (
|
||||
<button
|
||||
key={d}
|
||||
onClick={() => setTraceabilityDomainFilter(d)}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
traceabilityDomainFilter === d
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{DOMAIN_LABELS[d] || d}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls List */}
|
||||
<div className="space-y-2">
|
||||
{filteredControls.length === 0 ? (
|
||||
<div className="text-center py-8 text-slate-400">
|
||||
Keine Controls fuer diesen Filter gefunden.
|
||||
</div>
|
||||
) : filteredControls.map(ctrl => {
|
||||
const isExpanded = expandedControls.has(ctrl.id)
|
||||
const coverageIcon = ctrl.coverage.all_assertions_verified
|
||||
? { symbol: '\u2713', color: 'text-green-600 bg-green-50' }
|
||||
: ctrl.coverage.has_evidence
|
||||
? { symbol: '\u25D0', color: 'text-yellow-600 bg-yellow-50' }
|
||||
: { symbol: '\u2717', color: 'text-red-600 bg-red-50' }
|
||||
|
||||
return (
|
||||
<div key={ctrl.id} className="border rounded-lg overflow-hidden">
|
||||
{/* Control Row */}
|
||||
<button
|
||||
onClick={() => toggleControlExpanded(ctrl.id)}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<span className="text-slate-400 text-xs">{isExpanded ? '\u25BC' : '\u25B6'}</span>
|
||||
<span className={`w-7 h-7 flex items-center justify-center rounded-full text-sm font-medium ${coverageIcon.color}`}>
|
||||
{coverageIcon.symbol}
|
||||
</span>
|
||||
<code className="text-xs bg-slate-100 px-2 py-0.5 rounded text-slate-600 font-mono">{ctrl.control_id}</code>
|
||||
<span className="text-sm text-slate-800 flex-1 truncate">{ctrl.title}</span>
|
||||
<span className="text-xs bg-slate-100 text-slate-500 px-2 py-0.5 rounded">{DOMAIN_LABELS[ctrl.domain] || ctrl.domain}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${
|
||||
ctrl.status === 'implemented' ? 'bg-green-100 text-green-700'
|
||||
: ctrl.status === 'in_progress' ? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-slate-100 text-slate-600'
|
||||
}`}>
|
||||
{ctrl.status}
|
||||
</span>
|
||||
<ConfidenceLevelBadge level={ctrl.coverage.min_confidence_level} />
|
||||
<span className="text-xs text-slate-400 min-w-[3rem] text-right">
|
||||
{ctrl.evidence.length} Ev.
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Expanded: Evidence list */}
|
||||
{isExpanded && (
|
||||
<div className="border-t bg-slate-50">
|
||||
{ctrl.evidence.length === 0 ? (
|
||||
<div className="px-8 py-3 text-xs text-slate-400 italic">
|
||||
Kein Evidence verknuepft.
|
||||
</div>
|
||||
) : ctrl.evidence.map(ev => {
|
||||
const evExpanded = expandedEvidence.has(ev.id)
|
||||
return (
|
||||
<div key={ev.id} className="border-b last:border-b-0">
|
||||
<button
|
||||
onClick={() => toggleEvidenceExpanded(ev.id)}
|
||||
className="w-full flex items-center gap-3 px-8 py-2 text-left hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
<span className="text-slate-400 text-xs">{evExpanded ? '\u25BC' : '\u25B6'}</span>
|
||||
<span className="text-sm text-slate-700 flex-1 truncate">{ev.title}</span>
|
||||
<span className="text-xs bg-white border px-2 py-0.5 rounded text-slate-500">{ev.evidence_type}</span>
|
||||
<ConfidenceLevelBadge level={ev.confidence_level} />
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${
|
||||
ev.status === 'valid' ? 'bg-green-100 text-green-700'
|
||||
: ev.status === 'expired' ? 'bg-red-100 text-red-700'
|
||||
: 'bg-slate-100 text-slate-600'
|
||||
}`}>
|
||||
{ev.status}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 min-w-[3rem] text-right">
|
||||
{ev.assertions.length} Ass.
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Expanded: Assertions list */}
|
||||
{evExpanded && ev.assertions.length > 0 && (
|
||||
<div className="bg-white border-t">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-12 py-1.5 text-left text-slate-500 font-medium">Aussage</th>
|
||||
<th className="px-3 py-1.5 text-center text-slate-500 font-medium w-20">Typ</th>
|
||||
<th className="px-3 py-1.5 text-center text-slate-500 font-medium w-24">Konfidenz</th>
|
||||
<th className="px-3 py-1.5 text-center text-slate-500 font-medium w-16">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{ev.assertions.map(a => (
|
||||
<tr key={a.id} className="hover:bg-slate-50">
|
||||
<td className="px-12 py-1.5 text-slate-700">{a.sentence_text}</td>
|
||||
<td className="px-3 py-1.5 text-center text-slate-500">{a.assertion_type}</td>
|
||||
<td className="px-3 py-1.5 text-center">
|
||||
<span className={`font-medium ${
|
||||
a.confidence >= 0.8 ? 'text-green-600'
|
||||
: a.confidence >= 0.5 ? 'text-yellow-600'
|
||||
: 'text-red-600'
|
||||
}`}>
|
||||
{(a.confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-1.5 text-center">
|
||||
{a.verified
|
||||
? <span className="text-green-600 font-medium">{'\u2713'}</span>
|
||||
: <span className="text-slate-400">{'\u2717'}</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
104
admin-compliance/app/sdk/compliance-hub/_components/TrendTab.tsx
Normal file
104
admin-compliance/app/sdk/compliance-hub/_components/TrendTab.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client'
|
||||
|
||||
import type { ScoreSnapshot } from './types'
|
||||
|
||||
interface TrendTabProps {
|
||||
scoreHistory: ScoreSnapshot[]
|
||||
savingSnapshot: boolean
|
||||
saveSnapshot: () => Promise<void>
|
||||
}
|
||||
|
||||
export function TrendTab({ scoreHistory, savingSnapshot, saveSnapshot }: TrendTabProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Score-Verlauf</h3>
|
||||
<button
|
||||
onClick={saveSnapshot}
|
||||
disabled={savingSnapshot}
|
||||
className="px-3 py-1.5 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
{savingSnapshot ? 'Speichere...' : 'Aktuellen Score speichern'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{scoreHistory.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-slate-500">Noch keine Score-Snapshots vorhanden.</p>
|
||||
<p className="text-sm text-slate-400 mt-1">Klicken Sie auf "Aktuellen Score speichern", um den ersten Datenpunkt zu erstellen.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Simple SVG Line Chart */}
|
||||
<div className="relative h-64 mb-6">
|
||||
<svg className="w-full h-full" viewBox="0 0 800 200" preserveAspectRatio="none">
|
||||
{/* Grid lines */}
|
||||
{[0, 25, 50, 75, 100].map(pct => (
|
||||
<line key={pct} x1="0" y1={200 - pct * 2} x2="800" y2={200 - pct * 2}
|
||||
stroke="#e2e8f0" strokeWidth="1" />
|
||||
))}
|
||||
{/* Score line */}
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#9333ea"
|
||||
strokeWidth="3"
|
||||
strokeLinejoin="round"
|
||||
points={scoreHistory.map((s, i) => {
|
||||
const x = scoreHistory.length === 1 ? 400 : (i / (scoreHistory.length - 1)) * 780 + 10
|
||||
const y = 200 - (s.score / 100) * 200
|
||||
return `${x},${y}`
|
||||
}).join(' ')}
|
||||
/>
|
||||
{/* Points */}
|
||||
{scoreHistory.map((s, i) => {
|
||||
const x = scoreHistory.length === 1 ? 400 : (i / (scoreHistory.length - 1)) * 780 + 10
|
||||
const y = 200 - (s.score / 100) * 200
|
||||
return (
|
||||
<circle key={i} cx={x} cy={y} r="5" fill="#9333ea" stroke="white" strokeWidth="2" />
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
{/* Y-axis labels */}
|
||||
<div className="absolute left-0 top-0 h-full flex flex-col justify-between text-xs text-slate-400 -ml-2">
|
||||
<span>100%</span>
|
||||
<span>75%</span>
|
||||
<span>50%</span>
|
||||
<span>25%</span>
|
||||
<span>0%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Snapshot Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">Datum</th>
|
||||
<th className="px-4 py-2 text-center text-xs font-medium text-slate-500 uppercase">Score</th>
|
||||
<th className="px-4 py-2 text-center text-xs font-medium text-slate-500 uppercase">Controls</th>
|
||||
<th className="px-4 py-2 text-center text-xs font-medium text-slate-500 uppercase">Bestanden</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{scoreHistory.slice().reverse().map(snap => (
|
||||
<tr key={snap.id} className="hover:bg-slate-50">
|
||||
<td className="px-4 py-2 text-slate-700">{new Date(snap.snapshot_date).toLocaleDateString('de-DE')}</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
<span className={`font-bold ${
|
||||
snap.score >= 80 ? 'text-green-600' : snap.score >= 60 ? 'text-yellow-600' : 'text-red-600'
|
||||
}`}>
|
||||
{typeof snap.score === 'number' ? snap.score.toFixed(1) : snap.score}%
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center text-slate-600">{snap.controls_total}</td>
|
||||
<td className="px-4 py-2 text-center text-slate-600">{snap.controls_pass}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
169
admin-compliance/app/sdk/compliance-hub/_components/types.ts
Normal file
169
admin-compliance/app/sdk/compliance-hub/_components/types.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
// Shared types for Compliance Hub
|
||||
|
||||
export interface DashboardData {
|
||||
compliance_score: number
|
||||
total_regulations: number
|
||||
total_requirements: number
|
||||
total_controls: number
|
||||
controls_by_status: Record<string, number>
|
||||
controls_by_domain: Record<string, Record<string, number>>
|
||||
total_evidence: number
|
||||
evidence_by_status: Record<string, number>
|
||||
total_risks: number
|
||||
risks_by_level: Record<string, number>
|
||||
multi_score?: {
|
||||
requirement_coverage: number
|
||||
evidence_strength: number
|
||||
validation_quality: number
|
||||
evidence_freshness: number
|
||||
control_effectiveness: number
|
||||
overall_readiness: number
|
||||
hard_blocks: string[]
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface Regulation {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
full_name: string
|
||||
regulation_type: string
|
||||
effective_date: string | null
|
||||
description: string
|
||||
requirement_count: number
|
||||
}
|
||||
|
||||
export interface MappingsData {
|
||||
total: number
|
||||
by_regulation: Record<string, number>
|
||||
}
|
||||
|
||||
export interface FindingsData {
|
||||
major_count: number
|
||||
minor_count: number
|
||||
ofi_count: number
|
||||
total: number
|
||||
open_majors: number
|
||||
open_minors: number
|
||||
}
|
||||
|
||||
export interface RoadmapItem {
|
||||
id: string
|
||||
control_id: string
|
||||
title: string
|
||||
status: string
|
||||
domain: string
|
||||
owner: string | null
|
||||
next_review_at: string | null
|
||||
days_overdue: number
|
||||
weight: number
|
||||
}
|
||||
|
||||
export interface RoadmapData {
|
||||
buckets: Record<string, RoadmapItem[]>
|
||||
counts: Record<string, number>
|
||||
}
|
||||
|
||||
export interface ModuleInfo {
|
||||
key: string
|
||||
label: string
|
||||
count: number
|
||||
status: string
|
||||
progress: number
|
||||
}
|
||||
|
||||
export interface ModuleStatusData {
|
||||
modules: ModuleInfo[]
|
||||
total: number
|
||||
started: number
|
||||
complete: number
|
||||
overall_progress: number
|
||||
}
|
||||
|
||||
export interface NextAction {
|
||||
id: string
|
||||
control_id: string
|
||||
title: string
|
||||
status: string
|
||||
domain: string
|
||||
owner: string | null
|
||||
days_overdue: number
|
||||
urgency_score: number
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface ScoreSnapshot {
|
||||
id: string
|
||||
score: number
|
||||
controls_total: number
|
||||
controls_pass: number
|
||||
snapshot_date: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface TraceabilityAssertion {
|
||||
id: string
|
||||
sentence_text: string
|
||||
assertion_type: string
|
||||
confidence: number
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
export interface TraceabilityEvidence {
|
||||
id: string
|
||||
title: string
|
||||
evidence_type: string
|
||||
confidence_level: string
|
||||
status: string
|
||||
assertions: TraceabilityAssertion[]
|
||||
}
|
||||
|
||||
export interface TraceabilityCoverage {
|
||||
has_evidence: boolean
|
||||
has_assertions: boolean
|
||||
all_assertions_verified: boolean
|
||||
min_confidence_level: string | null
|
||||
}
|
||||
|
||||
export interface TraceabilityControl {
|
||||
id: string
|
||||
control_id: string
|
||||
title: string
|
||||
status: string
|
||||
domain: string
|
||||
evidence: TraceabilityEvidence[]
|
||||
coverage: TraceabilityCoverage
|
||||
}
|
||||
|
||||
export interface TraceabilityMatrixData {
|
||||
controls: TraceabilityControl[]
|
||||
summary: Record<string, number>
|
||||
}
|
||||
|
||||
export type TabKey = 'overview' | 'roadmap' | 'modules' | 'trend' | 'traceability'
|
||||
|
||||
export const DOMAIN_LABELS: Record<string, string> = {
|
||||
gov: 'Governance',
|
||||
priv: 'Datenschutz',
|
||||
iam: 'Identity & Access',
|
||||
crypto: 'Kryptografie',
|
||||
sdlc: 'Secure Dev',
|
||||
ops: 'Operations',
|
||||
ai: 'KI-spezifisch',
|
||||
cra: 'Supply Chain',
|
||||
aud: 'Audit',
|
||||
}
|
||||
|
||||
export const BUCKET_LABELS: Record<string, { label: string; color: string; bg: string }> = {
|
||||
quick_wins: { label: 'Quick Wins', color: 'text-green-700', bg: 'bg-green-50 border-green-200' },
|
||||
must_have: { label: 'Must Have', color: 'text-red-700', bg: 'bg-red-50 border-red-200' },
|
||||
should_have: { label: 'Should Have', color: 'text-yellow-700', bg: 'bg-yellow-50 border-yellow-200' },
|
||||
nice_to_have: { label: 'Nice to Have', color: 'text-slate-700', bg: 'bg-slate-50 border-slate-200' },
|
||||
}
|
||||
|
||||
export const MODULE_ICONS: Record<string, string> = {
|
||||
vvt: '📋', tom: '🔒', dsfa: '⚠️', loeschfristen: '🗑️', risks: '🎯',
|
||||
controls: '✅', evidence: '📎', obligations: '📜', incidents: '🚨',
|
||||
vendor: '🤝', legal_templates: '📄', training: '🎓', audit: '🔍',
|
||||
security_backlog: '🛡️', quality: '⭐',
|
||||
}
|
||||
Reference in New Issue
Block a user