// ───────────────────────────────────────────────────────────────────────────── // src/lib/api/studentsapi.ts // Uses the same TOKEN_KEY + request() pattern as studentApi.ts // ───────────────────────────────────────────────────────────────────────────── import { ApiError } from './studentApi'; import { getApiBaseUrl } from './config'; const BASE_URL = getApiBaseUrl(); const SHEIKH_URL = `${BASE_URL}/api/sheikh/my-students`; const TOKEN_KEY = 'authToken'; // must match studentApi.ts // ─── Types ──────────────────────────────────────────────────────────────────── /** Shape returned by list endpoints (all / active / top / search) */ export interface StudentListItem { id: number; fullName: string; quranLevel: string; currentStreak: number; email: string; country: string; sessionCount: number; lastSessionDate: string | null; totalEarnings: number; averageRate: number; } /** Shape returned by View-Student endpoint */ export interface StudentDetail { id: number; fullName: string; quranLevel: string; currentStreak: number; email: string; country: string; phone: string | null; goal: string | null; membershipDate: string | null; sessionCount: number; completedSessions: number; canceledSessions: number; lastSessionDate: string | null; totalEarnings: number; totalTimeMinutes: number; averageRate: number; avgRating: number | null; totalHoursSpent: number; totalRevenue: number; cancelledSessions: number; completionRate: number; } /** Shape returned by /stats endpoint */ export interface StudentsStats { totalStudents: number; activeStudentsLast7Days: number; totalEarnings: number; averageRating: number; } /** Stats for a specific student's sessions with this sheikh */ export interface StudentSessionStats { avgRating: number | null; totalMinutesSpent: number; totalRevenue: number; completedSessions: number; cancelledSessions: number; totalSessions: number; completionRate: number; lastSessionDate: string | null; } /** Normalised student shape used by the UI */ export interface Student { id: string; name: string; email: string; phone?: string; location?: string; joinedDate: string | null; totalSessions: number; completedSessions: number; cancelledSessions: number; totalMinutes: number; totalSpent: number; averageRating: number; lastSession: string | null; currentStreak: number; progress: number; level: string; goals: string[]; } /** Session shape (backend doesn't expose history yet) */ export interface Session { id?: string; date: string | null; duration: number; status: 'completed' | 'cancelled' | 'upcoming'; rating?: number; feedback?: string; price: number; topic: string; } // ─── Shared request helper (mirrors studentApi.ts) ──────────────────────────── function getHeaders(): HeadersInit { const token = localStorage.getItem(TOKEN_KEY) ?? ''; return { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }; } async function request(path: string): Promise { const url = `${SHEIKH_URL}${path}`; // Guard: check token expiry before sending const token = localStorage.getItem(TOKEN_KEY); if (token) { try { const payload = JSON.parse(atob(token.split('.')[1])); if (payload?.exp && payload.exp * 1000 < Date.now()) { localStorage.removeItem(TOKEN_KEY); throw new ApiError(401, 'Session expired — please log in again', path); } } catch (e) { if (e instanceof ApiError) throw e; localStorage.removeItem(TOKEN_KEY); // malformed token } } try { const res = await fetch(url, { headers: getHeaders() }); if (!res.ok) { const message = await res.text().catch(() => res.statusText); throw new ApiError(res.status, message, path); } const text = await res.text(); if (!text) return {} as T; try { return JSON.parse(text) as T; } catch { return text as unknown as T; } } catch (error) { if (error instanceof ApiError) throw error; throw new Error(`Network error while calling ${path}`); } } // ─── Mappers ────────────────────────────────────────────────────────────────── function capitalise(str: string): string { if (!str) return ''; return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); } function mapListItem(item: StudentListItem): Student { return { id: String(item.id), name: item.fullName, email: item.email, phone: undefined, location: item.country, joinedDate: null, totalSessions: item.sessionCount, completedSessions: item.sessionCount, // list endpoint has no split cancelledSessions: 0, totalMinutes: 0, totalSpent: item.totalEarnings, averageRating: item.averageRate, lastSession: item.lastSessionDate, currentStreak: item.currentStreak, progress: 0, level: capitalise(item.quranLevel), goals: [], }; } function mapDetail(d: StudentDetail): Student { const progress = d.sessionCount > 0 ? Math.min(100, Math.round((d.completedSessions / d.sessionCount) * 100)) : 0; return { id: String(d.id), name: d.fullName, email: d.email, phone: d.phone ?? undefined, location: d.country, joinedDate: d.membershipDate, totalSessions: d.sessionCount, completedSessions: d.completedSessions, cancelledSessions: d.canceledSessions, totalMinutes: d.totalTimeMinutes, totalSpent: d.totalEarnings, averageRating: d.averageRate, lastSession: d.lastSessionDate, currentStreak: d.currentStreak, progress, level: capitalise(d.quranLevel), goals: d.goal ? [d.goal] : [], }; } // ─── Public API functions ───────────────────────────────────────────────────── /** GET /api/sheikh/my-students */ export async function getAllStudents(): Promise { const data = await request(''); return data.map(mapListItem); } /** GET /api/sheikh/my-students/active-last-7-days */ export async function getActiveStudents(): Promise { const data = await request('/active-last-7-days'); return data.map(mapListItem); } /** GET /api/sheikh/my-students/top-students */ export async function getTopStudents(): Promise { const data = await request('/top-students'); return data.map(mapListItem); } /** GET /api/sheikh/my-students/search?keyword=… */ export async function searchStudents(keyword: string): Promise { const data = await request( `/search?keyword=${encodeURIComponent(keyword)}`, ); return data.map(mapListItem); } /** GET /api/sheikh/my-students/View-Student?id=… */ export async function getStudentDetails( id: number, ): Promise<{ student: Student; sessionHistory: Session[]; sessionStats: StudentSessionStats }> { const data = await request(`/View-Student?id=${id}`); const completed = data.completedSessions ?? 0; const cancelled = data.canceledSessions ?? 0; const total = data.sessionCount ?? 0; const completionRate = total > 0 ? Math.round((completed / total) * 1000) / 10 : 0; return { student: mapDetail(data), sessionHistory: [], sessionStats: { avgRating: data.averageRate ?? null, totalMinutesSpent: data.totalTimeMinutes ?? 0, totalRevenue: data.totalEarnings ?? 0, completedSessions: completed, cancelledSessions: cancelled, totalSessions: total, completionRate, lastSessionDate: data.lastSessionDate ?? null, }, }; } /** GET /api/sheikh/my-students/stats */ export async function getStudentsStats(): Promise { return request('/stats'); } /** GET /api/sheikh/my-students/student-session-stats?studentId=… */ export async function getStudentSessionStats(studentId: number): Promise { return request(`/student-session-stats?studentId=${studentId}`); }