Files
Grow/src/app/(app)/growth/page.tsx
T
arnaudne b40694d691 feat: finish all UI/UX improvements from backlog
- nav: aria-label on baby switcher select
- dashboard: active timers show baby name
- growth: PDF export includes percentile band per weight row
- settings: invite link note (permanent, reset if compromised)
- timeline: select mode toggle shows "Annuler" when active
- photos: delete photo from lightbox (PATCHes event metadata)
- notes: invisible date input overlay for date-jump via calendar picker
- IMPROVEMENTS.md: updated checkboxes, 8 remaining → 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 20:58:57 +02:00

572 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
} from "recharts";
import {
WHO_WEIGHT_PERCENTILES, PERCENTILE_LABELS, getAgeInWeeks, interpolatePercentile,
} from "@/lib/who-percentiles";
import { useBaby } from "@/contexts/baby-context";
import { format, subDays } from "date-fns";
import { fr } from "date-fns/locale";
import { Plus, Pencil, Trash2, FileDown } from "lucide-react";
interface GrowthLog {
id: string;
date: string;
weight?: number;
height?: number;
headCirc?: number;
notes?: string;
}
interface BabyShape {
id: string;
name: string;
birthDate: string;
}
const WHO_CURVE_KEYS = ["p3", "p10", "p25", "p50", "p75", "p90", "p97"];
const WHO_COLORS = ["#e2e8f0", "#cbd5e1", "#94a3b8", "#475569", "#94a3b8", "#cbd5e1", "#e2e8f0"];
function useDarkMode() {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const check = () => setIsDark(document.documentElement.classList.contains("dark"));
check();
const obs = new MutationObserver(check);
obs.observe(document.documentElement, { attributeFilter: ["class"] });
return () => obs.disconnect();
}, []);
return isDark;
}
type TimeRange = "7j" | "4sem" | "3mois" | "tout";
function filterLogsByRange(logs: GrowthLog[], range: TimeRange): GrowthLog[] {
if (range === "tout") return logs;
const now = new Date();
let cutoff: Date;
if (range === "7j") cutoff = subDays(now, 7);
else if (range === "4sem") cutoff = subDays(now, 28);
else cutoff = subDays(now, 90);
return logs.filter((l) => new Date(l.date) >= cutoff);
}
const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
function ageWeeksFloat(birthDate: string, measureDate: string): number {
return (new Date(measureDate).getTime() - new Date(birthDate).getTime()) / MS_PER_WEEK;
}
function buildWeightChartData(logs: GrowthLog[], baby: BabyShape, allLogs: GrowthLog[]) {
const actualPoints = logs
.filter((l) => l.weight)
.map((l) => ({ ageWeeks: ageWeeksFloat(baby.birthDate, l.date), weight: l.weight! / 1000 }));
if (actualPoints.length === 0) return [];
const maxWeek = Math.max(...actualPoints.map((p) => p.ageWeeks));
const globalMin = allLogs.filter((l) => l.weight).reduce((min, l) => {
const w = ageWeeksFloat(baby.birthDate, l.date);
return w < min ? w : min;
}, Infinity);
const localMin = Math.min(...actualPoints.map((p) => p.ageWeeks));
const minWeek = localMin > globalMin ? Math.max(0, localMin - 1) : 0;
const whoAges = Object.keys(WHO_WEIGHT_PERCENTILES)
.map(Number)
.filter((a) => a >= Math.floor(minWeek) && a <= Math.ceil(maxWeek) + 2)
.sort((a, b) => a - b);
// Use string keys so float actual-point ages don't collide with integer WHO ages
const rows = new Map<number, Record<string, number | null>>();
for (const ageWeeks of whoAges) {
const row: Record<string, number | null> = { ageWeeks };
PERCENTILE_LABELS.forEach((label, i) => { row[`p${label.slice(1)}`] = interpolatePercentile(ageWeeks, i); });
rows.set(ageWeeks, row);
}
for (const pt of actualPoints) {
const existing = rows.get(pt.ageWeeks) ?? { ageWeeks: pt.ageWeeks };
rows.set(pt.ageWeeks, { ...existing, actualWeight: pt.weight });
}
return Array.from(rows.values()).sort((a, b) => (a.ageWeeks as number) - (b.ageWeeks as number));
}
function buildSimpleChartData(logs: GrowthLog[], baby: BabyShape, field: "height" | "headCirc") {
return logs
.filter((l) => l[field])
.map((l) => ({ ageWeeks: ageWeeksFloat(baby.birthDate, l.date), value: l[field]! }))
.sort((a, b) => a.ageWeeks - b.ageWeeks);
}
const inputCls = "w-full px-3 py-2.5 rounded-lg border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100";
function fmtAgeWeeks(weeks: number): string {
if (weeks === 0) return "Naiss.";
if (weeks < 4) return `${weeks}sem`;
const months = Math.round(weeks / 4.345);
if (months >= 12) return "1 an";
return `${months}m`;
}
function fmtAgeLabel(weeks: number): string {
if (weeks === 0) return "Naissance";
if (weeks < 4) return `${weeks} semaine${weeks > 1 ? "s" : ""}`;
const months = Math.floor(weeks / 4.345);
const remWeeks = Math.round(weeks - months * 4.345);
if (months >= 12) return "1 an";
if (remWeeks > 0) return `${months} mois ${remWeeks} sem.`;
return `${months} mois`;
}
function WeightTooltip(props: Record<string, unknown> & { style: React.CSSProperties }) {
const { active, payload, label, style } = props as {
active?: boolean;
payload?: { dataKey: string; value: number }[];
label?: number;
style: React.CSSProperties;
};
if (!active || !payload) return null;
const baby = payload.find((p) => p.dataKey === "actualWeight");
if (!baby) return null;
return (
<div style={style} className="px-3 py-2 rounded-lg text-xs">
<p className="text-slate-400 mb-1">{fmtAgeLabel(label ?? 0)}</p>
<p className="font-semibold text-indigo-500">Bébé : {baby.value.toFixed(3)} kg</p>
</div>
);
}
export default function GrowthPage() {
const qc = useQueryClient();
const { selectedBaby: baby } = useBaby();
const isDark = useDarkMode();
const tooltipStyle = {
borderRadius: 8,
border: `1px solid ${isDark ? "#334155" : "#e2e8f0"}`,
background: isDark ? "#1e293b" : "#fff",
color: isDark ? "#f1f5f9" : "#0f172a",
boxShadow: "0 4px 16px rgba(0,0,0,0.12)",
fontSize: 12,
};
const gridStroke = isDark ? "#1e293b" : "#f1f5f9";
const [showForm, setShowForm] = useState(false);
const [form, setForm] = useState({ date: "", weight: "", height: "", headCirc: "", notes: "" });
function openAddForm() {
const last = logs.at(-1);
setForm({
date: format(new Date(), "yyyy-MM-dd"),
weight: last?.weight ? (last.weight / 1000).toFixed(3) : "",
height: last?.height ? String(last.height) : "",
headCirc: last?.headCirc ? String(last.headCirc) : "",
notes: "",
});
setShowForm(true);
}
const [saving, setSaving] = useState(false);
const [timeRange, setTimeRange] = useState<TimeRange>("tout");
const [editingId, setEditingId] = useState<string | null>(null);
const [editForm, setEditForm] = useState({ date: "", weight: "", height: "", headCirc: "", notes: "" });
const [editSaving, setEditSaving] = useState(false);
const { data: logs = [] } = useQuery<GrowthLog[]>({
queryKey: ["growth", baby?.id],
queryFn: () => fetch(`/api/growth?babyId=${baby!.id}`).then((r) => r.json()),
enabled: !!baby?.id,
});
const filteredLogs = filterLogsByRange(logs, timeRange);
const weightData = baby ? buildWeightChartData(filteredLogs, baby, logs) : [];
const heightData = baby ? buildSimpleChartData(filteredLogs, baby, "height") : [];
const headData = baby ? buildSimpleChartData(filteredLogs, baby, "headCirc") : [];
async function handleSave(e: React.FormEvent) {
e.preventDefault();
setSaving(true);
await fetch("/api/growth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
babyId: baby!.id,
date: new Date(form.date).toISOString(),
weight: form.weight ? parseFloat(form.weight) * 1000 : null,
height: form.height ? parseFloat(form.height) : null,
headCirc: form.headCirc ? parseFloat(form.headCirc) : null,
notes: form.notes || null,
}),
});
qc.invalidateQueries({ queryKey: ["growth"] });
setSaving(false);
setShowForm(false);
setForm({ date: "", weight: "", height: "", headCirc: "", notes: "" });
}
function startEdit(log: GrowthLog) {
setEditingId(log.id);
setEditForm({
date: format(new Date(log.date), "yyyy-MM-dd"),
weight: log.weight ? (log.weight / 1000).toFixed(3) : "",
height: log.height ? String(log.height) : "",
headCirc: log.headCirc ? String(log.headCirc) : "",
notes: log.notes ?? "",
});
}
async function handleEditSave(e: React.FormEvent, logId: string) {
e.preventDefault();
setEditSaving(true);
await fetch(`/api/growth/${logId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
date: new Date(editForm.date).toISOString(),
weight: editForm.weight ? parseFloat(editForm.weight) * 1000 : null,
height: editForm.height ? parseFloat(editForm.height) : null,
headCirc: editForm.headCirc ? parseFloat(editForm.headCirc) : null,
notes: editForm.notes || null,
}),
});
qc.invalidateQueries({ queryKey: ["growth"] });
setEditSaving(false);
setEditingId(null);
}
async function handleDelete(logId: string) {
if (!confirm("Supprimer cette mesure ?")) return;
await fetch(`/api/growth/${logId}`, { method: "DELETE" });
qc.invalidateQueries({ queryKey: ["growth"] });
}
function exportPDF() {
function getPercentileBand(ageWeeks: number, weightKg: number): string {
const values = PERCENTILE_LABELS.map((_, i) => interpolatePercentile(ageWeeks, i) ?? 0);
if (weightKg < values[0]) return "< P3";
for (let i = 0; i < values.length - 1; i++) {
if (weightKg >= values[i] && weightKg < values[i + 1]) return `${PERCENTILE_LABELS[i]}${PERCENTILE_LABELS[i + 1]}`;
}
return "> P97";
}
import("jspdf").then(({ default: jsPDF }) => {
const doc = new jsPDF();
const pageW = doc.internal.pageSize.getWidth();
doc.setFontSize(18);
doc.setTextColor(30, 41, 59);
doc.text("Courbe de croissance", 14, 20);
doc.setFontSize(11);
doc.setTextColor(100, 116, 139);
doc.text(`${baby!.name} · né${baby!.gender === "F" ? "e" : ""} le ${format(new Date(baby!.birthDate), "d MMMM yyyy", { locale: fr })}`, 14, 28);
doc.text(`Généré le ${format(new Date(), "d MMMM yyyy", { locale: fr })}`, 14, 34);
// Table header
const COL = [14, 55, 95, 130, 155];
const headers = ["Date", "Poids (kg)", "Taille (cm)", "PC (cm)", "Notes"];
let y = 46;
doc.setFillColor(238, 242, 255);
doc.rect(14, y - 5, pageW - 28, 9, "F");
doc.setFontSize(9);
doc.setTextColor(79, 70, 229);
headers.forEach((h, i) => doc.text(h, COL[i], y));
y += 8;
doc.setFontSize(9);
const sorted = [...logs].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
sorted.forEach((log, idx) => {
if (y > 270) { doc.addPage(); y = 20; }
if (idx % 2 === 0) {
doc.setFillColor(248, 250, 252);
doc.rect(14, y - 4, pageW - 28, 7, "F");
}
doc.setTextColor(30, 41, 59);
doc.text(format(new Date(log.date), "d MMM yyyy", { locale: fr }), COL[0], y);
doc.text(log.weight ? (() => {
const ageW = getAgeInWeeks(new Date(baby!.birthDate), new Date(log.date));
const band = ageW >= 0 && ageW <= 104 ? ` (${getPercentileBand(ageW, log.weight! / 1000)})` : "";
return `${(log.weight! / 1000).toFixed(3)}${band}`;
})() : "—", COL[1], y);
doc.text(log.height ? String(log.height) : "—", COL[2], y);
doc.text(log.headCirc ? String(log.headCirc) : "—", COL[3], y);
if (log.notes) doc.text(log.notes.slice(0, 30), COL[4], y);
y += 7;
});
doc.save(`croissance-${baby!.name.toLowerCase()}.pdf`);
});
}
const latest = logs.at(-1);
const rangeOptions: { label: string; value: TimeRange }[] = [
{ label: "7j", value: "7j" },
{ label: "4 sem", value: "4sem" },
{ label: "3 mois", value: "3mois" },
{ label: "Tout", value: "tout" },
];
return (
<div className="p-4 md:p-8 pb-24 md:pb-8">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Courbes de croissance</h1>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-0.5">{baby?.name}</p>
</div>
<div className="flex gap-2">
{logs.length > 0 && (
<button onClick={exportPDF}
className="flex items-center gap-2 px-3 py-2 border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-300 text-sm font-medium rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition">
<FileDown className="w-4 h-4" />
PDF
</button>
)}
<button
onClick={() => showForm ? setShowForm(false) : openAddForm()}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition"
>
<Plus className="w-4 h-4" />
Mesure
</button>
</div>
</div>
{/* Add measurement form */}
{showForm && (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-5 mb-5">
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-200 mb-4">Nouvelle mesure</h3>
<form onSubmit={handleSave} className="space-y-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-300 mb-1.5">Date</label>
<input type="date" value={form.date} onChange={(e) => setForm((f) => ({ ...f, date: e.target.value }))} required className={inputCls} />
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-300 mb-1.5">Poids (kg)</label>
<input type="number" value={form.weight} onChange={(e) => setForm((f) => ({ ...f, weight: e.target.value }))}
step="0.001" min="1" max="30" placeholder="3.500" className={inputCls} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-300 mb-1.5">Taille (cm)</label>
<input type="number" value={form.height} onChange={(e) => setForm((f) => ({ ...f, height: e.target.value }))}
step="0.1" min="30" max="120" placeholder="50" className={inputCls} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-300 mb-1.5">PC Périmètre crânien (cm)</label>
<input type="number" value={form.headCirc} onChange={(e) => setForm((f) => ({ ...f, headCirc: e.target.value }))}
step="0.1" min="20" max="60" placeholder="34" className={inputCls} />
</div>
</div>
<input type="text" value={form.notes} onChange={(e) => setForm((f) => ({ ...f, notes: e.target.value }))}
placeholder="RDV pédiatre, contexte..." className={inputCls} />
<div className="flex gap-2">
<button type="button" onClick={() => setShowForm(false)} className="flex-1 py-2.5 border border-slate-200 dark:border-slate-700 rounded-lg text-sm text-slate-600 dark:text-slate-300">Annuler</button>
<button type="submit" disabled={saving} className="flex-1 py-2.5 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700 transition">
{saving ? "..." : "Enregistrer"}
</button>
</div>
</form>
</div>
)}
{/* Latest values */}
{latest && (
<div className="grid grid-cols-3 gap-3 mb-5">
{latest.weight && (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4 text-center">
<p className="text-xs text-slate-400 dark:text-slate-500 font-medium mb-1">Poids</p>
<p className="text-xl font-bold text-slate-900 dark:text-slate-100">{(latest.weight / 1000).toFixed(3).replace(".", ",")} kg</p>
</div>
)}
{latest.height && (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4 text-center">
<p className="text-xs text-slate-400 dark:text-slate-500 font-medium mb-1">Taille</p>
<p className="text-xl font-bold text-slate-900 dark:text-slate-100">{latest.height} cm</p>
</div>
)}
{latest.headCirc && (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-4 text-center">
<p className="text-xs text-slate-400 dark:text-slate-500 font-medium mb-1">Périmètre crânien (PC)</p>
<p className="text-xl font-bold text-slate-900 dark:text-slate-100">{latest.headCirc} cm</p>
</div>
)}
</div>
)}
{/* Time range selector */}
{logs.length > 0 && (
<div className="flex gap-1.5 mb-5">
{rangeOptions.map((opt) => (
<button
key={opt.value}
onClick={() => setTimeRange(opt.value)}
className={`px-3 py-1.5 rounded-full text-xs font-medium transition ${
timeRange === opt.value
? "bg-indigo-600 text-white"
: "bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600"
}`}
>
{opt.label}
</button>
))}
</div>
)}
<div className="space-y-4">
{/* Weight + WHO */}
{weightData.length > 0 && (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-200">Poids (kg)</h3>
<div className="flex items-center gap-3 text-xs text-slate-400 dark:text-slate-500">
<span className="flex items-center gap-1"><span className="inline-block w-3 h-0.5 bg-indigo-600 rounded" />Bébé</span>
<span className="flex items-center gap-1">
<span className="inline-block w-3 h-0.5 bg-slate-300 rounded" />
OMS P3P97
<span className="text-slate-300 dark:text-slate-600 text-[10px]" title="P50 = médiane : 50% des bébés ont un poids inférieur. P3P97 représente la plage normale OMS."></span>
</span>
</div>
</div>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={weightData} margin={{ top: 0, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke} />
<XAxis dataKey="ageWeeks" type="number" domain={["dataMin", "dataMax"]} tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }} axisLine={false} tickLine={false} />
<Tooltip content={(props) => <WeightTooltip {...props} style={tooltipStyle} />} />
{WHO_CURVE_KEYS.map((key, i) => (
<Line key={key} dataKey={key} stroke={WHO_COLORS[i]} strokeWidth={key === "p50" ? 1.5 : 1} dot={false} strokeDasharray={key === "p50" ? undefined : "3 2"} connectNulls />
))}
<Line dataKey="actualWeight" stroke="#4f46e5" strokeWidth={2.5} dot={{ r: 4, fill: "#4f46e5", strokeWidth: 2, stroke: "#fff" }} connectNulls={false} />
</LineChart>
</ResponsiveContainer>
</div>
)}
<div className="grid md:grid-cols-2 gap-4">
{/* Height */}
{heightData.length > 0 && (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-5">
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-200 mb-4">Taille (cm)</h3>
<ResponsiveContainer width="100%" height={160}>
<LineChart data={heightData} margin={{ top: 0, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke} />
<XAxis dataKey="ageWeeks" type="number" domain={["dataMin", "dataMax"]} tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }} axisLine={false} tickLine={false} domain={["auto", "auto"]} />
<Tooltip formatter={(val) => [`${val} cm`, "Taille"]} labelFormatter={(v) => fmtAgeLabel(Number(v))} contentStyle={tooltipStyle} />
<Line dataKey="value" stroke="#0ea5e9" strokeWidth={2.5} dot={{ r: 4, fill: "#0ea5e9", strokeWidth: 2, stroke: "#fff" }} />
</LineChart>
</ResponsiveContainer>
</div>
)}
{/* Head circumference */}
{headData.length > 0 && (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl p-5">
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-200 mb-4">Périmètre crânien (cm)</h3>
<ResponsiveContainer width="100%" height={160}>
<LineChart data={headData} margin={{ top: 0, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke} />
<XAxis dataKey="ageWeeks" type="number" domain={["dataMin", "dataMax"]} tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }} axisLine={false} tickLine={false} domain={["auto", "auto"]} />
<Tooltip formatter={(val) => [`${val} cm`, "PC"]} labelFormatter={(v) => fmtAgeLabel(Number(v))} contentStyle={tooltipStyle} />
<Line dataKey="value" stroke="#10b981" strokeWidth={2.5} dot={{ r: 4, fill: "#10b981", strokeWidth: 2, stroke: "#fff" }} />
</LineChart>
</ResponsiveContainer>
</div>
)}
</div>
{/* History */}
{logs.length > 0 && (
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden">
<div className="px-4 py-3 border-b border-slate-100 dark:border-slate-700">
<h3 className="text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider">Historique</h3>
</div>
<div className="divide-y divide-slate-100 dark:divide-slate-700">
{[...logs].reverse().map((log) => (
<div key={log.id}>
{editingId === log.id ? (
<div className="px-4 py-4 bg-slate-50 dark:bg-slate-900">
<form onSubmit={(e) => handleEditSave(e, log.id)} className="space-y-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-300 mb-1.5">Date</label>
<input type="date" value={editForm.date} onChange={(e) => setEditForm((f) => ({ ...f, date: e.target.value }))} required className={inputCls} />
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-300 mb-1.5">Poids (kg)</label>
<input type="number" value={editForm.weight} onChange={(e) => setEditForm((f) => ({ ...f, weight: e.target.value }))}
step="0.001" min="1" max="30" placeholder="3.500" className={inputCls} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-300 mb-1.5">Taille (cm)</label>
<input type="number" value={editForm.height} onChange={(e) => setEditForm((f) => ({ ...f, height: e.target.value }))}
step="0.1" min="30" max="120" placeholder="50" className={inputCls} />
</div>
<div>
<label className="block text-xs font-medium text-slate-600 dark:text-slate-300 mb-1.5">PC (cm)</label>
<input type="number" value={editForm.headCirc} onChange={(e) => setEditForm((f) => ({ ...f, headCirc: e.target.value }))}
step="0.1" min="20" max="60" placeholder="34" className={inputCls} />
</div>
</div>
<input type="text" value={editForm.notes} onChange={(e) => setEditForm((f) => ({ ...f, notes: e.target.value }))}
placeholder="RDV pédiatre, contexte..." className={inputCls} />
<div className="flex gap-2">
<button type="button" onClick={() => setEditingId(null)} className="flex-1 py-2 border border-slate-200 dark:border-slate-700 rounded-lg text-sm text-slate-600 dark:text-slate-300">Annuler</button>
<button type="submit" disabled={editSaving} className="flex-1 py-2 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700 transition">
{editSaving ? "..." : "Enregistrer"}
</button>
</div>
</form>
</div>
) : (
<div className="px-4 py-3 flex items-center justify-between group">
<span className="text-sm text-slate-600 dark:text-slate-300">
{format(new Date(log.date), "d MMMM yyyy", { locale: fr })}
</span>
<div className="flex items-center gap-3">
<div className="flex gap-4 text-xs text-slate-500 dark:text-slate-400 font-mono">
{log.weight && <span>{(log.weight / 1000).toFixed(3)} kg</span>}
{log.height && <span>{log.height} cm</span>}
{log.headCirc && <span>PC {log.headCirc} cm</span>}
</div>
<div className="flex gap-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
<button
onClick={() => startEdit(log)}
className="p-1.5 rounded-md text-slate-400 dark:text-slate-500 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-950 transition"
>
<Pencil className="w-3.5 h-3.5" />
</button>
<button
onClick={() => handleDelete(log.id)}
className="p-1.5 rounded-md text-slate-400 dark:text-slate-500 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-950 transition"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
)}
</div>
))}
</div>
</div>
)}
{logs.length === 0 && !showForm && (
<div className="text-center py-16 text-slate-400 dark:text-slate-500">
<p className="text-sm">Aucune mesure enregistrée</p>
<button onClick={openAddForm} className="mt-3 text-indigo-600 font-medium text-sm hover:underline">
Ajouter la première mesure
</button>
</div>
)}
</div>
</div>
);
}