setSelectedAssignment(a)}
+ className="border-b hover:bg-gray-50 cursor-pointer"
+ >
{a.module_title || a.module_code}
{a.module_code}
@@ -583,6 +654,455 @@ export default function TrainingPage() {
{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
+
+
+
+
+
+
+
+
+
+ Modul-Code *
+ 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" />
+
+
+ Regelungsbereich
+ setRegulationArea(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">
+ {Object.entries(REGULATION_LABELS).map(([k, l]) => {l} )}
+
+
+
+
+ Titel *
+ 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" />
+
+
+ Beschreibung
+
+
+
+ Frequenz
+ setFrequencyType(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">
+ {Object.entries(FREQUENCY_LABELS).map(([k, l]) => {l} )}
+
+
+
+ Dauer (Min.)
+ 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" />
+
+
+ Bestehensgrenze (%)
+ 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}
+
+
+
+
+
+
+
+
+
+ Titel
+ 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" />
+
+
+ Beschreibung
+
+
+
+ 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}
+
+ Modul
+ setModuleId(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">
+ {activeModules.map(m => {m.module_code} — {m.title} )}
+
+
+
+ Pflichtmodul
+ setIsMandatory(!isMandatory)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${isMandatory ? 'bg-blue-600' : 'bg-gray-200'}`}>
+
+
+
+
+ Prioritaet
+ 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 */}
+
+
Deadline bearbeiten
+
+ 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}
}
+
+
)
}
diff --git a/admin-compliance/lib/sdk/academy/api.ts b/admin-compliance/lib/sdk/academy/api.ts
index 21a081e..c272939 100644
--- a/admin-compliance/lib/sdk/academy/api.ts
+++ b/admin-compliance/lib/sdk/academy/api.ts
@@ -112,7 +112,7 @@ async function fetchWithTimeout(
* Alle Kurse abrufen
*/
export async function fetchCourses(): Promise {
- const res = await fetchWithTimeout<{ courses: Course[]; total: number }>(
+ const res = await fetchWithTimeout<{ courses: BackendCourse[]; total: number }>(
`${ACADEMY_API_BASE}/courses`
)
return mapCoursesFromBackend(res.courses || [])
@@ -137,6 +137,8 @@ interface BackendCourse {
duration_minutes: number
required_for_roles: string[]
is_active: boolean
+ passing_score?: number
+ status?: string
lessons?: BackendLesson[]
created_at: string
updated_at: string
@@ -169,6 +171,9 @@ function mapCourseFromBackend(bc: BackendCourse): Course {
description: bc.description || '',
category: bc.category,
durationMinutes: bc.duration_minutes || 0,
+ passingScore: bc.passing_score ?? 70,
+ isActive: bc.is_active ?? true,
+ status: (bc.status as 'draft' | 'published') ?? 'draft',
requiredForRoles: bc.required_for_roles || [],
lessons: (bc.lessons || []).map(l => ({
id: l.id,
@@ -316,6 +321,39 @@ export async function generateCertificate(enrollmentId: string): Promise {
+ const res = await fetchWithTimeout<{ certificates: Certificate[]; total: number }>(
+ `${ACADEMY_API_BASE}/certificates`
+ )
+ return res.certificates || []
+}
+
+/**
+ * Einschreibung loeschen
+ */
+export async function deleteEnrollment(id: string): Promise {
+ await fetchWithTimeout(
+ `${ACADEMY_API_BASE}/enrollments/${id}`,
+ { method: 'DELETE' }
+ )
+}
+
+/**
+ * Einschreibung aktualisieren (z.B. Deadline)
+ */
+export async function updateEnrollment(id: string, data: { deadline?: string }): Promise {
+ return fetchWithTimeout(
+ `${ACADEMY_API_BASE}/enrollments/${id}`,
+ {
+ method: 'PUT',
+ body: JSON.stringify(data),
+ }
+ )
+}
+
// =============================================================================
// QUIZ
// =============================================================================
@@ -366,6 +404,8 @@ export async function fetchAcademyStatistics(): Promise {
completion_rate: number
overdue_count: number
avg_completion_days: number
+ by_category?: Record
+ by_status?: Record
}>(`${ACADEMY_API_BASE}/stats`)
return {
@@ -373,8 +413,8 @@ export async function fetchAcademyStatistics(): Promise {
totalEnrollments: res.total_enrollments || 0,
completionRate: res.completion_rate || 0,
overdueCount: res.overdue_count || 0,
- byCategory: {} as Record,
- byStatus: {} as Record,
+ byCategory: (res.by_category || {}) as Record,
+ byStatus: (res.by_status || {}) as Record,
}
}
@@ -484,6 +524,9 @@ export function createMockCourses(): Course[] {
description: 'Umfassende Einfuehrung in die Datenschutz-Grundverordnung. Dieses Pflichttraining vermittelt die wichtigsten Grundsaetze des Datenschutzes, Betroffenenrechte und die korrekte Handhabung personenbezogener Daten im Arbeitsalltag.',
category: 'dsgvo_basics',
durationMinutes: 90,
+ passingScore: 80,
+ isActive: true,
+ status: 'published',
requiredForRoles: ['all'],
createdAt: new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(),
updatedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
@@ -543,6 +586,9 @@ export function createMockCourses(): Course[] {
description: 'Sensibilisierung fuer IT-Sicherheitsrisiken und Best Practices im Umgang mit Phishing, Passwoertern, Social Engineering und sicherer Kommunikation.',
category: 'it_security',
durationMinutes: 60,
+ passingScore: 75,
+ isActive: true,
+ status: 'published',
requiredForRoles: ['all'],
createdAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(),
updatedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
@@ -592,6 +638,9 @@ export function createMockCourses(): Course[] {
description: 'Grundlagen kuenstlicher Intelligenz, EU AI Act, verantwortungsvoller Einsatz von KI-Werkzeugen und Risiken bei der Nutzung von Large Language Models (LLMs) im Unternehmen.',
category: 'ai_literacy',
durationMinutes: 75,
+ passingScore: 70,
+ isActive: true,
+ status: 'draft',
requiredForRoles: ['admin', 'data_protection_officer'],
createdAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
updatedAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000).toISOString(),
diff --git a/admin-compliance/lib/sdk/academy/types.ts b/admin-compliance/lib/sdk/academy/types.ts
index e05f7ee..623acb1 100644
--- a/admin-compliance/lib/sdk/academy/types.ts
+++ b/admin-compliance/lib/sdk/academy/types.ts
@@ -117,6 +117,9 @@ export interface Course {
category: CourseCategory
lessons: Lesson[]
durationMinutes: number
+ passingScore: number
+ isActive: boolean
+ status: 'draft' | 'published'
requiredForRoles: string[]
createdAt: string
updatedAt: string
@@ -205,6 +208,7 @@ export interface CourseCreateRequest {
description: string
category: CourseCategory
durationMinutes: number
+ passingScore?: number
requiredForRoles?: string[]
}
@@ -213,6 +217,8 @@ export interface CourseUpdateRequest {
description?: string
category?: CourseCategory
durationMinutes?: number
+ passingScore?: number
+ status?: 'draft' | 'published'
requiredForRoles?: string[]
}
diff --git a/admin-compliance/lib/sdk/training/api.ts b/admin-compliance/lib/sdk/training/api.ts
index 2fb4f26..bcb56c7 100644
--- a/admin-compliance/lib/sdk/training/api.ts
+++ b/admin-compliance/lib/sdk/training/api.ts
@@ -89,6 +89,10 @@ export async function updateModule(id: string, data: Record): P
})
}
+export async function deleteModule(id: string): Promise {
+ return apiFetch(`/modules/${id}`, { method: 'DELETE' })
+}
+
// =============================================================================
// MATRIX
// =============================================================================
@@ -177,6 +181,13 @@ export async function completeAssignment(id: string): Promise<{ status: string }
return apiFetch(`/assignments/${id}/complete`, { method: 'POST' })
}
+export async function updateAssignment(id: string, data: { deadline?: string }): Promise {
+ return apiFetch(`/assignments/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify(data),
+ })
+}
+
// =============================================================================
// QUIZ
// =============================================================================