Files
breakpilot-lehrer/website/app/admin/compliance/_components/UebersichtTab.tsx
Benjamin Admin 0b37c5e692 [split-required] Split website + studio-v2 monoliths (Phase 3 continued)
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>
2026-04-24 17:52:36 +02:00

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>
)
}