Files
FamilyPlanner/frontend/src/screens/ChildPlanningScreen.tsx
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

189 lines
7.1 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import { useParams } from "react-router-dom";
import styled from "styled-components";
import { useChildren } from "../state/ChildrenContext";
import { apiClient } from "../services/api-client";
import { TimeGridMulti } from "../components/TimeGridMulti";
import { Link } from "react-router-dom";
const Container = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
`;
const Title = styled.h1`
margin: 0;
font-size: 1.6rem;
`;
const Toggle = styled.div`
display: inline-flex;
gap: 8px;
`;
const ToggleBtn = styled.button<{ $active?: boolean }>`
padding: 8px 12px;
border-radius: 10px;
border: 1px solid rgba(126, 136, 180, 0.25);
background: ${({ $active }) => ($active ? "rgba(85,98,255,0.2)" : "transparent")};
color: #e9ebff;
cursor: pointer;
`;
type Act = { title: string; startDateTime: string; endDateTime: string; notes?: string };
export const ChildPlanningScreen = () => {
const { childId } = useParams();
const { children } = useChildren();
const [mode, setMode] = useState<"day" | "week">("week");
const [dayEvents, setDayEvents] = useState<Act[]>([]);
const [weekDays, setWeekDays] = useState<Record<string, Act[]>>({});
const [timeZone, setTimeZone] = useState<string | null>(null);
const [lastSchedule, setLastSchedule] = useState<{ sourceFileUrl?: string; exportCsvUrl?: string } | null>(null);
const child = children.find((c) => c.id === childId);
const color = child?.colorHex ?? "#5562ff";
const today = useMemo(() => new Date(), []);
useEffect(() => {
const saved = localStorage.getItem("fp:view:timeZone");
if (!saved || saved === "auto") {
setTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone);
} else {
setTimeZone(saved);
}
}, []);
useEffect(() => {
const loadLast = async () => {
const items = await apiClient.get<Array<{ id: string; childId: string; sourceFileUrl?: string; exportCsvUrl?: string; createdAt: string }>>("/schedules");
const forChild = items.filter((i) => i.childId === childId);
forChild.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
if (forChild.length > 0) {
setLastSchedule({ sourceFileUrl: forChild[0].sourceFileUrl, exportCsvUrl: (forChild[0] as any).exportCsvUrl });
// Reload planning data when we detect a new schedule
void loadPlanningData();
}
};
if (childId) void loadLast();
}, [childId]);
// Reload last schedule periodically to detect new uploads
useEffect(() => {
if (!childId) return;
const interval = setInterval(async () => {
const items = await apiClient.get<Array<{ id: string; childId: string; sourceFileUrl?: string; exportCsvUrl?: string; createdAt: string }>>("/schedules");
const forChild = items.filter((i) => i.childId === childId);
forChild.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
if (forChild.length > 0) {
const newSchedule = { sourceFileUrl: forChild[0].sourceFileUrl, exportCsvUrl: (forChild[0] as any).exportCsvUrl };
// Only reload if schedule changed
if (newSchedule.sourceFileUrl !== lastSchedule?.sourceFileUrl) {
setLastSchedule(newSchedule);
void loadPlanningData();
}
}
}, 5000); // Check every 5 seconds
return () => clearInterval(interval);
}, [childId, lastSchedule]);
// Load planning data
const loadPlanningData = async () => {
if (!childId) return;
const y = today.getFullYear();
const m = String(today.getMonth() + 1).padStart(2, "0");
const d = String(today.getDate()).padStart(2, "0");
const date = `${y}-${m}-${d}`; // local date
const day = await apiClient.get<{ date: string; items: Array<{ childId: string; activities: Act[] }> }>(`/schedules/day/activities?date=${date}&childId=${childId}`);
setDayEvents(day.items?.[0]?.activities ?? []);
const start = new Date(today);
const weekday = (start.getDay() + 6) % 7; // 0=Mon
start.setDate(start.getDate() - weekday);
const sy = start.getFullYear();
const sm = String(start.getMonth() + 1).padStart(2, "0");
const sd = String(start.getDate()).padStart(2, "0");
const startISO = `${sy}-${sm}-${sd}`;
const week = await apiClient.get<{ start: string; items: Array<{ childId: string; days: Record<string, Act[]> }> }>(`/schedules/week/activities?start=${startISO}&childId=${childId}`);
setWeekDays(week.items?.[0]?.days ?? {});
};
useEffect(() => {
void loadPlanningData();
}, [childId, today]);
// Auto-reload when user returns to the tab
useEffect(() => {
const handleVisibilityChange = () => {
if (!document.hidden) {
void loadPlanningData();
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
}, [childId, today]);
const dayColumns = useMemo(() => {
return [
{
id: childId || "child",
title: child?.fullName ?? "Enfant",
color,
events: dayEvents,
dateISO: today.toISOString().slice(0, 10)
}
];
}, [childId, child?.fullName, color, dayEvents]);
const weekColumns = useMemo(() => {
// Build 7 days columns Mon..Sun
const start = new Date(today);
const weekday = (start.getDay() + 6) % 7; // 0=Mon
start.setDate(start.getDate() - weekday);
const cols = [] as { id: string; title: string; color: string; events: Act[]; dateISO?: string }[];
for (let i = 0; i < 7; i++) {
const d = new Date(start);
d.setDate(start.getDate() + i);
const dISO = d.toISOString().slice(0, 10);
const label = d.toLocaleDateString("fr-FR", { weekday: "short", day: "2-digit", month: "2-digit" });
cols.push({ id: dISO, title: label, color, events: weekDays[dISO] ?? [], dateISO: dISO });
}
return cols;
}, [today, weekDays, color]);
return (
<Container>
<Title>Planning {child?.fullName ?? "Enfant"}</Title>
<Toggle>
<ToggleBtn $active={mode === "day"} onClick={() => setMode("day")}>Jour</ToggleBtn>
<ToggleBtn $active={mode === "week"} onClick={() => setMode("week")}>Semaine</ToggleBtn>
</Toggle>
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
<a
href={lastSchedule?.sourceFileUrl}
target="_blank"
rel="noreferrer"
style={{ pointerEvents: lastSchedule?.sourceFileUrl ? "auto" : "none", opacity: lastSchedule?.sourceFileUrl ? 1 : 0.6 }}
>
Voir le fichier brut
</a>
<a
href={lastSchedule?.exportCsvUrl}
target="_blank"
rel="noreferrer"
style={{ pointerEvents: lastSchedule?.exportCsvUrl ? "auto" : "none", opacity: lastSchedule?.exportCsvUrl ? 1 : 0.6 }}
>
Télécharger analyse
</a>
</div>
{mode === "day" ? (
<TimeGridMulti columns={dayColumns} timeZone={timeZone ?? undefined} showNowLine now={new Date()} />
) : (
<TimeGridMulti columns={weekColumns} timeZone={timeZone ?? undefined} showNowLine now={new Date()} />
)}
</Container>
);
};