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