Files
Sharang Parnerkar 375b34a0d8 refactor(admin): split consent-management, control-library, incidents, training pages
Agent-completed splits committed after agents hit rate limits before
committing their work. All 4 pages now under 500 LOC:

- consent-management: 1303 -> 193 LOC (+ 7 _components, _hooks, _data, _types)
- control-library: 1210 -> 298 LOC (+ _components, _types)
- incidents: 1150 -> 373 LOC (+ _components)
- training: 1127 -> 366 LOC (+ _components)

Verification: next build clean (142 pages generated).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:52:45 +02:00

367 lines
12 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import {
getModules, getMatrix, getAssignments, getStats, getDeadlines, getModuleMedia,
getAuditLog, generateContent, generateQuiz,
publishContent, checkEscalation, getContent,
generateAllContent, generateAllQuizzes,
deleteMatrixEntry,
} from '@/lib/sdk/training/api'
import type {
TrainingModule, TrainingAssignment,
MatrixResponse, TrainingStats, DeadlineInfo, AuditLogEntry, ModuleContent, TrainingMedia,
} from '@/lib/sdk/training/types'
import { OverviewTab } from './_components/OverviewTab'
import { ModulesTab } from './_components/ModulesTab'
import { MatrixTab } from './_components/MatrixTab'
import { AssignmentsTab } from './_components/AssignmentsTab'
import { ContentTab } from './_components/ContentTab'
import { AuditTab } from './_components/AuditTab'
import { ModuleCreateModal } from './_components/ModuleCreateModal'
import { ModuleEditDrawer } from './_components/ModuleEditDrawer'
import { MatrixAddModal } from './_components/MatrixAddModal'
import { AssignmentDetailDrawer } from './_components/AssignmentDetailDrawer'
type Tab = 'overview' | 'modules' | 'matrix' | 'assignments' | 'content' | 'audit'
export default function TrainingPage() {
const [activeTab, setActiveTab] = useState<Tab>('overview')
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [stats, setStats] = useState<TrainingStats | null>(null)
const [modules, setModules] = useState<TrainingModule[]>([])
const [matrix, setMatrix] = useState<MatrixResponse | null>(null)
const [assignments, setAssignments] = useState<TrainingAssignment[]>([])
const [deadlines, setDeadlines] = useState<DeadlineInfo[]>([])
const [auditLog, setAuditLog] = useState<AuditLogEntry[]>([])
const [selectedModuleId, setSelectedModuleId] = useState<string>('')
const [generatedContent, setGeneratedContent] = useState<ModuleContent | null>(null)
const [generating, setGenerating] = useState(false)
const [bulkGenerating, setBulkGenerating] = useState(false)
const [bulkResult, setBulkResult] = useState<{ generated: number; skipped: number; errors: string[] } | null>(null)
const [moduleMedia, setModuleMedia] = useState<TrainingMedia[]>([])
const [statusFilter, setStatusFilter] = useState<string>('')
const [regulationFilter, setRegulationFilter] = useState<string>('')
// Modal/Drawer states
const [showModuleCreate, setShowModuleCreate] = useState(false)
const [selectedModule, setSelectedModule] = useState<TrainingModule | null>(null)
const [matrixAddRole, setMatrixAddRole] = useState<string | null>(null)
const [selectedAssignment, setSelectedAssignment] = useState<TrainingAssignment | null>(null)
const [escalationResult, setEscalationResult] = useState<{ total_checked: number; escalated: number } | null>(null)
useEffect(() => {
loadData()
}, [])
useEffect(() => {
if (selectedModuleId) {
loadModuleMedia(selectedModuleId)
}
}, [selectedModuleId])
async function loadData() {
setLoading(true)
setError(null)
try {
const [statsRes, modulesRes, matrixRes, assignmentsRes, deadlinesRes, auditRes] = await Promise.allSettled([
getStats(),
getModules(),
getMatrix(),
getAssignments({ limit: 50 }),
getDeadlines(10),
getAuditLog({ limit: 30 }),
])
if (statsRes.status === 'fulfilled') setStats(statsRes.value)
if (modulesRes.status === 'fulfilled') setModules(modulesRes.value.modules)
if (matrixRes.status === 'fulfilled') setMatrix(matrixRes.value)
if (assignmentsRes.status === 'fulfilled') setAssignments(assignmentsRes.value.assignments)
if (deadlinesRes.status === 'fulfilled') setDeadlines(deadlinesRes.value.deadlines)
if (auditRes.status === 'fulfilled') setAuditLog(auditRes.value.entries)
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}
async function handleGenerateContent() {
if (!selectedModuleId) return
setGenerating(true)
try {
const content = await generateContent(selectedModuleId)
setGeneratedContent(content)
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler bei der Content-Generierung')
} finally {
setGenerating(false)
}
}
async function handleGenerateQuiz() {
if (!selectedModuleId) return
setGenerating(true)
try {
await generateQuiz(selectedModuleId, 5)
await loadData()
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler bei der Quiz-Generierung')
} finally {
setGenerating(false)
}
}
async function handlePublishContent(contentId: string) {
try {
await publishContent(contentId)
setGeneratedContent(null)
await loadData()
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Veroeffentlichen')
}
}
async function handleCheckEscalation() {
try {
const result = await checkEscalation()
setEscalationResult({ total_checked: result.total_checked, escalated: result.escalated })
await loadData()
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler bei der Eskalationspruefung')
}
}
async function handleDeleteMatrixEntry(roleCode: string, moduleId: string) {
if (!window.confirm('Modulzuordnung entfernen?')) return
try {
await deleteMatrixEntry(roleCode, moduleId)
await loadData()
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Entfernen')
}
}
async function handleLoadContent(moduleId: string) {
try {
const content = await getContent(moduleId)
setGeneratedContent(content)
} catch {
setGeneratedContent(null)
}
}
async function handleBulkContent() {
setBulkGenerating(true)
setBulkResult(null)
try {
const result = await generateAllContent('de')
setBulkResult({ generated: result.generated ?? 0, skipped: result.skipped ?? 0, errors: result.errors ?? [] })
await loadData()
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler bei der Bulk-Generierung')
} finally {
setBulkGenerating(false)
}
}
async function loadModuleMedia(moduleId: string) {
try {
const result = await getModuleMedia(moduleId)
setModuleMedia(result.media)
} catch {
setModuleMedia([])
}
}
async function handleBulkQuiz() {
setBulkGenerating(true)
setBulkResult(null)
try {
const result = await generateAllQuizzes()
setBulkResult({ generated: result.generated ?? 0, skipped: result.skipped ?? 0, errors: result.errors ?? [] })
await loadData()
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler bei der Bulk-Quiz-Generierung')
} finally {
setBulkGenerating(false)
}
}
const tabs: { id: Tab; label: string }[] = [
{ id: 'overview', label: 'Uebersicht' },
{ id: 'modules', label: 'Modulkatalog' },
{ id: 'matrix', label: 'Training Matrix' },
{ id: 'assignments', label: 'Zuweisungen' },
{ id: 'content', label: 'Content-Generator' },
{ id: 'audit', label: 'Audit Trail' },
]
const filteredModules = modules.filter(m =>
(!regulationFilter || m.regulation_area === regulationFilter)
)
const filteredAssignments = assignments.filter(a =>
(!statusFilter || a.status === statusFilter)
)
if (loading) {
return (
<div className="p-6">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-1/3"></div>
<div className="grid grid-cols-4 gap-4">
{[1,2,3,4].map(i => <div key={i} className="h-24 bg-gray-200 rounded"></div>)}
</div>
</div>
</div>
)
}
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Compliance Training Engine</h1>
<p className="text-sm text-gray-500 mt-1">
Training-Module, Zuweisungen und Compliance-Schulungen verwalten
</p>
</div>
<button
onClick={handleCheckEscalation}
className="px-4 py-2 text-sm bg-orange-50 text-orange-700 border border-orange-200 rounded-lg hover:bg-orange-100"
>
Eskalation pruefen
</button>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm">
{error}
<button onClick={() => setError(null)} className="ml-2 underline">Schliessen</button>
</div>
)}
{/* Tabs */}
<div className="border-b border-gray-200">
<nav className="flex -mb-px space-x-6">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`pb-3 px-1 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
{/* Tab Content */}
{activeTab === 'overview' && stats && (
<OverviewTab
stats={stats}
deadlines={deadlines}
escalationResult={escalationResult}
onDismissEscalation={() => setEscalationResult(null)}
/>
)}
{activeTab === 'modules' && (
<ModulesTab
modules={filteredModules}
regulationFilter={regulationFilter}
onRegulationFilterChange={setRegulationFilter}
onCreateClick={() => setShowModuleCreate(true)}
onModuleClick={setSelectedModule}
/>
)}
{activeTab === 'matrix' && matrix && (
<MatrixTab
matrix={matrix}
onDeleteEntry={handleDeleteMatrixEntry}
onAddEntry={setMatrixAddRole}
/>
)}
{activeTab === 'assignments' && (
<AssignmentsTab
assignments={filteredAssignments}
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
onAssignmentClick={setSelectedAssignment}
/>
)}
{activeTab === 'content' && (
<ContentTab
modules={modules}
selectedModuleId={selectedModuleId}
onSelectModule={id => {
setSelectedModuleId(id)
setGeneratedContent(null)
setModuleMedia([])
if (id) {
handleLoadContent(id)
loadModuleMedia(id)
}
}}
generatedContent={generatedContent}
generating={generating}
bulkGenerating={bulkGenerating}
bulkResult={bulkResult}
moduleMedia={moduleMedia}
onGenerateContent={handleGenerateContent}
onGenerateQuiz={handleGenerateQuiz}
onBulkContent={handleBulkContent}
onBulkQuiz={handleBulkQuiz}
onPublishContent={handlePublishContent}
onReloadMedia={() => loadModuleMedia(selectedModuleId)}
/>
)}
{activeTab === 'audit' && <AuditTab auditLog={auditLog} />}
{/* Modals & Drawers */}
{showModuleCreate && (
<ModuleCreateModal
onClose={() => setShowModuleCreate(false)}
onSaved={() => { setShowModuleCreate(false); loadData() }}
/>
)}
{selectedModule && (
<ModuleEditDrawer
module={selectedModule}
onClose={() => setSelectedModule(null)}
onSaved={() => { setSelectedModule(null); loadData() }}
/>
)}
{matrixAddRole && (
<MatrixAddModal
roleCode={matrixAddRole}
modules={modules}
onClose={() => setMatrixAddRole(null)}
onSaved={() => { setMatrixAddRole(null); loadData() }}
/>
)}
{selectedAssignment && (
<AssignmentDetailDrawer
assignment={selectedAssignment}
onClose={() => setSelectedAssignment(null)}
onSaved={() => { setSelectedAssignment(null); loadData() }}
/>
)}
</div>
)
}