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>
367 lines
12 KiB
TypeScript
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>
|
|
)
|
|
}
|