diff --git a/admin-compliance/app/(sdk)/sdk/academy/[id]/page.tsx b/admin-compliance/app/(sdk)/sdk/academy/[id]/page.tsx new file mode 100644 index 0000000..548ad65 --- /dev/null +++ b/admin-compliance/app/(sdk)/sdk/academy/[id]/page.tsx @@ -0,0 +1,517 @@ +'use client' + +import React, { useState, useEffect, useMemo } from 'react' +import { useParams, useRouter } from 'next/navigation' +import Link from 'next/link' +import { + Course, + Lesson, + Enrollment, + QuizQuestion, + COURSE_CATEGORY_INFO, + ENROLLMENT_STATUS_INFO, + isEnrollmentOverdue, + getDaysUntilDeadline +} from '@/lib/sdk/academy/types' +import { + fetchCourse, + fetchEnrollments, + deleteCourse, + submitQuiz, + generateVideos, + getVideoStatus +} from '@/lib/sdk/academy/api' + +type TabId = 'overview' | 'lessons' | 'enrollments' | 'videos' + +export default function CourseDetailPage() { + const params = useParams() + const router = useRouter() + const courseId = params.id as string + + const [course, setCourse] = useState(null) + const [enrollments, setEnrollments] = useState([]) + const [activeTab, setActiveTab] = useState('overview') + const [isLoading, setIsLoading] = useState(true) + const [selectedLesson, setSelectedLesson] = useState(null) + const [quizAnswers, setQuizAnswers] = useState>({}) + const [quizResult, setQuizResult] = useState(null) + const [isSubmittingQuiz, setIsSubmittingQuiz] = useState(false) + const [videoStatus, setVideoStatus] = useState(null) + const [isGeneratingVideos, setIsGeneratingVideos] = useState(false) + + useEffect(() => { + const loadData = async () => { + setIsLoading(true) + try { + const [courseData, enrollmentData] = await Promise.all([ + fetchCourse(courseId).catch(() => null), + fetchEnrollments(courseId).catch(() => []) + ]) + setCourse(courseData) + setEnrollments(Array.isArray(enrollmentData) ? enrollmentData : []) + if (courseData && courseData.lessons && courseData.lessons.length > 0) { + setSelectedLesson(courseData.lessons[0]) + } + } catch (error) { + console.error('Failed to load course:', error) + } finally { + setIsLoading(false) + } + } + loadData() + }, [courseId]) + + const handleDeleteCourse = async () => { + if (!confirm('Sind Sie sicher, dass Sie diesen Kurs loeschen moechten? Diese Aktion kann nicht rueckgaengig gemacht werden.')) return + try { + await deleteCourse(courseId) + router.push('/sdk/academy') + } catch (error) { + console.error('Failed to delete course:', error) + } + } + + const handleSubmitQuiz = async () => { + if (!selectedLesson) return + const questions = selectedLesson.quizQuestions || [] + const answers = questions.map((q: QuizQuestion) => quizAnswers[q.id] ?? -1) + + setIsSubmittingQuiz(true) + try { + const result = await submitQuiz(selectedLesson.id, { answers }) + setQuizResult(result) + } catch (error: any) { + console.error('Quiz submission failed:', error) + setQuizResult({ error: error.message || 'Fehler bei der Auswertung' }) + } finally { + setIsSubmittingQuiz(false) + } + } + + const handleGenerateVideos = async () => { + setIsGeneratingVideos(true) + try { + const status = await generateVideos(courseId) + setVideoStatus(status) + } catch (error) { + console.error('Video generation failed:', error) + } finally { + setIsGeneratingVideos(false) + } + } + + const handleCheckVideoStatus = async () => { + try { + const status = await getVideoStatus(courseId) + setVideoStatus(status) + } catch (error) { + console.error('Failed to check video status:', error) + } + } + + if (isLoading) { + return ( +
+ + + + +
+ ) + } + + if (!course) { + return ( +
+

Kurs nicht gefunden

+ + Zurueck zur Uebersicht + +
+ ) + } + + const categoryInfo = COURSE_CATEGORY_INFO[course.category] || COURSE_CATEGORY_INFO['custom'] + const sortedLessons = [...(course.lessons || [])].sort((a, b) => a.order - b.order) + const completedEnrollments = enrollments.filter(e => e.status === 'completed').length + const overdueEnrollments = enrollments.filter(e => isEnrollmentOverdue(e)).length + + return ( +
+ {/* Header */} +
+
+ + + + + +
+
+ + {categoryInfo.label} + + + {course.status === 'published' ? 'Veroeffentlicht' : 'Entwurf'} + +
+

{course.title}

+

{course.description}

+
+
+
+ +
+
+ + {/* Stats Row */} +
+
+
Lektionen
+
{sortedLessons.length}
+
+
+
Dauer
+
{course.durationMinutes} Min.
+
+
+
Teilnehmer
+
{enrollments.length}
+
+
+
Abgeschlossen
+
{completedEnrollments}
+
+
+ + {/* Tabs */} +
+ +
+ + {/* Overview Tab */} + {activeTab === 'overview' && ( +
+
+

Kurs-Details

+
+
Bestehensgrenze
{course.passingScore}%
+
Pflicht fuer
{course.requiredForRoles?.join(', ') || 'Alle'}
+
Erstellt am
{new Date(course.createdAt).toLocaleDateString('de-DE')}
+
Aktualisiert am
{new Date(course.updatedAt).toLocaleDateString('de-DE')}
+
+
+ + {/* Lesson List Preview */} +
+

Lektionen ({sortedLessons.length})

+
+ {sortedLessons.map((lesson, i) => ( +
+
+ {i + 1} +
+
+
{lesson.title}
+
{lesson.durationMinutes} Min. | {lesson.type === 'video' ? 'Video' : lesson.type === 'quiz' ? 'Quiz' : 'Text'}
+
+ + {lesson.type === 'quiz' ? 'Quiz' : lesson.type === 'video' ? 'Video' : 'Text'} + +
+ ))} +
+
+
+ )} + + {/* Lessons Tab - with content viewer and quiz player */} + {activeTab === 'lessons' && ( +
+ {/* Lesson Navigation */} +
+

Lektionen

+
+ {sortedLessons.map((lesson, i) => ( + + ))} +
+
+ + {/* Lesson Content */} +
+ {selectedLesson ? ( +
+
+

{selectedLesson.title}

+ + {selectedLesson.type === 'quiz' ? 'Quiz' : selectedLesson.type === 'video' ? 'Video' : 'Text'} + +
+ + {/* Video Player */} + {selectedLesson.type === 'video' && selectedLesson.videoUrl && ( +
+
+ )} + + {/* Text Content */} + {(selectedLesson.type === 'text' || selectedLesson.type === 'video') && selectedLesson.contentMarkdown && ( +
+
+ {selectedLesson.contentMarkdown.split('\n').map((line, i) => { + if (line.startsWith('# ')) return

{line.slice(2)}

+ if (line.startsWith('## ')) return

{line.slice(3)}

+ if (line.startsWith('### ')) return

{line.slice(4)}

+ if (line.startsWith('- **')) { + const parts = line.slice(2).split('**') + return
  • {parts[1]}{parts[2] || ''}
  • + } + if (line.startsWith('- ')) return
  • {line.slice(2)}
  • + if (line.trim() === '') return
    + return

    {line}

    + })} +
    +
    + )} + + {/* Quiz Player */} + {selectedLesson.type === 'quiz' && selectedLesson.quizQuestions && ( +
    + {selectedLesson.quizQuestions.map((q: QuizQuestion, qi: number) => ( +
    +

    Frage {qi + 1}: {q.question}

    +
    + {q.options.map((option: string, oi: number) => { + const isSelected = quizAnswers[q.id] === oi + const showResult = quizResult && !quizResult.error + const isCorrect = showResult && quizResult.results?.[qi]?.correct + const wasSelected = showResult && isSelected + + let bgClass = 'bg-white border-gray-200 hover:border-purple-300' + if (isSelected && !showResult) bgClass = 'bg-purple-50 border-purple-500' + if (showResult && oi === q.correctOptionIndex) bgClass = 'bg-green-50 border-green-500' + if (showResult && wasSelected && !isCorrect) bgClass = 'bg-red-50 border-red-500' + + return ( + + ) + })} +
    + {quizResult && !quizResult.error && quizResult.results?.[qi] && ( +
    + {quizResult.results[qi].correct ? 'Richtig!' : 'Falsch.'} {q.explanation} +
    + )} +
    + ))} + + {/* Quiz Submit / Result */} + {!quizResult ? ( + + ) : quizResult.error ? ( +
    {quizResult.error}
    + ) : ( +
    +
    + {quizResult.score}% +
    +
    + {quizResult.passed ? 'Bestanden!' : 'Nicht bestanden'} — {quizResult.correctAnswers}/{quizResult.totalQuestions} richtig +
    + +
    + )} +
    + )} +
    + ) : ( +

    Waehlen Sie eine Lektion aus.

    + )} +
    +
    + )} + + {/* Enrollments Tab */} + {activeTab === 'enrollments' && ( +
    + {overdueEnrollments > 0 && ( +
    + {overdueEnrollments} ueberfaellige Einschreibung(en) +
    + )} + {enrollments.length === 0 ? ( +
    +

    Noch keine Einschreibungen fuer diesen Kurs.

    +
    + ) : ( + enrollments.map(enrollment => { + const statusInfo = ENROLLMENT_STATUS_INFO[enrollment.status] + const overdue = isEnrollmentOverdue(enrollment) + const daysUntil = getDaysUntilDeadline(enrollment.deadline) + return ( +
    +
    +
    +
    + + {statusInfo?.label} + + {overdue && Ueberfaellig} +
    +
    {enrollment.userName}
    +
    {enrollment.userEmail}
    +
    +
    +
    {enrollment.progress}%
    +
    + {enrollment.status === 'completed' ? 'Abgeschlossen' : `${daysUntil > 0 ? daysUntil + ' Tage verbleibend' : Math.abs(daysUntil) + ' Tage ueberfaellig'}`} +
    +
    +
    +
    +
    +
    +
    + ) + }) + )} +
    + )} + + {/* Videos Tab */} + {activeTab === 'videos' && ( +
    +
    +
    +

    Video-Generierung

    +
    + + +
    +
    + +
    + Videos werden mit ElevenLabs (Stimme) und HeyGen (Avatar) generiert. + Konfigurieren Sie die API-Keys in den Umgebungsvariablen. +
    + + {videoStatus && ( +
    +
    + Gesamtstatus: + + {videoStatus.status} + +
    + {videoStatus.lessons?.map((ls: any) => ( +
    + Lektion {ls.lessonId.slice(-4)} + + {ls.status} + +
    + ))} +
    + )} + + {!videoStatus && ( +

    + Klicken Sie auf "Videos generieren" um den Prozess zu starten. +

    + )} +
    +
    + )} +
    + ) +} diff --git a/admin-compliance/app/(sdk)/sdk/academy/new/page.tsx b/admin-compliance/app/(sdk)/sdk/academy/new/page.tsx new file mode 100644 index 0000000..c76fee7 --- /dev/null +++ b/admin-compliance/app/(sdk)/sdk/academy/new/page.tsx @@ -0,0 +1,385 @@ +'use client' + +import React, { useState } from 'react' +import { useRouter } from 'next/navigation' +import Link from 'next/link' +import { useSDK } from '@/lib/sdk' +import { + CourseCategory, + COURSE_CATEGORY_INFO, + CreateCourseRequest, + GenerateCourseRequest +} from '@/lib/sdk/academy/types' +import { createCourse, generateCourse } from '@/lib/sdk/academy/api' + +type CreationMode = 'manual' | 'ai' + +export default function NewCoursePage() { + const router = useRouter() + const [mode, setMode] = useState('ai') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + // Manual form state + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [category, setCategory] = useState('dsgvo_basics') + const [duration, setDuration] = useState(60) + const [passingScore, setPassingScore] = useState(70) + + // AI generation state + const [topic, setTopic] = useState('') + const [targetGroup, setTargetGroup] = useState('Alle Mitarbeiter') + const [useRag, setUseRag] = useState(true) + + const handleManualCreate = async () => { + if (!title.trim()) { + setError('Bitte geben Sie einen Kurstitel ein.') + return + } + setIsLoading(true) + setError(null) + + try { + const tenantId = typeof window !== 'undefined' + ? localStorage.getItem('bp_tenant_id') || 'default-tenant' + : 'default-tenant' + + const result = await createCourse({ + tenantId, + title: title.trim(), + description: description.trim(), + category, + durationMinutes: duration, + passingScore, + requiredForRoles: ['all'] + } as any) + + // Navigate to the new course + if (result && (result as any).id) { + router.push(`/sdk/academy/${(result as any).id}`) + } else { + router.push('/sdk/academy') + } + } catch (err: any) { + setError(err.message || 'Fehler beim Erstellen des Kurses') + } finally { + setIsLoading(false) + } + } + + const handleAIGenerate = async () => { + if (!topic.trim()) { + setError('Bitte geben Sie ein Thema fuer den Kurs ein.') + return + } + setIsLoading(true) + setError(null) + + try { + const tenantId = typeof window !== 'undefined' + ? localStorage.getItem('bp_tenant_id') || 'default-tenant' + : 'default-tenant' + + const result = await generateCourse({ + tenantId, + topic: topic.trim(), + category, + targetGroup: targetGroup.trim(), + language: 'de', + useRag + }) + + if (result && result.course && result.course.id) { + router.push(`/sdk/academy/${result.course.id}`) + } else { + router.push('/sdk/academy') + } + } catch (err: any) { + setError(err.message || 'Fehler bei der KI-Generierung') + } finally { + setIsLoading(false) + } + } + + return ( +
    + {/* Header */} +
    + + + + + +
    +

    Neuen Kurs erstellen

    +

    + Erstellen Sie einen Compliance-Schulungskurs manuell oder lassen Sie ihn von der KI generieren. +

    +
    +
    + + {/* Mode Toggle */} +
    + + +
    + + {/* Error Message */} + {error && ( +
    + + + +

    {error}

    +
    + )} + + {/* AI Generation Form */} + {mode === 'ai' && ( +
    +
    + + + +
    +

    KI-generierter Kurs

    +

    + Die KI erstellt automatisch Lektionen, Inhalte und Quizfragen basierend auf dem gewaehlten Thema. + Optionaler RAG-Kontext aus relevanten Gesetzestexten wird einbezogen. +

    +
    +
    + + {/* Topic */} +
    + + setTopic(e.target.value)} + placeholder="z.B. DSGVO-Grundlagen fuer neue Mitarbeiter" + className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-base" + /> +
    + + {/* Category */} +
    + +
    + {Object.entries(COURSE_CATEGORY_INFO).map(([cat, info]) => ( + + ))} +
    +
    + + {/* Target Group */} +
    + + setTargetGroup(e.target.value)} + placeholder="z.B. Alle Mitarbeiter, IT-Abteilung, Fuehrungskraefte" + className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + /> +
    + + {/* RAG Toggle */} +
    + +
    + RAG-Kontext verwenden +

    Relevante Gesetzestexte (DSGVO, AI Act, NIS2) einbeziehen

    +
    +
    + + {/* Submit */} +
    + + Abbrechen + + +
    +
    + )} + + {/* Manual Creation Form */} + {mode === 'manual' && ( +
    + {/* Title */} +
    + + setTitle(e.target.value)} + placeholder="z.B. DSGVO-Grundlagen fuer Mitarbeiter" + className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-base" + /> +
    + + {/* Description */} +
    + +