Spaces:
Sleeping
Sleeping
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 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<T>(path: string, options?: RequestInit): Promise<T | null> { | |
| 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<T> { | |
| 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<SheikhApprovalDto[]> { | |
| const res = await request<SheikhApprovalDto[]>( | |
| `/api/admin/sheikh-approval?status=${status}`, | |
| ); | |
| return Array.isArray(res) ? res : []; | |
| } | |
| /** | |
| * GET /api/admin/dashboard/platform-analytics-and-stats | |
| */ | |
| export async function fetchPlatformAnalytics(): Promise<PlatformAnalyticsDto> { | |
| 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<PlatformAnalyticsDto>( | |
| '/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<PaginatedResponse<ApiUser>> { | |
| const empty: PaginatedResponse<ApiUser> = { | |
| 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<PaginatedResponse<ApiUser>>(`/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<PendingSheikhDto[]> { | |
| const res = await request<PendingSheikhDto[]>('/api/admin/dashboard/pending-sheikhs'); | |
| return Array.isArray(res) ? res : []; | |
| } | |
| export async function approveSheikh( | |
| sheikhId: number, | |
| ): Promise<void> { | |
| await request<void>( | |
| `/api/admin/sheikh-approval/${sheikhId}/approve`, | |
| { method: 'PATCH' }, | |
| ); | |
| } | |
| export async function rejectSheikh( | |
| sheikhId: number, | |
| reason: string, | |
| ): Promise<void> { | |
| await request<void>( | |
| `/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<any> { | |
| try { | |
| return await request(`/api/admin/sheikhs/${sheikhId}`); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| export async function saveInterviewNotes( | |
| sheikhId: string | number | undefined, | |
| notes: string, | |
| ): Promise<void> { | |
| if (!sheikhId) return; | |
| await request<void>( | |
| `/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<AdminDashboardData>; | |
| errors: Record<string, string>; | |
| }> { | |
| const [sheikhsRes, analyticsRes] = await Promise.allSettled([ | |
| fetchPendingSheikhs(), | |
| fetchPlatformAnalytics(), | |
| ]); | |
| const data: Partial<AdminDashboardData> = {}; | |
| const errors: Record<string, string> = {}; | |
| 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; |