Spaces:
Sleeping
Sleeping
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 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; |