All 5 files reduced below 500 LOC (hard cap) by extracting sub-components: - training/page.tsx: 780→278 LOC — imports existing _components/, adds BlocksSection - control-provenance/page.tsx: 739→145 LOC — extracts provenance-data.ts, ProvenanceHelpers, LicenseMatrix, SourceRegistry - iace/[projectId]/verification/page.tsx: 673→164 LOC — extracts VerificationForm, CompleteModal, SuggestEvidenceModal, VerificationTable - training/learner/page.tsx: 560→216 LOC — extracts AssignmentsList, ContentView, QuizView, CertificatesView - ControlDetail.tsx: 878→311 LOC — adds ControlSourceCitation, ControlTraceability, ControlRegulatorySection, ControlSimilarControls, ControlReviewActions siblings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
279 lines
14 KiB
TypeScript
279 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 { REGULATION_LABELS, REGULATION_COLORS, FREQUENCY_LABELS, STATUS_LABELS, STATUS_COLORS, ROLE_LABELS, ALL_ROLES, TARGET_AUDIENCE_LABELS } 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 { BlocksSection } from './_components/BlocksSection'
|
|
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 r = await getModuleMedia(moduleId); setModuleMedia(r.media) } catch { setModuleMedia([]) }
|
|
}
|
|
|
|
async function handleGenerateContent() {
|
|
if (!selectedModuleId) return; setGenerating(true)
|
|
try { setGeneratedContent(await generateContent(selectedModuleId)) }
|
|
catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
|
|
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') }
|
|
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') }
|
|
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') }
|
|
}
|
|
|
|
async function handleCheckEscalation() {
|
|
try {
|
|
const r = await checkEscalation()
|
|
setEscalationResult({ total_checked: r.total_checked, escalated: r.escalated })
|
|
await loadData()
|
|
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
|
|
}
|
|
|
|
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') }
|
|
}
|
|
|
|
async function handleLoadContent(moduleId: string) {
|
|
try { setGeneratedContent(await getContent(moduleId)) } catch { setGeneratedContent(null) }
|
|
}
|
|
|
|
async function handleBulkContent() {
|
|
setBulkGenerating(true); setBulkResult(null)
|
|
try {
|
|
const r = await generateAllContent('de')
|
|
setBulkResult({ generated: r.generated ?? 0, skipped: r.skipped ?? 0, errors: r.errors ?? [] })
|
|
await loadData()
|
|
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
|
|
finally { setBulkGenerating(false) }
|
|
}
|
|
|
|
async function handleBulkQuiz() {
|
|
setBulkGenerating(true); setBulkResult(null)
|
|
try {
|
|
const r = await generateAllQuizzes()
|
|
setBulkResult({ generated: r.generated ?? 0, skipped: r.skipped ?? 0, errors: r.errors ?? [] })
|
|
await loadData()
|
|
} catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
|
|
finally { setBulkGenerating(false) }
|
|
}
|
|
|
|
async function handleCreateBlock(data: Parameters<typeof createBlockConfig>[0]) {
|
|
try { await createBlockConfig(data); setShowBlockCreate(false); const r = await listBlockConfigs(); setBlocks(r.blocks) }
|
|
catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
|
|
}
|
|
|
|
async function handleDeleteBlock(id: string) {
|
|
if (!confirm('Block-Konfiguration wirklich loeschen?')) return
|
|
try { await deleteBlockConfig(id); const r = await listBlockConfigs(); setBlocks(r.blocks) }
|
|
catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
|
|
}
|
|
|
|
async function handlePreviewBlock(id: string) {
|
|
setBlockPreviewId(id); setBlockPreview(null); setBlockResult(null)
|
|
try { setBlockPreview(await previewBlock(id)) }
|
|
catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
|
|
}
|
|
|
|
async function handleGenerateBlock(id: string) {
|
|
setBlockGenerating(true); setBlockResult(null)
|
|
try { setBlockResult(await generateBlock(id, { language: 'de', auto_matrix: true })); await loadData() }
|
|
catch (e) { setError(e instanceof Error ? e.message : 'Fehler') }
|
|
finally { setBlockGenerating(false) }
|
|
}
|
|
|
|
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">
|
|
<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={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' && (
|
|
<div className="space-y-6">
|
|
<BlocksSection
|
|
blocks={blocks} canonicalMeta={canonicalMeta} blockPreview={blockPreview}
|
|
blockPreviewId={blockPreviewId} blockGenerating={blockGenerating} blockResult={blockResult}
|
|
showBlockCreate={showBlockCreate} onShowBlockCreate={setShowBlockCreate}
|
|
onPreviewBlock={handlePreviewBlock} onGenerateBlock={handleGenerateBlock}
|
|
onDeleteBlock={handleDeleteBlock} onCreateBlock={handleCreateBlock}
|
|
/>
|
|
<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}
|
|
interactiveGenerating={interactiveGenerating}
|
|
onGenerateContent={handleGenerateContent} onGenerateQuiz={handleGenerateQuiz}
|
|
onBulkContent={handleBulkContent} onBulkQuiz={handleBulkQuiz}
|
|
onPublishContent={handlePublishContent} onReloadMedia={() => loadModuleMedia(selectedModuleId)}
|
|
onGenerateInteractiveVideo={handleGenerateInteractiveVideo}
|
|
/>
|
|
</div>
|
|
)}
|
|
{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>
|
|
)
|
|
}
|