Files
breakpilot-compliance/admin-compliance/lib/sdk/academy/api-courses.ts
Sharang Parnerkar 528abc86ab refactor(admin): split 8 oversized lib/ files into focused modules under 500 LOC
Split these files that exceeded the 500-line hard cap:
- privacy-policy.ts (965 LOC) -> sections + renderers
- academy/api.ts (787 LOC) -> courses + mock-data
- whistleblower/api.ts (755 LOC) -> operations + mock-data
- vvt-profiling.ts (659 LOC) -> data + logic
- cookie-banner.ts (595 LOC) -> config + embed
- dsr/types.ts (581 LOC) -> core + api types
- tom-generator/rules-engine.ts (560 LOC) -> evaluator + gap-analysis
- datapoint-helpers.ts (548 LOC) -> generators + validators

Each original file becomes a barrel re-export for backward compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:05:59 +02:00

386 lines
11 KiB
TypeScript

/**
* 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<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)
}
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<Course[]> {
const res = await fetchWithTimeout<{ courses: BackendCourse[]; total: number }>(
`${ACADEMY_API_BASE}/courses`
)
return mapCoursesFromBackend(res.courses || [])
}
export async function fetchCourse(id: string): Promise<Course> {
const res = await fetchWithTimeout<{ course: BackendCourse }>(
`${ACADEMY_API_BASE}/courses/${id}`
)
return mapCourseFromBackend(res.course)
}
export async function createCourse(request: CourseCreateRequest): Promise<Course> {
return fetchWithTimeout<Course>(
`${ACADEMY_API_BASE}/courses`,
{ method: 'POST', body: JSON.stringify(request) }
)
}
export async function updateCourse(id: string, update: CourseUpdateRequest): Promise<Course> {
return fetchWithTimeout<Course>(
`${ACADEMY_API_BASE}/courses/${id}`,
{ method: 'PUT', body: JSON.stringify(update) }
)
}
export async function deleteCourse(id: string): Promise<void> {
await fetchWithTimeout<void>(
`${ACADEMY_API_BASE}/courses/${id}`,
{ method: 'DELETE' }
)
}
// =============================================================================
// ENROLLMENTS
// =============================================================================
export async function fetchEnrollments(courseId?: string): Promise<Enrollment[]> {
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<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/enrollments`,
{ method: 'POST', body: JSON.stringify(request) }
)
}
export async function updateProgress(enrollmentId: string, update: UpdateProgressRequest): Promise<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/progress`,
{ method: 'PUT', body: JSON.stringify(update) }
)
}
export async function completeEnrollment(enrollmentId: string): Promise<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/complete`,
{ method: 'POST' }
)
}
export async function deleteEnrollment(id: string): Promise<void> {
await fetchWithTimeout<void>(
`${ACADEMY_API_BASE}/enrollments/${id}`,
{ method: 'DELETE' }
)
}
export async function updateEnrollment(id: string, data: { deadline?: string }): Promise<Enrollment> {
return fetchWithTimeout<Enrollment>(
`${ACADEMY_API_BASE}/enrollments/${id}`,
{ method: 'PUT', body: JSON.stringify(data) }
)
}
// =============================================================================
// CERTIFICATES
// =============================================================================
export async function fetchCertificate(id: string): Promise<Certificate> {
return fetchWithTimeout<Certificate>(
`${ACADEMY_API_BASE}/certificates/${id}`
)
}
export async function generateCertificate(enrollmentId: string): Promise<Certificate> {
return fetchWithTimeout<Certificate>(
`${ACADEMY_API_BASE}/enrollments/${enrollmentId}/certificate`,
{ method: 'POST' }
)
}
export async function fetchCertificates(): Promise<Certificate[]> {
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<SubmitQuizResponse> {
return fetchWithTimeout<SubmitQuizResponse>(
`${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<AcademyStatistics> {
const res = await fetchWithTimeout<{
total_courses: number
total_enrollments: number
completion_rate: number
overdue_count: number
avg_completion_days: number
by_category?: Record<string, number>
by_status?: Record<string, number>
}>(`${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<CourseCategory, number>,
byStatus: (res.by_status || {}) as Record<EnrollmentStatus, number>,
}
}
// =============================================================================
// 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`
)
}