@@ -651,17 +180,11 @@ export default function VerificationPage() {
{mitigations.length > 0 && (
-
diff --git a/admin-compliance/app/sdk/training/_components/ContentTab.tsx b/admin-compliance/app/sdk/training/_components/ContentTab.tsx
new file mode 100644
index 0000000..755bec7
--- /dev/null
+++ b/admin-compliance/app/sdk/training/_components/ContentTab.tsx
@@ -0,0 +1,392 @@
+'use client'
+
+import AudioPlayer from '@/components/training/AudioPlayer'
+import VideoPlayer from '@/components/training/VideoPlayer'
+import ScriptPreview from '@/components/training/ScriptPreview'
+import type {
+ TrainingModule, ModuleContent, TrainingMedia,
+ TrainingBlockConfig, CanonicalControlMeta, BlockPreview, BlockGenerateResult,
+} from '@/lib/sdk/training/types'
+import { TARGET_AUDIENCE_LABELS, ROLE_LABELS, REGULATION_LABELS } from '@/lib/sdk/training/types'
+
+export function ContentTab({
+ modules,
+ blocks,
+ canonicalMeta,
+ selectedModuleId,
+ onSelectedModuleIdChange,
+ generatedContent,
+ generating,
+ bulkGenerating,
+ bulkResult,
+ moduleMedia,
+ interactiveGenerating,
+ blockPreview,
+ blockPreviewId,
+ blockGenerating,
+ blockResult,
+ showBlockCreate,
+ onShowBlockCreate,
+ onGenerateContent,
+ onGenerateQuiz,
+ onGenerateInteractiveVideo,
+ onPublishContent,
+ onBulkContent,
+ onBulkQuiz,
+ onPreviewBlock,
+ onGenerateBlock,
+ onDeleteBlock,
+ onCreateBlock,
+}: {
+ modules: TrainingModule[]
+ blocks: TrainingBlockConfig[]
+ canonicalMeta: CanonicalControlMeta | null
+ selectedModuleId: string
+ onSelectedModuleIdChange: (id: string) => void
+ generatedContent: ModuleContent | null
+ generating: boolean
+ bulkGenerating: boolean
+ bulkResult: { generated: number; skipped: number; errors: string[] } | null
+ moduleMedia: TrainingMedia[]
+ interactiveGenerating: boolean
+ blockPreview: BlockPreview | null
+ blockPreviewId: string
+ blockGenerating: boolean
+ blockResult: BlockGenerateResult | null
+ showBlockCreate: boolean
+ onShowBlockCreate: (show: boolean) => void
+ onGenerateContent: () => void
+ onGenerateQuiz: () => void
+ onGenerateInteractiveVideo: () => void
+ onPublishContent: (id: string) => void
+ onBulkContent: () => void
+ onBulkQuiz: () => void
+ onPreviewBlock: (id: string) => void
+ onGenerateBlock: (id: string) => void
+ onDeleteBlock: (id: string) => void
+ onCreateBlock: (data: {
+ name: string; description?: string; domain_filter?: string; category_filter?: string;
+ severity_filter?: string; target_audience_filter?: string; regulation_area: string;
+ module_code_prefix: string; max_controls_per_module?: number;
+ }) => void
+}) {
+ return (
+
+ {/* Training Blocks */}
+
+
+
+
Schulungsbloecke aus Controls
+
+ Canonical Controls nach Kriterien filtern und automatisch Schulungsmodule generieren
+ {canonicalMeta && ({canonicalMeta.total} Controls verfuegbar)}
+
+
+
onShowBlockCreate(true)}
+ className="px-3 py-1.5 text-xs bg-blue-600 text-white rounded-lg hover:bg-blue-700"
+ >
+ + Neuen Block erstellen
+
+
+
+ {blocks.length > 0 ? (
+
+
+
+
+ | Name |
+ Domain |
+ Zielgruppe |
+ Severity |
+ Prefix |
+ Letzte Generierung |
+ Aktionen |
+
+
+
+ {blocks.map(block => (
+
+ |
+ {block.name}
+ {block.description && {block.description} }
+ |
+ {block.domain_filter || 'Alle'} |
+
+ {block.target_audience_filter ? (TARGET_AUDIENCE_LABELS[block.target_audience_filter] || block.target_audience_filter) : 'Alle'}
+ |
+ {block.severity_filter || 'Alle'} |
+ {block.module_code_prefix} |
+
+ {block.last_generated_at ? new Date(block.last_generated_at).toLocaleString('de-DE') : 'Noch nie'}
+ |
+
+
+ onPreviewBlock(block.id)} className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded hover:bg-gray-200">Preview
+ onGenerateBlock(block.id)} disabled={blockGenerating} className="px-2 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50">
+ {blockGenerating ? 'Generiert...' : 'Generieren'}
+
+ onDeleteBlock(block.id)} className="px-2 py-1 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200">Loeschen
+
+ |
+
+ ))}
+
+
+
+ ) : (
+
+ Noch keine Schulungsbloecke konfiguriert. Erstelle einen Block, um Controls automatisch in Module umzuwandeln.
+
+ )}
+
+ {blockPreview && blockPreviewId && (
+
+
Preview: {blocks.find(b => b.id === blockPreviewId)?.name}
+
+ Controls: {blockPreview.control_count}
+ Module: {blockPreview.module_count}
+ Rollen: {blockPreview.proposed_roles.map(r => ROLE_LABELS[r] || r).join(', ')}
+
+ {blockPreview.controls.length > 0 && (
+
+ Passende Controls anzeigen ({blockPreview.control_count})
+
+ {blockPreview.controls.slice(0, 50).map(ctrl => (
+
+ {ctrl.control_id}
+ {ctrl.title}
+ {ctrl.severity}
+
+ ))}
+ {blockPreview.control_count > 50 &&
... und {blockPreview.control_count - 50} weitere
}
+
+
+ )}
+
+ )}
+
+ {blockResult && (
+
+
Generierung abgeschlossen
+
+ Module erstellt: {blockResult.modules_created}
+ Controls verknuepft: {blockResult.controls_linked}
+ Matrix-Eintraege: {blockResult.matrix_entries_created}
+ Content generiert: {blockResult.content_generated}
+
+ {blockResult.errors && blockResult.errors.length > 0 && (
+
{blockResult.errors.map((err, i) =>
{err}
)}
+ )}
+
+ )}
+
+
+ {/* Block Create Modal */}
+ {showBlockCreate && (
+
+
+
Neuen Schulungsblock erstellen
+
+
+
+ )}
+
+ {/* Bulk Generation */}
+
+
Bulk-Generierung
+
Generiere Inhalte und Quiz-Fragen fuer alle Module auf einmal
+
+
+ {bulkGenerating ? 'Generiere...' : 'Alle Inhalte generieren'}
+
+
+ {bulkGenerating ? 'Generiere...' : 'Alle Quizfragen generieren'}
+
+
+ {bulkResult && (
+
+
+ Generiert: {bulkResult.generated}
+ Uebersprungen: {bulkResult.skipped}
+ {bulkResult.errors?.length > 0 && Fehler: {bulkResult.errors.length}}
+
+ {bulkResult.errors?.length > 0 && (
+
{bulkResult.errors.map((err, i) =>
{err}
)}
+ )}
+
+ )}
+
+
+ {/* LLM Content Generator */}
+
+
LLM-Content-Generator
+
Generiere Schulungsinhalte und Quiz-Fragen automatisch via KI
+
+
+
+
+
+
+ {generating ? 'Generiere...' : 'Inhalt generieren'}
+
+
+ {generating ? 'Generiere...' : 'Quiz generieren'}
+
+
+
+
+ {generatedContent && (
+
+
+
+
Generierter Inhalt (v{generatedContent.version})
+
Generiert von: {generatedContent.generated_by} ({generatedContent.llm_model})
+
+ {!generatedContent.is_published ? (
+
onPublishContent(generatedContent.id)} className="px-3 py-1.5 text-xs bg-green-600 text-white rounded hover:bg-green-700">Veroeffentlichen
+ ) : (
+
Veroeffentlicht
+ )}
+
+
+
{generatedContent.content_body}
+
+
+ )}
+
+ {selectedModuleId && generatedContent?.is_published && (
+
m.media_type === 'audio') || null}
+ onMediaUpdate={() => {}}
+ />
+ )}
+
+ {selectedModuleId && generatedContent?.is_published && (
+ m.media_type === 'video') || null}
+ onMediaUpdate={() => {}}
+ />
+ )}
+
+ {selectedModuleId && generatedContent?.is_published && (
+
+
+
+
Interaktives Video
+
Video mit Narrator-Persona und Checkpoint-Quizzes
+
+ {moduleMedia.some(m => m.media_type === 'interactive_video' && m.status === 'completed') ? (
+
Interaktiv erstellt
+ ) : (
+
+ {interactiveGenerating ? 'Generiere interaktives Video...' : 'Interaktives Video generieren'}
+
+ )}
+
+ {moduleMedia.filter(m => m.media_type === 'interactive_video' && m.status === 'completed').map(m => (
+
+
Dauer: {Math.round(m.duration_seconds / 60)} Min | Groesse: {(m.file_size_bytes / 1024 / 1024).toFixed(1)} MB
+
Generiert: {new Date(m.created_at).toLocaleString('de-DE')}
+
+ ))}
+
+ )}
+
+ {selectedModuleId && generatedContent?.is_published && (
+
+ )}
+
+ )
+}
diff --git a/admin-compliance/app/sdk/training/page.tsx b/admin-compliance/app/sdk/training/page.tsx
index ae003e6..c15e479 100644
--- a/admin-compliance/app/sdk/training/page.tsx
+++ b/admin-compliance/app/sdk/training/page.tsx
@@ -18,15 +18,28 @@ import type {
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 AudioPlayer from '@/components/training/AudioPlayer'
-import VideoPlayer from '@/components/training/VideoPlayer'
-import ScriptPreview from '@/components/training/ScriptPreview'
+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
('overview')
const [loading, setLoading] = useState(true)
@@ -50,14 +63,12 @@ export default function TrainingPage() {
const [statusFilter, setStatusFilter] = useState('')
const [regulationFilter, setRegulationFilter] = useState('')
- // Modal/Drawer states
const [showModuleCreate, setShowModuleCreate] = useState(false)
const [selectedModule, setSelectedModule] = useState(null)
const [matrixAddRole, setMatrixAddRole] = useState(null)
const [selectedAssignment, setSelectedAssignment] = useState(null)
const [escalationResult, setEscalationResult] = useState<{ total_checked: number; escalated: number } | null>(null)
- // Block (Controls → Module) state
const [blocks, setBlocks] = useState([])
const [canonicalMeta, setCanonicalMeta] = useState(null)
const [showBlockCreate, setShowBlockCreate] = useState(false)
@@ -66,31 +77,16 @@ export default function TrainingPage() {
const [blockGenerating, setBlockGenerating] = useState(false)
const [blockResult, setBlockResult] = useState(null)
- useEffect(() => {
- loadData()
- }, [])
-
- useEffect(() => {
- if (selectedModuleId) {
- loadModuleMedia(selectedModuleId)
- }
- }, [selectedModuleId])
+ useEffect(() => { loadData() }, [])
+ useEffect(() => { if (selectedModuleId) loadModuleMedia(selectedModuleId) }, [selectedModuleId])
async function loadData() {
- setLoading(true)
- setError(null)
+ 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(),
+ 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)
@@ -99,60 +95,42 @@ export default function TrainingPage() {
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)
- }
+ } 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)
- }
+ 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)
- }
+ 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)
- }
+ 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')
- }
+ try { await publishContent(contentId); setGeneratedContent(null); await loadData() }
+ catch (e) { setError(e instanceof Error ? e.message : 'Fehler beim Veroeffentlichen') }
}
async function handleCheckEscalation() {
@@ -160,144 +138,74 @@ export default function TrainingPage() {
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')
- }
+ } 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)
- }
+ 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)
+ 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([])
- }
+ } catch (e) { setError(e instanceof Error ? e.message : 'Fehler bei der Bulk-Generierung') }
+ finally { setBulkGenerating(false) }
}
async function handleBulkQuiz() {
- setBulkGenerating(true)
- setBulkResult(null)
+ 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)
- }
+ } catch (e) { setError(e instanceof Error ? e.message : 'Fehler bei der Bulk-Quiz-Generierung') }
+ finally { setBulkGenerating(false) }
}
- // Block handlers
- async function handleCreateBlock(data: {
- name: string; description?: string; domain_filter?: string; category_filter?: string;
- severity_filter?: string; target_audience_filter?: string; regulation_area: string;
- module_code_prefix: string; max_controls_per_module?: number;
- }) {
- 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 handleCreateBlock(data: Parameters[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')
- }
+ 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')
- }
+ 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)
- }
+ 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 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)
- )
+ const handleModuleSelect = (id: string) => {
+ setSelectedModuleId(id); setGeneratedContent(null); setModuleMedia([])
+ if (id) {
+ getContent(id).then(setGeneratedContent).catch(() => setGeneratedContent(null))
+ loadModuleMedia(id)
+ }
+ }
if (loading) {
return (
-
+
- {[1,2,3,4].map(i =>
)}
+ {[1,2,3,4].map(i =>
)}
@@ -306,40 +214,28 @@ export default function TrainingPage() {
return (
- {/* Header */}
Compliance Training Engine
-
- Training-Module, Zuweisungen und Compliance-Schulungen verwalten
-
+
Training-Module, Zuweisungen und Compliance-Schulungen verwalten
-
+
Eskalation pruefen
{error && (
- {error}
- setError(null)} className="ml-2 underline">Schliessen
+ {error} setError(null)} className="ml-2 underline">Schliessen
)}
- {/* Tabs */}
- {/* Tab Content */}
{activeTab === 'overview' && stats && (
-
- {escalationResult && (
-
-
- Eskalation abgeschlossen:
-
- {escalationResult.total_checked} Zuweisungen geprueft, {escalationResult.escalated} eskaliert
-
-
-
setEscalationResult(null)} className="text-blue-400 hover:text-blue-600 text-lg font-bold">×
-
- )}
-
-
-
- = 80 ? 'green' : stats.completion_rate >= 50 ? 'yellow' : 'red'} />
- 0 ? 'red' : 'green'} />
-
-
-
-
-
-
- 5 ? 'yellow' : 'green'} />
-
-
- {/* Status Bar */}
-
-
Status-Verteilung
- {stats.total_assignments > 0 && (
-
- {stats.completed_count > 0 &&
}
- {stats.in_progress_count > 0 &&
}
- {stats.pending_count > 0 &&
}
- {stats.overdue_count > 0 &&
}
-
- )}
-
- Abgeschlossen
- In Bearbeitung
- Ausstehend
- Ueberfaellig
-
-
-
- {/* Deadlines */}
- {deadlines.length > 0 && (
-
-
Naechste Deadlines
-
- {deadlines.slice(0, 5).map(d => (
-
-
- {d.module_title}
- ({d.user_name})
-
-
- {d.days_left <= 0 ? `${Math.abs(d.days_left)} Tage ueberfaellig` : `${d.days_left} Tage`}
-
-
- ))}
-
-
- )}
-
+
setEscalationResult(null)} />
)}
-
{activeTab === 'modules' && (
-
-
-
- setShowModuleCreate(true)}
- className="ml-auto px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors"
- >
- + Neues Modul
-
-
-
-
- {filteredModules.map(m => (
-
setSelectedModule(m)}
- className="bg-white border rounded-lg p-4 hover:shadow-md cursor-pointer transition-shadow"
- >
-
-
-
- {REGULATION_LABELS[m.regulation_area] || m.regulation_area}
-
-
{m.title}
-
{m.module_code}
-
-
- {m.nis2_relevant && (
- NIS2
- )}
- {!m.is_active && (
- Inaktiv
- )}
-
-
- {m.description &&
{m.description}
}
-
- {m.duration_minutes} Min.
- {FREQUENCY_LABELS[m.frequency_type]}
- Quiz: {m.pass_threshold}%
-
-
- ))}
-
- {filteredModules.length === 0 &&
Keine Module gefunden
}
-
+ !regulationFilter || m.regulation_area === regulationFilter)}
+ regulationFilter={regulationFilter} onRegulationFilterChange={setRegulationFilter}
+ onCreateClick={() => setShowModuleCreate(true)} onModuleClick={setSelectedModule} />
)}
-
{activeTab === 'matrix' && matrix && (
-
-
Compliance Training Matrix (CTM): Welche Rollen benoetigen welche Schulungsmodule
-
-
-
-
- | Rolle |
- Module |
- Anzahl |
-
-
-
- {ALL_ROLES.map(role => {
- const entries = matrix.entries[role] || []
- return (
-
- |
- {role}
- {ROLE_LABELS[role]}
- |
-
-
- {entries.map(e => (
-
- {e.is_mandatory ? '🔴' : '🔵'} {e.module_code}
- handleDeleteMatrixEntry(role, e.module_id)}
- className="ml-1 text-gray-400 hover:text-red-500 font-bold leading-none"
- title="Zuordnung entfernen"
- >×
-
- ))}
- {entries.length === 0 && Keine Module}
- setMatrixAddRole(role)}
- className="px-2 py-1 text-xs text-blue-600 border border-blue-300 rounded-full hover:bg-blue-50 transition-colors"
- >
- + Hinzufuegen
-
-
- |
- {entries.length} |
-
- )
- })}
-
-
-
-
+
)}
-
{activeTab === 'assignments' && (
-
-
-
-
-
-
-
-
-
- | Modul |
- Mitarbeiter |
- Rolle |
- Fortschritt |
- Status |
- Quiz |
- Deadline |
- Eskalation |
-
-
-
- {filteredAssignments.map(a => (
- setSelectedAssignment(a)}
- className="border-b hover:bg-gray-50 cursor-pointer"
- >
- |
- {a.module_title || a.module_code}
- {a.module_code}
- |
-
- {a.user_name}
- {a.user_email}
- |
- {a.role_code || '-'} |
-
-
-
- {a.progress_percent}%
-
- |
-
-
- {STATUS_LABELS[a.status] || a.status}
-
- |
-
- {a.quiz_score != null ? (
- {a.quiz_score.toFixed(0)}%
- ) : '-'}
- |
- {new Date(a.deadline).toLocaleDateString('de-DE')} |
-
- {a.escalation_level > 0 ? L{a.escalation_level} : '-'}
- |
-
- ))}
-
-
-
- {filteredAssignments.length === 0 &&
Keine Zuweisungen
}
-
+ !statusFilter || a.status === statusFilter)}
+ statusFilter={statusFilter} onStatusFilterChange={setStatusFilter} onAssignmentClick={setSelectedAssignment} />
)}
-
{activeTab === 'content' && (
-
-
- {/* Training Blocks — Controls → Schulungsmodule */}
-
-
-
-
Schulungsbloecke aus Controls
-
- Canonical Controls nach Kriterien filtern und automatisch Schulungsmodule generieren
- {canonicalMeta && ({canonicalMeta.total} Controls verfuegbar)}
-
-
-
setShowBlockCreate(true)}
- className="px-3 py-1.5 text-xs bg-blue-600 text-white rounded-lg hover:bg-blue-700"
- >
- + Neuen Block erstellen
-
-
-
- {/* Block list */}
- {blocks.length > 0 ? (
-
-
-
-
- | Name |
- Domain |
- Zielgruppe |
- Severity |
- Prefix |
- Letzte Generierung |
- Aktionen |
-
-
-
- {blocks.map(block => (
-
- |
- {block.name}
- {block.description && {block.description} }
- |
- {block.domain_filter || 'Alle'} |
- {block.target_audience_filter ? (TARGET_AUDIENCE_LABELS[block.target_audience_filter] || block.target_audience_filter) : 'Alle'} |
- {block.severity_filter || 'Alle'} |
- {block.module_code_prefix} |
- {block.last_generated_at ? new Date(block.last_generated_at).toLocaleString('de-DE') : 'Noch nie'} |
-
-
- handlePreviewBlock(block.id)}
- className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded hover:bg-gray-200"
- >
- Preview
-
- handleGenerateBlock(block.id)}
- disabled={blockGenerating}
- className="px-2 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
- >
- {blockGenerating ? 'Generiert...' : 'Generieren'}
-
- handleDeleteBlock(block.id)}
- className="px-2 py-1 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200"
- >
- Loeschen
-
-
- |
-
- ))}
-
-
-
- ) : (
-
- Noch keine Schulungsbloecke konfiguriert. Erstelle einen Block, um Controls automatisch in Module umzuwandeln.
-
- )}
-
- {/* Preview result */}
- {blockPreview && blockPreviewId && (
-
-
Preview: {blocks.find(b => b.id === blockPreviewId)?.name}
-
- Controls: {blockPreview.control_count}
- Module: {blockPreview.module_count}
- Rollen: {blockPreview.proposed_roles.map(r => ROLE_LABELS[r] || r).join(', ')}
-
- {blockPreview.controls.length > 0 && (
-
- Passende Controls anzeigen ({blockPreview.control_count})
-
- {blockPreview.controls.slice(0, 50).map(ctrl => (
-
- {ctrl.control_id}
- {ctrl.title}
- {ctrl.severity}
-
- ))}
- {blockPreview.control_count > 50 &&
... und {blockPreview.control_count - 50} weitere
}
-
-
- )}
-
- )}
-
- {/* Generate result */}
- {blockResult && (
-
-
Generierung abgeschlossen
-
- Module erstellt: {blockResult.modules_created}
- Controls verknuepft: {blockResult.controls_linked}
- Matrix-Eintraege: {blockResult.matrix_entries_created}
- Content generiert: {blockResult.content_generated}
-
- {blockResult.errors && blockResult.errors.length > 0 && (
-
- {blockResult.errors.map((err, i) =>
{err}
)}
-
- )}
-
- )}
-
-
- {/* Block Create Modal */}
- {showBlockCreate && (
-
-
-
Neuen Schulungsblock erstellen
-
-
-
- )}
-
- {/* Bulk Generation */}
-
-
Bulk-Generierung
-
Generiere Inhalte und Quiz-Fragen fuer alle Module auf einmal
-
-
- {bulkGenerating ? 'Generiere...' : 'Alle Inhalte generieren'}
-
-
- {bulkGenerating ? 'Generiere...' : 'Alle Quizfragen generieren'}
-
-
- {bulkResult && (
-
-
- Generiert: {bulkResult.generated}
- Uebersprungen: {bulkResult.skipped}
- {bulkResult.errors?.length > 0 && (
- Fehler: {bulkResult.errors.length}
- )}
-
- {bulkResult.errors?.length > 0 && (
-
- {bulkResult.errors.map((err, i) =>
{err}
)}
-
- )}
-
- )}
-
-
-
-
LLM-Content-Generator
-
Generiere Schulungsinhalte und Quiz-Fragen automatisch via KI
-
-
-
-
-
-
- {generating ? 'Generiere...' : 'Inhalt generieren'}
-
-
- {generating ? 'Generiere...' : 'Quiz generieren'}
-
-
-
-
- {generatedContent && (
-
-
-
-
Generierter Inhalt (v{generatedContent.version})
-
Generiert von: {generatedContent.generated_by} ({generatedContent.llm_model})
-
- {!generatedContent.is_published ? (
-
handlePublishContent(generatedContent.id)} className="px-3 py-1.5 text-xs bg-green-600 text-white rounded hover:bg-green-700">Veroeffentlichen
- ) : (
-
Veroeffentlicht
- )}
-
-
-
{generatedContent.content_body}
-
-
- )}
-
- {/* Audio Player */}
- {selectedModuleId && generatedContent?.is_published && (
-
m.media_type === 'audio') || null}
- onMediaUpdate={() => loadModuleMedia(selectedModuleId)}
- />
- )}
-
- {/* Video Player */}
- {selectedModuleId && generatedContent?.is_published && (
- m.media_type === 'video') || null}
- onMediaUpdate={() => loadModuleMedia(selectedModuleId)}
- />
- )}
-
- {/* Interactive Video */}
- {selectedModuleId && generatedContent?.is_published && (
-
-
-
-
Interaktives Video
-
Video mit Narrator-Persona und Checkpoint-Quizzes
-
- {moduleMedia.some(m => m.media_type === 'interactive_video' && m.status === 'completed') ? (
-
Interaktiv erstellt
- ) : (
-
- {interactiveGenerating ? 'Generiere interaktives Video...' : 'Interaktives Video generieren'}
-
- )}
-
- {moduleMedia.filter(m => m.media_type === 'interactive_video' && m.status === 'completed').map(m => (
-
-
Dauer: {Math.round(m.duration_seconds / 60)} Min | Groesse: {(m.file_size_bytes / 1024 / 1024).toFixed(1)} MB
-
Generiert: {new Date(m.created_at).toLocaleString('de-DE')}
-
- ))}
-
- )}
-
- {/* Script Preview */}
- {selectedModuleId && generatedContent?.is_published && (
-
- )}
-
- )}
-
- {activeTab === 'audit' && (
-
-
-
-
-
- | Zeitpunkt |
- Aktion |
- Entitaet |
- Details |
-
-
-
- {auditLog.map(entry => (
-
- | {new Date(entry.created_at).toLocaleString('de-DE')} |
- {entry.action} |
- {entry.entity_type} |
- {JSON.stringify(entry.details).substring(0, 100)} |
-
- ))}
-
-
-
- {auditLog.length === 0 &&
Keine Audit-Eintraege
}
-
- )}
-
- {/* Modals & Drawers */}
- {showModuleCreate && (
- setShowModuleCreate(false)}
- onSaved={() => { setShowModuleCreate(false); loadData() }}
- />
- )}
- {selectedModule && (
- setSelectedModule(null)}
- onSaved={() => { setSelectedModule(null); loadData() }}
- />
- )}
- {matrixAddRole && (
- setMatrixAddRole(null)}
- onSaved={() => { setMatrixAddRole(null); loadData() }}
- />
- )}
- {selectedAssignment && (
- setSelectedAssignment(null)}
- onSaved={() => { setSelectedAssignment(null); loadData() }}
+
)}
-
- )
-}
-
-// =============================================================================
-// MODULE CREATE MODAL
-// =============================================================================
-
-function ModuleCreateModal({ onClose, onSaved }: { onClose: () => void; onSaved: () => void }) {
- const [moduleCode, setModuleCode] = useState('')
- const [title, setTitle] = useState('')
- const [description, setDescription] = useState('')
- const [regulationArea, setRegulationArea] = useState('dsgvo')
- const [frequencyType, setFrequencyType] = useState('annual')
- const [durationMinutes, setDurationMinutes] = useState(45)
- const [passThreshold, setPassThreshold] = useState(70)
- const [saving, setSaving] = useState(false)
- const [error, setError] = useState(null)
-
- const handleSave = async () => {
- if (!moduleCode || !title) return
- setSaving(true)
- setError(null)
- try {
- await createModule({ module_code: moduleCode, title, description, regulation_area: regulationArea, frequency_type: frequencyType, duration_minutes: durationMinutes, pass_threshold: passThreshold })
- onSaved()
- } catch (e) {
- setError(e instanceof Error ? e.message : 'Fehler beim Erstellen')
- } finally {
- setSaving(false)
- }
- }
-
- return (
-
-
-
-
Neues Trainingsmodul
-
-
-
-
-
-
-
-
- setModuleCode(e.target.value.toUpperCase())} placeholder="DSGVO-BASICS" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
-
-
-
-
-
-
-
-
- setTitle(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
-
-
-
-
-
-
-
-
-
-
-
- setDurationMinutes(Number(e.target.value))} className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
-
-
-
- setPassThreshold(Number(e.target.value))} className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
-
-
- {error &&
{error}
}
-
-
- Abbrechen
-
- {saving ? 'Erstelle...' : 'Modul erstellen'}
-
-
-
-
- )
-}
-
-// =============================================================================
-// MODULE EDIT DRAWER
-// =============================================================================
-
-function ModuleEditDrawer({ module, onClose, onSaved }: { module: TrainingModule; onClose: () => void; onSaved: () => void }) {
- const [title, setTitle] = useState(module.title)
- const [description, setDescription] = useState(module.description || '')
- const [durationMinutes, setDurationMinutes] = useState(module.duration_minutes)
- const [passThreshold, setPassThreshold] = useState(module.pass_threshold)
- const [isActive, setIsActive] = useState(module.is_active)
- const [saving, setSaving] = useState(false)
- const [deleting, setDeleting] = useState(false)
- const [error, setError] = useState(null)
-
- const handleSave = async () => {
- setSaving(true)
- setError(null)
- try {
- await updateModule(module.id, { title, description, duration_minutes: durationMinutes, pass_threshold: passThreshold, is_active: isActive })
- onSaved()
- } catch (e) {
- setError(e instanceof Error ? e.message : 'Fehler beim Speichern')
- } finally {
- setSaving(false)
- }
- }
-
- const handleDelete = async () => {
- if (!window.confirm(`Modul "${module.title}" wirklich loeschen?`)) return
- setDeleting(true)
- try {
- await deleteModule(module.id)
- onSaved()
- } catch (e) {
- setError(e instanceof Error ? e.message : 'Fehler beim Loeschen')
- setDeleting(false)
- }
- }
-
- return (
-
-
-
-
-
-
- {REGULATION_LABELS[module.regulation_area] || module.regulation_area}
-
- {module.nis2_relevant && NIS2}
-
-
{module.module_code}
-
-
-
-
-
-
-
-
- setTitle(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
-
-
-
-
-
-
- Modul aktiv
- setIsActive(!isActive)}
- className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${isActive ? 'bg-blue-600' : 'bg-gray-200'}`}
- >
-
-
-
- {error &&
{error}
}
-
-
-
- {saving ? 'Speichern...' : 'Aenderungen speichern'}
-
-
- {deleting ? 'Loeschen...' : 'Modul loeschen'}
-
-
-
-
- )
-}
-
-// =============================================================================
-// MATRIX ADD MODAL
-// =============================================================================
-
-function MatrixAddModal({ roleCode, modules, onClose, onSaved }: {
- roleCode: string
- modules: TrainingModule[]
- onClose: () => void
- onSaved: () => void
-}) {
- const activeModules = modules.filter(m => m.is_active).sort((a, b) => a.module_code.localeCompare(b.module_code))
- const [moduleId, setModuleId] = useState(activeModules[0]?.id || '')
- const [isMandatory, setIsMandatory] = useState(true)
- const [priority, setPriority] = useState(1)
- const [saving, setSaving] = useState(false)
- const [error, setError] = useState(null)
-
- const handleSave = async () => {
- if (!moduleId) return
- setSaving(true)
- setError(null)
- try {
- await setMatrixEntry({ role_code: roleCode, module_id: moduleId, is_mandatory: isMandatory, priority })
- onSaved()
- } catch (e) {
- setError(e instanceof Error ? e.message : 'Fehler beim Hinzufuegen')
- } finally {
- setSaving(false)
- }
- }
-
- return (
-
-
-
-
Modul zu Rolle hinzufuegen
-
-
-
-
-
-
Rolle: {roleCode}
-
-
-
-
-
-
- setIsMandatory(!isMandatory)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${isMandatory ? 'bg-blue-600' : 'bg-gray-200'}`}>
-
-
-
-
-
- setPriority(Number(e.target.value))} className="w-24 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
-
- {error &&
{error}
}
-
-
- Abbrechen
-
- {saving ? 'Hinzufuegen...' : 'Hinzufuegen'}
-
-
-
-
- )
-}
-
-// =============================================================================
-// ASSIGNMENT DETAIL DRAWER
-// =============================================================================
-
-function AssignmentDetailDrawer({ assignment, onClose, onSaved }: {
- assignment: TrainingAssignment
- onClose: () => void
- onSaved: () => void
-}) {
- const [deadline, setDeadline] = useState(assignment.deadline ? assignment.deadline.split('T')[0] : '')
- const [savingDeadline, setSavingDeadline] = useState(false)
- const [actionLoading, setActionLoading] = useState(false)
- const [error, setError] = useState(null)
-
- const handleAction = async (action: () => Promise) => {
- setActionLoading(true)
- setError(null)
- try {
- await action()
- onSaved()
- } catch (e) {
- setError(e instanceof Error ? e.message : 'Fehler')
- setActionLoading(false)
- }
- }
-
- const handleSaveDeadline = async () => {
- setSavingDeadline(true)
- setError(null)
- try {
- await updateAssignment(assignment.id, { deadline: new Date(deadline).toISOString() })
- onSaved()
- } catch (e) {
- setError(e instanceof Error ? e.message : 'Fehler beim Speichern')
- setSavingDeadline(false)
- }
- }
-
- const statusActions: Record Promise } | null> = {
- pending: { label: 'Starten', action: () => startAssignment(assignment.id) },
- in_progress: { label: 'Als abgeschlossen markieren', action: () => completeAssignment(assignment.id) },
- overdue: { label: 'Als erledigt markieren', action: () => completeAssignment(assignment.id) },
- completed: null,
- expired: null,
- }
- const currentAction = statusActions[assignment.status] || null
-
- return (
-
-
-
-
-
{assignment.module_title || assignment.module_code}
-
{assignment.module_code}
-
-
-
-
-
-
- {/* Employee */}
-
-
{assignment.user_name}
-
{assignment.user_email}
- {assignment.role_code &&
Rolle: {assignment.role_code}
}
-
-
- {/* Timestamps */}
-
-
Erstellt:{new Date(assignment.created_at).toLocaleString('de-DE')}
- {assignment.started_at &&
Gestartet:{new Date(assignment.started_at).toLocaleString('de-DE')}
}
- {assignment.completed_at &&
Abgeschlossen:{new Date(assignment.completed_at).toLocaleString('de-DE')}
}
-
-
- {/* Progress */}
-
-
- Fortschritt
- {assignment.progress_percent}%
-
-
-
-
- {/* Quiz Score */}
-
- Quiz-Score
- {assignment.quiz_score != null ? (
-
- {assignment.quiz_score.toFixed(0)}% {assignment.quiz_passed ? '(Bestanden)' : '(Nicht bestanden)'}
-
- ) : (
- Noch kein Quiz
- )}
-
-
- {/* Escalation */}
- {assignment.escalation_level > 0 && (
-
- Eskalationslevel
-
- Level {assignment.escalation_level}
-
-
- )}
-
- {/* Certificate */}
- {assignment.certificate_id && (
-
- Zertifikat
- Zertifikat vorhanden
-
- )}
-
- {/* Status Action */}
- {currentAction && (
-
handleAction(currentAction.action)}
- disabled={actionLoading}
- className="w-full px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
- >
- {actionLoading ? 'Bitte warten...' : currentAction.label}
-
- )}
-
- {/* Deadline Edit */}
-
-
-
- setDeadline(e.target.value)}
- className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
- />
-
- {savingDeadline ? 'Speichern...' : 'Deadline speichern'}
-
-
-
-
- {error &&
{error}
}
-
-
-
- )
-}
-
-function KPICard({ label, value, color }: { label: string; value: string | number; color?: string }) {
- const colorMap: Record = {
- green: 'bg-green-50 border-green-200',
- yellow: 'bg-yellow-50 border-yellow-200',
- red: 'bg-red-50 border-red-200',
- }
- const textMap: Record = {
- green: 'text-green-700',
- yellow: 'text-yellow-700',
- red: 'text-red-700',
- }
- return (
-
-
{label}
-
{value}
+ {activeTab === 'audit' &&
}
+
+ {showModuleCreate &&
setShowModuleCreate(false)} onSaved={() => { setShowModuleCreate(false); loadData() }} />}
+ {selectedModule && setSelectedModule(null)} onSaved={() => { setSelectedModule(null); loadData() }} />}
+ {matrixAddRole && setMatrixAddRole(null)} onSaved={() => { setMatrixAddRole(null); loadData() }} />}
+ {selectedAssignment && setSelectedAssignment(null)} onSaved={() => { setSelectedAssignment(null); loadData() }} />}
)
}