Quran_Tech_Server / F_Pro /src /lib /api /sheikhApi.ts
aboalaa147's picture
Initial deployment
eb6a2f9
Raw
History Blame Contribute Delete
7.54 kB
// ─────────────────────────────────────────────────────────────────────────────
// src/lib/api/sheikhApi.ts
// ─────────────────────────────────────────────────────────────────────────────
import { getApiBaseUrl } from './config';
const BASE_URL = getApiBaseUrl();
const TOKEN_KEY = 'authToken';
function getHeaders(): HeadersInit {
const token = localStorage.getItem(TOKEN_KEY) ?? '';
return {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}
export class SheikhApiError extends Error {
public readonly status: number;
public readonly path: string;
constructor(status: number, message: string, path: string) {
super(`[${status}] ${path} β†’ ${message}`);
this.status = status;
this.path = path;
this.name = 'SheikhApiError';
}
}
async function request<T>(path: string, options?: RequestInit): Promise<T | null> {
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: { ...getHeaders(), ...options?.headers },
});
if (res.status === 204) return null;
if (!res.ok) {
const message = await res.text().catch(() => res.statusText);
throw new SheikhApiError(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;
}
}
// ─── Response Types β€” matching ACTUAL API response fields ─────────────────────
export interface SheikhStatsDto {
hourlyRate: number;
ratingAvg: number;
totalEarnings: number;
totalEarningsThisMonth: number;
numberOfStudents: number;
numberOfActiveStudents: number;
numberOfCompletedSessionsThisMonth: number | null;
numberOfSessionsThisMonth: number | null;
numberOfSessionsScheduled?: number;
numberOfCompletedSessions?: number;
}
/**
* Matches actual API response:
* {
* sessionId, studentName, price, duration,
* scheduledStart: "2026-03-13T05:01:12.500697",
* sessionDescription, meetingLink, ...
* }
* Note: scheduledEnd is NOT in the response β€” duration field is used instead.
*/
export interface UpcomingClassDto {
sessionId: number;
studentId?: number;
sheikhId?: number;
studentName: string;
sheikhName?: string;
price: number;
duration: number; // minutes β€” use this directly, don't calculate
scheduledStartDate?: string;
scheduledStartTime?: string;
scheduledStart: string; // ISO: "2026-03-13T05:01:12.500697"
actualStart?: string | null;
actualEnd?: string | null;
sessionNotes?: string | null;
sessionDescription?: string | null;
sessionStatus?: string;
paymentStatus?: string;
meetingLink?: string;
createdAt?: string;
}
/**
* Matches actual API response:
* {
* comment, date, rate, studentName
* }
* Note: field names are `date` and `rate` β€” NOT `createdAt` and `rating`
*/
export interface ReviewDto {
studentName: string;
comment: string;
date: string; // "2026-03-06"
rate: number; // 1-5 ← NOT "rating"
// Legacy aliases (kept for safety if backend changes)
reviewId?: number;
createdAt?: string;
rating?: number;
}
export interface UpdatePriceBody {
newPrice: number;
}
export interface AvailabilitySlotDto {
id: number;
dayOfWeek: string;
startTime: string; // "HH:mm:ss"
endTime: string; // "HH:mm:ss"
}
// ─── API Functions ────────────────────────────────────────────────────────────
export async function fetchSheikhStats(): Promise<SheikhStatsDto | null> {
return request<SheikhStatsDto>('/api/sheikh/sheikh-dashboard-view/stats');
}
export async function fetchUpcomingClasses(): Promise<UpcomingClassDto[]> {
const raw = await request<unknown>('/api/sheikh/sheikh-dashboard-view/upcoming-classes');
if (raw === null) return [];
if (Array.isArray(raw)) return raw as UpcomingClassDto[];
// Single object returned by backend
return [raw as UpcomingClassDto];
}
export async function fetchTwoReviews(): Promise<ReviewDto[]> {
const raw = await request<ReviewDto[]>('/api/sheikh/sheikh-dashboard-view/Two-reviews');
if (raw === null) return [];
if (!Array.isArray(raw)) return [];
return raw;
}
export async function fetchAllReviews(): Promise<ReviewDto[]> {
const raw = await request<ReviewDto[]>('/api/sheikh/sheikh-dashboard-view/reviews');
if (raw === null) return [];
if (!Array.isArray(raw)) return [];
return raw;
}
export async function updateSheikhPrice(newPrice: number): Promise<void> {
await request<void>('/api/sheikh/sheikh-dashboard-view/update-price', {
method: 'PUT',
body: JSON.stringify({ newPrice } satisfies UpdatePriceBody),
});
}
export async function fetchSheikhAvailability(): Promise<AvailabilitySlotDto[]> {
const raw = await request<AvailabilitySlotDto[]>('/api/sheikh/availability');
if (!Array.isArray(raw)) return [];
return raw;
}
export async function updateSheikhAvailability(
slots: Array<{ dayOfWeek: string; startTime: string; endTime: string }>
): Promise<void> {
await request<void>('/api/sheikh/availability', {
method: 'PUT',
body: JSON.stringify({ slots }),
});
}
// ─── Dashboard data loader ────────────────────────────────────────────────────
export interface SheikhDashboardData {
stats: SheikhStatsDto;
upcomingClasses: UpcomingClassDto[];
recentReviews: ReviewDto[];
}
export async function fetchSheikhDashboardData(): Promise<{
data: Partial<SheikhDashboardData>;
errors: Record<string, string>;
}> {
const [statsResult, upcomingResult, reviewsResult] = await Promise.allSettled([
fetchSheikhStats(),
fetchUpcomingClasses(),
fetchTwoReviews(),
]);
const data: Partial<SheikhDashboardData> = {};
const errors: Record<string, string> = {};
if (statsResult.status === 'fulfilled') {
if (statsResult.value !== null) {
data.stats = statsResult.value;
} else {
errors.stats = 'No stats available (204)';
}
} else {
errors.stats = statsResult.reason?.message ?? 'Unknown error';
console.error('[sheikhApi] stats failed:', statsResult.reason);
}
if (upcomingResult.status === 'fulfilled') {
data.upcomingClasses = upcomingResult.value;
} else {
errors.upcomingClasses = upcomingResult.reason?.message ?? 'Unknown error';
data.upcomingClasses = [];
console.error('[sheikhApi] upcomingClasses failed:', upcomingResult.reason);
}
if (reviewsResult.status === 'fulfilled') {
data.recentReviews = reviewsResult.value;
} else {
errors.recentReviews = reviewsResult.reason?.message ?? 'Unknown error';
data.recentReviews = [];
console.error('[sheikhApi] recentReviews failed:', reviewsResult.reason);
}
return { data, errors };
}
const sheikhApi = {
fetchSheikhStats,
fetchUpcomingClasses,
fetchTwoReviews,
fetchAllReviews,
updateSheikhPrice,
fetchSheikhDashboardData,
fetchSheikhAvailability,
updateSheikhAvailability,
};
export default sheikhApi;