refactor(admin): split dsr/new, compliance-hub, iace/monitoring, cookie-banner pages

Extract components and hooks from 4 oversized pages (518–508 LOC each) to bring
each page.tsx under 300 LOC (hard cap 500). Zero behavior changes.

- dsr/new: TypeSelector, SourceSelector → _components/; useNewDSRForm → _hooks/
- compliance-hub: QuickActions, StatsRow, DomainChart, MappingsAndFindings,
  RegulationsTable → _components/; useComplianceHub → _hooks/
- iace/[projectId]/monitoring: Badges, EventForm, ResolveModal, TimelineEvent →
  _components/; useMonitoring → _hooks/
- cookie-banner: BannerPreview, CategoryCard → _components/; useCookieBanner → _hooks/

Result: page.tsx LOC: dsr/new=259, compliance-hub=95, monitoring=157, cookie-banner=212

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-16 13:22:01 +02:00
parent 519ffdc8dc
commit e04816cfe5
20 changed files with 1514 additions and 1378 deletions

View File

@@ -0,0 +1,91 @@
'use client'
import Link from 'next/link'
import { DashboardData, MappingsData, FindingsData } from '../_hooks/useComplianceHub'
export function MappingsAndFindings({
dashboard,
mappings,
findings,
}: {
dashboard: DashboardData | null
mappings: MappingsData | null
findings: FindingsData | null
}) {
return (
<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>
<p className="text-sm text-slate-600">
Automatisch generierte Verknuepfungen zwischen {dashboard?.total_controls || 0} Controls
und {dashboard?.total_requirements || 0} Anforderungen aus {dashboard?.total_regulations || 0} Verordnungen.
</p>
</div>
<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>
)
}

View File

@@ -0,0 +1,91 @@
'use client'
import Link from 'next/link'
import { DashboardData } from '../_hooks/useComplianceHub'
export function QuickActions({ dashboard }: { dashboard: DashboardData | null }) {
return (
<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">
<Link
href="/sdk/audit-checklist"
className="p-4 rounded-lg border border-slate-200 hover:border-purple-500 hover:bg-purple-50 transition-colors text-center"
>
<div className="text-purple-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="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" />
</svg>
</div>
<p className="font-medium text-slate-900 text-sm">Audit Checkliste</p>
<p className="text-xs text-slate-500 mt-1">{dashboard?.total_requirements || '...'} Anforderungen</p>
</Link>
<Link
href="/sdk/controls"
className="p-4 rounded-lg border border-slate-200 hover:border-green-500 hover:bg-green-50 transition-colors text-center"
>
<div className="text-green-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="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p className="font-medium text-slate-900 text-sm">Controls</p>
<p className="text-xs text-slate-500 mt-1">{dashboard?.total_controls || '...'} Massnahmen</p>
</Link>
<Link
href="/sdk/evidence"
className="p-4 rounded-lg border border-slate-200 hover:border-blue-500 hover:bg-blue-50 transition-colors text-center"
>
<div className="text-blue-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="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" />
</svg>
</div>
<p className="font-medium text-slate-900 text-sm">Evidence</p>
<p className="text-xs text-slate-500 mt-1">Nachweise</p>
</Link>
<Link
href="/sdk/risks"
className="p-4 rounded-lg border border-slate-200 hover:border-red-500 hover:bg-red-50 transition-colors text-center"
>
<div className="text-red-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="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" />
</svg>
</div>
<p className="font-medium text-slate-900 text-sm">Risk Matrix</p>
<p className="text-xs text-slate-500 mt-1">5x5 Risiken</p>
</Link>
<Link
href="/sdk/modules"
className="p-4 rounded-lg border border-slate-200 hover:border-pink-500 hover:bg-pink-50 transition-colors text-center"
>
<div className="text-pink-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="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
<p className="font-medium text-slate-900 text-sm">Service Registry</p>
<p className="text-xs text-slate-500 mt-1">Module</p>
</Link>
<Link
href="/sdk/audit-report"
className="p-4 rounded-lg border border-slate-200 hover:border-orange-500 hover:bg-orange-50 transition-colors text-center"
>
<div className="text-orange-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="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" />
</svg>
</div>
<p className="font-medium text-slate-900 text-sm">Audit Report</p>
<p className="text-xs text-slate-500 mt-1">PDF Export</p>
</Link>
</div>
</div>
)
}

View File

@@ -0,0 +1,62 @@
'use client'
import { Regulation } from '../_hooks/useComplianceHub'
export function RegulationsTable({
regulations,
onRefresh,
}: {
regulations: Regulation[]
onRefresh: () => void
}) {
return (
<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={onRefresh} 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>
)
}

View File

@@ -0,0 +1,142 @@
'use client'
import { DashboardData } from '../_hooks/useComplianceHub'
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 function StatsRow({
dashboard,
scoreColor,
scoreBgColor,
score,
}: {
dashboard: DashboardData | null
scoreColor: string
scoreBgColor: string
score: number
}) {
return (
<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>
<div 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">Verordnungen</p>
<p className="text-2xl font-bold text-slate-900">{dashboard?.total_regulations || 0}</p>
</div>
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
</svg>
</div>
</div>
<p className="mt-2 text-sm text-slate-500">{dashboard?.total_requirements || 0} Anforderungen</p>
</div>
<div 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">Controls</p>
<p className="text-2xl font-bold text-slate-900">{dashboard?.total_controls || 0}</p>
</div>
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<p className="mt-2 text-sm text-slate-500">{dashboard?.controls_by_status?.pass || 0} bestanden</p>
</div>
<div 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">Nachweise</p>
<p className="text-2xl font-bold text-slate-900">{dashboard?.total_evidence || 0}</p>
</div>
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
</svg>
</div>
</div>
<p className="mt-2 text-sm text-slate-500">{dashboard?.evidence_by_status?.valid || 0} aktiv</p>
</div>
<div 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">Risiken</p>
<p className="text-2xl font-bold text-slate-900">{dashboard?.total_risks || 0}</p>
</div>
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
</svg>
</div>
</div>
<p className="mt-2 text-sm text-slate-500">
{(dashboard?.risks_by_level?.high || 0) + (dashboard?.risks_by_level?.critical || 0)} kritisch
</p>
</div>
</div>
)
}
export function DomainChart({ dashboard }: { dashboard: DashboardData | null }) {
return (
<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="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(dashboard?.controls_by_domain || {}).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="p-3 rounded-lg bg-slate-50">
<div className="flex justify-between text-sm mb-1">
<span className="font-medium text-slate-700">
{DOMAIN_LABELS[domain] || domain.toUpperCase()}
</span>
<span className="text-slate-500">
{pass}/{total} ({passPercent.toFixed(0)}%)
</span>
</div>
<div className="h-2 bg-slate-200 rounded-full overflow-hidden flex">
<div className="bg-green-500 h-full" style={{ width: `${(pass / total) * 100}%` }} />
<div className="bg-yellow-500 h-full" style={{ width: `${(partial / total) * 100}%` }} />
</div>
</div>
)
})}
</div>
</div>
)
}