refactor(admin): split 9 more oversized lib/ files into focused modules
Files split by agents before rate limit: - dsr/api.ts (669 → barrel + helpers) - einwilligungen/context.tsx (669 → barrel + hooks/reducer) - export.ts (753 → barrel + domain exporters) - incidents/api.ts (845 → barrel + api-helpers) - tom-generator/context.tsx (720 → barrel + hooks/reducer) - vendor-compliance/context.tsx (1010 → 234 provider + hooks/reducer) - api-docs/endpoints.ts — partially split (3 domain files created) - academy/api.ts — partially split (helpers extracted) - whistleblower/api.ts — partially split (helpers extracted) next build passes. api-client.ts (885) deferred to next session. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
165
admin-compliance/lib/sdk/academy/api-helpers.ts
Normal file
165
admin-compliance/lib/sdk/academy/api-helpers.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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<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)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user