Website (14 monoliths split): - compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20) - quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11) - i18n.ts (1,173 → 8 language files) - unity-bridge (1,094 → 12), backlog (1,087 → 6) - training (1,066 → 8), rag (1,063 → 8) - Deleted index_original.ts (4,899 LOC dead backup) Studio-v2 (5 monoliths split): - meet/page.tsx (1,481 → 9), messages (1,166 → 9) - AlertsB2BContext.tsx (1,165 → 5 modules) - alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6) All existing imports preserved. Zero new TypeScript errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
294 lines
13 KiB
TypeScript
294 lines
13 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { DashboardData, Regulation, AIStatus, DOMAIN_LABELS } from '../types'
|
|
|
|
interface UebersichtTabProps {
|
|
dashboard: DashboardData | null
|
|
regulations: Regulation[]
|
|
aiStatus: AIStatus | null
|
|
loading: boolean
|
|
onRefresh: () => void
|
|
}
|
|
|
|
export default function UebersichtTab({
|
|
dashboard,
|
|
regulations,
|
|
aiStatus,
|
|
loading,
|
|
onRefresh,
|
|
}: UebersichtTabProps) {
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const score = dashboard?.compliance_score || 0
|
|
const scoreColor = score >= 80 ? 'text-green-600' : score >= 60 ? 'text-yellow-600' : 'text-red-600'
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* AI Status Banner */}
|
|
<AIStatusBanner aiStatus={aiStatus} />
|
|
|
|
{/* Score and Stats Row */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
|
<ScoreCard score={score} scoreColor={scoreColor} dashboard={dashboard} />
|
|
<StatCard
|
|
label="Verordnungen"
|
|
value={dashboard?.total_regulations || 0}
|
|
detail={`${dashboard?.total_requirements || 0} Anforderungen`}
|
|
iconBg="bg-blue-100"
|
|
iconColor="text-blue-600"
|
|
iconPath="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"
|
|
/>
|
|
<StatCard
|
|
label="Controls"
|
|
value={dashboard?.total_controls || 0}
|
|
detail={`${dashboard?.controls_by_status?.pass || 0} bestanden`}
|
|
iconBg="bg-green-100"
|
|
iconColor="text-green-600"
|
|
iconPath="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
<StatCard
|
|
label="Nachweise"
|
|
value={dashboard?.total_evidence || 0}
|
|
detail={`${dashboard?.evidence_by_status?.valid || 0} aktiv`}
|
|
iconBg="bg-purple-100"
|
|
iconColor="text-purple-600"
|
|
iconPath="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"
|
|
/>
|
|
<StatCard
|
|
label="Risiken"
|
|
value={dashboard?.total_risks || 0}
|
|
detail={`${(dashboard?.risks_by_level?.high || 0) + (dashboard?.risks_by_level?.critical || 0)} kritisch`}
|
|
iconBg="bg-red-100"
|
|
iconColor="text-red-600"
|
|
iconPath="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"
|
|
/>
|
|
</div>
|
|
|
|
{/* Domain Chart and Quick Actions */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<DomainChart dashboard={dashboard} />
|
|
<QuickActions />
|
|
</div>
|
|
|
|
{/* Regulations Table */}
|
|
<RegulationsTable regulations={regulations} onRefresh={onRefresh} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Sub-components
|
|
// ============================================================================
|
|
|
|
function AIStatusBanner({ aiStatus }: { aiStatus: AIStatus | null }) {
|
|
if (!aiStatus) return null
|
|
|
|
return (
|
|
<div className={`rounded-lg p-4 flex items-center justify-between ${
|
|
aiStatus.is_available && !aiStatus.is_mock
|
|
? 'bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-200'
|
|
: aiStatus.is_mock
|
|
? 'bg-yellow-50 border border-yellow-200'
|
|
: 'bg-red-50 border border-red-200'
|
|
}`}>
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-2xl">🤖</span>
|
|
<div>
|
|
<div className="font-medium text-slate-900">
|
|
AI-Compliance-Assistent {aiStatus.is_available ? 'aktiv' : 'nicht verfuegbar'}
|
|
</div>
|
|
<div className="text-sm text-slate-600">
|
|
{aiStatus.is_mock ? (
|
|
<span className="text-yellow-700">Mock-Modus (kein API-Key konfiguriert)</span>
|
|
) : (
|
|
<>Provider: <span className="font-mono">{aiStatus.provider}</span> | Modell: <span className="font-mono">{aiStatus.model}</span></>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className={`px-3 py-1 rounded-full text-sm font-medium ${
|
|
aiStatus.is_available && !aiStatus.is_mock
|
|
? 'bg-green-100 text-green-700'
|
|
: aiStatus.is_mock
|
|
? 'bg-yellow-100 text-yellow-700'
|
|
: 'bg-red-100 text-red-700'
|
|
}`}>
|
|
{aiStatus.is_available && !aiStatus.is_mock ? 'Online' : aiStatus.is_mock ? 'Mock' : 'Offline'}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ScoreCard({ score, scoreColor, dashboard }: { score: number; scoreColor: string; dashboard: DashboardData | null }) {
|
|
return (
|
|
<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 ${score >= 80 ? 'bg-green-500' : score >= 60 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
|
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>
|
|
)
|
|
}
|
|
|
|
function StatCard({ label, value, detail, iconBg, iconColor, iconPath }: {
|
|
label: string
|
|
value: number
|
|
detail: string
|
|
iconBg: string
|
|
iconColor: string
|
|
iconPath: string
|
|
}) {
|
|
return (
|
|
<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">{label}</p>
|
|
<p className="text-2xl font-bold text-slate-900">{value}</p>
|
|
</div>
|
|
<div className={`w-10 h-10 ${iconBg} rounded-lg flex items-center justify-center`}>
|
|
<svg className={`w-5 h-5 ${iconColor}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={iconPath} />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<p className="mt-2 text-sm text-slate-500">{detail}</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function DomainChart({ dashboard }: { dashboard: DashboardData | null }) {
|
|
return (
|
|
<div className="lg:col-span-2 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-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}>
|
|
<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>
|
|
)
|
|
}
|
|
|
|
function QuickActions() {
|
|
const actions = [
|
|
{ href: '/admin/compliance/controls', label: 'Controls', color: 'primary', hoverBorder: 'hover:border-primary-500', hoverBg: 'hover:bg-primary-50', iconColor: 'text-primary-600', iconPath: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' },
|
|
{ href: '/admin/compliance/evidence', label: 'Evidence', color: 'purple', hoverBorder: 'hover:border-primary-500', hoverBg: 'hover:bg-primary-50', iconColor: 'text-purple-600', iconPath: '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' },
|
|
{ href: '/admin/compliance/risks', label: 'Risiken', color: 'red', hoverBorder: 'hover:border-primary-500', hoverBg: 'hover:bg-primary-50', iconColor: 'text-red-600', iconPath: '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' },
|
|
{ href: '/admin/compliance/scraper', label: 'Scraper', color: 'orange', hoverBorder: 'hover:border-orange-500', hoverBg: 'hover:bg-orange-50', iconColor: 'text-orange-600', iconPath: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15' },
|
|
{ href: '/admin/compliance/export', label: 'Export', color: 'green', hoverBorder: 'hover:border-primary-500', hoverBg: 'hover:bg-primary-50', iconColor: 'text-green-600', iconPath: 'M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4' },
|
|
{ href: '/admin/compliance/audit-workspace', label: 'Audit Workspace', color: 'blue', hoverBorder: 'hover:border-blue-500', hoverBg: 'hover:bg-blue-50', iconColor: 'text-blue-600', iconPath: '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' },
|
|
{ href: '/admin/compliance/modules', label: 'Service Module Registry', color: 'pink', hoverBorder: 'hover:border-pink-500', hoverBg: 'hover:bg-pink-50', iconColor: 'text-pink-600', iconPath: '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' },
|
|
]
|
|
|
|
return (
|
|
<div className="bg-white rounded-xl shadow-sm border p-6">
|
|
<h3 className="text-lg font-semibold text-slate-900 mb-4">Schnellaktionen</h3>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{actions.map((action) => (
|
|
<Link
|
|
key={action.href}
|
|
href={action.href}
|
|
className={`p-4 rounded-lg border border-slate-200 ${action.hoverBorder} ${action.hoverBg} transition-colors`}
|
|
>
|
|
<div className={`${action.iconColor} mb-2`}>
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={action.iconPath} />
|
|
</svg>
|
|
</div>
|
|
<p className="font-medium text-slate-900 text-sm">{action.label}</p>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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</h3>
|
|
<button onClick={onRefresh} className="text-sm text-primary-600 hover:text-primary-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, 10).map((reg) => (
|
|
<tr key={reg.id} className="hover:bg-slate-50">
|
|
<td className="px-4 py-3">
|
|
<span className="font-mono font-medium text-primary-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>
|
|
)
|
|
}
|