Files
breakpilot-lehrer/website/app/admin/compliance/page.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

246 lines
9.2 KiB
TypeScript

'use client'
/**
* Compliance & Audit Framework Dashboard
*
* Tabs:
* - Executive: KPI Dashboard mit Ampel-Status
* - Uebersicht: Dashboard mit Score, Stats, Quick Actions
* - Architektur: Systemarchitektur und Datenfluss
* - Roadmap: Implementierungsfortschritt und Backlog
* - Technisch: API-Dokumentation und Datenmodell
* - Audit: Export und Audit-Historie
* - Dokumentation: Handbuecher und Guides
*/
import { useState, useEffect } from 'react'
import Link from 'next/link'
import AdminLayout from '@/components/admin/AdminLayout'
import LanguageSwitch from '@/components/compliance/LanguageSwitch'
import LLMProviderToggle from '@/components/compliance/LLMProviderToggle'
import { Language } from '@/lib/compliance-i18n'
import { DashboardData, Regulation, AIStatus, TabId, tabs } from './types'
import UebersichtTab from './_components/UebersichtTab'
import ExecutiveTab from './_components/ExecutiveTab'
import ArchitekturTab from './_components/ArchitekturTab'
import RoadmapTab from './_components/RoadmapTab'
import TechnischTab from './_components/TechnischTab'
import AuditTab from './_components/AuditTab'
import DokumentationTab from './_components/DokumentationTab'
export default function CompliancePage() {
const [activeTab, setActiveTab] = useState<TabId>('executive')
const [dashboard, setDashboard] = useState<DashboardData | null>(null)
const [regulations, setRegulations] = useState<Regulation[]>([])
const [aiStatus, setAiStatus] = useState<AIStatus | null>(null)
const [loading, setLoading] = useState(true)
const [seeding, setSeeding] = useState(false)
const [error, setError] = useState<string | null>(null)
const [language, setLanguage] = useState<Language>('de')
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
useEffect(() => {
loadData()
}, [])
const loadData = async () => {
setLoading(true)
setError(null)
try {
const [dashboardRes, regulationsRes, aiStatusRes] = await Promise.all([
fetch(`${BACKEND_URL}/api/v1/compliance/dashboard`),
fetch(`${BACKEND_URL}/api/v1/compliance/regulations`),
fetch(`${BACKEND_URL}/api/v1/compliance/ai/status`),
])
if (dashboardRes.ok) {
setDashboard(await dashboardRes.json())
}
if (regulationsRes.ok) {
const data = await regulationsRes.json()
setRegulations(data.regulations || [])
}
if (aiStatusRes.ok) {
setAiStatus(await aiStatusRes.json())
}
} catch (err) {
console.error('Failed to load compliance data:', err)
setError('Verbindung zum Backend fehlgeschlagen')
} finally {
setLoading(false)
}
}
const seedDatabase = async () => {
setSeeding(true)
try {
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/seed`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ force: false }),
})
if (res.ok) {
const result = await res.json()
alert(`Datenbank erfolgreich initialisiert!\n\nRegulations: ${result.counts.regulations}\nControls: ${result.counts.controls}\nRequirements: ${result.counts.requirements}\nMappings: ${result.counts.mappings}`)
loadData()
} else {
const errorText = await res.text()
alert(`Fehler beim Seeding: ${errorText}`)
}
} catch (err) {
console.error('Seeding failed:', err)
alert('Fehler beim Initialisieren der Datenbank')
} finally {
setSeeding(false)
}
}
return (
<AdminLayout title="Compliance & Audit" description="Regulatory Compliance Framework">
{/* Error Banner */}
{error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3">
<svg className="w-5 h-5 text-red-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-red-700">{error}</span>
<button onClick={loadData} className="ml-auto text-red-600 hover:text-red-800 text-sm font-medium">
Erneut versuchen
</button>
</div>
)}
{/* Action Bar */}
<ActionBar
aiStatus={aiStatus}
language={language}
onRefresh={loadData}
onLanguageChange={setLanguage}
/>
{/* Seed Button if no data */}
{!loading && (dashboard?.total_controls || 0) === 0 && (
<SeedBanner seeding={seeding} onSeed={seedDatabase} />
)}
{/* Tab Navigation */}
<div className="border-b border-slate-200 mb-6">
<nav className="-mb-px flex space-x-8 overflow-x-auto">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm transition-colors whitespace-nowrap
${activeTab === tab.id
? 'border-primary-500 text-primary-600'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
}
`}
>
{tab.icon}
{tab.name}
</button>
))}
</nav>
</div>
{/* Tab Content */}
{activeTab === 'executive' && (
<ExecutiveTab loading={loading} onRefresh={loadData} />
)}
{activeTab === 'uebersicht' && (
<UebersichtTab
dashboard={dashboard}
regulations={regulations}
aiStatus={aiStatus}
loading={loading}
onRefresh={loadData}
/>
)}
{activeTab === 'architektur' && <ArchitekturTab />}
{activeTab === 'roadmap' && <RoadmapTab />}
{activeTab === 'technisch' && <TechnischTab />}
{activeTab === 'audit' && <AuditTab />}
{activeTab === 'dokumentation' && <DokumentationTab />}
</AdminLayout>
)
}
// ============================================================================
// Page-level sub-components
// ============================================================================
function ActionBar({
aiStatus,
language,
onRefresh,
onLanguageChange,
}: {
aiStatus: AIStatus | null
language: Language
onRefresh: () => void
onLanguageChange: (lang: Language) => void
}) {
return (
<div className="mb-6 flex items-center justify-between">
<div className="flex items-center gap-3">
<Link
href="/admin/compliance/scraper"
className="inline-flex items-center gap-2 px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
</svg>
Scraper oeffnen
</Link>
<Link
href="/admin/compliance/audit-workspace"
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<svg className="w-4 h-4" 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>
Audit Workspace
</Link>
</div>
<div className="flex items-center gap-3">
<LLMProviderToggle aiStatus={aiStatus} onStatusChange={onRefresh} />
<LanguageSwitch onChange={onLanguageChange} />
<button
onClick={onRefresh}
className="inline-flex items-center gap-2 px-3 py-2 text-sm text-slate-600 hover:text-slate-900 hover:bg-slate-100 rounded-lg transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
</svg>
{language === 'de' ? 'Aktualisieren' : 'Refresh'}
</button>
</div>
</div>
)
}
function SeedBanner({ seeding, onSeed }: { seeding: boolean; onSeed: () => void }) {
return (
<div className="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-yellow-800">Keine Compliance-Daten vorhanden</p>
<p className="text-sm text-yellow-700">Initialisieren Sie die Datenbank mit den Seed-Daten.</p>
</div>
<button
onClick={onSeed}
disabled={seeding}
className="px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 disabled:opacity-50"
>
{seeding ? 'Initialisiere...' : 'Datenbank initialisieren'}
</button>
</div>
</div>
)
}