import type { CalendarConnectionStatus, CalendarProvider, ConnectedCalendar } from "../types/calendar"; import type { ChildProfile } from "@family-planner/types"; import type { ScheduleUploadResponse, IngestionStatusResponse, IngestionConfigResponse, DiagnoseUploadResponse, RepairIngestionResponse, ApiSuccessResponse } from "../types/api"; export const API_BASE_URL = import.meta.env.VITE_API_URL ?? "http://localhost:5000/api"; type HttpVerb = "GET" | "POST" | "PATCH" | "DELETE"; async function request( path: string, method: HttpVerb, body?: unknown ): Promise { const response = await fetch(`${API_BASE_URL}${path}`, { method, headers: { "Content-Type": "application/json", "Cache-Control": "no-cache" // Force le backend à retourner 200 au lieu de 304 }, body: body ? JSON.stringify(body) : undefined }); if (!response.ok) { throw new Error(`API error ${response.status}`); } // Gérer 304 Not Modified (pas de body, utiliser le cache navigateur) if (response.status === 304) { throw new Error("Data not modified (304) - this should not happen with Cache-Control: no-cache"); } if (response.status === 204) { return undefined as TResponse; } const contentLength = response.headers.get("Content-Length"); if (contentLength === "0" || response.status === 202) { return undefined as TResponse; } const contentType = response.headers.get("Content-Type") ?? ""; if (!contentType.includes("application/json")) { return undefined as TResponse; } return (await response.json()) as TResponse; } export const apiClient = { get: (path: string) => request(path, "GET"), post: (path: string, payload: unknown) => request(path, "POST", payload), patch: (path: string, payload: unknown) => request(path, "PATCH", payload), delete: (path: string) => request(path, "DELETE") }; export const uploadAvatar = async ( file: File ): Promise<{ url: string; filename: string }> => { const formData = new FormData(); formData.append("avatar", file); const response = await fetch(`${API_BASE_URL}/uploads/avatar`, { method: "POST", body: formData }); if (!response.ok) { throw new Error(`Upload error ${response.status}`); } return (await response.json()) as { url: string; filename: string }; }; export const uploadPlanning = async ( childId: string, file: File ): Promise => { const form = new FormData(); form.append("childId", childId); form.append("planning", file); const response = await fetch(`${API_BASE_URL}/uploads/planning`, { method: "POST", body: form }); if (!response.ok) { throw new Error(`Upload planning error ${response.status}`); } return (await response.json()) as ScheduleUploadResponse; }; export const listAvatars = async (): Promise> => { const response = await fetch(`${API_BASE_URL}/uploads/avatars`); if (!response.ok) { throw new Error(`Gallery error ${response.status}`); } return (await response.json()) as Array<{ filename: string; url: string }>; }; // Ingestion helper endpoints (proxied via backend) export const getIngestionStatus = async (): Promise => { const response = await fetch(`${API_BASE_URL}/ingestion/status`); return (await response.json()) as IngestionStatusResponse; }; export const getIngestionConfig = async (): Promise => { const response = await fetch(`${API_BASE_URL}/ingestion/config`); return (await response.json()) as IngestionConfigResponse; }; export const setOpenAIConfig = async (apiKey: string, model?: string): Promise => { const response = await fetch(`${API_BASE_URL}/ingestion/config/openai`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ apiKey, model }) }); if (!response.ok) { throw new Error("Config update failed"); } return (await response.json()) as ApiSuccessResponse; }; export const startIngestion = async (): Promise => { const response = await fetch(`${API_BASE_URL}/ingestion/start`, { method: "POST" }); return (await response.json()) as ApiSuccessResponse; }; export const diagnoseUpload = async ( file: File ): Promise => { const form = new FormData(); form.append("planning", file); const resp = await fetch(`${API_BASE_URL}/uploads/diagnose`, { method: "POST", body: form }); if (!resp.ok) throw new Error("Diagnostic failed"); return (await resp.json()) as DiagnoseUploadResponse; }; export const repairIngestion = async (): Promise => { const resp = await fetch(`${API_BASE_URL}/ingestion/repair`, { method: "POST" }); return (await resp.json()) as RepairIngestionResponse; }; // Parents API export type ParentPayload = { fullName: string; colorHex: string; email?: string; notes?: string; avatar?: ChildProfile["avatar"] }; export const listParents = async (): Promise> => apiClient.get("/parents"); export const createParent = async (payload: ParentPayload) => apiClient.post("/parents", payload); export const updateParent = async (parentId: string, payload: Partial & { avatar?: ChildProfile["avatar"] | null }) => apiClient.patch(`/parents/${parentId}`, payload); export const deleteParent = async (parentId: string) => apiClient.delete(`/parents/${parentId}`); export type GrandParentPayload = ParentPayload; export const listGrandParents = async (): Promise> => apiClient.get("/grandparents"); export const createGrandParent = async (payload: GrandParentPayload) => apiClient.post("/grandparents", payload); export const updateGrandParent = async (grandParentId: string, payload: Partial & { avatar?: ChildProfile["avatar"] | null }) => apiClient.patch(`/grandparents/${grandParentId}`, payload); export const deleteGrandParent = async (grandParentId: string) => apiClient.delete(`/grandparents/${grandParentId}`); // Calendar integrations type CalendarOAuthStartResponse = { authUrl?: string; url?: string; authorizeUrl?: string; state?: string; }; type CalendarOAuthCompleteResponse = { success: boolean; email?: string; label?: string; connectionId?: string; profileId?: string; }; export const startCalendarOAuth = async ( provider: CalendarProvider, payload: { profileId: string; state: string } ) => apiClient.post(`/calendar/${provider}/oauth/start`, payload); export const completeCalendarOAuth = async (payload: { provider: CalendarProvider; state: string; code?: string; error?: string; profileId?: string; }) => apiClient.post("/calendar/oauth/complete", payload); export const connectCalendarWithCredentials = async ( provider: CalendarProvider, payload: { profileId: string; email: string; password: string; label?: string; shareWithFamily?: boolean } ) => apiClient.post<{ connection: ConnectedCalendar }>(`/calendar/${provider}/credentials`, payload); export const listCalendarConnections = async (profileId: string) => apiClient.get(`/calendar/${profileId}/connections`); export const refreshCalendarConnection = async (profileId: string, connectionId: string) => apiClient.post<{ status: CalendarConnectionStatus; lastSyncedAt?: string }>( `/calendar/${profileId}/connections/${connectionId}/refresh`, {} ); export const removeCalendarConnection = async (profileId: string, connectionId: string) => apiClient.delete(`/calendar/${profileId}/connections/${connectionId}`); // Holidays and Personal Leaves API import type { Holiday, PersonalLeave, SchoolRegion } from "@family-planner/types"; export const getHolidays = async (region?: SchoolRegion, year?: number): Promise<{ holidays: Holiday[] }> => { const params = new URLSearchParams(); if (region) params.append("region", region); if (year) params.append("year", year.toString()); return apiClient.get(`/holidays?${params.toString()}`); }; export const getPublicHolidays = async (year?: number): Promise<{ holidays: Holiday[] }> => { const params = new URLSearchParams(); if (year) params.append("year", year.toString()); return apiClient.get(`/holidays/public?${params.toString()}`); }; export const getSchoolHolidays = async (region?: SchoolRegion, year?: number): Promise<{ holidays: Holiday[] }> => { const params = new URLSearchParams(); if (region) params.append("region", region); if (year) params.append("year", year.toString()); return apiClient.get(`/holidays/school?${params.toString()}`); }; export const getPersonalLeaves = async (profileId?: string): Promise<{ leaves: PersonalLeave[] }> => { const params = new URLSearchParams(); if (profileId) params.append("profileId", profileId); return apiClient.get(`/personal-leaves?${params.toString()}`); };