/** * Academy API Client — Course, Enrollment, Certificate, Quiz, Statistics, Generation * * API client for the Compliance E-Learning Academy module * Connects to the ai-compliance-sdk backend via Next.js proxy */ import type { Course, CourseCategory, CourseCreateRequest, CourseUpdateRequest, GenerateCourseRequest, Enrollment, EnrollmentStatus, EnrollmentListResponse, EnrollUserRequest, UpdateProgressRequest, Certificate, AcademyStatistics, LessonType, SubmitQuizRequest, SubmitQuizResponse, } from './types' // ============================================================================= // CONFIGURATION // ============================================================================= const ACADEMY_API_BASE = '/api/sdk/v1/academy' const API_TIMEOUT = 30000 // ============================================================================= // 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( url: string, options: RequestInit = {}, timeout: number = API_TIMEOUT ): Promise { 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) } const contentType = response.headers.get('content-type') if (contentType && contentType.includes('application/json')) { return response.json() } return {} as T } finally { clearTimeout(timeoutId) } } // ============================================================================= // BACKEND TYPES // ============================================================================= interface BackendCourse { id: string title: string description: string category: CourseCategory duration_minutes: number required_for_roles: string[] is_active: boolean passing_score?: number status?: string lessons?: BackendLesson[] created_at: string updated_at: string } interface BackendQuizQuestion { id: string question: string options: string[] correct_index: number explanation: string } interface BackendLesson { id: string course_id: string title: string description?: string lesson_type: LessonType content_url?: string duration_minutes: number order_index: number quiz_questions?: BackendQuizQuestion[] } function mapCourseFromBackend(bc: BackendCourse): Course { return { id: bc.id, title: bc.title, 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, courseId: l.course_id, title: l.title, type: l.lesson_type, contentMarkdown: l.content_url || '', durationMinutes: l.duration_minutes || 0, order: l.order_index, quizQuestions: (l.quiz_questions || []).map(q => ({ id: q.id || `q-${Math.random().toString(36).slice(2)}`, lessonId: l.id, question: q.question, options: q.options, correctOptionIndex: q.correct_index, explanation: q.explanation, })), })), createdAt: bc.created_at, updatedAt: bc.updated_at, } } function mapCoursesFromBackend(courses: BackendCourse[]): Course[] { return courses.map(mapCourseFromBackend) } // ============================================================================= // COURSE CRUD // ============================================================================= export async function fetchCourses(): Promise { const res = await fetchWithTimeout<{ courses: BackendCourse[]; total: number }>( `${ACADEMY_API_BASE}/courses` ) return mapCoursesFromBackend(res.courses || []) } export async function fetchCourse(id: string): Promise { const res = await fetchWithTimeout<{ course: BackendCourse }>( `${ACADEMY_API_BASE}/courses/${id}` ) return mapCourseFromBackend(res.course) } export async function createCourse(request: CourseCreateRequest): Promise { return fetchWithTimeout( `${ACADEMY_API_BASE}/courses`, { method: 'POST', body: JSON.stringify(request) } ) } export async function updateCourse(id: string, update: CourseUpdateRequest): Promise { return fetchWithTimeout( `${ACADEMY_API_BASE}/courses/${id}`, { method: 'PUT', body: JSON.stringify(update) } ) } export async function deleteCourse(id: string): Promise { await fetchWithTimeout( `${ACADEMY_API_BASE}/courses/${id}`, { method: 'DELETE' } ) } // ============================================================================= // ENROLLMENTS // ============================================================================= export async function fetchEnrollments(courseId?: string): Promise { const params = new URLSearchParams() if (courseId) { params.set('course_id', courseId) } const queryString = params.toString() const url = `${ACADEMY_API_BASE}/enrollments${queryString ? `?${queryString}` : ''}` const res = await fetchWithTimeout<{ enrollments: Enrollment[]; total: number }>(url) return res.enrollments || [] } export async function enrollUser(request: EnrollUserRequest): Promise { return fetchWithTimeout( `${ACADEMY_API_BASE}/enrollments`, { method: 'POST', body: JSON.stringify(request) } ) } export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise { return fetchWithTimeout( `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/progress`, { method: 'PUT', body: JSON.stringify(update) } ) } export async function completeEnrollment(enrollmentId: string): Promise { return fetchWithTimeout( `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/complete`, { method: 'POST' } ) } export async function deleteEnrollment(id: string): Promise { await fetchWithTimeout( `${ACADEMY_API_BASE}/enrollments/${id}`, { method: 'DELETE' } ) } export async function updateEnrollment(id: string, data: { deadline?: string }): Promise { return fetchWithTimeout( `${ACADEMY_API_BASE}/enrollments/${id}`, { method: 'PUT', body: JSON.stringify(data) } ) } // ============================================================================= // CERTIFICATES // ============================================================================= export async function fetchCertificate(id: string): Promise { return fetchWithTimeout( `${ACADEMY_API_BASE}/certificates/${id}` ) } export async function generateCertificate(enrollmentId: string): Promise { return fetchWithTimeout( `${ACADEMY_API_BASE}/enrollments/${enrollmentId}/certificate`, { method: 'POST' } ) } export async function fetchCertificates(): Promise { const res = await fetchWithTimeout<{ certificates: Certificate[]; total: number }>( `${ACADEMY_API_BASE}/certificates` ) return res.certificates || [] } // ============================================================================= // QUIZ // ============================================================================= export async function submitQuiz(lessonId: string, answers: SubmitQuizRequest): Promise { return fetchWithTimeout( `${ACADEMY_API_BASE}/lessons/${lessonId}/quiz-test`, { method: 'POST', body: JSON.stringify(answers) } ) } export async function updateLesson(lessonId: string, update: { title?: string description?: string content_url?: string duration_minutes?: number quiz_questions?: Array<{ question: string; options: string[]; correct_index: number; explanation: string }> }): Promise<{ lesson: any }> { return fetchWithTimeout( `${ACADEMY_API_BASE}/lessons/${lessonId}`, { method: 'PUT', body: JSON.stringify(update) } ) } // ============================================================================= // STATISTICS // ============================================================================= export async function fetchAcademyStatistics(): Promise { const res = await fetchWithTimeout<{ total_courses: number total_enrollments: number completion_rate: number overdue_count: number avg_completion_days: number by_category?: Record by_status?: Record }>(`${ACADEMY_API_BASE}/stats`) return { totalCourses: res.total_courses || 0, totalEnrollments: res.total_enrollments || 0, completionRate: res.completion_rate || 0, overdueCount: res.overdue_count || 0, byCategory: (res.by_category || {}) as Record, byStatus: (res.by_status || {}) as Record, } } // ============================================================================= // COURSE GENERATION // ============================================================================= export async function generateCourse(request: GenerateCourseRequest): Promise<{ course: Course }> { return fetchWithTimeout<{ course: Course }>( `${ACADEMY_API_BASE}/courses/generate`, { method: 'POST', body: JSON.stringify({ module_id: request.moduleId || request.title }) }, 120000 ) } export async function generateAllCourses(): Promise<{ generated: number; skipped: number; errors: string[]; total: number }> { return fetchWithTimeout( `${ACADEMY_API_BASE}/courses/generate-all`, { method: 'POST' }, 300000 ) } export async function generateVideos(courseId: string): Promise<{ status: string; jobId?: string }> { return fetchWithTimeout<{ status: string; jobId?: string }>( `${ACADEMY_API_BASE}/courses/${courseId}/generate-videos`, { method: 'POST' }, 300000 ) } export async function getVideoStatus(courseId: string): Promise<{ status: string total: number completed: number failed: number videos: Array<{ lessonId: string; status: string; url?: string }> }> { return fetchWithTimeout( `${ACADEMY_API_BASE}/courses/${courseId}/video-status` ) }