import { useAuthStore } from '../stores/authStore'; const BASE_URL = ''; function getToken(): string | null { try { const raw = localStorage.getItem('clinicpro-auth'); if (!raw) return null; const parsed = JSON.parse(raw); return parsed?.state?.token ?? null; } catch { return null; } } export class ApiError extends Error { constructor( public status: number, public code: string, message: string, ) { super(message); } } // چند درخواست همزمان که ۴۰۱ می‌گیرند، فقط یک‌بار refresh را اجرا کنند let refreshPromise: Promise | null = null; function refreshOnce(): Promise { if (!refreshPromise) { refreshPromise = useAuthStore.getState().refresh().finally(() => { refreshPromise = null; }); } return refreshPromise; } async function request( path: string, options: RequestInit = {}, retry = true, ): Promise { const token = getToken(); const headers: Record = { 'Content-Type': 'application/json', ...(options.headers as Record), }; if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(`${BASE_URL}${path}`, { ...options, headers }); if (!res.ok) { if (res.status === 401) { // تلاش یک‌باره برای تازه‌سازی توکن، سپس اجرای مجدد همان درخواست if (retry) { const newToken = await refreshOnce(); if (newToken) { return request(path, options, false); } } useAuthStore.getState().logout(); window.location.replace('/admin/login'); throw new ApiError(401, 'ERR_UNAUTHORIZED', 'نشست منقضی شده است'); } const body = await res.json().catch(() => ({})); const firstErr = body?.errors?.[0]; throw new ApiError( res.status, firstErr?.code ?? 'ERR_UNKNOWN', firstErr?.message ?? 'خطای ناشناخته', ); } // پاسخ بدون بدنه (۲۰۴ یا Content-Length صفر) نباید به res.json() برسد if (res.status === 204 || res.headers.get('Content-Length') === '0') { return null as T; } return res.json() as Promise; } export const api = { get: (path: string) => request(path), post: (path: string, body: unknown) => request(path, { method: 'POST', body: JSON.stringify(body) }), patch: (path: string, body: unknown) => request(path, { method: 'PATCH', body: JSON.stringify(body) }), put: (path: string, body: unknown) => request(path, { method: 'PUT', body: JSON.stringify(body) }), delete: (path: string) => request(path, { method: 'DELETE' }), download: (path: string) => downloadFile(path), }; // دانلود فایل با ارسال توکن JWT در هدر و ذخیره روی دیسک کاربر async function downloadFile(path: string, retry = true): Promise { const token = getToken(); const headers: Record = {}; if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(`${BASE_URL}${path}`, { headers }); if (!res.ok) { if (res.status === 401 && retry) { const newToken = await refreshOnce(); if (newToken) return downloadFile(path, false); useAuthStore.getState().logout(); window.location.replace('/admin/login'); } throw new ApiError(res.status, 'ERR_DOWNLOAD', 'دانلود ناموفق بود'); } const blob = await res.blob(); const disposition = res.headers.get('Content-Disposition') ?? ''; const match = disposition.match(/filename="?([^"]+)"?/); const filename = match ? match[1] : 'download'; const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } export interface ApiResponse { success: boolean; data: T; errors: { code: string; message: string; field?: string }[]; } export interface PaginatedResponse { success: boolean; data: T[]; meta: { totalRecords: number; totalPages: number; currentPage: number; }; errors: []; }