/** * Academy API - Shared configuration, helpers, and backend type mapping */ import type { Course, CourseCategory, LessonType, } from './types' // ============================================================================= // CONFIGURATION // ============================================================================= export const ACADEMY_API_BASE = '/api/sdk/v1/academy' export 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' } export 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 } export 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) } // 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) } } // ============================================================================= // BACKEND TYPE MAPPING (snake_case -> camelCase) // ============================================================================= export 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[] } export 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, } } export function mapCoursesFromBackend(courses: BackendCourse[]): Course[] { return courses.map(mapCourseFromBackend) }