Spaces:
Sleeping
Sleeping
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 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<T>(path: string): Promise<T> { | |
| 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<Student[]> { | |
| const data = await request<StudentListItem[]>(''); | |
| return data.map(mapListItem); | |
| } | |
| /** GET /api/sheikh/my-students/active-last-7-days */ | |
| export async function getActiveStudents(): Promise<Student[]> { | |
| const data = await request<StudentListItem[]>('/active-last-7-days'); | |
| return data.map(mapListItem); | |
| } | |
| /** GET /api/sheikh/my-students/top-students */ | |
| export async function getTopStudents(): Promise<Student[]> { | |
| const data = await request<StudentListItem[]>('/top-students'); | |
| return data.map(mapListItem); | |
| } | |
| /** GET /api/sheikh/my-students/search?keyword=β¦ */ | |
| export async function searchStudents(keyword: string): Promise<Student[]> { | |
| const data = await request<StudentListItem[]>( | |
| `/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<StudentDetail>(`/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<StudentsStats> { | |
| return request<StudentsStats>('/stats'); | |
| } | |
| /** GET /api/sheikh/my-students/student-session-stats?studentId=β¦ */ | |
| export async function getStudentSessionStats(studentId: number): Promise<StudentSessionStats> { | |
| return request<StudentSessionStats>(`/student-session-stats?studentId=${studentId}`); | |
| } |