'use client' import React, { useState, useEffect, useMemo } from 'react' import Link from 'next/link' import { useSDK } from '@/lib/sdk' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' import { Course, CourseCategory, Enrollment, EnrollmentStatus, AcademyStatistics, COURSE_CATEGORY_INFO, ENROLLMENT_STATUS_INFO, isEnrollmentOverdue, getDaysUntilDeadline } from '@/lib/sdk/academy/types' import { fetchSDKAcademyList, generateAllCourses, fetchCertificates, deleteEnrollment, updateEnrollment, updateCourse, completeEnrollment } from '@/lib/sdk/academy/api' import type { Certificate } from '@/lib/sdk/academy/types' // ============================================================================= // TYPES // ============================================================================= type TabId = 'overview' | 'courses' | 'enrollments' | 'certificates' | 'settings' interface Tab { id: TabId label: string count?: number countColor?: string } // ============================================================================= // COMPONENTS // ============================================================================= function TabNavigation({ tabs, activeTab, onTabChange }: { tabs: Tab[] activeTab: TabId onTabChange: (tab: TabId) => void }) { return (
) } function StatCard({ label, value, color = 'gray', icon, trend }: { label: string value: number | string color?: 'gray' | 'blue' | 'yellow' | 'red' | 'green' | 'purple' icon?: React.ReactNode trend?: { value: number; label: string } }) { const colorClasses = { gray: 'border-gray-200 text-gray-900', blue: 'border-blue-200 text-blue-600', yellow: 'border-yellow-200 text-yellow-600', red: 'border-red-200 text-red-600', green: 'border-green-200 text-green-600', purple: 'border-purple-200 text-purple-600' } return (
{label}
{value}
{trend && (
= 0 ? 'text-green-600' : 'text-red-600'}`}> {trend.value >= 0 ? '+' : ''}{trend.value} {trend.label}
)}
{icon && (
{icon}
)}
) } function CourseCard({ course, enrollmentCount, onEdit }: { course: Course; enrollmentCount: number; onEdit?: (course: Course) => void }) { const categoryInfo = COURSE_CATEGORY_INFO[course.category] || COURSE_CATEGORY_INFO['custom'] return (
{categoryInfo.label} {course.status === 'published' && ( Veroeffentlicht )}

{course.title}

{course.description}

{course.lessons.length} Lektionen {course.durationMinutes} Min. {enrollmentCount} Teilnehmer Bestehensgrenze: {course.passingScore}%
{course.requiredForRoles.includes('all') ? 'Pflicht fuer alle' : `${course.requiredForRoles.length} Rollen`}
{new Date(course.updatedAt).toLocaleDateString('de-DE')}
Erstellt: {new Date(course.createdAt).toLocaleDateString('de-DE')}
Details
{onEdit && ( )}
) } function EnrollmentCard({ enrollment, courseName, onEdit, onComplete, onDelete }: { enrollment: Enrollment courseName: string onEdit?: (enrollment: Enrollment) => void onComplete?: (id: string) => void onDelete?: (id: string) => void }) { const statusInfo = ENROLLMENT_STATUS_INFO[enrollment.status] const overdue = isEnrollmentOverdue(enrollment) const daysUntil = getDaysUntilDeadline(enrollment.deadline) return (
{/* Status Badge */}
{statusInfo.label} {overdue && ( Ueberfaellig )}
{/* User Info */}

{enrollment.userName}

{enrollment.userEmail}

{courseName}

{/* Progress Bar */}
Fortschritt {enrollment.progress}%
{/* Right Side - Deadline */}
{enrollment.status === 'completed' ? 'Abgeschlossen' : overdue ? `${Math.abs(daysUntil)} Tage ueberfaellig` : `${daysUntil} Tage verbleibend` }
Frist: {new Date(enrollment.deadline).toLocaleDateString('de-DE')}
{/* Footer */}
Gestartet: {new Date(enrollment.startedAt).toLocaleDateString('de-DE')} {enrollment.completedAt && ( Abgeschlossen: {new Date(enrollment.completedAt).toLocaleDateString('de-DE')} )}
{enrollment.status === 'in_progress' && onComplete && ( )} {onEdit && ( )} {onDelete && ( )}
) } function FilterBar({ selectedCategory, selectedStatus, onCategoryChange, onStatusChange, onClear }: { selectedCategory: CourseCategory | 'all' selectedStatus: EnrollmentStatus | 'all' onCategoryChange: (category: CourseCategory | 'all') => void onStatusChange: (status: EnrollmentStatus | 'all') => void onClear: () => void }) { const hasFilters = selectedCategory !== 'all' || selectedStatus !== 'all' return (
Filter: {/* Category Filter */} {/* Enrollment Status Filter */} {/* Clear Filters */} {hasFilters && ( )}
) } // ============================================================================= // MAIN PAGE // ============================================================================= export default function AcademyPage() { const { state } = useSDK() const [activeTab, setActiveTab] = useState('overview') const [courses, setCourses] = useState([]) const [enrollments, setEnrollments] = useState([]) const [certificates, setCertificates] = useState([]) const [statistics, setStatistics] = useState(null) const [isLoading, setIsLoading] = useState(true) const [isGenerating, setIsGenerating] = useState(false) const [generateResult, setGenerateResult] = useState<{ generated: number; skipped: number; errors: string[] } | null>(null) // Modal states const [editingEnrollment, setEditingEnrollment] = useState(null) const [editingCourse, setEditingCourse] = useState(null) const [settingsSaved, setSettingsSaved] = useState(false) // Filters const [selectedCategory, setSelectedCategory] = useState('all') const [selectedStatus, setSelectedStatus] = useState('all') const [certSearch, setCertSearch] = useState('') // Load data from SDK backend useEffect(() => { loadData() }, []) // Calculate tab counts const tabCounts = useMemo(() => { return { courses: courses.length, enrollments: enrollments.filter(e => e.status !== 'completed').length, certificates: certificates.length || enrollments.filter(e => e.certificateId).length, overdue: enrollments.filter(e => isEnrollmentOverdue(e)).length } }, [courses, enrollments, certificates]) // Filtered courses const filteredCourses = useMemo(() => { let filtered = [...courses] if (selectedCategory !== 'all') { filtered = filtered.filter(c => c.category === selectedCategory) } return filtered.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) }, [courses, selectedCategory]) // Filtered enrollments const filteredEnrollments = useMemo(() => { let filtered = [...enrollments] if (selectedStatus !== 'all') { filtered = filtered.filter(e => e.status === selectedStatus) } // Sort: overdue first, then by deadline return filtered.sort((a, b) => { const aOverdue = isEnrollmentOverdue(a) ? -1 : 0 const bOverdue = isEnrollmentOverdue(b) ? -1 : 0 if (aOverdue !== bOverdue) return aOverdue - bOverdue return getDaysUntilDeadline(a.deadline) - getDaysUntilDeadline(b.deadline) }) }, [enrollments, selectedStatus]) // Enrollment counts per course const enrollmentCountByCourseId = useMemo(() => { const counts: Record = {} enrollments.forEach(e => { counts[e.courseId] = (counts[e.courseId] || 0) + 1 }) return counts }, [enrollments]) // Course name lookup const courseNameById = useMemo(() => { const map: Record = {} courses.forEach(c => { map[c.id] = c.title }) return map }, [courses]) const tabs: Tab[] = [ { id: 'overview', label: 'Uebersicht' }, { id: 'courses', label: 'Kurse', count: tabCounts.courses, countColor: 'bg-blue-100 text-blue-600' }, { id: 'enrollments', label: 'Einschreibungen', count: tabCounts.enrollments, countColor: 'bg-yellow-100 text-yellow-600' }, { id: 'certificates', label: 'Zertifikate', count: tabCounts.certificates, countColor: 'bg-green-100 text-green-600' }, { id: 'settings', label: 'Einstellungen' } ] const stepInfo = STEP_EXPLANATIONS['academy'] const loadData = async () => { setIsLoading(true) try { const [data, certs] = await Promise.allSettled([ fetchSDKAcademyList(), fetchCertificates(), ]) if (data.status === 'fulfilled') { setCourses(data.value.courses) setEnrollments(data.value.enrollments) setStatistics(data.value.statistics) } else { console.error('Failed to load Academy data:', data.reason) } if (certs.status === 'fulfilled') { setCertificates(certs.value) } } finally { setIsLoading(false) } } const handleCompleteEnrollment = async (id: string) => { try { await completeEnrollment(id) await loadData() } catch (e) { console.error('Failed to complete enrollment:', e) } } const handleDeleteEnrollment = async (id: string) => { if (!window.confirm('Einschreibung wirklich loeschen?')) return try { await deleteEnrollment(id) await loadData() } catch (e) { console.error('Failed to delete enrollment:', e) } } const handleGenerateAll = async () => { setIsGenerating(true) setGenerateResult(null) try { const result = await generateAllCourses() setGenerateResult({ generated: result.generated, skipped: result.skipped, errors: result.errors || [] }) // Reload data to show new courses await loadData() } catch (error) { console.error('Failed to generate courses:', error) setGenerateResult({ generated: 0, skipped: 0, errors: [error instanceof Error ? error.message : 'Fehler bei der Generierung'] }) } finally { setIsGenerating(false) } } const clearFilters = () => { setSelectedCategory('all') setSelectedStatus('all') } return (
{/* Step Header */}
Kurs erstellen
{/* Generation Result */} {generateResult && (
0 ? 'bg-yellow-50 border-yellow-200' : 'bg-green-50 border-green-200'}`}>
{generateResult.generated} Kurse generiert {generateResult.skipped} uebersprungen {generateResult.errors.length > 0 && ( {generateResult.errors.length} Fehler )}
{generateResult.errors.length > 0 && (
{generateResult.errors.map((err, i) =>
{err}
)}
)}
)} {/* Tab Navigation */} {/* Loading State */} {isLoading ? (
) : activeTab === 'settings' ? ( /* Settings Tab — localStorage-basiert */ { setSettingsSaved(true); setTimeout(() => setSettingsSaved(false), 2000) }} saved={settingsSaved} /> ) : activeTab === 'certificates' ? ( /* Certificates Tab */
{/* Stats */}
{(() => { const now = new Date() const total = certificates.length const valid = certificates.filter(c => new Date(c.validUntil) > now).length const expired = certificates.filter(c => new Date(c.validUntil) <= now).length return ( <>
{total}
Gesamt
{valid}
Gueltig
{expired}
Abgelaufen
) })()}
{/* Search */}
setCertSearch(e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
{/* Table */} {certificates.length === 0 ? (

Noch keine Zertifikate ausgestellt

Zertifikate werden automatisch nach Kursabschluss generiert.

) : (
{certificates .filter(c => !certSearch || c.userName.toLowerCase().includes(certSearch.toLowerCase()) || c.courseName.toLowerCase().includes(certSearch.toLowerCase()) ) .map(cert => ) }
Mitarbeiter Kurs Ausgestellt am Gueltig bis Status Aktionen
)}
) : ( <> {/* Statistics (Overview Tab) */} {activeTab === 'overview' && statistics && (
0 ? 'red' : 'green'} />
)} {/* Overdue Alert */} {tabCounts.overdue > 0 && (

Achtung: {tabCounts.overdue} ueberfaellige Schulung(en)

Mitarbeiter haben Pflichtschulungen nicht fristgerecht abgeschlossen. Handeln Sie umgehend.

)} {/* Info Box (Overview Tab) */} {activeTab === 'overview' && (

Schulungspflicht nach Art. 39 DSGVO

Gemaess Art. 39 Abs. 1 lit. b DSGVO gehoert die Sensibilisierung und Schulung der an den Verarbeitungsvorgaengen beteiligten Mitarbeiter zu den Aufgaben des Datenschutzbeauftragten. Nachweisbare Compliance-Schulungen sind Pflicht und sollten mindestens jaehrlich aufgefrischt werden.

)} {/* Filters */} {/* Courses Tab */} {(activeTab === 'overview' || activeTab === 'courses') && (
{activeTab === 'courses' && (

Kurse ({filteredCourses.length})

)} {filteredCourses.map(course => ( ))}
)} {/* Enrollments Tab */} {activeTab === 'enrollments' && (

Einschreibungen ({filteredEnrollments.length})

{filteredEnrollments.map(enrollment => ( ))}
)} {/* Empty States */} {activeTab === 'courses' && filteredCourses.length === 0 && (

Keine Kurse gefunden

{selectedCategory !== 'all' ? 'Passen Sie die Filter an oder' : 'Es sind noch keine Kurse vorhanden.' }

{selectedCategory !== 'all' ? ( ) : ( Ersten Kurs erstellen )}
)} {activeTab === 'enrollments' && filteredEnrollments.length === 0 && (

Keine Einschreibungen gefunden

{selectedStatus !== 'all' ? 'Passen Sie die Filter an.' : 'Es sind noch keine Mitarbeiter in Kurse eingeschrieben.' }

{selectedStatus !== 'all' && ( )}
)} )} {/* Enrollment Edit Modal */} {editingEnrollment && ( setEditingEnrollment(null)} onSaved={() => { setEditingEnrollment(null); loadData() }} /> )} {/* Course Edit Modal */} {editingCourse && ( setEditingCourse(null)} onSaved={() => { setEditingCourse(null); loadData() }} /> )}
) } // ============================================================================= // CERTIFICATE ROW COMPONENT // ============================================================================= function CertificateRow({ cert }: { cert: Certificate }) { const now = new Date() const validUntil = new Date(cert.validUntil) const daysLeft = Math.ceil((validUntil.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) const isExpired = daysLeft <= 0 const isExpiringSoon = daysLeft > 0 && daysLeft <= 30 return ( {cert.userName} {cert.courseName} {new Date(cert.issuedAt).toLocaleDateString('de-DE')} {validUntil.toLocaleDateString('de-DE')} {isExpired ? ( Abgelaufen ) : isExpiringSoon ? ( Laeuft bald ab ) : ( Gueltig )} {cert.pdfUrl ? ( PDF Download ) : ( Nicht verfuegbar )} ) } // ============================================================================= // ENROLLMENT EDIT MODAL // ============================================================================= function EnrollmentEditModal({ enrollment, onClose, onSaved }: { enrollment: Enrollment onClose: () => void onSaved: () => void }) { const [deadline, setDeadline] = useState(enrollment.deadline ? enrollment.deadline.split('T')[0] : '') const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const handleSave = async () => { setSaving(true) setError(null) try { await updateEnrollment(enrollment.id, { deadline: new Date(deadline).toISOString() }) onSaved() } catch (e) { setError(e instanceof Error ? e.message : 'Fehler beim Speichern') } finally { setSaving(false) } } return (

Einschreibung bearbeiten

Teilnehmer: {enrollment.userName}

setDeadline(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
{error &&

{error}

}
) } // ============================================================================= // COURSE EDIT MODAL // ============================================================================= function CourseEditModal({ course, onClose, onSaved }: { course: Course onClose: () => void onSaved: () => void }) { const [title, setTitle] = useState(course.title) const [description, setDescription] = useState(course.description) const [category, setCategory] = useState(course.category) const [durationMinutes, setDurationMinutes] = useState(course.durationMinutes) const [passingScore, setPassingScore] = useState(course.passingScore ?? 70) const [status, setStatus] = useState<'draft' | 'published'>(course.status ?? 'draft') const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const handleSave = async () => { setSaving(true) setError(null) try { await updateCourse(course.id, { title, description, category, durationMinutes, passingScore, status }) onSaved() } catch (e) { setError(e instanceof Error ? e.message : 'Fehler beim Speichern') } finally { setSaving(false) } } return (

Kurs bearbeiten

setTitle(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />