Files
breakpilot-compliance/admin-compliance/app/sdk/training/page.tsx
Sharang Parnerkar 0125199c76 refactor(admin): split controls, training, control-provenance, iace/verification pages
Each page.tsx exceeded the 500-LOC hard cap. Extracted components and hooks into
colocated _components/ and _hooks/ directories; page.tsx is now a thin orchestrator.

- controls/page.tsx: 944 → 180 LOC; extracted ControlCard, AddControlForm,
  LoadingSkeleton, TransitionErrorBanner, StatsCards, FilterBar, RAGPanel into
  _components/ and useControlsData, useRAGSuggestions into _hooks/; types into _types.ts
- training/page.tsx: 780 → 288 LOC; extracted ContentTab (inline content generator tab)
  into _components/ContentTab.tsx
- control-provenance/page.tsx: 739 → 122 LOC; extracted MarkdownRenderer, UsageBadge,
  PermBadge, LicenseMatrix, SourceRegistry into _components/; PROVENANCE_SECTIONS
  static data into _data/provenance-sections.ts
- iace/[projectId]/verification/page.tsx: 673 → 196 LOC; extracted StatusBadge,
  VerificationForm, CompleteModal, SuggestEvidenceModal, VerificationTable into _components/

Zero behavior changes; logic relocated verbatim.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 22:50:15 +02:00

289 lines
14 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import {
getModules, getMatrix, getAssignments, getStats, getDeadlines, getModuleMedia,
getAuditLog, generateContent, generateQuiz,
publishContent, checkEscalation, getContent,
generateAllContent, generateAllQuizzes,
createModule, updateModule, deleteModule,
deleteMatrixEntry, setMatrixEntry,
startAssignment, completeAssignment, updateAssignment,
listBlockConfigs, createBlockConfig, deleteBlockConfig,
previewBlock, generateBlock, getCanonicalMeta,
generateInteractiveVideo,
} from '@/lib/sdk/training/api'
import type {
TrainingModule, TrainingAssignment,
MatrixResponse, TrainingStats, DeadlineInfo, AuditLogEntry, ModuleContent, TrainingMedia,
TrainingBlockConfig, CanonicalControlMeta, BlockPreview, BlockGenerateResult,
} from '@/lib/sdk/training/types'
import { ContentTab } from './_components/ContentTab'
import OverviewTab from './_components/OverviewTab'
import ModulesTab from './_components/ModulesTab'
import MatrixTab from './_components/MatrixTab'
import AssignmentsTab from './_components/AssignmentsTab'
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'
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' },
]
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 [interactiveGenerating, setInteractiveGenerating] = useState(false)
const [statusFilter, setStatusFilter] = useState<string>('')
const [regulationFilter, setRegulationFilter] = useState<string>('')
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)
const [blocks, setBlocks] = useState<TrainingBlockConfig[]>([])
const [canonicalMeta, setCanonicalMeta] = useState<CanonicalControlMeta | null>(null)
const [showBlockCreate, setShowBlockCreate] = useState(false)
const [blockPreview, setBlockPreview] = useState<BlockPreview | null>(null)
const [blockPreviewId, setBlockPreviewId] = useState<string>('')
const [blockGenerating, setBlockGenerating] = useState(false)
const [blockResult, setBlockResult] = useState<BlockGenerateResult | 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, blocksRes, metaRes] = await Promise.allSettled([
getStats(), getModules(), getMatrix(), getAssignments({ limit: 50 }),
getDeadlines(10), getAuditLog({ limit: 30 }), listBlockConfigs(), getCanonicalMeta(),
])
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)
if (blocksRes.status === 'fulfilled') setBlocks(blocksRes.value.blocks)
if (metaRes.status === 'fulfilled') setCanonicalMeta(metaRes.value)
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler beim Laden') }
finally { setLoading(false) }
}
async function loadModuleMedia(moduleId: string) {
try { const result = await getModuleMedia(moduleId); setModuleMedia(result.media) }
catch { setModuleMedia([]) }
}
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 handleGenerateInteractiveVideo() {
if (!selectedModuleId) return
setInteractiveGenerating(true)
try { await generateInteractiveVideo(selectedModuleId); await loadModuleMedia(selectedModuleId) }
catch (e) { setError(e instanceof Error ? e.message : 'Fehler bei der interaktiven Video-Generierung') }
finally { setInteractiveGenerating(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 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 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) }
}
async function handleCreateBlock(data: Parameters<typeof createBlockConfig>[0]) {
try { await createBlockConfig(data); setShowBlockCreate(false); const res = await listBlockConfigs(); setBlocks(res.blocks) }
catch (e) { setError(e instanceof Error ? e.message : 'Fehler beim Erstellen') }
}
async function handleDeleteBlock(id: string) {
if (!confirm('Block-Konfiguration wirklich loeschen?')) return
try { await deleteBlockConfig(id); const res = await listBlockConfigs(); setBlocks(res.blocks) }
catch (e) { setError(e instanceof Error ? e.message : 'Fehler beim Loeschen') }
}
async function handlePreviewBlock(id: string) {
setBlockPreviewId(id); setBlockPreview(null); setBlockResult(null)
try { const preview = await previewBlock(id); setBlockPreview(preview) }
catch (e) { setError(e instanceof Error ? e.message : 'Fehler beim Preview') }
}
async function handleGenerateBlock(id: string) {
setBlockGenerating(true); setBlockResult(null)
try { const result = await generateBlock(id, { language: 'de', auto_matrix: true }); setBlockResult(result); await loadData() }
catch (e) { setError(e instanceof Error ? e.message : 'Fehler bei der Block-Generierung') }
finally { setBlockGenerating(false) }
}
const handleModuleSelect = (id: string) => {
setSelectedModuleId(id); setGeneratedContent(null); setModuleMedia([])
if (id) {
getContent(id).then(setGeneratedContent).catch(() => setGeneratedContent(null))
loadModuleMedia(id)
}
}
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 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>
)
}
return (
<div className="p-6 space-y-6">
<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>
)}
<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>
{activeTab === 'overview' && stats && (
<OverviewTab stats={stats} deadlines={deadlines} escalationResult={escalationResult} onDismissEscalation={() => setEscalationResult(null)} />
)}
{activeTab === 'modules' && (
<ModulesTab modules={modules.filter(m => !regulationFilter || m.regulation_area === regulationFilter)}
regulationFilter={regulationFilter} onRegulationFilterChange={setRegulationFilter}
onCreateClick={() => setShowModuleCreate(true)} onModuleClick={setSelectedModule} />
)}
{activeTab === 'matrix' && matrix && (
<MatrixTab matrix={matrix} onDeleteEntry={handleDeleteMatrixEntry} onAddEntry={setMatrixAddRole} />
)}
{activeTab === 'assignments' && (
<AssignmentsTab assignments={assignments.filter(a => !statusFilter || a.status === statusFilter)}
statusFilter={statusFilter} onStatusFilterChange={setStatusFilter} onAssignmentClick={setSelectedAssignment} />
)}
{activeTab === 'content' && (
<ContentTab
modules={modules} blocks={blocks} canonicalMeta={canonicalMeta}
selectedModuleId={selectedModuleId} onSelectedModuleIdChange={handleModuleSelect}
generatedContent={generatedContent} generating={generating}
bulkGenerating={bulkGenerating} bulkResult={bulkResult}
moduleMedia={moduleMedia} interactiveGenerating={interactiveGenerating}
blockPreview={blockPreview} blockPreviewId={blockPreviewId}
blockGenerating={blockGenerating} blockResult={blockResult}
showBlockCreate={showBlockCreate} onShowBlockCreate={setShowBlockCreate}
onGenerateContent={handleGenerateContent} onGenerateQuiz={handleGenerateQuiz}
onGenerateInteractiveVideo={handleGenerateInteractiveVideo}
onPublishContent={handlePublishContent}
onBulkContent={handleBulkContent} onBulkQuiz={handleBulkQuiz}
onPreviewBlock={handlePreviewBlock} onGenerateBlock={handleGenerateBlock}
onDeleteBlock={handleDeleteBlock} onCreateBlock={handleCreateBlock}
/>
)}
{activeTab === 'audit' && <AuditTab auditLog={auditLog} />}
{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>
)
}