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>
246 lines
9.2 KiB
TypeScript
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>
|
|
)
|
|
}
|