From b5d20a4c1d173dd5706512319e1b6bd9bbdd892a Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:32:35 +0200 Subject: [PATCH] fix: add missing training page components to fix admin-compliance Docker build All 8 components imported by app/sdk/training/page.tsx were missing. Docker build was failing with Module not found errors. Co-Authored-By: Claude Sonnet 4.6 --- .../_components/AssignmentDetailDrawer.tsx | 125 +++++++++++++++ .../training/_components/AssignmentsTab.tsx | 104 ++++++++++++ .../app/sdk/training/_components/AuditTab.tsx | 73 +++++++++ .../training/_components/MatrixAddModal.tsx | 88 +++++++++++ .../sdk/training/_components/MatrixTab.tsx | 80 ++++++++++ .../_components/ModuleCreateModal.tsx | 98 ++++++++++++ .../training/_components/ModuleEditDrawer.tsx | 149 ++++++++++++++++++ .../sdk/training/_components/ModulesTab.tsx | 96 +++++++++++ .../sdk/training/_components/OverviewTab.tsx | 89 +++++++++++ 9 files changed, 902 insertions(+) create mode 100644 admin-compliance/app/sdk/training/_components/AssignmentDetailDrawer.tsx create mode 100644 admin-compliance/app/sdk/training/_components/AssignmentsTab.tsx create mode 100644 admin-compliance/app/sdk/training/_components/AuditTab.tsx create mode 100644 admin-compliance/app/sdk/training/_components/MatrixAddModal.tsx create mode 100644 admin-compliance/app/sdk/training/_components/MatrixTab.tsx create mode 100644 admin-compliance/app/sdk/training/_components/ModuleCreateModal.tsx create mode 100644 admin-compliance/app/sdk/training/_components/ModuleEditDrawer.tsx create mode 100644 admin-compliance/app/sdk/training/_components/ModulesTab.tsx create mode 100644 admin-compliance/app/sdk/training/_components/OverviewTab.tsx diff --git a/admin-compliance/app/sdk/training/_components/AssignmentDetailDrawer.tsx b/admin-compliance/app/sdk/training/_components/AssignmentDetailDrawer.tsx new file mode 100644 index 0000000..99f0606 --- /dev/null +++ b/admin-compliance/app/sdk/training/_components/AssignmentDetailDrawer.tsx @@ -0,0 +1,125 @@ +'use client' + +import { useState } from 'react' +import { updateAssignment, completeAssignment } from '@/lib/sdk/training/api' +import type { TrainingAssignment } from '@/lib/sdk/training/types' +import { STATUS_LABELS, STATUS_COLORS } from '@/lib/sdk/training/types' + +export default function AssignmentDetailDrawer({ + assignment, + onClose, + onSaved, +}: { + assignment: TrainingAssignment + onClose: () => void + onSaved: () => void +}) { + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + const colors = STATUS_COLORS[assignment.status] + + async function handleComplete() { + if (!window.confirm('Zuweisung als abgeschlossen markieren?')) return + setSaving(true) + try { + await completeAssignment(assignment.id) + onSaved() + } catch (err) { + setError(err instanceof Error ? err.message : 'Fehler') + } finally { + setSaving(false) + } + } + + async function handleExtend(e: React.FormEvent) { + e.preventDefault() + setSaving(true) + setError(null) + const fd = new FormData(e.currentTarget) + try { + await updateAssignment(assignment.id, { deadline: fd.get('deadline') as string }) + onSaved() + } catch (err) { + setError(err instanceof Error ? err.message : 'Fehler beim Aktualisieren') + } finally { + setSaving(false) + } + } + + return ( +
+
+
+
+

Zuweisung

+ +
+ +
+ {error && ( +
{error}
+ )} + +
+ + + + + {STATUS_LABELS[assignment.status]} + + + + + {assignment.started_at && } + {assignment.completed_at && } + {assignment.quiz_score != null && ( + + )} + + {assignment.escalation_level > 0 && ( + + )} +
+ + {assignment.status !== 'completed' && ( +
+

Frist verlaengern

+
+ + +
+
+ )} +
+ + {assignment.status !== 'completed' && ( +
+ +
+ )} +
+
+ ) +} + +function Row({ label, value, children }: { label: string; value?: string; children?: React.ReactNode }) { + return ( +
+ {label}: + {children ?? {value}} +
+ ) +} diff --git a/admin-compliance/app/sdk/training/_components/AssignmentsTab.tsx b/admin-compliance/app/sdk/training/_components/AssignmentsTab.tsx new file mode 100644 index 0000000..ab34e3f --- /dev/null +++ b/admin-compliance/app/sdk/training/_components/AssignmentsTab.tsx @@ -0,0 +1,104 @@ +'use client' + +import type { TrainingAssignment } from '@/lib/sdk/training/types' +import { STATUS_LABELS, STATUS_COLORS } from '@/lib/sdk/training/types' + +export default function AssignmentsTab({ + assignments, + statusFilter, + onStatusFilterChange, + onAssignmentClick, +}: { + assignments: TrainingAssignment[] + statusFilter: string + onStatusFilterChange: (v: string) => void + onAssignmentClick: (assignment: TrainingAssignment) => void +}) { + return ( +
+
+ + {assignments.length} Zuweisungen +
+ + {assignments.length === 0 ? ( +
Keine Zuweisungen gefunden.
+ ) : ( +
+ + + + + + + + + + + + + {assignments.map(a => { + const colors = STATUS_COLORS[a.status] + const deadline = new Date(a.deadline) + const isOverdue = deadline < new Date() && a.status !== 'completed' + return ( + onAssignmentClick(a)} + className="hover:bg-gray-50 cursor-pointer" + > + + + + + + + + ) + })} + +
NutzerModulStatusFortschrittFristQuiz
+
{a.user_name}
+
{a.user_email}
+
+ {a.module_code ?? a.module_id.slice(0, 8)} + {a.module_title &&
{a.module_title}
} +
+ + {STATUS_LABELS[a.status]} + + +
+
+
+
+ {a.progress_percent}% +
+
+ {deadline.toLocaleDateString('de-DE')} + + {a.quiz_score != null ? ( + + {Math.round(a.quiz_score)}% {a.quiz_passed ? '✓' : '✗'} + + ) : ( + + )} +
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/training/_components/AuditTab.tsx b/admin-compliance/app/sdk/training/_components/AuditTab.tsx new file mode 100644 index 0000000..72bec99 --- /dev/null +++ b/admin-compliance/app/sdk/training/_components/AuditTab.tsx @@ -0,0 +1,73 @@ +'use client' + +import type { AuditLogEntry } from '@/lib/sdk/training/types' + +const ACTION_LABELS: Record = { + assigned: 'Zugewiesen', + started: 'Gestartet', + completed: 'Abgeschlossen', + quiz_submitted: 'Quiz eingereicht', + escalated: 'Eskaliert', + certificate_issued: 'Zertifikat ausgestellt', + content_generated: 'Content generiert', +} + +const ACTION_COLORS: Record = { + assigned: 'bg-blue-100 text-blue-700', + started: 'bg-yellow-100 text-yellow-700', + completed: 'bg-green-100 text-green-700', + quiz_submitted: 'bg-purple-100 text-purple-700', + escalated: 'bg-red-100 text-red-700', + certificate_issued: 'bg-emerald-100 text-emerald-700', + content_generated: 'bg-gray-100 text-gray-700', +} + +export default function AuditTab({ auditLog }: { auditLog: AuditLogEntry[] }) { + return ( +
+
+

{auditLog.length} Eintraege

+
+ + {auditLog.length === 0 ? ( +
Keine Audit-Eintraege gefunden.
+ ) : ( +
+ + + + + + + + + + + {auditLog.map(entry => ( + + + + + + + ))} + +
ZeitpunktAktionEntitaetDetails
+ {new Date(entry.created_at).toLocaleString('de-DE')} + + + {ACTION_LABELS[entry.action] ?? entry.action} + + + {entry.entity_type} + {entry.entity_id && {entry.entity_id.slice(0, 8)}} + + {Object.keys(entry.details).length > 0 + ? Object.entries(entry.details).map(([k, v]) => `${k}: ${v}`).join(', ') + : '—'} +
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/training/_components/MatrixAddModal.tsx b/admin-compliance/app/sdk/training/_components/MatrixAddModal.tsx new file mode 100644 index 0000000..382fccf --- /dev/null +++ b/admin-compliance/app/sdk/training/_components/MatrixAddModal.tsx @@ -0,0 +1,88 @@ +'use client' + +import { useState } from 'react' +import { setMatrixEntry } from '@/lib/sdk/training/api' +import type { TrainingModule } from '@/lib/sdk/training/types' +import { ROLE_LABELS, REGULATION_LABELS } from '@/lib/sdk/training/types' + +export default function MatrixAddModal({ + roleCode, + modules, + onClose, + onSaved, +}: { + roleCode: string + modules: TrainingModule[] + onClose: () => void + onSaved: () => void +}) { + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setSaving(true) + setError(null) + const fd = new FormData(e.currentTarget) + try { + await setMatrixEntry({ + role_code: roleCode, + module_id: fd.get('module_id') as string, + is_mandatory: fd.get('is_mandatory') === 'on', + priority: parseInt(fd.get('priority') as string) || 1, + }) + onSaved() + } catch (err) { + setError(err instanceof Error ? err.message : 'Fehler beim Hinzufuegen') + } finally { + setSaving(false) + } + } + + return ( +
+
+

Modul zuweisen

+

+ Rolle: {ROLE_LABELS[roleCode] ?? roleCode} ({roleCode}) +

+ + {error && ( +
{error}
+ )} + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/training/_components/MatrixTab.tsx b/admin-compliance/app/sdk/training/_components/MatrixTab.tsx new file mode 100644 index 0000000..ce77fae --- /dev/null +++ b/admin-compliance/app/sdk/training/_components/MatrixTab.tsx @@ -0,0 +1,80 @@ +'use client' + +import type { MatrixResponse } from '@/lib/sdk/training/types' +import { ALL_ROLES, ROLE_LABELS } from '@/lib/sdk/training/types' + +export default function MatrixTab({ + matrix, + onDeleteEntry, + onAddEntry, +}: { + matrix: MatrixResponse + onDeleteEntry: (roleCode: string, moduleId: string) => void + onAddEntry: (roleCode: string) => void +}) { + return ( +
+
+

Pflichtzuordnung von Schulungsmodulen zu Rollen

+
+ +
+ + + + + + + + + + {ALL_ROLES.map(role => { + const entries = matrix.entries[role] ?? [] + return ( + + + + + + ) + })} + +
RolleZugewiesene ModuleAktion
+
{ROLE_LABELS[role] ?? role}
+
{role}
+
+ {entries.length === 0 ? ( + Keine Module zugewiesen + ) : ( +
+ {entries.map(entry => ( + + {entry.module_code ?? entry.module_id.slice(0, 8)} + {entry.is_mandatory && *} + + + ))} +
+ )} +
+ +
+
+

* = Pflichtmodul

+
+ ) +} diff --git a/admin-compliance/app/sdk/training/_components/ModuleCreateModal.tsx b/admin-compliance/app/sdk/training/_components/ModuleCreateModal.tsx new file mode 100644 index 0000000..62f87c8 --- /dev/null +++ b/admin-compliance/app/sdk/training/_components/ModuleCreateModal.tsx @@ -0,0 +1,98 @@ +'use client' + +import { useState } from 'react' +import { createModule } from '@/lib/sdk/training/api' +import { REGULATION_LABELS, FREQUENCY_LABELS } from '@/lib/sdk/training/types' + +export default function ModuleCreateModal({ + onClose, + onSaved, +}: { + onClose: () => void + onSaved: () => void +}) { + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + setSaving(true) + setError(null) + const fd = new FormData(e.currentTarget) + try { + await createModule({ + module_code: fd.get('module_code') as string, + title: fd.get('title') as string, + description: (fd.get('description') as string) || undefined, + regulation_area: fd.get('regulation_area') as string, + frequency_type: fd.get('frequency_type') as string, + duration_minutes: parseInt(fd.get('duration_minutes') as string) || 30, + pass_threshold: parseInt(fd.get('pass_threshold') as string) || 80, + }) + onSaved() + } catch (err) { + setError(err instanceof Error ? err.message : 'Fehler beim Erstellen') + } finally { + setSaving(false) + } + } + + return ( +
+
+

Neues Schulungsmodul

+ + {error && ( +
{error}
+ )} + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ +