// ───────────────────────────────────────────────────────────────────────────── // src/lib/api/adminApi.ts // ───────────────────────────────────────────────────────────────────────────── import { getApiBaseUrl } from './config'; const BASE_URL = getApiBaseUrl(); const TOKEN_KEY = 'authToken'; // ─── Custom Error Class ─────────────────────────────────────────────────────── export class ApiError extends Error { readonly status: number; readonly path: string; constructor( status: number, message: string, path: string, ) { super(`[${status}] ${path} → ${message}`); this.status = status; this.path = path; this.name = 'ApiError'; } } // ─── Shared helper ──────────────────────────────────────────────────────────── function getHeaders(): HeadersInit { const token = localStorage.getItem(TOKEN_KEY) ?? ''; return { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), }; } async function request(path: string, options?: RequestInit): Promise { const url = `${BASE_URL}${path}`; // Guard: expired token 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); } } try { const res = await fetch(url, { ...options, headers: { ...getHeaders(), ...options?.headers }, }); if (res.status === 204) return null; 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.trim()) return null; 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}`); } } // ─── Response Types ─────────────────────────────────────────────────────────── export interface PendingSheikhDto { id: number; name: string; County: string; // API returns capital-C "County" specialization: string; experienceYears: number; } /** * Matches actual API response from: * GET /api/admin/dashboard/platform-analytics-and-stats */ export interface PlatformAnalyticsDto { totalUsers: number; usersThisMonth: number; pendingSheikhs: number; ongoingSessions: number; revenueThisMonth: number; totalRevenue: number; totalStudents: number; totalSheikhs: number; growth: number; } export interface ApiUser { id: number; fullName: string; email: string; role: 'STUDENT' | 'SHEIKH' | 'ADMIN'; status: 'ACTIVE' | 'BLOCKED' | 'PENDING'; createdAt: string; totalSessions?: number; rating?: number; earnings?: number; streak?: number; } export interface PaginatedResponse { content: T[]; totalElements: number; totalPages: number; size: number; number: number; } // ─── API Functions ──────────────────────────────────────────────────────────── /** * GET /api/admin/sheikh-approval?status=PENDING (or UNDER_REVIEW, etc.) */ export interface SheikhApprovalDto { id: number; name: string; email: string; country: string; experienceYears: number; description: string; specializations: string; pricePerHour: number; statusOfSheikh: 'PENDING' | 'UNDER_REVIEW' | 'APPROVED' | 'REJECTED'; interviewDateTime: string | null; // ISO: "2026-05-25T14:00:00", null if not scheduled } export async function fetchSheikhsByStatus( status: 'PENDING' | 'UNDER_REVIEW' | 'APPROVED' | 'REJECTED', ): Promise { const res = await request( `/api/admin/sheikh-approval?status=${status}`, ); return Array.isArray(res) ? res : []; } /** * GET /api/admin/dashboard/platform-analytics-and-stats */ export async function fetchPlatformAnalytics(): Promise { const empty: PlatformAnalyticsDto = { totalUsers: 0, usersThisMonth: 0, pendingSheikhs: 0, ongoingSessions: 0, revenueThisMonth: 0, totalRevenue: 0, totalStudents: 0, totalSheikhs: 0, growth: 0, }; try { const res = await request( '/api/admin/dashboard/platform-analytics-and-stats', ); return res ?? empty; } catch (error) { if (error instanceof ApiError && error.status === 403) { throw new ApiError(403, 'Admin access required.', '/api/admin/dashboard/platform-analytics-and-stats'); } return empty; } } /** * GET /api/admin/users (may return 403 if endpoint not implemented yet) * Falls back to empty paginated response gracefully. */ export async function fetchAllUsers(params?: { role?: string; status?: string; page?: number; size?: number; }): Promise> { const empty: PaginatedResponse = { content: [], totalElements: 0, totalPages: 0, size: 0, number: 0, }; const queryParams = new URLSearchParams(); if (params?.role) queryParams.append('role', params.role); if (params?.status) queryParams.append('status', params.status); if (params?.page !== undefined) queryParams.append('page', String(params.page)); if (params?.size !== undefined) queryParams.append('size', String(params.size)); const query = queryParams.toString() ? `?${queryParams.toString()}` : ''; try { const res = await request>(`/api/admin/users${query}`); if (!res) return empty; // Handle both paginated { content: [] } and plain array responses if (Array.isArray(res)) { return { content: res as ApiUser[], totalElements: (res as ApiUser[]).length, totalPages: 1, size: (res as ApiUser[]).length, number: 0 }; } return res; } catch (error) { // 403 → endpoint not available for this role/token, return empty silently if (error instanceof ApiError && (error.status === 403 || error.status === 404)) { return empty; } throw error; } } /** * POST /api/admin/users/:id/block or /api/admin/users/:id/unblock */ export async function toggleUserBlock( userId: number, block: boolean, ): Promise<{ success: boolean; message: string }> { const action = block ? 'block' : 'unblock'; try { const res = await request<{ success: boolean; message: string }>( `/api/admin/users/${userId}/${action}`, { method: 'POST' }, ); return res ?? { success: true, message: 'Done' }; } catch (error) { if (error instanceof ApiError) throw error; throw new Error(`Failed to ${action} user`); } } /** * DELETE /api/admin/users/:id */ export async function deleteUser( userId: number, ): Promise<{ success: boolean; message: string }> { try { const res = await request<{ success: boolean; message: string }>( `/api/admin/user-management/users/${userId}`, { method: 'DELETE' }, ); return res ?? { success: true, message: 'Deleted' }; } catch (error) { if (error instanceof ApiError) throw error; throw new Error('Failed to delete user'); } } /** * GET /api/admin/dashboard/pending-sheikhs (used by AdminDashboard) */ export async function fetchPendingSheikhs(): Promise { const res = await request('/api/admin/dashboard/pending-sheikhs'); return Array.isArray(res) ? res : []; } export async function approveSheikh( sheikhId: number, ): Promise { await request( `/api/admin/sheikh-approval/${sheikhId}/approve`, { method: 'PATCH' }, ); } export async function rejectSheikh( sheikhId: number, reason: string, ): Promise { await request( `/api/admin/sheikh-approval/${sheikhId}/reject`, { method: 'PATCH', body: JSON.stringify({ rejectionReason: reason }) }, ); } /** * POST /api/admin/sheikh-approval/:id/schedule-interview * Schedules an interview and moves the sheikh status to UNDER_REVIEW. */ export async function scheduleInterview( sheikhId: number, interviewDate: string, // "YYYY-MM-DD" interviewTime: string, // "HH:mm" optionalMessage?: string, ): Promise<{ success: boolean; message: string }> { const res = await request<{ success: boolean; message: string }>( `/api/admin/sheikh-approval/${sheikhId}/schedule-interview`, { method: 'POST', body: JSON.stringify({ interviewDate, interviewTime, optionalMessage: optionalMessage ?? '' }) }, ); return res ?? { success: true, message: 'Interview scheduled' }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function fetchSheikhDetails(sheikhId: number): Promise { try { return await request(`/api/admin/sheikhs/${sheikhId}`); } catch { return null; } } export async function saveInterviewNotes( sheikhId: string | number | undefined, notes: string, ): Promise { if (!sheikhId) return; await request( `/api/admin/sheikh-approval/${sheikhId}/notes`, { method: 'PATCH', body: JSON.stringify({ notes }) }, ); } // ─── Dashboard loader ───────────────────────────────────────────────────────── export interface AdminDashboardData { pendingSheikhs: PendingSheikhDto[]; platformAnalytics: PlatformAnalyticsDto; } export async function fetchAdminDashboardData(): Promise<{ data: Partial; errors: Record; }> { const [sheikhsRes, analyticsRes] = await Promise.allSettled([ fetchPendingSheikhs(), fetchPlatformAnalytics(), ]); const data: Partial = {}; const errors: Record = {}; data.pendingSheikhs = sheikhsRes.status === 'fulfilled' ? sheikhsRes.value : (errors.pendingSheikhs = sheikhsRes.reason?.message ?? 'Error', []); data.platformAnalytics = analyticsRes.status === 'fulfilled' ? analyticsRes.value : (errors.platformAnalytics = analyticsRes.reason?.message ?? 'Error', { totalUsers: 0, usersThisMonth: 0, pendingSheikhs: 0, ongoingSessions: 0, revenueThisMonth: 0, totalRevenue: 0, totalStudents: 0, totalSheikhs: 0, growth: 0 }); return { data, errors }; } const adminApi = { fetchPendingSheikhs, fetchSheikhsByStatus, fetchPlatformAnalytics, fetchAdminDashboardData, approveSheikh, rejectSheikh, scheduleInterview, fetchSheikhDetails, saveInterviewNotes, fetchAllUsers, toggleUserBlock, deleteUser, }; export default adminApi;