Quran_Tech_Server / F_Pro /src /lib /api /student.ts
aboalaa147's picture
Initial deployment
eb6a2f9
Raw
History Blame Contribute Delete
8.8 kB
// ─────────────────────────────────────────────────────────────────────────────
// 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}`);
}