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>
This commit is contained in:
244
frontend/src/services/api-client.ts
Normal file
244
frontend/src/services/api-client.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
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()}`);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user