Some checks failed
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
ci/woodpecker/push/integration Pipeline failed
ci/woodpecker/push/main Pipeline failed
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Add complete Academy backend (Go) and frontend (Next.js) for DSGVO/IT-Security/AI-Literacy compliance training: - Go backend: Course CRUD, enrollments, quiz evaluation, PDF certificates (gofpdf), video generation pipeline (ElevenLabs + HeyGen) - In-memory data store with PostgreSQL migration for future DB support - Frontend: Course creation (AI + manual), lesson viewer, interactive quiz, certificate viewer with PDF download - Fix existing compile errors in generate.go (SearchResult type mismatch), llm/service.go (unused var), rag/service.go (Unicode chars) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
664 lines
20 KiB
TypeScript
664 lines
20 KiB
TypeScript
/**
|
|
* Academy API Client
|
|
*
|
|
* API client for the Compliance E-Learning Academy module
|
|
* Connects to the ai-compliance-sdk backend via Next.js proxy
|
|
*/
|
|
|
|
import {
|
|
Course,
|
|
CourseCategory,
|
|
CourseCreateRequest,
|
|
CourseUpdateRequest,
|
|
Enrollment,
|
|
EnrollmentStatus,
|
|
EnrollmentListResponse,
|
|
EnrollUserRequest,
|
|
UpdateProgressRequest,
|
|
Certificate,
|
|
AcademyStatistics,
|
|
SubmitQuizRequest,
|
|
SubmitQuizResponse,
|
|
GenerateCourseRequest,
|
|
GenerateCourseResponse,
|
|
VideoStatus,
|
|
isEnrollmentOverdue
|
|
} from './types'
|
|
|
|
// =============================================================================
|
|
// CONFIGURATION
|
|
// =============================================================================
|
|
|
|
const ACADEMY_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093'
|
|
const API_TIMEOUT = 30000 // 30 seconds
|
|
|
|
// =============================================================================
|
|
// HELPER FUNCTIONS
|
|
// =============================================================================
|
|
|
|
function getTenantId(): string {
|
|
if (typeof window !== 'undefined') {
|
|
return localStorage.getItem('bp_tenant_id') || 'default-tenant'
|
|
}
|
|
return 'default-tenant'
|
|
}
|
|
|
|
function getAuthHeaders(): HeadersInit {
|
|
const headers: HeadersInit = {
|
|
'Content-Type': 'application/json',
|
|
'X-Tenant-ID': getTenantId()
|
|
}
|
|
|
|
if (typeof window !== 'undefined') {
|
|
const token = localStorage.getItem('authToken')
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`
|
|
}
|
|
const userId = localStorage.getItem('bp_user_id')
|
|
if (userId) {
|
|
headers['X-User-ID'] = userId
|
|
}
|
|
}
|
|
|
|
return headers
|
|
}
|
|
|
|
async function fetchWithTimeout<T>(
|
|
url: string,
|
|
options: RequestInit = {},
|
|
timeout: number = API_TIMEOUT
|
|
): Promise<T> {
|
|
const controller = new AbortController()
|
|
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
signal: controller.signal,
|
|
headers: {
|
|
...getAuthHeaders(),
|
|
...options.headers
|
|
}
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorBody = await response.text()
|
|
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
|
|
try {
|
|
const errorJson = JSON.parse(errorBody)
|
|
errorMessage = errorJson.error || errorJson.message || errorMessage
|
|
} catch {
|
|
// Keep the HTTP status message
|
|
}
|
|
throw new Error(errorMessage)
|
|
}
|
|
|
|
// Handle empty responses
|
|
const contentType = response.headers.get('content-type')
|
|
if (contentType && contentType.includes('application/json')) {
|
|
return response.json()
|
|
}
|
|
|
|
return {} as T
|
|
} finally {
|
|
clearTimeout(timeoutId)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// COURSE CRUD
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Alle Kurse abrufen
|
|
*/
|
|
export async function fetchCourses(): Promise<Course[]> {
|
|
return fetchWithTimeout<Course[]>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/courses`
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Einzelnen Kurs abrufen
|
|
*/
|
|
export async function fetchCourse(id: string): Promise<Course> {
|
|
return fetchWithTimeout<Course>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/courses/${id}`
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Neuen Kurs erstellen
|
|
*/
|
|
export async function createCourse(request: CourseCreateRequest): Promise<Course> {
|
|
return fetchWithTimeout<Course>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/courses`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(request)
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Kurs aktualisieren
|
|
*/
|
|
export async function updateCourse(id: string, update: CourseUpdateRequest): Promise<Course> {
|
|
return fetchWithTimeout<Course>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/courses/${id}`,
|
|
{
|
|
method: 'PUT',
|
|
body: JSON.stringify(update)
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Kurs loeschen
|
|
*/
|
|
export async function deleteCourse(id: string): Promise<void> {
|
|
await fetchWithTimeout<void>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/courses/${id}`,
|
|
{
|
|
method: 'DELETE'
|
|
}
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// ENROLLMENTS
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Einschreibungen abrufen (optional gefiltert nach Kurs-ID)
|
|
*/
|
|
export async function fetchEnrollments(courseId?: string): Promise<Enrollment[]> {
|
|
const params = new URLSearchParams()
|
|
if (courseId) {
|
|
params.set('courseId', courseId)
|
|
}
|
|
const queryString = params.toString()
|
|
const url = `${ACADEMY_API_BASE}/api/v1/academy/enrollments${queryString ? `?${queryString}` : ''}`
|
|
|
|
return fetchWithTimeout<Enrollment[]>(url)
|
|
}
|
|
|
|
/**
|
|
* Benutzer in einen Kurs einschreiben
|
|
*/
|
|
export async function enrollUser(request: EnrollUserRequest): Promise<Enrollment> {
|
|
return fetchWithTimeout<Enrollment>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/enrollments`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(request)
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Fortschritt einer Einschreibung aktualisieren
|
|
*/
|
|
export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise<Enrollment> {
|
|
return fetchWithTimeout<Enrollment>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/enrollments/${enrollmentId}/progress`,
|
|
{
|
|
method: 'PUT',
|
|
body: JSON.stringify(update)
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Einschreibung als abgeschlossen markieren
|
|
*/
|
|
export async function completeEnrollment(enrollmentId: string): Promise<Enrollment> {
|
|
return fetchWithTimeout<Enrollment>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/enrollments/${enrollmentId}/complete`,
|
|
{
|
|
method: 'POST'
|
|
}
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// CERTIFICATES
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Zertifikat abrufen
|
|
*/
|
|
export async function fetchCertificate(id: string): Promise<Certificate> {
|
|
return fetchWithTimeout<Certificate>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/certificates/${id}`
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Zertifikat generieren nach erfolgreichem Kursabschluss
|
|
*/
|
|
export async function generateCertificate(enrollmentId: string): Promise<Certificate> {
|
|
return fetchWithTimeout<Certificate>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/enrollments/${enrollmentId}/certificate`,
|
|
{
|
|
method: 'POST'
|
|
}
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// QUIZ
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Quiz-Antworten einreichen und auswerten
|
|
*/
|
|
export async function submitQuiz(lessonId: string, answers: SubmitQuizRequest): Promise<SubmitQuizResponse> {
|
|
return fetchWithTimeout<SubmitQuizResponse>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/lessons/${lessonId}/quiz`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(answers)
|
|
}
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// STATISTICS
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Academy-Statistiken abrufen
|
|
*/
|
|
export async function fetchAcademyStatistics(): Promise<AcademyStatistics> {
|
|
return fetchWithTimeout<AcademyStatistics>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/statistics`
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// AI GENERATION
|
|
// =============================================================================
|
|
|
|
/**
|
|
* KI-generiert einen kompletten Kurs
|
|
*/
|
|
export async function generateCourse(request: GenerateCourseRequest): Promise<GenerateCourseResponse> {
|
|
return fetchWithTimeout<GenerateCourseResponse>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/courses/generate`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(request)
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Einzelne Lektion neu generieren
|
|
*/
|
|
export async function regenerateLesson(lessonId: string): Promise<{ lessonId: string; status: string }> {
|
|
return fetchWithTimeout<{ lessonId: string; status: string }>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/lessons/${lessonId}/regenerate`,
|
|
{
|
|
method: 'POST'
|
|
}
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// VIDEO GENERATION
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Videos fuer alle Lektionen eines Kurses generieren
|
|
*/
|
|
export async function generateVideos(courseId: string): Promise<VideoStatus> {
|
|
return fetchWithTimeout<VideoStatus>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/courses/${courseId}/generate-videos`,
|
|
{
|
|
method: 'POST'
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Video-Generierungs-Status abrufen
|
|
*/
|
|
export async function getVideoStatus(courseId: string): Promise<VideoStatus> {
|
|
return fetchWithTimeout<VideoStatus>(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/courses/${courseId}/video-status`
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// CERTIFICATES (Extended)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Zertifikat als PDF herunterladen
|
|
*/
|
|
export async function downloadCertificatePDF(certificateId: string): Promise<Blob> {
|
|
const controller = new AbortController()
|
|
const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT)
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`${ACADEMY_API_BASE}/api/v1/academy/certificates/${certificateId}/pdf`,
|
|
{
|
|
signal: controller.signal,
|
|
headers: getAuthHeaders()
|
|
}
|
|
)
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}`)
|
|
}
|
|
|
|
return response.blob()
|
|
} finally {
|
|
clearTimeout(timeoutId)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// SDK PROXY FUNCTION (wraps fetchCourses + fetchAcademyStatistics)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Kurse und Statistiken laden - mit Fallback auf Mock-Daten
|
|
*/
|
|
export async function fetchSDKAcademyList(): Promise<{
|
|
courses: Course[]
|
|
enrollments: Enrollment[]
|
|
statistics: AcademyStatistics
|
|
}> {
|
|
try {
|
|
const [courses, enrollments, statistics] = await Promise.all([
|
|
fetchCourses(),
|
|
fetchEnrollments(),
|
|
fetchAcademyStatistics()
|
|
])
|
|
|
|
return { courses, enrollments, statistics }
|
|
} catch (error) {
|
|
console.error('Failed to load Academy data from backend, using mock data:', error)
|
|
|
|
// Fallback to mock data
|
|
const courses = createMockCourses()
|
|
const enrollments = createMockEnrollments()
|
|
const statistics = createMockStatistics(courses, enrollments)
|
|
|
|
return { courses, enrollments, statistics }
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// MOCK DATA (Fallback / Demo)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Demo-Kurse mit deutschen Titeln erstellen
|
|
*/
|
|
export function createMockCourses(): Course[] {
|
|
const now = new Date()
|
|
|
|
return [
|
|
{
|
|
id: 'course-001',
|
|
title: 'DSGVO-Grundlagen fuer Mitarbeiter',
|
|
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,
|
|
requiredForRoles: ['all'],
|
|
createdAt: new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(),
|
|
updatedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
lessons: [
|
|
{
|
|
id: 'lesson-001-01',
|
|
courseId: 'course-001',
|
|
order: 1,
|
|
title: 'Was ist die DSGVO?',
|
|
type: 'text',
|
|
contentMarkdown: '# Was ist die DSGVO?\n\nDie Datenschutz-Grundverordnung (DSGVO) ist eine Verordnung der Europaeischen Union...',
|
|
durationMinutes: 15
|
|
},
|
|
{
|
|
id: 'lesson-001-02',
|
|
courseId: 'course-001',
|
|
order: 2,
|
|
title: 'Die 7 Grundsaetze der DSGVO',
|
|
type: 'video',
|
|
contentMarkdown: 'Videoerklaerung der Grundsaetze: Rechtmaessigkeit, Zweckbindung, Datenminimierung, Richtigkeit, Speicherbegrenzung, Integritaet und Vertraulichkeit, Rechenschaftspflicht.',
|
|
durationMinutes: 20,
|
|
videoUrl: '/videos/dsgvo-grundsaetze.mp4'
|
|
},
|
|
{
|
|
id: 'lesson-001-03',
|
|
courseId: 'course-001',
|
|
order: 3,
|
|
title: 'Betroffenenrechte (Art. 15-21)',
|
|
type: 'text',
|
|
contentMarkdown: '# Betroffenenrechte\n\nUebersicht der Betroffenenrechte: Auskunft, Berichtigung, Loeschung, Einschraenkung, Datenuebertragbarkeit, Widerspruch.',
|
|
durationMinutes: 20
|
|
},
|
|
{
|
|
id: 'lesson-001-04',
|
|
courseId: 'course-001',
|
|
order: 4,
|
|
title: 'Personenbezogene Daten im Arbeitsalltag',
|
|
type: 'video',
|
|
contentMarkdown: 'Praxisbeispiele fuer den korrekten Umgang mit personenbezogenen Daten am Arbeitsplatz.',
|
|
durationMinutes: 15,
|
|
videoUrl: '/videos/dsgvo-praxis.mp4'
|
|
},
|
|
{
|
|
id: 'lesson-001-05',
|
|
courseId: 'course-001',
|
|
order: 5,
|
|
title: 'Wissenstest: DSGVO-Grundlagen',
|
|
type: 'quiz',
|
|
contentMarkdown: 'Testen Sie Ihr Wissen zu den DSGVO-Grundlagen.',
|
|
durationMinutes: 20
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'course-002',
|
|
title: 'IT-Sicherheit & Cybersecurity Awareness',
|
|
description: 'Sensibilisierung fuer IT-Sicherheitsrisiken und Best Practices im Umgang mit Phishing, Passwoertern, Social Engineering und sicherer Kommunikation.',
|
|
category: 'it_security',
|
|
durationMinutes: 60,
|
|
requiredForRoles: ['all'],
|
|
createdAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(),
|
|
updatedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
|
lessons: [
|
|
{
|
|
id: 'lesson-002-01',
|
|
courseId: 'course-002',
|
|
order: 1,
|
|
title: 'Phishing erkennen und vermeiden',
|
|
type: 'video',
|
|
contentMarkdown: 'Wie erkennt man Phishing-E-Mails und was tut man im Verdachtsfall?',
|
|
durationMinutes: 15,
|
|
videoUrl: '/videos/phishing-awareness.mp4'
|
|
},
|
|
{
|
|
id: 'lesson-002-02',
|
|
courseId: 'course-002',
|
|
order: 2,
|
|
title: 'Sichere Passwoerter und MFA',
|
|
type: 'text',
|
|
contentMarkdown: '# Sichere Passwoerter\n\nRichtlinien fuer starke Passwoerter, Passwort-Manager und Multi-Faktor-Authentifizierung.',
|
|
durationMinutes: 15
|
|
},
|
|
{
|
|
id: 'lesson-002-03',
|
|
courseId: 'course-002',
|
|
order: 3,
|
|
title: 'Social Engineering und Manipulation',
|
|
type: 'text',
|
|
contentMarkdown: '# Social Engineering\n\nMethoden von Angreifern zur Manipulation von Mitarbeitern und Schutzmassnahmen.',
|
|
durationMinutes: 15
|
|
},
|
|
{
|
|
id: 'lesson-002-04',
|
|
courseId: 'course-002',
|
|
order: 4,
|
|
title: 'Wissenstest: IT-Sicherheit',
|
|
type: 'quiz',
|
|
contentMarkdown: 'Testen Sie Ihr Wissen zur IT-Sicherheit.',
|
|
durationMinutes: 15
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'course-003',
|
|
title: 'AI Literacy - Sicherer Umgang mit KI',
|
|
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,
|
|
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(),
|
|
lessons: [
|
|
{
|
|
id: 'lesson-003-01',
|
|
courseId: 'course-003',
|
|
order: 1,
|
|
title: 'Was ist Kuenstliche Intelligenz?',
|
|
type: 'text',
|
|
contentMarkdown: '# Was ist KI?\n\nGrundlagen von Machine Learning, Deep Learning und Large Language Models in verstaendlicher Sprache.',
|
|
durationMinutes: 15
|
|
},
|
|
{
|
|
id: 'lesson-003-02',
|
|
courseId: 'course-003',
|
|
order: 2,
|
|
title: 'Der EU AI Act - Was bedeutet er fuer uns?',
|
|
type: 'video',
|
|
contentMarkdown: 'Ueberblick ueber den EU AI Act, Risikoklassen und Anforderungen fuer Unternehmen.',
|
|
durationMinutes: 20,
|
|
videoUrl: '/videos/eu-ai-act.mp4'
|
|
},
|
|
{
|
|
id: 'lesson-003-03',
|
|
courseId: 'course-003',
|
|
order: 3,
|
|
title: 'KI-Werkzeuge sicher nutzen',
|
|
type: 'text',
|
|
contentMarkdown: '# KI-Werkzeuge sicher nutzen\n\nRichtlinien fuer den Einsatz von ChatGPT, Copilot & Co.',
|
|
durationMinutes: 20
|
|
},
|
|
{
|
|
id: 'lesson-003-04',
|
|
courseId: 'course-003',
|
|
order: 4,
|
|
title: 'Wissenstest: AI Literacy',
|
|
type: 'quiz',
|
|
contentMarkdown: 'Testen Sie Ihr Wissen zum sicheren Umgang mit KI.',
|
|
durationMinutes: 20
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
/**
|
|
* Demo-Einschreibungen erstellen
|
|
*/
|
|
export function createMockEnrollments(): Enrollment[] {
|
|
const now = new Date()
|
|
|
|
return [
|
|
{
|
|
id: 'enr-001',
|
|
courseId: 'course-001',
|
|
userId: 'user-001',
|
|
userName: 'Maria Fischer',
|
|
userEmail: 'maria.fischer@example.de',
|
|
status: 'in_progress',
|
|
progress: 40,
|
|
startedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
|
deadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString()
|
|
},
|
|
{
|
|
id: 'enr-002',
|
|
courseId: 'course-002',
|
|
userId: 'user-002',
|
|
userName: 'Stefan Mueller',
|
|
userEmail: 'stefan.mueller@example.de',
|
|
status: 'completed',
|
|
progress: 100,
|
|
startedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
|
completedAt: new Date(now.getTime() - 12 * 24 * 60 * 60 * 1000).toISOString(),
|
|
certificateId: 'cert-001',
|
|
deadline: new Date(now.getTime() + 10 * 24 * 60 * 60 * 1000).toISOString()
|
|
},
|
|
{
|
|
id: 'enr-003',
|
|
courseId: 'course-001',
|
|
userId: 'user-003',
|
|
userName: 'Laura Schneider',
|
|
userEmail: 'laura.schneider@example.de',
|
|
status: 'not_started',
|
|
progress: 0,
|
|
startedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
deadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString()
|
|
},
|
|
{
|
|
id: 'enr-004',
|
|
courseId: 'course-003',
|
|
userId: 'user-004',
|
|
userName: 'Thomas Wagner',
|
|
userEmail: 'thomas.wagner@example.de',
|
|
status: 'expired',
|
|
progress: 25,
|
|
startedAt: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000).toISOString(),
|
|
deadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString()
|
|
},
|
|
{
|
|
id: 'enr-005',
|
|
courseId: 'course-002',
|
|
userId: 'user-005',
|
|
userName: 'Julia Becker',
|
|
userEmail: 'julia.becker@example.de',
|
|
status: 'in_progress',
|
|
progress: 50,
|
|
startedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
deadline: new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000).toISOString()
|
|
}
|
|
]
|
|
}
|
|
|
|
/**
|
|
* Demo-Statistiken aus Kursen und Einschreibungen berechnen
|
|
*/
|
|
export function createMockStatistics(courses?: Course[], enrollments?: Enrollment[]): AcademyStatistics {
|
|
const c = courses || createMockCourses()
|
|
const e = enrollments || createMockEnrollments()
|
|
|
|
const completedCount = e.filter(en => en.status === 'completed').length
|
|
const completionRate = e.length > 0 ? Math.round((completedCount / e.length) * 100) : 0
|
|
const overdueCount = e.filter(en => isEnrollmentOverdue(en)).length
|
|
|
|
return {
|
|
totalCourses: c.length,
|
|
totalEnrollments: e.length,
|
|
completionRate,
|
|
overdueCount,
|
|
byCategory: {
|
|
dsgvo_basics: c.filter(co => co.category === 'dsgvo_basics').length,
|
|
it_security: c.filter(co => co.category === 'it_security').length,
|
|
ai_literacy: c.filter(co => co.category === 'ai_literacy').length,
|
|
whistleblower_protection: c.filter(co => co.category === 'whistleblower_protection').length,
|
|
custom: c.filter(co => co.category === 'custom').length,
|
|
},
|
|
byStatus: {
|
|
not_started: e.filter(en => en.status === 'not_started').length,
|
|
in_progress: e.filter(en => en.status === 'in_progress').length,
|
|
completed: e.filter(en => en.status === 'completed').length,
|
|
expired: e.filter(en => en.status === 'expired').length,
|
|
}
|
|
}
|
|
}
|