// CompetenceTrack — API Client // All API calls go through these helpers /* ── Generic helpers ──────────────────────────────────────────────── */ async function apiRequest(url: string, options?: RequestInit): Promise { const res = await fetch(url, { headers: { 'Content-Type': 'application/json' }, ...options, }); if (!res.ok) { const body = await res.json().catch(() => ({})); // Handle 429 Too Many Requests (rate limiting) if (res.status === 429) { const retryAfter = body.retryAfter || parseInt(res.headers.get('Retry-After') || '60', 10); const rateLimitError = new Error(body.error || 'Too many requests'); (rateLimitError as RateLimitError).isRateLimit = true; (rateLimitError as RateLimitError).retryAfter = retryAfter; (rateLimitError as RateLimitError).limit = body.limit || 0; throw rateLimitError; } throw new Error(body.error || `API error ${res.status}`); } return res.json(); } // Rate limit error type export interface RateLimitError extends Error { isRateLimit: boolean; retryAfter: number; limit: number; } export function apiGet(url: string): Promise { return apiRequest(url); } export function apiPost(url: string, data?: unknown): Promise { return apiRequest(url, { method: 'POST', body: data ? JSON.stringify(data) : undefined, }); } export function apiPut(url: string, data: unknown): Promise { return apiRequest(url, { method: 'PUT', body: JSON.stringify(data), }); } export function apiDelete(url: string): Promise { return apiRequest(url, { method: 'DELETE' }); } /* ── Auth ─────────────────────────────────────────────────────────── */ export async function login(email: string, password: string) { return apiPost<{ id: string; email: string; firstName: string; lastName: string; role: string; schoolId: string | null; locale: string; isDemo?: boolean }>('/api/auth', { action: 'login', email, password, }); } export async function register(data: { email: string; password: string; firstName: string; lastName: string; schoolId?: string; role?: string; }) { return apiPost<{ id: string; email: string; firstName: string; lastName: string; role: string; schoolId: string | null; locale: string }>('/api/auth', { action: 'register', ...data, }); } export async function fetchCurrentUser() { try { const res = await fetch('/api/auth'); if (!res.ok) return null; return res.json(); } catch { return null; } } /* ── Dashboard ────────────────────────────────────────────────────── */ export interface DashboardData { classesOverview: Array<{ id: string; name: string; gradeLevel: number; schoolType: string; schoolYear: { id: string; label: string }; studentCount: number; assessmentCount: number; teachers: Array<{ id: string; firstName: string; lastName: string; teacherRole: string }>; }>; recentEntries: Array<{ id: string; date: string; masteryLevelValue: number; note: string | null; student: { id: string; firstName: string; lastName: string }; competency: { id: string; code: string; title: string; category: { id: string; name: string; color: string | null } }; teacher: { id: string; firstName: string; lastName: string }; classGroup: { id: string; name: string }; }>; recentAssessments: Array<{ id: string; title: string; date: string; type: string; maxScore: number | null; weight: number; classGroup: { id: string; name: string }; subject: { id: string; name: string }; _count: { assessmentResults: number }; }>; studentsNeedingAttention: Array<{ id: string; firstName: string; lastName: string; averageMastery: number; enrollments: Array<{ classGroup: { id: string; name: string; gradeLevel: number } }>; }>; stats: { totalStudents: number; totalClasses: number; totalAssessments: number; totalProgressEntries: number; totalReports: number; }; } export function fetchDashboard(schoolId?: string, schoolYearId?: string): Promise { const params = new URLSearchParams(); if (schoolId) params.set('schoolId', schoolId); if (schoolYearId) params.set('schoolYearId', schoolYearId); return apiGet(`/api/dashboard?${params.toString()}`); } /* ── Classes ──────────────────────────────────────────────────────── */ export interface ClassGroup { id: string; schoolId: string; schoolYearId: string; name: string; gradeLevel: number; schoolType: string; school: { id: string; name: string }; schoolYear: { id: string; label: string }; teachers: Array<{ id: string; userId: string; role: string; user: { id: string; firstName: string; lastName: string; email: string } }>; studentCount?: number; teacherList?: Array<{ id: string; firstName: string; lastName: string; teacherRole: string }>; _count?: { enrollments?: number; competencyAssignments?: number; [key: string]: unknown }; } export function fetchClasses(schoolId?: string, schoolYearId?: string): Promise { const params = new URLSearchParams(); if (schoolId) params.set('schoolId', schoolId); if (schoolYearId) params.set('schoolYearId', schoolYearId); return apiGet(`/api/classes?${params.toString()}`); } export function createClass(data: { schoolId: string; schoolYearId: string; name: string; gradeLevel: number; schoolType?: string; teacherIds?: string[]; }) { return apiPost('/api/classes', data); } /* ── Students ─────────────────────────────────────────────────────── */ export interface Student { id: string; schoolId: string; firstName: string; lastName: string; dateOfBirth: string | null; externalId: string | null; avatarUrl: string | null; avatarInitials: string | null; school: { id: string; name: string }; enrollments: Array<{ classGroup: { id: string; name: string; gradeLevel: number } }>; _count?: { learningProgressEntries: number; assessmentResults: number }; } export function fetchStudents(schoolId?: string, classGroupId?: string, search?: string): Promise { const params = new URLSearchParams(); if (schoolId) params.set('schoolId', schoolId); if (classGroupId) params.set('classGroupId', classGroupId); if (search) params.set('search', search); return apiGet(`/api/students?${params.toString()}`); } export function createStudent(data: { schoolId: string; firstName: string; lastName: string; dateOfBirth?: string; externalId?: string; avatarUrl?: string; }) { return apiPost('/api/students', data); } export function fetchClassStudents(classGroupId: string): Promise { return apiGet(`/api/classes/${classGroupId}/students`); } export function enrollStudent(classGroupId: string, data: { studentId: string; schoolYearId: string; startDate?: string }) { return apiPost(`/api/classes/${classGroupId}/students`, data); } /* ── Subjects ─────────────────────────────────────────────────────── */ export interface Subject { id: string; schoolId: string | null; name: string; gradeLevelMin: number; gradeLevelMax: number; } export function fetchSubjects(schoolId?: string): Promise { const params = new URLSearchParams(); if (schoolId) params.set('schoolId', schoolId); return apiGet(`/api/subjects?${params.toString()}`); } /* ── Competency Templates ─────────────────────────────────────────── */ export interface MasteryLevelDef { id: string; competencyId: string; levelValue: number; label: string; description: string | null; } export interface CompetencyItem { id: string; categoryId: string; code: string; title: string; description: string | null; order: number; masteryLevelDefinitions: MasteryLevelDef[]; } export interface CompetencyCategory { id: string; competencyTemplateId: string; name: string; order: number; color: string | null; competencies: CompetencyItem[]; } export interface CompetencyTemplate { id: string; name: string; description: string | null; subjectId: string | null; schoolType: string; gradeLevelMin: number; gradeLevelMax: number; isGlobalTemplate: boolean; version: number; schoolId: string | null; subject: { id: string; name: string } | null; school: { id: string; name: string } | null; categories: CompetencyCategory[]; _count: { classCompetencyAssignments: number }; } export function fetchCompetencyTemplates(params?: { schoolType?: string; subjectId?: string; gradeLevel?: string; schoolId?: string; }): Promise { const sp = new URLSearchParams(); if (params?.schoolType) sp.set('schoolType', params.schoolType); if (params?.subjectId) sp.set('subjectId', params.subjectId); if (params?.gradeLevel) sp.set('gradeLevel', params.gradeLevel); if (params?.schoolId) sp.set('schoolId', params.schoolId); return apiGet(`/api/competency-templates?${sp.toString()}`); } export function fetchCompetencyTemplate(id: string): Promise { return apiGet(`/api/competency-templates/${id}`); } /* ── Class Competency Assignments ─────────────────────────────────── */ export interface ClassCompetencyAssignment { id: string; classGroupId: string; subjectId: string; competencyTemplateId: string; schoolYearId: string; clonedTemplateId: string | null; classGroup: { id: string; name: string; gradeLevel: number }; subject: { id: string; name: string }; competencyTemplate: { id: string; name: string; schoolType: string; gradeLevelMin: number; gradeLevelMax: number }; } export function fetchClassCompetencyAssignments(params?: { classGroupId?: string; subjectId?: string; schoolYearId?: string; }): Promise { const sp = new URLSearchParams(); if (params?.classGroupId) sp.set('classGroupId', params.classGroupId); if (params?.subjectId) sp.set('subjectId', params.subjectId); if (params?.schoolYearId) sp.set('schoolYearId', params.schoolYearId); return apiGet(`/api/class-competency-assignments?${sp.toString()}`); } export function createClassCompetencyAssignment(data: { classGroupId: string; subjectId: string; competencyTemplateId: string; schoolYearId: string; clonedTemplateId?: string; }) { return apiPost('/api/class-competency-assignments', data); } /* ── Learning Progress ────────────────────────────────────────────── */ export interface LearningProgressEntry { id: string; studentId: string; competencyId: string; teacherId: string; classGroupId: string; date: string; masteryLevelValue: number; note: string | null; createdAt: string; student: { id: string; firstName: string; lastName: string }; competency: { id: string; code: string; title: string; category: { id: string; name: string; color: string | null } }; teacher: { id: string; firstName: string; lastName: string }; classGroup: { id: string; name: string }; } export function fetchLearningProgress(params?: { studentId?: string; classGroupId?: string; competencyId?: string; startDate?: string; endDate?: string; }): Promise { const sp = new URLSearchParams(); if (params?.studentId) sp.set('studentId', params.studentId); if (params?.classGroupId) sp.set('classGroupId', params.classGroupId); if (params?.competencyId) sp.set('competencyId', params.competencyId); if (params?.startDate) sp.set('startDate', params.startDate); if (params?.endDate) sp.set('endDate', params.endDate); return apiGet(`/api/learning-progress?${sp.toString()}`); } export function createLearningProgressEntry(data: { studentId: string; competencyId: string; teacherId: string; classGroupId: string; date: string; masteryLevelValue: number; note?: string; }) { return apiPost('/api/learning-progress', data); } export function createBulkLearningProgress(data: Array<{ studentId: string; competencyId: string; teacherId: string; classGroupId: string; date: string; masteryLevelValue: number; note?: string; }>) { return apiPost<{ created: number }>('/api/learning-progress', data); } /* ── Competence Flower ────────────────────────────────────────────── */ export interface FlowerCategory { categoryId: string; categoryName: string; color: string | null; averageMasteryLevel: number; competencyCount: number; assessedCompetencyCount: number; } export interface FlowerData { studentId: string; studentName: string; categories: FlowerCategory[]; } export function fetchCompetenceFlower(params: { studentId?: string; classGroupId: string; subjectId: string; }): Promise { const sp = new URLSearchParams(); if (params.studentId) sp.set('studentId', params.studentId); sp.set('classGroupId', params.classGroupId); sp.set('subjectId', params.subjectId); return apiGet(`/api/competence-flower?${sp.toString()}`); } /* ── Assessments ──────────────────────────────────────────────────── */ export interface AssessmentCompetencyLink { id: string; assessmentId: string; competencyId: string; weight: number; competency: { id: string; code: string; title: string }; } export interface Assessment { id: string; classGroupId: string; subjectId: string; teacherId: string; title: string; date: string; type: string; maxScore: number | null; weight: number; classGroup: { id: string; name: string }; subject: { id: string; name: string }; teacher: { id: string; firstName: string; lastName: string }; assessmentCompetencyLinks: AssessmentCompetencyLink[]; _count: { assessmentResults: number }; } export function fetchAssessments(params?: { classGroupId?: string; subjectId?: string; }): Promise { const sp = new URLSearchParams(); if (params?.classGroupId) sp.set('classGroupId', params.classGroupId); if (params?.subjectId) sp.set('subjectId', params.subjectId); return apiGet(`/api/assessments?${sp.toString()}`); } export function createAssessment(data: { classGroupId: string; subjectId: string; teacherId: string; title: string; date: string; type?: string; maxScore?: number | null; weight?: number; competencyLinks?: Array<{ competencyId: string; weight: number }>; }) { return apiPost('/api/assessments', data); } /* ── Assessment Results ───────────────────────────────────────────── */ export interface AssessmentResult { id: string; assessmentId: string; studentId: string; score: number | null; masteryLevelValue: number | null; note: string | null; student: { id: string; firstName: string; lastName: string }; assessment: { id: string; title: string; maxScore: number | null }; } export function fetchAssessmentResults(assessmentId: string): Promise { return apiGet(`/api/assessments/${assessmentId}/results`); } export function createAssessmentResults(assessmentId: string, data: Array<{ studentId: string; score?: number | null; masteryLevelValue?: number | null; note?: string; }>) { return apiPost(`/api/assessments/${assessmentId}/results`, data); } /* ── Grading ──────────────────────────────────────────────────────── */ export interface GradingWeightRule { id: string; gradingSchemeId: string; sourceType: string; targetRef: string | null; weightPercent: number; } export interface GradingScheme { id: string; classGroupId: string | null; subjectId: string | null; schoolId: string | null; name: string; type: string; scaleDefinition: string; gradingWeightRules: GradingWeightRule[]; classGroup: { id: string; name: string } | null; subject: { id: string; name: string } | null; school: { id: string; name: string } | null; } export interface ComputedGradeResult { studentId: string; computedValue: number; breakdown: Array<{ source: string; weight: number; value: number }>; } export function fetchGradingSchemes(params?: { classGroupId?: string; subjectId?: string; schoolId?: string; computeGrades?: boolean; schoolYearId?: string; }): Promise { const sp = new URLSearchParams(); if (params?.classGroupId) sp.set('classGroupId', params.classGroupId); if (params?.subjectId) sp.set('subjectId', params.subjectId); if (params?.schoolId) sp.set('schoolId', params.schoolId); if (params?.computeGrades) sp.set('computeGrades', 'true'); if (params?.schoolYearId) sp.set('schoolYearId', params.schoolYearId); return apiGet(`/api/grading?${sp.toString()}`); } export function createGradingScheme(data: { classGroupId?: string; subjectId?: string; schoolId?: string; name: string; type?: string; scaleDefinition: string; weightRules?: Array<{ sourceType: string; targetRef?: string; weightPercent: number }>; }) { return apiPost('/api/grading', data); } /* ── Grading Annotations ─────────────────────────────────────────── */ export interface GradingAnnotation { id: string; annotationData: string; annotationImage: string | null; } export function fetchGradingAnnotation(assessmentId: string, studentId: string): Promise<{ annotation: GradingAnnotation | null }> { return apiGet(`/api/grading/annotate?assessmentId=${assessmentId}&studentId=${studentId}`); } export function saveGradingAnnotation(data: { assessmentId: string; studentId: string; annotationData: string; annotationImage?: string; }): Promise<{ annotation: GradingAnnotation }> { return apiPost('/api/grading/annotate', data); } /* ── Reports ──────────────────────────────────────────────────────── */ export interface ReportSection { id: string; reportId: string; competencyCategoryId: string | null; generatedText: string; order: number; competencyCategory: { id: string; name: string; color: string | null } | null; } export interface Report { id: string; studentId: string; classGroupId: string; schoolYearId: string; period: string; generatedByUserId: string; generatedAt: string; status: string; pdfFilePath: string | null; includesGrades: boolean; student: { id: string; firstName: string; lastName: string; avatarUrl: string | null }; classGroup: { id: string; name: string; gradeLevel: number }; schoolYear: { id: string; label: string }; generatedByUser: { id: string; firstName: string; lastName: string }; sections: ReportSection[]; } export function fetchReports(params?: { studentId?: string; classGroupId?: string; schoolYearId?: string; status?: string; }): Promise { const sp = new URLSearchParams(); if (params?.studentId) sp.set('studentId', params.studentId); if (params?.classGroupId) sp.set('classGroupId', params.classGroupId); if (params?.schoolYearId) sp.set('schoolYearId', params.schoolYearId); if (params?.status) sp.set('status', params.status); return apiGet(`/api/reports?${sp.toString()}`); } export function createReport(data: { studentId: string; classGroupId: string; schoolYearId: string; period: string; includesGrades?: boolean; sections?: Array<{ competencyCategoryId?: string; generatedText: string; order: number }>; }) { return apiPost('/api/reports', data); } export function updateReport(data: { id: string; status?: string; includesGrades?: boolean; pdfFilePath?: string; sections?: Array<{ id: string; generatedText: string }>; }) { return apiPut('/api/reports', data); } /* ── Grade Override ──────────────────────────────────────────────── */ export function overrideGrade(data: { studentId: string; subjectId: string; classGroupId: string; schoolYearId: string; overriddenValue: number; overrideReason: string; }) { return apiPut<{ success: boolean }>('/api/grading', data); } /* ── Schools ──────────────────────────────────────────────────────── */ export interface School { id: string; name: string; schoolType: string; country: string; timezone: string; logoUrl?: string | null; primaryColor?: string | null; secondaryColor?: string | null; accentColor?: string | null; fontFamily?: string | null; customCss?: string | null; motto?: string | null; websiteUrl?: string | null; emailDomain?: string | null; address?: string | null; phone?: string | null; _count: { users: number; classGroups: number; students: number }; } export function fetchSchools(schoolType?: string): Promise { const sp = new URLSearchParams(); if (schoolType) sp.set('schoolType', schoolType); return apiGet(`/api/schools?${sp.toString()}`); } /* ── School Years ─────────────────────────────────────────────────── */ export interface SchoolYear { id: string; schoolId: string; label: string; startDate: string; endDate: string; } export function fetchSchoolYears(schoolId: string): Promise { return apiGet(`/api/school-years?schoolId=${schoolId}`); } export function createSchoolYear(data: { schoolId: string; label: string; startDate: string; endDate: string; }) { return apiPost('/api/school-years', data); } /* ── School Update ────────────────────────────────────────────────── */ export function updateSchool(data: { id: string; name?: string; schoolType?: string; country?: string; timezone?: string; logoUrl?: string | null; primaryColor?: string | null; secondaryColor?: string | null; accentColor?: string | null; fontFamily?: string | null; customCss?: string | null; motto?: string | null; websiteUrl?: string | null; emailDomain?: string | null; address?: string | null; phone?: string | null; }) { return apiPut('/api/schools', data); } /* ── Subject CRUD ─────────────────────────────────────────────────── */ export function createSubject(data: { schoolId?: string | null; name: string; gradeLevelMin?: number; gradeLevelMax?: number; }) { return apiPost('/api/subjects', data); } export function updateSubject(data: { id: string; name?: string; gradeLevelMin?: number; gradeLevelMax?: number; }) { return apiPut('/api/subjects', data); } export function deleteSubject(id: string) { return apiDelete<{ success: boolean }>(`/api/subjects?id=${id}`); } /* ── Audit Log ────────────────────────────────────────────────────── */ export interface AuditLogEntry { id: string; userId: string | null; schoolId: string | null; action: string; entityType: string; entityId: string | null; changes: string | null; ipAddress: string | null; userAgent: string | null; timestamp: string; metadata: string | null; user: { id: string; firstName: string; lastName: string } | null; } export interface AuditLogPaginatedResponse { entries: AuditLogEntry[]; pagination: { page: number; pageSize: number; total: number; totalPages: number; }; } export function fetchAuditLog(params?: { schoolId?: string; action?: string; entityType?: string; userId?: string; startDate?: string; endDate?: string; page?: number; pageSize?: number; }): Promise { const sp = new URLSearchParams(); if (params?.schoolId) sp.set('schoolId', params.schoolId); if (params?.action) sp.set('action', params.action); if (params?.entityType) sp.set('entityType', params.entityType); if (params?.userId) sp.set('userId', params.userId); if (params?.startDate) sp.set('startDate', params.startDate); if (params?.endDate) sp.set('endDate', params.endDate); if (params?.page) sp.set('page', String(params.page)); if (params?.pageSize) sp.set('pageSize', String(params.pageSize)); return apiGet(`/api/audit-log?${sp.toString()}`); } export function exportAuditLogCsv(params?: { schoolId?: string; action?: string; entityType?: string; userId?: string; startDate?: string; endDate?: string; }): void { const sp = new URLSearchParams(); sp.set('export', 'csv'); if (params?.schoolId) sp.set('schoolId', params.schoolId); if (params?.action) sp.set('action', params.action); if (params?.entityType) sp.set('entityType', params.entityType); if (params?.userId) sp.set('userId', params.userId); if (params?.startDate) sp.set('startDate', params.startDate); if (params?.endDate) sp.set('endDate', params.endDate); const url = `/api/audit-log?${sp.toString()}`; const a = document.createElement('a'); a.href = url; a.download = 'audit-log.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } /* ── Behavior Interventions ─────────────────────────────────────────── */ export interface BehaviorInterventionType { id: string; schoolId: string; studentId: string; incidentId: string | null; type: string; description: string; status: string; assignedTo: string | null; startDate: string | null; endDate: string | null; outcome: string | null; isDemo: boolean; createdAt: string; updatedAt: string; student: { id: string; firstName: string; lastName: string }; incident: { id: string; description: string; date: string; severity: string } | null; assignedUser: { id: string; firstName: string; lastName: string } | null; } export function fetchBehaviorInterventions(params: { schoolId: string; studentId?: string; incidentId?: string; type?: string; status?: string; }): Promise { const sp = new URLSearchParams(); sp.set('schoolId', params.schoolId); if (params.studentId) sp.set('studentId', params.studentId); if (params.incidentId) sp.set('incidentId', params.incidentId); if (params.type) sp.set('type', params.type); if (params.status) sp.set('status', params.status); return apiGet(`/api/behavior-interventions?${sp.toString()}`); } export function createBehaviorIntervention(data: { schoolId: string; studentId: string; incidentId?: string; type: string; description: string; status?: string; assignedTo?: string; startDate?: string; endDate?: string; outcome?: string; isDemo?: boolean; }): Promise { return apiPost('/api/behavior-interventions', data); } export function updateBehaviorIntervention( id: string, data: { type?: string; description?: string; status?: string; assignedTo?: string; startDate?: string; endDate?: string; outcome?: string; incidentId?: string; } ): Promise { return apiPut(`/api/behavior-interventions/${id}`, data); } export function deleteBehaviorIntervention(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/behavior-interventions/${id}`); } /* ── Data Export (CSV) ────────────────────────────────────────────── */ export function downloadCsvExport(params: { type: 'students' | 'progress' | 'assessments' | 'grades' | 'attendance'; classGroupId?: string; schoolYearId?: string; schoolId?: string; format?: 'csv' | 'json' | 'pdf'; dateFrom?: string; dateTo?: string; }): void { const sp = new URLSearchParams(); sp.set('type', params.type); if (params.classGroupId) sp.set('classGroupId', params.classGroupId); if (params.schoolYearId) sp.set('schoolYearId', params.schoolYearId); if (params.schoolId) sp.set('schoolId', params.schoolId); if (params.dateFrom) sp.set('dateFrom', params.dateFrom); if (params.dateTo) sp.set('dateTo', params.dateTo); const format = params.format || 'csv'; const url = `/api/data-export/csv?${sp.toString()}`; const a = document.createElement('a'); a.href = url; a.download = `${params.type}_export.${format}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); } /* ── Email Templates ──────────────────────────────────────────────── */ export interface EmailTemplate { id: string; schoolId: string; name: string; subject: string; body: string; isDefault: boolean; isDemo: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; } export interface EmailLog { id: string; schoolId: string; templateId: string | null; recipientEmail: string; recipientName: string | null; subject: string; body: string; status: string; sentAt: string | null; error: string | null; createdAt: string; } export function fetchEmailTemplates(schoolId: string): Promise { return apiGet(`/api/email-templates?schoolId=${schoolId}`); } export function createEmailTemplate(data: { schoolId: string; name: string; subject: string; body: string; isDefault?: boolean; }): Promise { return apiPost('/api/email-templates', data); } export function updateEmailTemplate(id: string, data: { name?: string; subject?: string; body?: string; isDefault?: boolean; }): Promise { return apiPut(`/api/email-templates/${id}`, data); } export function deleteEmailTemplate(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/email-templates/${id}`); } export function sendTestEmail(data: { schoolId: string; templateId?: string; recipientEmail: string; recipientName?: string; subject: string; body: string; }): Promise<{ success: boolean; logId: string; message: string }> { return apiPost<{ success: boolean; logId: string; message: string }>('/api/email-templates/send', { ...data, isTest: true, }); } export function fetchEmailLogs(schoolId: string, status?: string): Promise<{ logs: EmailLog[]; counts: { total: number; sent: number; failed: number; pending: number; bounced: number } }> { const sp = new URLSearchParams(); sp.set('schoolId', schoolId); if (status) sp.set('status', status); return apiGet(`/api/email-logs?${sp.toString()}`); } /* ── Data Erasure (GDPR) ──────────────────────────────────────────── */ export function requestDataErasure(data: { scope: string; scopeId?: string; }) { return apiPost<{ id: string; status: string }>('/api/data-export', { scope: data.scope, scopeId: data.scopeId, format: 'JSON', }); } /* ── Report PDF ───────────────────────────────────────────────────── */ export function getReportPdfUrl(reportId: string): string { return `/api/reports/pdf?reportId=${reportId}`; } /* ── Notification helpers ─────────────────────────────────────────── */ export interface AppNotification { id: string; type: 'progress' | 'assessment' | 'grade' | 'report'; message: string; timestamp: string; read: boolean; } export function getStoredNotifications(): AppNotification[] { try { const stored = localStorage.getItem('ct_notifications'); if (stored) return JSON.parse(stored); } catch { // ignore } return []; } export function addNotification(notification: Omit): void { try { const stored = getStoredNotifications(); const newNotification: AppNotification = { ...notification, id: `n_${Date.now()}_${Math.random().toString(36).slice(2)}`, read: false, }; stored.unshift(newNotification); // Keep only last 50 if (stored.length > 50) stored.splice(50); localStorage.setItem('ct_notifications', JSON.stringify(stored)); } catch { // ignore } } export function markNotificationsRead(): void { try { const stored = getStoredNotifications(); stored.forEach((n) => { n.read = true; }); localStorage.setItem('ct_notifications', JSON.stringify(stored)); } catch { // ignore } } /* ── User Management ──────────────────────────────────────────────── */ export interface UserAccount { id: string; email: string; firstName: string; lastName: string; role: string; schoolId: string | null; locale: string; createdAt: string; updatedAt: string; deletedAt: string | null; school?: { id: string; name: string } | null; classGroupTeachers?: Array<{ id: string; role: string; classGroup: { id: string; name: string; gradeLevel: number }; }>; _count?: { classGroupTeachers: number; learningProgressEntries: number; assessments: number }; } export function fetchUsers(schoolId?: string): Promise { const sp = new URLSearchParams(); if (schoolId) sp.set('schoolId', schoolId); return apiGet(`/api/users?${sp.toString()}`); } export function createUser(data: { schoolId?: string | null; email: string; password: string; firstName: string; lastName: string; role?: string; locale?: string; }) { return apiPost('/api/users', data); } export function updateUser(data: { id: string; firstName?: string; lastName?: string; email?: string; role?: string; locale?: string; }) { return apiPut(`/api/users/${data.id}`, data); } export function deleteUser(id: string) { return apiDelete<{ success: boolean }>(`/api/users/${id}`); } export function assignTeacherClasses(userId: string, classGroupIds: string[]) { return apiPost<{ success: boolean; assigned: number }>(`/api/users/${userId}/classes`, { classGroupIds }); } /* ── Bulk Student Import ──────────────────────────────────────────── */ export function bulkCreateStudents(data: { schoolId: string; classGroupId?: string; schoolYearId?: string; students: Array<{ firstName: string; lastName: string; dateOfBirth?: string; externalId?: string; }>; }) { return apiPost<{ created: number; enrolled: number; errors: Array<{ row: number; error: string }> }>( '/api/students/bulk', data ); } /* ── Student Detail ───────────────────────────────────────────────── */ export interface StudentDetailData { student: { id: string; firstName: string; lastName: string; dateOfBirth: string | null; externalId: string | null; avatarUrl: string | null; avatarInitials: string | null; createdAt: string; school: { id: string; name: string }; enrollments: Array<{ classGroup: { id: string; name: string; gradeLevel: number; schoolType: string }; schoolYear: { id: string; label: string }; }>; }; progressEntries: Array<{ id: string; date: string; masteryLevelValue: number; note: string | null; competency: { id: string; code: string; title: string; category: { id: string; name: string; color: string | null } }; teacher: { id: string; firstName: string; lastName: string }; classGroup: { id: string; name: string }; }>; assessmentResults: Array<{ id: string; score: number | null; masteryLevelValue: number | null; note: string | null; assessment: { id: string; title: string; date: string; type: string; maxScore: number | null; subject: { id: string; name: string }; }; }>; computedGrades: Array<{ id: string; computedValue: number; overriddenValue: number | null; period: string; isFinalized: boolean; computedAt: string; subject: { id: string; name: string }; classGroup: { id: string; name: string }; schoolYear: { id: string; label: string }; }>; reports: Array<{ id: string; period: string; status: string; generatedAt: string; includesGrades: boolean; classGroup: { id: string; name: string }; schoolYear: { id: string; label: string }; generatedByUser: { id: string; firstName: string; lastName: string }; sections: Array<{ id: string; generatedText: string; order: number }>; }>; flowers: Array<{ studentId: string; studentName: string; categories: Array<{ categoryId: string; categoryName: string; color: string | null; averageMasteryLevel: number; competencyCount: number; assessedCompetencyCount: number; }>; subjectId: string; subjectName: string; }>; stats: { totalProgressEntries: number; averageMastery: number; latestGrade: { value: number; period: string; subjectName: string } | null; totalReports: number; totalAssessments: number; }; } export function fetchStudentDetail(studentId: string): Promise { return apiGet(`/api/students/${studentId}/details`); } /* ─── Progress Analytics ──────────────────────────────────────────── */ export interface AnalyticsMasteryTrendPoint { date: string; avgMastery: number; count: number; } export interface AnalyticsClassComparisonPoint { classId: string; className: string; avgMastery: number; studentCount: number; } export interface AnalyticsMasteryDistributionPoint { level: number; count: number; percentage: number; } export interface AnalyticsCompetencyPoint { competencyId: string; code: string; title: string; avgMastery: number; entryCount: number; } export interface AnalyticsHeatmapPoint { date: string; count: number; } export interface AnalyticsGradeTrendPoint { week: string; // e.g. "2026-W28" weekLabel: string; // e.g. "KW28" avgMastery: number; entryCount: number; uniqueStudents: number; classAvg: number; } export type AnalyticsRiskLevel = 'low' | 'medium' | 'high' | 'critical'; export type AnalyticsRiskSignal = | 'low_mastery' | 'no_recent_progress' | 'low_attendance' | 'declining'; export interface AnalyticsAtRiskStudent { studentId: string; studentName: string; className: string; riskScore: number; riskLevel: AnalyticsRiskLevel; signals: AnalyticsRiskSignal[]; latestMastery: number; latestEntryDate: string; } export interface AnalyticsSubjectMasteryPoint { categoryId: string; categoryName: string; avgMastery: number; entryCount: number; } export interface AnalyticsTeacherPerformancePoint { teacherId: string; teacherName: string; assessmentCount: number; progressCount: number; improvementRate: number; notebookCount: number; } export interface AnalyticsSelfVsTeacherAgg { avgSelfLevel: number; avgTeacherLevel: number; avgGap: number; totalComparisons: number; } export interface AnalyticsGapAnalysisPoint { studentId: string; studentName: string; competencyId: string; selfLevel: number; teacherLevel: number; gap: number; } export interface AnalyticsExcellingStudent { studentId: string; studentName: string; className: string; latestMastery: number; improvement: number; } export interface AnalyticsClassRadarPoint { classId: string; className: string; mastery: number; attendance: number; engagement: number; progress: number; behavior: number; } export interface AnalyticsData { masteryTrend: AnalyticsMasteryTrendPoint[]; classComparison: AnalyticsClassComparisonPoint[]; masteryDistribution: AnalyticsMasteryDistributionPoint[]; topCompetencies: AnalyticsCompetencyPoint[]; bottomCompetencies: AnalyticsCompetencyPoint[]; activityHeatmap: AnalyticsHeatmapPoint[]; gradeTrend: AnalyticsGradeTrendPoint[]; atRiskStudents: AnalyticsAtRiskStudent[]; totalEntries: number; totalStudents: number; overallAvgMastery: number; // New fields totalClasses: number; totalTeachers: number; totalCompetencies: number; competencyCoveragePct: number; subjectMasteryAvg: AnalyticsSubjectMasteryPoint[]; teacherPerformance: AnalyticsTeacherPerformancePoint[]; selfVsTeacherAgg: AnalyticsSelfVsTeacherAgg | null; gapAnalysis: AnalyticsGapAnalysisPoint[]; excellingStudents: AnalyticsExcellingStudent[]; classRadarData: AnalyticsClassRadarPoint[]; } export function fetchAnalytics(params: { classGroupId?: string; subjectId?: string; schoolYearId?: string; schoolId?: string; startDate?: string; endDate?: string; }): Promise { const sp = new URLSearchParams(); if (params.classGroupId) sp.set('classGroupId', params.classGroupId); if (params.subjectId) sp.set('subjectId', params.subjectId); if (params.schoolYearId) sp.set('schoolYearId', params.schoolYearId); if (params.schoolId) sp.set('schoolId', params.schoolId); if (params.startDate) sp.set('startDate', params.startDate); if (params.endDate) sp.set('endDate', params.endDate); return apiGet(`/api/analytics?${sp.toString()}`); } /* ─── Mastery Matrix ──────────────────────────────────────────────── */ export interface MasteryMatrixStudent { id: string; firstName: string; lastName: string; } export interface MasteryMatrixCompetency { id: string; code: string; title: string; category: { id: string; name: string; color: string | null }; } export interface MasteryMatrixCell { studentId: string; competencyId: string; latestMasteryLevel: number | null; entryCount: number; lastEntryDate: string | null; } export interface MasteryMatrixData { students: MasteryMatrixStudent[]; competencies: MasteryMatrixCompetency[]; matrix: MasteryMatrixCell[]; } export function fetchMasteryMatrix(params: { classGroupId: string; subjectId: string; }): Promise { const sp = new URLSearchParams(); sp.set('classGroupId', params.classGroupId); sp.set('subjectId', params.subjectId); return apiGet(`/api/mastery-matrix?${sp.toString()}`); } /* ─── Curriculum Coverage ─────────────────────────────────────────── */ export interface CurriculumCoverageByCategory { categoryId: string; categoryName: string; categoryColor: string; total: number; assessed: number; notAssessed: number; coveragePercent: number; } export interface CurriculumCoverageCompetency { competencyId: string; code: string; title: string; categoryName: string; categoryColor: string; assessmentCount: number; progressCount: number; studentsAssessed: number; lastAssessedDate: string | null; lastMasteryLevel: number | null; status: 'covered' | 'partial' | 'untouched'; } export interface CurriculumCoverageNotAssessed { competencyId: string; code: string; title: string; categoryName: string; categoryColor: string; status: 'covered' | 'partial' | 'untouched'; studentsAssessed: number; studentsCount: number; } export interface CurriculumCoverageRecent { competencyId: string; code: string; title: string; categoryName: string; date: string; masteryLevelValue: number | null; } export interface CurriculumCoverage { classGroup: { id: string; name: string; gradeLevel: number; schoolType: string }; subject: { id: string; name: string } | null; totals: { competencies: number; assessed: number; notAssessed: number; coveragePercent: number; studentsCount: number; assessmentsCount: number; progressEntriesCount: number; }; byCategory: CurriculumCoverageByCategory[]; byCompetency: CurriculumCoverageCompetency[]; notAssessedList: CurriculumCoverageNotAssessed[]; recentAssessed: CurriculumCoverageRecent[]; hasAssignments: boolean; } export function fetchCurriculumCoverage(params: { classGroupId: string; subjectId?: string; }): Promise { const sp = new URLSearchParams(); sp.set('classGroupId', params.classGroupId); if (params.subjectId) sp.set('subjectId', params.subjectId); return apiGet(`/api/curriculum-coverage?${sp.toString()}`); } /* ─── Teacher Notes ───────────────────────────────────────────────── */ export type TeacherNoteCategory = | 'GENERAL' | 'BEHAVIOR' | 'ACADEMIC' | 'INTERVENTION' | 'PARENT_CONTACT'; export interface TeacherNote { id: string; studentId: string; teacherId: string; category: TeacherNoteCategory; content: string; isPrivate: boolean; createdAt: string; updatedAt: string; teacher: { id: string; firstName: string; lastName: string }; } export function fetchTeacherNotes(studentId: string): Promise { const sp = new URLSearchParams(); sp.set('studentId', studentId); return apiGet(`/api/teacher-notes?${sp.toString()}`); } export function createTeacherNote(data: { studentId: string; category: TeacherNoteCategory; content: string; isPrivate?: boolean; }): Promise { return apiPost('/api/teacher-notes', data); } export function updateTeacherNote(id: string, data: { category?: TeacherNoteCategory; content?: string; isPrivate?: boolean; }): Promise { return apiPut(`/api/teacher-notes/${id}`, data); } export function deleteTeacherNote(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/teacher-notes/${id}`); } /* ─── Attendance ─────────────────────────────────────────────────── */ export type AttendanceStatus = 'PRESENT' | 'ABSENT' | 'EXCUSED' | 'LATE'; export interface AttendanceRecord { id: string; sessionId: string; studentId: string; status: AttendanceStatus; arrivalTime: string | null; comment: string | null; student: { id: string; firstName: string; lastName: string; avatarUrl: string | null }; } export interface AttendanceSession { id: string; classGroupId: string; teacherId: string; date: string; subjectId: string | null; period: string | null; status: 'OPEN' | 'COMPLETED'; note: string | null; createdAt: string; updatedAt: string; subject: { id: string; name: string } | null; teacher: { id: string; firstName: string; lastName: string }; records: AttendanceRecord[]; } export function fetchAttendanceSessions( classGroupId: string, dateFrom?: string, dateTo?: string, status?: string ): Promise { const sp = new URLSearchParams(); sp.set('classGroupId', classGroupId); if (dateFrom) sp.set('dateFrom', dateFrom); if (dateTo) sp.set('dateTo', dateTo); if (status) sp.set('status', status); return apiGet(`/api/attendance?${sp.toString()}`); } export function createAttendanceSession(data: { classGroupId: string; date: string; subjectId?: string; period?: string; }): Promise { return apiPost('/api/attendance', data); } export function updateAttendanceSession( id: string, data: { status?: 'OPEN' | 'COMPLETED'; records?: Array<{ id: string; status?: AttendanceStatus; arrivalTime?: string; comment?: string; }>; } ): Promise { return apiPut(`/api/attendance`, { id, ...data }); } export function updateAttendanceRecord( id: string, data: { status?: AttendanceStatus; arrivalTime?: string; comment?: string; } ): Promise { return apiPut(`/api/attendance/${id}`, data); } export function deleteAttendanceSession(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/attendance/${id}`); } /* ── Calendar ────────────────────────────────────────────────────── */ export type CalendarEventType = 'assessment' | 'attendance' | 'progress' | 'report' | 'lesson'; export interface CalendarEvent { date: string; // YYYY-MM-DD type: CalendarEventType; id: string; title: string; meta: Record; } export interface CalendarResponse { events: CalendarEvent[]; month: string; // YYYY-MM } export function fetchCalendarEvents(schoolId: string, month: string): Promise { const sp = new URLSearchParams(); sp.set('schoolId', schoolId); sp.set('month', month); return apiGet(`/api/calendar?${sp.toString()}`); } /* ── Calendar Events (Custom) ──────────────────────────────────── */ export type CalendarEventItemType = 'assessment' | 'lesson' | 'reminder' | 'exam'; export interface RecurrencePattern { type: 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly'; interval: number; endDate?: string; daysOfWeek?: number[]; } export interface CalendarEventItem { id: string; schoolId: string; teacherId: string; title: string; date: string; startTime: string | null; endTime: string | null; eventType: CalendarEventItemType; subjectId: string | null; classGroupId: string | null; notes: string | null; allDay: boolean; recurrencePattern: string | null; recurrenceEnd: string | null; parentEventId: string | null; createdAt: string; updatedAt: string; subject: { id: string; name: string } | null; classGroup: { id: string; name: string } | null; childEvents?: { id: string; date: string }[]; childCount?: number; } export function fetchCalendarEventItems(schoolId: string, month: string): Promise { const sp = new URLSearchParams(); sp.set('schoolId', schoolId); sp.set('month', month); return apiGet(`/api/calendar-events?${sp.toString()}`); } export function createCalendarEventItem(data: { schoolId: string; title: string; date: string; startTime?: string | null; endTime?: string | null; eventType: CalendarEventItemType; subjectId?: string | null; classGroupId?: string | null; notes?: string | null; allDay?: boolean; recurrencePattern?: RecurrencePattern | null; recurrenceEnd?: string | null; }): Promise { return apiPost('/api/calendar-events', data); } export function updateCalendarEventItem(id: string, data: { title?: string; date?: string; startTime?: string | null; endTime?: string | null; eventType?: CalendarEventItemType; subjectId?: string | null; classGroupId?: string | null; notes?: string | null; allDay?: boolean; recurrencePattern?: RecurrencePattern | null; recurrenceEnd?: string | null; editMode?: 'series' | 'instance'; }): Promise { return apiPut(`/api/calendar-events/${id}`, data); } export function deleteCalendarEventItem(id: string, mode?: 'series' | 'instance'): Promise<{ success: boolean }> { const query = mode ? `?mode=${mode}` : ''; return apiDelete<{ success: boolean }>(`/api/calendar-events/${id}${query}`); } /* ── Lesson Plans ────────────────────────────────────────────────── */ export type LessonPlanStatus = 'draft' | 'scheduled' | 'completed' | 'cancelled'; export interface LessonPlan { id: string; teacherId: string; classGroupId: string; subjectId: string | null; title: string; description: string | null; date: string; durationMin: number; status: LessonPlanStatus; objectives: string | null; materials: string | null; homework: string | null; reflection: string | null; linkedCompetencyIds: string | null; createdAt: string; updatedAt: string; teacher: { id: string; firstName: string; lastName: string }; classGroup: { id: string; name: string; gradeLevel: number }; subject: { id: string; name: string } | null; } export interface LessonPlanInput { title: string; description?: string; classGroupId: string; subjectId?: string | null; date: string; durationMin?: number; status?: LessonPlanStatus; objectives?: string[]; materials?: string[]; homework?: string; reflection?: string; linkedCompetencyIds?: string[]; } export function listLessonPlans(params?: { classGroupId?: string; dateFrom?: string; dateTo?: string; status?: string; }): Promise { const sp = new URLSearchParams(); if (params?.classGroupId) sp.set('classGroupId', params.classGroupId); if (params?.dateFrom) sp.set('dateFrom', params.dateFrom); if (params?.dateTo) sp.set('dateTo', params.dateTo); if (params?.status) sp.set('status', params.status); return apiGet(`/api/lesson-plans?${sp.toString()}`); } export function getLessonPlan(id: string): Promise { return apiGet(`/api/lesson-plans/${id}`); } export function createLessonPlan(data: LessonPlanInput): Promise { return apiPost('/api/lesson-plans', data); } export function updateLessonPlan(id: string, data: Partial): Promise { return apiPut(`/api/lesson-plans/${id}`, data); } export function deleteLessonPlan(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/lesson-plans/${id}`); } /* ── Parent Communication ────────────────────────────────────────── */ export type ParentRelationship = 'parent' | 'guardian' | 'emergency'; export type ParentPreferredContact = 'email' | 'phone' | 'both'; export type ParentMessageCategory = 'general' | 'progress' | 'assessment' | 'behavior' | 'attendance' | 'event'; export type ParentMessagePriority = 'low' | 'normal' | 'high' | 'urgent'; export type ParentMessageStatus = 'draft' | 'sent' | 'delivered' | 'read' | 'replied'; export interface ParentContact { id: string; studentId: string; firstName: string; lastName: string; email: string | null; phone: string | null; relationship: string; preferredContact: string; preferredLanguage: string; notes: string | null; createdAt: string; updatedAt: string; student: { id: string; firstName: string; lastName: string; enrollments: Array<{ classGroup: { id: string; name: string; gradeLevel: number }; }>; }; } export interface ParentContactInput { studentId: string; firstName: string; lastName: string; email?: string | null; phone?: string | null; relationship?: ParentRelationship; preferredContact?: ParentPreferredContact; preferredLanguage?: string; notes?: string | null; } export interface ParentMessage { id: string; parentId: string; teacherId: string; studentId: string; subject: string; body: string; category: string; priority: string; status: string; readAt: string | null; reply: string | null; replyAt: string | null; createdAt: string; updatedAt: string; parent: { id: string; firstName: string; lastName: string; email: string | null; phone: string | null; relationship: string; preferredContact: string; preferredLanguage: string; notes: string | null; }; teacher: { id: string; firstName: string; lastName: string }; student: { id: string; firstName: string; lastName: string; enrollments: Array<{ classGroup: { id: string; name: string; gradeLevel: number }; }>; }; } export interface ParentMessageInput { parentId: string; studentId: string; subject: string; body: string; category?: ParentMessageCategory; priority?: ParentMessagePriority; status?: ParentMessageStatus; } export interface ParentMessageUpdate { subject?: string; body?: string; category?: ParentMessageCategory; priority?: ParentMessagePriority; status?: ParentMessageStatus; readAt?: string | null; reply?: string | null; replyAt?: string | null; } export interface ParentMessageListResponse { items: ParentMessage[]; nextCursor: string | null; hasMore: boolean; } export function listParentContacts(params?: { studentId?: string; search?: string; }): Promise { const sp = new URLSearchParams(); if (params?.studentId) sp.set('studentId', params.studentId); if (params?.search) sp.set('search', params.search); return apiGet(`/api/parents?${sp.toString()}`); } export function createParentContact(data: ParentContactInput): Promise { return apiPost('/api/parents', data); } export function updateParentContact(id: string, data: Partial): Promise { return apiPut(`/api/parents/${id}`, data); } export function deleteParentContact(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/parents/${id}`); } export function listParentMessages(params?: { teacherId?: string; parentId?: string; studentId?: string; category?: string; status?: string; cursor?: string; limit?: number; }): Promise { const sp = new URLSearchParams(); if (params?.teacherId) sp.set('teacherId', params.teacherId); if (params?.parentId) sp.set('parentId', params.parentId); if (params?.studentId) sp.set('studentId', params.studentId); if (params?.category) sp.set('category', params.category); if (params?.status) sp.set('status', params.status); if (params?.cursor) sp.set('cursor', params.cursor); if (params?.limit) sp.set('limit', String(params.limit)); return apiGet(`/api/parent-messages?${sp.toString()}`); } export function createParentMessage(data: ParentMessageInput): Promise { return apiPost('/api/parent-messages', data); } export function updateParentMessage(id: string, data: ParentMessageUpdate): Promise { return apiPut(`/api/parent-messages/${id}`, data); } export function deleteParentMessage(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/parent-messages/${id}`); } /* ── Behavior Tracking ───────────────────────────────────────────── */ export type BehaviorValence = 'positive' | 'negative' | 'neutral'; export type BehaviorSeverity = 'minor' | 'moderate' | 'major'; export interface BehaviorCategory { id: string; schoolId: string; name: string; color: string; valence: BehaviorValence; icon: string | null; createdAt: string; updatedAt: string; _count?: { incidents: number }; } export interface BehaviorCategoryInput { schoolId: string; name: string; color: string; valence: BehaviorValence; icon?: string | null; } export interface BehaviorIncident { id: string; studentId: string; teacherId: string; classGroupId: string | null; schoolId: string; categoryId: string; date: string; severity: BehaviorSeverity; description: string; location: string | null; followUpAction: string | null; resolved: boolean; resolvedAt: string | null; resolvedById: string | null; createdAt: string; updatedAt: string; student: { id: string; firstName: string; lastName: string; deletedAt: string | null; }; teacher: { id: string; firstName: string; lastName: string }; classGroup: { id: string; name: string; gradeLevel: number } | null; category: { id: string; name: string; color: string; valence: BehaviorValence; icon: string | null; }; resolvedBy: { id: string; firstName: string; lastName: string } | null; } export interface BehaviorIncidentInput { studentId: string; classGroupId?: string | null; schoolId: string; categoryId: string; date: string; severity: BehaviorSeverity; description: string; location?: string | null; followUpAction?: string | null; resolved?: boolean; } export interface BehaviorIncidentUpdate { studentId?: string; classGroupId?: string | null; categoryId?: string; date?: string; severity?: BehaviorSeverity; description?: string; location?: string | null; followUpAction?: string | null; resolved?: boolean; } export function fetchBehaviorCategories(schoolId: string): Promise { const sp = new URLSearchParams(); sp.set('schoolId', schoolId); return apiGet(`/api/behavior-categories?${sp.toString()}`); } export function createBehaviorCategory(data: BehaviorCategoryInput): Promise { return apiPost('/api/behavior-categories', data); } export function updateBehaviorCategory( id: string, data: Partial ): Promise { return apiPut(`/api/behavior-categories/${id}`, data); } export function deleteBehaviorCategory(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/behavior-categories/${id}`); } export function fetchBehaviorIncidents(filters?: { schoolId?: string; studentId?: string; classGroupId?: string; dateFrom?: string; dateTo?: string; categoryId?: string; severity?: string; resolved?: string; teacherId?: string; }): Promise { const sp = new URLSearchParams(); if (filters?.schoolId) sp.set('schoolId', filters.schoolId); if (filters?.studentId) sp.set('studentId', filters.studentId); if (filters?.classGroupId) sp.set('classGroupId', filters.classGroupId); if (filters?.dateFrom) sp.set('dateFrom', filters.dateFrom); if (filters?.dateTo) sp.set('dateTo', filters.dateTo); if (filters?.categoryId) sp.set('categoryId', filters.categoryId); if (filters?.severity) sp.set('severity', filters.severity); if (filters?.resolved) sp.set('resolved', filters.resolved); if (filters?.teacherId) sp.set('teacherId', filters.teacherId); return apiGet(`/api/behavior-incidents?${sp.toString()}`); } export function createBehaviorIncident(data: BehaviorIncidentInput): Promise { return apiPost('/api/behavior-incidents', data); } export function updateBehaviorIncident( id: string, data: BehaviorIncidentUpdate ): Promise { return apiPut(`/api/behavior-incidents/${id}`, data); } export function deleteBehaviorIncident(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/behavior-incidents/${id}`); } /* ── Rubric Library ───────────────────────────────────────────────── */ export interface RubricLevel { id: string; criterionId: string; label: string; description: string; points: number; order: number; createdAt: string; updatedAt: string; } export interface RubricCriterion { id: string; rubricId: string; name: string; description: string | null; weight: number; maxPoints: number; order: number; createdAt: string; updatedAt: string; levels: RubricLevel[]; } export interface Rubric { id: string; schoolId: string; teacherId: string; subjectId: string | null; title: string; description: string | null; type: string; // ANALYTIC | HOLISTIC maxPoints: number; isPublic: boolean; version: number; createdAt: string; updatedAt: string; criteria: RubricCriterion[]; teacher?: { id: string; firstName: string; lastName: string }; subject?: { id: string; name: string } | null; } export interface RubricLevelInput { id?: string; label: string; description: string; points: number; order?: number; } export interface RubricCriterionInput { id?: string; name: string; description?: string | null; weight?: number; maxPoints: number; order?: number; levels: RubricLevelInput[]; } export interface RubricInput { schoolId: string; title: string; description?: string | null; type?: 'ANALYTIC' | 'HOLISTIC'; subjectId?: string | null; maxPoints?: number; isPublic?: boolean; criteria: RubricCriterionInput[]; } export interface RubricUpdateInput { title?: string; description?: string | null; type?: 'ANALYTIC' | 'HOLISTIC'; subjectId?: string | null; maxPoints?: number; isPublic?: boolean; criteria?: RubricCriterionInput[]; } export function fetchRubrics(schoolId: string, teacherId?: string, subjectId?: string): Promise { const sp = new URLSearchParams(); sp.set('schoolId', schoolId); if (teacherId) sp.set('teacherId', teacherId); if (subjectId) sp.set('subjectId', subjectId); return apiGet(`/api/rubrics?${sp.toString()}`); } export function fetchRubric(id: string): Promise { return apiGet(`/api/rubrics/${id}`); } export function createRubric(data: RubricInput): Promise { return apiPost('/api/rubrics', data); } export function updateRubric(id: string, data: RubricUpdateInput): Promise { return apiPut(`/api/rubrics/${id}`, data); } export function deleteRubric(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/rubrics/${id}`); } export function duplicateRubric(id: string): Promise { return apiPost(`/api/rubrics/${id}/duplicate`); } /* ── Rubric Templates & Grading ───────────────────────────────────── */ export interface RubricTemplate { id: string; title: string; description: string; subject: string; type: 'ANALYTIC' | 'HOLISTIC'; maxPoints: number; criteria: Array<{ name: string; description: string; weight: number; maxPoints: number; levels: Array<{ label: string; description: string; points: number }>; }>; } export function fetchRubricTemplates(subject?: string): Promise { const sp = new URLSearchParams(); if (subject && subject !== 'all') sp.set('subject', subject); return apiGet(`/api/rubrics/templates?${sp.toString()}`); } export interface RubricGradeResult { rubricId: string; studentId: string; finalScore: number; maxPoints: number; overallPercentage: number; grade: number; gradeLabel: string; criterionBreakdown: Array<{ criterionId: string; criterionName: string; maxPoints: number; earnedPoints: number; selectedLevel: { id: string; label: string; description: string } | null; percentage: number; }>; } export function gradeWithRubric(rubricId: string, data: { studentId: string; scores: Array<{ criterionId: string; levelId: string; points: number }>; note?: string; }): Promise { return apiPost(`/api/rubrics/${rubricId}/grade`, data); } export interface RubricAnalytics { rubricId: string; rubricTitle: string; criteriaAnalytics: Array<{ criterionId: string; criterionName: string; maxPoints: number; weight: number; classAverage: number; classMedian: number; highestScore: number; lowestScore: number; levelDistribution: Array<{ levelId: string; label: string; points: number; description: string; percentage: number; }>; }>; gradeDistribution: Array<{ grade: string; range: string; count: number; color: string; }>; overallStats: { averagePercentage: number; totalAssessments: number; totalStudents: number; totalMaxPoints: number; averageScore: number; }; } export function fetchRubricAnalytics(rubricId: string): Promise { return apiGet(`/api/rubrics/${rubricId}/analytics`); } /* ── Curriculum Standards ─────────────────────────────────────────── */ export interface CurriculumStandard { id: string; schoolId: string; subjectId: string | null; code: string; title: string; description: string | null; gradeLevel: number | null; category: string | null; source: string | null; isDemo: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; subject: { id: string; name: string } | null; competencyLinks: CurriculumStandardLink[]; } export interface CurriculumStandardLink { id: string; standardId: string; competencyId: string; coverageLevel: string | null; notes: string | null; createdAt: string; competency: { id: string; code: string; title: string; category: { id: string; name: string; color: string | null } | null; }; } export function fetchCurriculumStandards(schoolId: string, subjectId?: string, gradeLevel?: number): Promise { const sp = new URLSearchParams(); sp.set('schoolId', schoolId); if (subjectId && subjectId !== 'all') sp.set('subjectId', subjectId); if (gradeLevel) sp.set('gradeLevel', String(gradeLevel)); return apiGet(`/api/curriculum-standards?${sp.toString()}`); } export function fetchCurriculumStandard(id: string): Promise { return apiGet(`/api/curriculum-standards/${id}`); } export function createCurriculumStandard(data: { schoolId: string; subjectId?: string | null; code: string; title: string; description?: string | null; gradeLevel?: number | null; category?: string | null; source?: string | null; isDemo?: boolean; }): Promise { return apiPost('/api/curriculum-standards', data); } export function updateCurriculumStandard(id: string, data: { subjectId?: string | null; code?: string; title?: string; description?: string | null; gradeLevel?: number | null; category?: string | null; source?: string | null; }): Promise { return apiPut(`/api/curriculum-standards/${id}`, data); } export function deleteCurriculumStandard(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/curriculum-standards/${id}`); } export function linkCurriculumStandard(standardId: string, data: { competencyId: string; coverageLevel?: 'full' | 'partial' | 'related' | null; notes?: string | null; }): Promise { return apiPost(`/api/curriculum-standards/${standardId}/links`, data); } export function unlinkCurriculumStandard(standardId: string, competencyId: string): Promise<{ success: boolean }> { return apiPost<{ success: boolean }>(`/api/curriculum-standards/${standardId}/links`, { action: 'unlink', competencyId, }); } export function fetchCurriculumStandardLinks(standardId: string): Promise { return apiGet(`/api/curriculum-standards/${standardId}/links`); } /* ── Attendance Analytics ─────────────────────────────────────────── */ export interface AttendanceAnalyticsData { trendData: Array<{ week: string; attendanceRate: number; absentRate: number; present: number; absent: number; excused: number; late: number; total: number; }>; dayOfWeekAnalysis: Array<{ day: number; dayName: string; dayNameDe: string; attendanceRate: number; absentCount: number; totalRecords: number; }>; absencePatterns: Array<{ studentId: string; firstName: string; lastName: string; present: number; absent: number; excused: number; late: number; total: number; absenceRate: number; attendanceRate: number; }>; riskIndicators: Array<{ studentId: string; firstName: string; lastName: string; absenceRate: number; totalAbsences: number; totalSessions: number; riskLevel: 'low' | 'medium' | 'high' | 'critical'; }>; classComparison: Array<{ classGroupId: string; className: string; gradeLevel: number; present: number; absent: number; excused: number; late: number; total: number; attendanceRate: number; absenceRate: number; }>; statusDistribution: { present: number; absent: number; excused: number; late: number; }; totalSessions: number; totalRecords: number; } export function fetchAttendanceAnalytics(schoolId: string, classGroupId?: string): Promise { const sp = new URLSearchParams(); sp.set('schoolId', schoolId); if (classGroupId) sp.set('classGroupId', classGroupId); return apiGet(`/api/attendance/analytics?${sp.toString()}`); } /* ── Comment Bank ────────────────────────────────────────────────── */ export interface CommentCategory { id: string; schoolId: string; name: string; color: string; icon: string | null; createdAt: string; updatedAt: string; _count?: { comments: number }; } export interface CommentCategoryInput { schoolId: string; name: string; color: string; icon?: string | null; } export interface CommentBankEntry { id: string; schoolId: string; teacherId: string; categoryId: string; subjectId: string | null; title: string; text: string; gradeLevel: string | null; schoolType: string | null; isPublic: boolean; usageCount: number; tags: string | null; createdAt: string; updatedAt: string; teacher: { id: string; firstName: string; lastName: string }; category: { id: string; name: string; color: string; icon: string | null }; subject: { id: string; name: string } | null; } export interface CommentBankEntryInput { schoolId: string; categoryId: string; subjectId?: string | null; title: string; text: string; gradeLevel?: string | null; schoolType?: string | null; isPublic?: boolean; tags?: string | null; } export interface CommentBankEntryUpdate { categoryId?: string; subjectId?: string | null; title?: string; text?: string; gradeLevel?: string | null; schoolType?: string | null; isPublic?: boolean; tags?: string | null; incrementUsage?: boolean; } export function fetchCommentCategories(schoolId: string): Promise { const sp = new URLSearchParams(); sp.set('schoolId', schoolId); return apiGet(`/api/comment-categories?${sp.toString()}`); } export function createCommentCategory(data: CommentCategoryInput): Promise { return apiPost('/api/comment-categories', data); } export function updateCommentCategory( id: string, data: Partial ): Promise { return apiPut(`/api/comment-categories/${id}`, data); } export function deleteCommentCategory(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/comment-categories/${id}`); } export function fetchCommentBank(filters?: { schoolId?: string; teacherId?: string; subjectId?: string; categoryId?: string; search?: string; gradeLevel?: string; isPublic?: string; }): Promise { const sp = new URLSearchParams(); if (filters?.schoolId) sp.set('schoolId', filters.schoolId); if (filters?.teacherId) sp.set('teacherId', filters.teacherId); if (filters?.subjectId) sp.set('subjectId', filters.subjectId); if (filters?.categoryId) sp.set('categoryId', filters.categoryId); if (filters?.search) sp.set('search', filters.search); if (filters?.gradeLevel) sp.set('gradeLevel', filters.gradeLevel); if (filters?.isPublic) sp.set('isPublic', filters.isPublic); return apiGet(`/api/comment-bank?${sp.toString()}`); } export function createCommentBankEntry(data: CommentBankEntryInput): Promise { return apiPost('/api/comment-bank', data); } export function updateCommentBankEntry( id: string, data: CommentBankEntryUpdate ): Promise { return apiPut(`/api/comment-bank/${id}`, data); } export function deleteCommentBankEntry(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/comment-bank/${id}`); } /* ── Notifications ─────────────────────────────────────────────────── */ export interface AssessmentNotification { id: string; type: 'assessment'; title: string; description: string; daysUntil: number; timestamp: string; classGroupId: string; subjectId: string; assessmentId: string; isRead: boolean; } export interface MissingObservationNotification { id: string; type: 'missing_observation'; title: string; description: string; daysSince: number; timestamp: string; studentId: string; classGroupId: string; isRead: boolean; } /** @deprecated Use DBNotification instead */ export interface NotificationData { upcomingAssessments: AssessmentNotification[]; missingObservations: MissingObservationNotification[]; unreadCount: number; } /** New DB-backed notification type */ export interface DBNotification { id: string; type: string; title: string; message: string; isRead: boolean; actionUrl: string | null; relatedId: string | null; createdAt: string; } export interface DBNotificationData { notifications: DBNotification[]; unreadCount: number; } export function fetchNotifications(): Promise { return apiGet('/api/notifications'); } export function fetchDBNotifications(): Promise { return apiGet('/api/notifications'); } export function markServerNotificationsRead(ids: string[]): Promise<{ success: boolean }> { return apiPut<{ success: boolean }>('/api/notifications', { ids }); } export function markAllNotificationsRead(): Promise<{ success: boolean }> { return apiPut<{ success: boolean }>('/api/notifications', { markAll: true }); } export function markSingleNotificationRead(id: string): Promise<{ success: boolean }> { return apiPut<{ success: boolean }>('/api/notifications', { ids: [id] }); } export function createNotification(data: { schoolId: string; userId: string; type: string; title: string; message: string; actionUrl?: string; relatedId?: string; }): Promise<{ notification: DBNotification }> { return apiPost<{ notification: DBNotification }>('/api/notifications', data); } export function deleteNotifications(ids?: string[]): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/notifications${ids ? `?ids=${ids.join(',')}` : ''}`); } /* ── Batch Delete & Reorder ─────────────────────────────────────────── */ export async function deleteBatchProgressEntries(ids: string[]): Promise { await apiRequest<{ deleted: number }>('/api/learning-progress/batch', { method: 'DELETE', body: JSON.stringify({ ids }), }); } export async function reorderStudents(classGroupId: string, studentIds: string[]): Promise { await apiPut<{ success: boolean }>('/api/students/reorder', { classGroupId, studentIds }); } /* ── Parent Links ────────────────────────────────────────────────────── */ export interface ParentStudentLinkData { id: string; parentId: string; studentId: string; schoolId: string; relationship: string | null; createdAt: string; updatedAt: string; parent: { id: string; firstName: string; lastName: string; email: string; role: string }; student: { id: string; firstName: string; lastName: string; dateOfBirth?: string | null; externalId?: string | null; schoolId: string; enrollments?: Array<{ classGroup: { id: string; name: string; gradeLevel: number } }> }; school: { id: string; name: string }; } export function fetchParentLinks(parentId?: string): Promise { const params = new URLSearchParams(); if (parentId) params.set('parentId', parentId); return apiGet(`/api/parent-links?${params.toString()}`); } export function createParentLink(data: { parentId: string; studentId: string; schoolId: string; relationship?: string; }): Promise { return apiPost('/api/parent-links', data); } export function updateParentLink(id: string, data: { relationship?: string }): Promise { return apiPut(`/api/parent-links/${id}`, data); } export function deleteParentLink(id: string): Promise<{ message: string }> { return apiDelete<{ message: string }>(`/api/parent-links/${id}`); } /* ── Student Account Creation ────────────────────────────────────────── */ export function createStudentUserAccount(data: { schoolId: string; email: string; password: string; firstName: string; lastName: string; role: string; studentId?: string; locale?: string; }): Promise> { return apiPost>('/api/users', data); } export function bulkCreateStudentAccounts(data: { schoolId: string; defaultPassword: string; studentIds: string[]; emailDomain?: string; locale?: string; }): Promise<{ created: Array>; count: number }> { return apiPost<{ created: Array>; count: number }>('/api/users', { ...data, action: 'bulkCreateStudents' }); } /* ── Report Schedules ──────────────────────────────────────────────── */ export interface ReportScheduleData { id: string; schoolId: string; classGroupId: string | null; template: string; frequency: string; dayOfWeek: number | null; dayOfMonth: number | null; monthOfYear: number | null; recipients: string | null; includeStudents: boolean; includeGrades: boolean; includeAttendance: boolean; includeBehavior: boolean; includeCompetencies: boolean; isActive: boolean; lastRunAt: string | null; nextRunAt: string | null; isDemo: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; classGroup?: { id: string; name: string; gradeLevel: number } | null; } export function fetchReportSchedules(schoolId: string): Promise { return apiGet(`/api/report-schedules?schoolId=${schoolId}`); } export function createReportSchedule(data: { schoolId: string; classGroupId?: string; template: string; frequency: string; dayOfWeek?: number | null; dayOfMonth?: number | null; monthOfYear?: number | null; recipients?: string[]; includeStudents?: boolean; includeGrades?: boolean; includeAttendance?: boolean; includeBehavior?: boolean; includeCompetencies?: boolean; }): Promise { return apiPost('/api/report-schedules', data); } export function updateReportSchedule(id: string, data: { classGroupId?: string; template?: string; frequency?: string; dayOfWeek?: number | null; dayOfMonth?: number | null; monthOfYear?: number | null; recipients?: string[]; includeStudents?: boolean; includeGrades?: boolean; includeAttendance?: boolean; includeBehavior?: boolean; includeCompetencies?: boolean; isActive?: boolean; }): Promise { return apiPut(`/api/report-schedules/${id}`, data); } export function deleteReportSchedule(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/report-schedules/${id}`); } export function runReportSchedule(id: string): Promise<{ success: boolean; lastRunAt: string; nextRunAt: string; recipientsNotified: number; }> { return apiPost(`/api/report-schedules/${id}/run`, {}); } /* ── School Districts ──────────────────────────────────────────────── */ export interface SchoolDistrictData { id: string; name: string; code: string | null; region: string | null; country: string; adminEmail: string | null; isActive: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; schools: Array<{ id: string; name: string; schoolType: string; country: string }>; schoolCount?: number; totalStudents?: number; } export function fetchDistricts(): Promise { return apiGet('/api/districts'); } export function createDistrict(data: { name: string; code?: string; region?: string; country?: string; adminEmail?: string; }): Promise { return apiPost('/api/districts', data); } export function updateDistrict(id: string, data: { name?: string; code?: string; region?: string; country?: string; adminEmail?: string; isActive?: boolean; }): Promise { return apiPut(`/api/districts/${id}`, data); } export function deleteDistrict(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/districts/${id}`); } export function fetchDistrictSchools(districtId: string): Promise> { return apiGet(`/api/districts/${districtId}/schools`); } export function assignSchoolToDistrict(districtId: string, schoolId: string): Promise<{ success: boolean }> { return apiPost<{ success: boolean }>(`/api/districts/${districtId}/schools`, { schoolId }); } /* ── Badges ──────────────────────────────────────────────────────── */ export interface BadgeData { id: string; schoolId: string; name: string; description: string | null; icon: string; color: string; category: string; requirementType: string; requirementValue: number | null; isAuto: boolean; isDemo: boolean; createdAt: string; updatedAt: string; _count?: { studentBadges: number }; } export interface StudentBadgeData { id: string; schoolId: string; studentId: string; badgeId: string; awardedAt: string; awardedBy: string | null; notes: string | null; badge: BadgeData; student: { id: string; firstName: string; lastName: string }; awardedByUser?: { id: string; firstName: string; lastName: string } | null; } export interface BadgeProgressData { badgeId: string; name: string; icon: string; color: string; category: string; earned: boolean; progress: number; current: number; target: number; } export function fetchBadges(schoolId: string, category?: string): Promise { const params = new URLSearchParams({ schoolId }); if (category) params.set('category', category); return apiGet(`/api/badges?${params.toString()}`); } export function createBadge(data: { schoolId: string; name: string; description?: string; icon: string; color?: string; category: string; requirementType: string; requirementValue?: number; isAuto?: boolean; }): Promise { return apiPost('/api/badges', data); } export function updateBadge(id: string, data: Partial): Promise { return apiPut(`/api/badges/${id}`, data); } export function deleteBadge(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/badges/${id}`); } export function fetchStudentBadges(schoolId: string, studentId?: string, badgeId?: string): Promise { const params = new URLSearchParams({ schoolId }); if (studentId) params.set('studentId', studentId); if (badgeId) params.set('badgeId', badgeId); return apiGet(`/api/student-badges?${params.toString()}`); } export function awardBadgeToStudent(data: { schoolId: string; studentId: string; badgeId: string; notes?: string; }): Promise { return apiPost('/api/student-badges', data); } export function removeStudentBadge(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/student-badges/${id}`); } export function fetchBadgeProgress(schoolId: string, studentId: string): Promise { const params = new URLSearchParams({ schoolId, studentId }); return apiGet(`/api/badge-progress?${params.toString()}`); } export function checkAndAwardBadges(schoolId: string, studentId: string): Promise { return apiPost('/api/badge-check', { schoolId, studentId }); } export function seedBadges(schoolId: string): Promise<{ success: boolean }> { return apiPost<{ success: boolean }>('/api/badge-seed', { schoolId }); } /* ── Newsletter / School Blog ─────────────────────────────────────── */ export interface NewsletterData { id: string; schoolId: string; authorId: string; title: string; content: string; summary: string | null; imageUrl: string | null; category: string; isPublished: boolean; publishedAt: string | null; tags: string | null; isDemo: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; author: { id: string; firstName: string; lastName: string }; } export function fetchNewsletters(schoolId: string, published?: boolean, category?: string, limit?: number): Promise<{ newsletters: NewsletterData[]; total: number }> { const params = new URLSearchParams({ schoolId }); if (published !== undefined) params.set('published', String(published)); if (category) params.set('category', category); if (limit) params.set('limit', String(limit)); return apiGet<{ newsletters: NewsletterData[]; total: number }>(`/api/newsletters?${params.toString()}`); } export function fetchNewsletter(id: string): Promise { return apiGet(`/api/newsletters/${id}`); } export function createNewsletter(data: { schoolId: string; authorId: string; title: string; content: string; summary?: string; imageUrl?: string; category?: string; tags?: string[]; isDemo?: boolean; }): Promise { return apiPost('/api/newsletters', data); } export function updateNewsletter(id: string, data: Partial<{ title: string; content: string; summary: string; imageUrl: string; category: string; tags: string[]; }>): Promise { return apiPut(`/api/newsletters/${id}`, data); } export function publishNewsletter(id: string): Promise { return apiPost(`/api/newsletters/${id}`, { action: 'publish' }); } export function unpublishNewsletter(id: string): Promise { return apiPost(`/api/newsletters/${id}`, { action: 'unpublish' }); } export function deleteNewsletter(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/newsletters/${id}`); } /* ── Competition System ───────────────────────────────────────────── */ export interface CompetitionData { id: string; schoolId: string; title: string; description: string | null; competitionType: string; category: string; subjectId: string | null; status: string; startDate: string; endDate: string; registrationDeadline: string | null; maxParticipants: number | null; scoringType: string; rules: string | null; isPublic: boolean; isDemo: boolean; isFederation: boolean; federationSchedule: string | null; createdById: string; createdAt: string; updatedAt: string; deletedAt: string | null; school?: { id: string; name: string; primaryColor?: string | null }; createdBy?: { id: string; firstName: string; lastName: string }; subject?: { id: string; name: string } | null; _count?: { participants: number; rewards: number }; } export interface CompetitionParticipantData { id: string; competitionId: string; participantType: string; participantId: string; userId: string | null; registeredAt: string; score: number; rank: number | null; isDisqualified: boolean; notes: string | null; isDemo: boolean; createdAt: string; updatedAt: string; } export interface CompetitionRewardData { id: string; competitionId: string; name: string; description: string | null; rewardType: string; rewardValue: string | null; rewardProvider: string | null; rankRequirement: number | null; pointsRequired: number | null; quantity: number; claimedCount: number; imageUrl: string | null; isDemo: boolean; createdAt: string; updatedAt: string; _count?: { claims: number }; } export interface CompetitionLeaderboardEntry { id: string; competitionId: string; participantType: string; participantId: string; participantName: string; score: number; rank: number | null; updatedAt: string; } export interface RewardClaimData { id: string; schoolId: string; competitionId: string; rewardId: string; userId: string; claimedAt: string; code: string | null; status: string; expiresAt: string | null; notes: string | null; isDemo: boolean; createdAt: string; user?: { id: string; firstName: string; lastName: string }; competition?: { id: string; title: string; competitionType?: string }; reward?: { id: string; name: string; rewardType: string; rewardProvider: string | null; imageUrl: string | null }; school?: { id: string; name: string }; } export function fetchCompetitions( schoolId: string, status?: string, competitionType?: string, category?: string, limit?: number, offset?: number ): Promise<{ competitions: CompetitionData[]; total: number }> { const params = new URLSearchParams({ schoolId }); if (status) params.set('status', status); if (competitionType) params.set('competitionType', competitionType); if (category) params.set('category', category); if (limit) params.set('limit', String(limit)); if (offset) params.set('offset', String(offset)); return apiGet<{ competitions: CompetitionData[]; total: number }>(`/api/competitions?${params.toString()}`); } export interface FederationLeaderboardEntry { schoolId: string; schoolName: string; teamScore: number; participantCount: number; rank: number; } export function fetchFederationCompetitions( category?: string, schedule?: string, status?: string, limit?: number, offset?: number ): Promise<{ competitions: CompetitionData[]; total: number; schoolLeaderboard: FederationLeaderboardEntry[] }> { const params = new URLSearchParams(); if (category) params.set('category', category); if (schedule) params.set('schedule', schedule); if (status) params.set('status', status); if (limit) params.set('limit', String(limit)); if (offset) params.set('offset', String(offset)); return apiGet<{ competitions: CompetitionData[]; total: number; schoolLeaderboard: FederationLeaderboardEntry[] }>(`/api/competitions/federation?${params.toString()}`); } export function createFederationCompetition(data: { schoolId: string; title: string; description?: string | null; category: string; subjectId?: string | null; schedule: string; startDate: string; endDate: string; registrationDeadline?: string | null; maxParticipants?: number | null; rules?: string | null; isPublic?: boolean; }): Promise { return apiPost('/api/competitions/federation', data); } export function fetchPublicCompetitions( limit?: number, offset?: number ): Promise<{ competitions: CompetitionData[]; total: number }> { const params = new URLSearchParams({ isPublic: 'true' }); if (limit) params.set('limit', String(limit)); if (offset) params.set('offset', String(offset)); return apiGet<{ competitions: CompetitionData[]; total: number }>(`/api/competitions?${params.toString()}`); } export function fetchCompetition(id: string): Promise { return apiGet(`/api/competitions/${id}`); } export function createCompetition(data: { schoolId: string; title: string; description?: string | null; competitionType: string; category: string; subjectId?: string | null; status?: string; startDate: string; endDate: string; registrationDeadline?: string | null; maxParticipants?: number | null; scoringType?: string; rules?: string | null; isPublic?: boolean; isDemo?: boolean; }): Promise { return apiPost('/api/competitions', data); } export function updateCompetition(id: string, data: Partial<{ title: string; description: string | null; competitionType: string; category: string; subjectId: string | null; status: string; startDate: string; endDate: string; registrationDeadline: string | null; maxParticipants: number | null; scoringType: string; rules: string | null; isPublic: boolean; }>): Promise { return apiPut(`/api/competitions/${id}`, data); } export function deleteCompetition(id: string): Promise<{ success: boolean }> { return apiDelete<{ success: boolean }>(`/api/competitions/${id}`); } export function registerCompetitionParticipant( competitionId: string, data: { participantType: string; participantId: string; userId?: string | null; notes?: string | null; } ): Promise { return apiPost(`/api/competitions/${competitionId}`, { action: 'register', ...data, }); } export function updateCompetitionScore( competitionId: string, data: { participantType: string; participantId: string; score: number; isDisqualified?: boolean; notes?: string | null; } ): Promise { return apiPost(`/api/competitions/${competitionId}`, { action: 'update_score', ...data, }); } export function fetchCompetitionLeaderboard( competitionId: string ): Promise<{ leaderboard: CompetitionLeaderboardEntry[] }> { return apiGet<{ leaderboard: CompetitionLeaderboardEntry[] }>(`/api/competitions/${competitionId}/leaderboard`); } export function fetchCompetitionRewards( competitionId: string ): Promise<{ rewards: CompetitionRewardData[] }> { return apiGet<{ rewards: CompetitionRewardData[] }>(`/api/competitions/${competitionId}/rewards`); } export function createCompetitionReward( competitionId: string, data: { name: string; description?: string | null; rewardType: string; rewardValue?: string | null; rewardProvider?: string | null; rankRequirement?: number | null; pointsRequired?: number | null; quantity?: number; imageUrl?: string | null; isDemo?: boolean; } ): Promise { return apiPost(`/api/competitions/${competitionId}/rewards`, data); } export function fetchRewardClaims( schoolId?: string, competitionId?: string, status?: string ): Promise<{ claims: RewardClaimData[] }> { const params = new URLSearchParams(); if (schoolId) params.set('schoolId', schoolId); if (competitionId) params.set('competitionId', competitionId); if (status) params.set('status', status); const query = params.toString(); return apiGet<{ claims: RewardClaimData[] }>(`/api/reward-claims${query ? `?${query}` : ''}`); } export function claimReward(data: { competitionId: string; rewardId: string; code?: string | null; expiresAt?: string | null; notes?: string | null; isDemo?: boolean; }): Promise { return apiPost('/api/reward-claims', data); } /* ── Subject Topics (German Curriculum) ─────────────────────────────── */ export interface SubjectTopicData { id: string; schoolId: string; subjectId: string; title: string; description: string | null; gradeLevel: string | null; curriculumCode: string | null; sortOrder: number; icon: string | null; color: string; isDemo: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; subject?: { id: string; name: string }; _count?: { lessons: number }; } export function fetchSubjectTopics(params?: { schoolId?: string; subjectId?: string; gradeLevel?: string; curriculumCode?: string; }): Promise { const searchParams = new URLSearchParams(); if (params?.schoolId) searchParams.set('schoolId', params.schoolId); if (params?.subjectId) searchParams.set('subjectId', params.subjectId); if (params?.gradeLevel) searchParams.set('gradeLevel', params.gradeLevel); if (params?.curriculumCode) searchParams.set('curriculumCode', params.curriculumCode); return apiGet(`/api/subject-topics?${searchParams.toString()}`); } export function fetchSubjectTopic(id: string): Promise { return apiGet(`/api/subject-topics/${id}`); } export function createSubjectTopic(data: { schoolId?: string; subjectId: string; title: string; description?: string | null; gradeLevel?: string | null; curriculumCode?: string | null; sortOrder?: number; icon?: string | null; color?: string; }): Promise { return apiPost('/api/subject-topics', data); } export function updateSubjectTopic(id: string, data: Partial<{ title: string; description: string | null; gradeLevel: string | null; curriculumCode: string | null; sortOrder: number; icon: string | null; color: string; }>): Promise { return apiPut(`/api/subject-topics/${id}`, data); } export function deleteSubjectTopic(id: string): Promise<{ message: string; id: string }> { return apiDelete<{ message: string; id: string }>(`/api/subject-topics/${id}`); } /* ── Subject Lessons (German Curriculum) ────────────────────────────── */ export interface SubjectLessonData { id: string; topicId: string; title: string; description: string | null; lessonType: string; content: string; difficulty: string; sortOrder: number; estimatedMinutes: number | null; isDemo: boolean; createdAt: string; updatedAt: string; deletedAt: string | null; _count?: { questions: number }; topic?: { id: string; title: string; schoolId: string; subjectId: string }; questions?: LessonQuestionData[]; } export function fetchSubjectLessons(topicId: string, params?: { lessonType?: string; difficulty?: string; }): Promise { const searchParams = new URLSearchParams({ topicId }); if (params?.lessonType) searchParams.set('lessonType', params.lessonType); if (params?.difficulty) searchParams.set('difficulty', params.difficulty); return apiGet(`/api/subject-lessons?${searchParams.toString()}`); } export function fetchSubjectLesson(id: string): Promise { return apiGet(`/api/subject-lessons/${id}`); } export function createSubjectLesson(data: { topicId: string; title: string; description?: string | null; lessonType?: 'explanation' | 'exercise' | 'quiz' | 'flashcard' | 'video_link'; content: string; difficulty?: 'easy' | 'medium' | 'hard'; sortOrder?: number; estimatedMinutes?: number | null; }): Promise { return apiPost('/api/subject-lessons', data); } export function updateSubjectLesson(id: string, data: Partial<{ title: string; description: string | null; lessonType: 'explanation' | 'exercise' | 'quiz' | 'flashcard' | 'video_link'; content: string; difficulty: 'easy' | 'medium' | 'hard'; sortOrder: number; estimatedMinutes: number | null; }>): Promise { return apiPut(`/api/subject-lessons/${id}`, data); } export function deleteSubjectLesson(id: string): Promise<{ message: string; id: string }> { return apiDelete<{ message: string; id: string }>(`/api/subject-lessons/${id}`); } /* ── Lesson Questions (German Curriculum) ───────────────────────────── */ export interface LessonQuestionData { id: string; lessonId: string; questionType: string; question: string; options: string | null; correctAnswer: string; explanation: string | null; points: number; sortOrder: number; isDemo: boolean; createdAt: string; updatedAt: string; } export function fetchLessonQuestions(lessonId: string, params?: { questionType?: string; }): Promise { const searchParams = new URLSearchParams({ lessonId }); if (params?.questionType) searchParams.set('questionType', params.questionType); return apiGet(`/api/lesson-questions?${searchParams.toString()}`); } export function fetchLessonQuestion(id: string): Promise { return apiGet(`/api/lesson-questions/${id}`); } export function createLessonQuestion(data: { lessonId: string; questionType: 'multiple_choice' | 'true_false' | 'fill_blank' | 'short_answer' | 'matching'; question: string; options?: string | null; correctAnswer: string; explanation?: string | null; points?: number; sortOrder?: number; }): Promise { return apiPost('/api/lesson-questions', data); } export function updateLessonQuestion(id: string, data: Partial<{ questionType: 'multiple_choice' | 'true_false' | 'fill_blank' | 'short_answer' | 'matching'; question: string; options: string | null; correctAnswer: string; explanation: string | null; points: number; sortOrder: number; }>): Promise { return apiPut(`/api/lesson-questions/${id}`, data); } export function deleteLessonQuestion(id: string): Promise<{ message: string; id: string }> { return apiDelete<{ message: string; id: string }>(`/api/lesson-questions/${id}`); } /* ── Student Answers (German Curriculum) ────────────────────────────── */ export interface StudentAnswerData { id: string; questionId: string; studentId: string; answer: string; isCorrect: boolean | null; timeTakenMs: number | null; attempts: number; isDemo: boolean; createdAt: string; updatedAt: string; correctAnswer?: string; explanation?: string | null; question?: LessonQuestionData; } export function fetchStudentAnswers(params?: { questionId?: string; lessonId?: string; }): Promise { const searchParams = new URLSearchParams(); if (params?.questionId) searchParams.set('questionId', params.questionId); if (params?.lessonId) searchParams.set('lessonId', params.lessonId); return apiGet(`/api/student-answers?${searchParams.toString()}`); } export function submitStudentAnswer(data: { questionId: string; answer: string; timeTakenMs?: number | null; }): Promise { return apiPost('/api/student-answers', data); }