Files
FamilyPlanner/frontend/src/services/api-client.ts
philippe fdd72c1135 Initial commit: Family Planner application
Complete family planning application with:
- React frontend with TypeScript
- Node.js/Express backend with TypeScript
- Python ingestion service for document processing
- Planning ingestion service with LLM integration
- Shared UI components and type definitions
- OAuth integration for calendar synchronization
- Comprehensive documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-14 10:43:33 +02:00

245 lines
9.0 KiB
TypeScript

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<TResponse>(
path: string,
method: HttpVerb,
body?: unknown
): Promise<TResponse> {
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: <T>(path: string) => request<T>(path, "GET"),
post: <T>(path: string, payload: unknown) => request<T>(path, "POST", payload),
patch: <T>(path: string, payload: unknown) => request<T>(path, "PATCH", payload),
delete: <T>(path: string) => request<T>(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<ScheduleUploadResponse> => {
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<Array<{ filename: string; url: string }>> => {
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<IngestionStatusResponse> => {
const response = await fetch(`${API_BASE_URL}/ingestion/status`);
return (await response.json()) as IngestionStatusResponse;
};
export const getIngestionConfig = async (): Promise<IngestionConfigResponse> => {
const response = await fetch(`${API_BASE_URL}/ingestion/config`);
return (await response.json()) as IngestionConfigResponse;
};
export const setOpenAIConfig = async (apiKey: string, model?: string): Promise<ApiSuccessResponse> => {
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<ApiSuccessResponse> => {
const response = await fetch(`${API_BASE_URL}/ingestion/start`, { method: "POST" });
return (await response.json()) as ApiSuccessResponse;
};
export const diagnoseUpload = async (
file: File
): Promise<DiagnoseUploadResponse> => {
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<RepairIngestionResponse> => {
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<Array<{ id: string; fullName: string; colorHex: string; email?: string; notes?: string; avatar?: ChildProfile["avatar"]; }>> =>
apiClient.get("/parents");
export const createParent = async (payload: ParentPayload) => apiClient.post("/parents", payload);
export const updateParent = async (parentId: string, payload: Partial<ParentPayload> & { 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<Array<{ id: string; fullName: string; colorHex: string; email?: string; notes?: string; avatar?: ChildProfile["avatar"]; }>> =>
apiClient.get("/grandparents");
export const createGrandParent = async (payload: GrandParentPayload) => apiClient.post("/grandparents", payload);
export const updateGrandParent = async (grandParentId: string, payload: Partial<GrandParentPayload> & { 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<CalendarOAuthStartResponse>(`/calendar/${provider}/oauth/start`, payload);
export const completeCalendarOAuth = async (payload: {
provider: CalendarProvider;
state: string;
code?: string;
error?: string;
profileId?: string;
}) => apiClient.post<CalendarOAuthCompleteResponse>("/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<ConnectedCalendar[]>(`/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()}`);
};