tia/src/app/growth/page.tsx
Mannu 96d28cbadc refactor: full codebase sweep — shared types, utilities, component splits
New foundations:
- src/types/index.ts — shared domain types (Child, Log, GrowthRecord, Medicine,
  Dose, Allergy, Visit, Illness, Vaccination, AIChat, ChatSession, Goal)
- src/lib/formatting.ts — calculateAge, formatAge, formatTimeAgo (eliminates
  3 duplicate implementations spread across page.tsx and growth/page.tsx)
- src/lib/api.ts — typed fetch helpers (api.get/post/patch/delete) with
  consistent error handling; replaces manual fetch boilerplate

New shared components:
- src/components/PageHeader.tsx — reusable back-link + title header
- src/components/TabBar.tsx — horizontal pill tab bar
- src/components/CalendarView.tsx — extracted from activity/page.tsx (was ~170 inline lines)
- src/components/medical/ — medical page split into 5 focused tab components:
  VaccineTab, MedicineTab, AllergyTab, VisitTab, IllnessTab

Pages updated:
- medical/page.tsx: 1029 → 42 lines (thin shell wiring the 5 tab components)
- activity/page.tsx: uses CalendarView + shared Log type + api.ts
- growth/page.tsx: uses shared GrowthRecord/Goal types + formatAge; fixes
  `any` catch clauses; fixes undefined → null in Chart.js dataset values
- page.tsx (home): uses shared Log/AIChat/ChatSession types + formatTimeAgo/
  calculateAge from formatting.ts; removes inline type definitions
- ai/page.tsx: uses shared AIChat/ChatSession types
- FamilyProvider.tsx: uses shared Child type; fixes `c: any` mapping

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:37:39 +05:30

670 lines
No EOL
29 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. 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 { useFamily } from "../FamilyProvider";
import { Button, Card, Input, ConfirmDialog } from "@/components/ui";
import { WHO_BOY_WEIGHT, WHO_GIRL_WEIGHT, getAgeInMonthsFromBirth, getPercentile } from "@/lib/growth-standards";
import { formatAge } from "@/lib/formatting";
import type { GrowthRecord, Goal } from "@/types";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler,
} from "chart.js";
import { Line } from "react-chartjs-2";
import { useTheme } from "../ThemeProvider";
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler);
interface Percentiles { p3: number; p15: number; p50: number; p85: number; p97: number }
interface WhoStandard {
weight: Percentiles;
height: Percentiles;
headCircumference: Percentiles;
}
export default function GrowthPage() {
const { childId, child, familyId } = useFamily();
useTheme(); // keep theme context alive for dark mode CSS
const [growthData, setGrowthData] = useState<GrowthRecord[]>([]);
const [whoStandard, setWhoStandard] = useState<WhoStandard | null>(null);
const [loading, setLoading] = useState(true);
const [showAdd, setShowAdd] = useState(false);
const [showGoals, setShowGoals] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
// Form state
const [weight, setWeight] = useState("");
const [height, setHeight] = useState("");
const [headCircumference, setHeadCircumference] = useState("");
const [measuredAt, setMeasuredAt] = useState(new Date().toISOString().split("T")[0]);
// Chart state
const [chartMetric, setChartMetric] = useState<"weight" | "height" | "head">("weight");
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
const [showWhoStandards, setShowWhoStandards] = useState(true);
const [showChart, setShowChart] = useState(true);
const [showHistory, setShowHistory] = useState(true);
// Goals state
const [goal, setGoal] = useState<Goal>({});
// Saved goals from localStorage
const [savedGoals, setSavedGoals] = useState<Goal>({});
useEffect(() => {
if (!childId) return;
fetchGrowthData();
}, [childId]);
useEffect(() => {
// Load saved goals for this child
if (childId) {
const saved = localStorage.getItem(`tia_growth_goal_${childId}`);
if (saved) {
const parsed = JSON.parse(saved);
setSavedGoals(parsed);
setGoal(parsed);
}
}
}, [childId]);
const fetchGrowthData = () => {
if (!childId) return;
fetch(`/api/growth?childId=${childId}`)
.then(r => r.json())
.then(data => {
setGrowthData(data.growth || []);
setWhoStandard(data.whoStandard);
setLoading(false);
})
.catch(() => setLoading(false));
};
const handleAdd = async () => {
if (!childId || (!weight && !height && !headCircumference)) {
setSaveError("Please enter at least one measurement");
return;
}
setSaving(true);
setSaveError(null);
try {
const res = await fetch("/api/growth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
childId,
measuredAt: new Date(measuredAt).toISOString(),
weightKg: weight ? parseFloat(weight) : null,
heightCm: height ? parseFloat(height) : null,
headCircumferenceCm: headCircumference ? parseFloat(headCircumference) : null,
}),
});
if (!res.ok) {
const err = await res.json();
setSaveError(err.error || "Failed to save");
return;
}
resetForm();
fetchGrowthData();
} catch (e) {
setSaveError(e instanceof Error ? e.message : "Failed to save");
} finally {
setSaving(false);
}
};
const handleEdit = async (id: string) => {
setSaving(true);
setSaveError(null);
try {
const res = await fetch("/api/growth", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id,
measuredAt: new Date(measuredAt).toISOString(),
weightKg: weight ? parseFloat(weight) : null,
heightCm: height ? parseFloat(height) : null,
headCircumferenceCm: headCircumference ? parseFloat(headCircumference) : null,
}),
});
if (!res.ok) {
const err = await res.json();
setSaveError(err.error || "Failed to update");
return;
}
setEditingId(null);
resetForm();
fetchGrowthData();
} catch (e: any) {
setSaveError(e.message || "Failed to update");
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
await fetch(`/api/growth?id=${id}`, { method: "DELETE" });
setConfirmDeleteId(null);
fetchGrowthData();
};
const resetForm = () => {
setShowAdd(false);
setEditingId(null);
setWeight("");
setHeight("");
setHeadCircumference("");
setMeasuredAt(new Date().toISOString().split("T")[0]);
};
const startEdit = (record: GrowthRecord) => {
setEditingId(record.id);
setShowAdd(true);
setWeight(record.weight_kg?.toString() || "");
setHeight(record.height_cm?.toString() || "");
setHeadCircumference(record.head_circumference_cm?.toString() || "");
setMeasuredAt(new Date(record.measured_at).toISOString().split("T")[0]);
};
const saveGoal = () => {
if (!childId) return;
localStorage.setItem(`tia_growth_goal_${childId}`, JSON.stringify(goal));
setSavedGoals(goal);
setShowGoals(false);
};
const exportCSV = () => {
if (growthData.length === 0) return;
const headers = ["Date", "Weight (kg)", "Height (cm)", "Head (cm)", "Notes"];
const rows = growthData.map(r => [
new Date(r.measured_at).toLocaleDateString(),
r.weight_kg || "",
r.height_cm || "",
r.head_circumference_cm || "",
r.notes || ""
]);
const csv = [headers, ...rows].map(row => row.join(",")).join("\n");
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${child?.name || "child"}_growth_${new Date().toISOString().split("T")[0]}.csv`;
a.click();
};
// Get latest measurement
const latest = growthData[0];
// Calculate age in months for WHO standard
const ageMonths = child ? getAgeInMonthsFromBirth(child.birthDate) : 12;
const standards = child?.sex === "male" ? WHO_BOY_WEIGHT : WHO_GIRL_WEIGHT;
const standard = standards.find(s => s.ageMonths === Math.min(ageMonths, 24)) || standards[standards.length - 1];
// Get percentile color
const getPercentileColor = (p: string | null): string => {
if (!p) return "text-gray-500";
if (p.includes("Below") || p.includes("3rd")) return "text-red-500";
if (p.includes("85th") || p.includes("97th")) return "text-amber-500";
return "text-green-600";
};
// Calculate percentile for latest reading
const weightPercentile = latest?.weight_kg && whoStandard ?
getPercentile(latest.weight_kg, whoStandard.weight.p3, whoStandard.weight.p15, whoStandard.weight.p50, whoStandard.weight.p85, whoStandard.weight.p97) : null;
const heightPercentile = latest?.height_cm && whoStandard ?
getPercentile(latest.height_cm, whoStandard.height.p3, whoStandard.height.p15, whoStandard.height.p50, whoStandard.height.p85, whoStandard.height.p97) : null;
const headPercentile = latest?.head_circumference_cm && whoStandard ?
getPercentile(latest.head_circumference_cm, whoStandard.headCircumference.p3, whoStandard.headCircumference.p15, whoStandard.headCircumference.p50, whoStandard.headCircumference.p85, whoStandard.headCircumference.p97) : null;
// Calculate growth velocity (change between last two readings)
const velocity = (() => {
if (growthData.length < 2) return null;
const current = growthData[0];
const previous = growthData[1];
const daysDiff = (new Date(current.measured_at).getTime() - new Date(previous.measured_at).getTime()) / (1000 * 60 * 60 * 24);
if (!current.weight_kg || !previous.weight_kg || daysDiff < 1) return null;
const weightChange = current.weight_kg - previous.weight_kg;
const monthsDiff = daysDiff / 30;
const monthlyGain = weightChange / monthsDiff;
return { weight: monthlyGain.toFixed(2), direction: monthlyGain > 0 ? "up" : "down" };
})();
// Prepare chart data
const chartData = (() => {
if (growthData.length === 0 || !child?.birthDate) return null;
const metricKey = chartMetric === "weight" ? "weight_kg" : chartMetric === "height" ? "height_cm" : "head_circumference_cm";
const whoKey = chartMetric === "weight" ? "weight" : chartMetric === "height" ? "height" : "headCircumference";
// Sort records by date (oldest first)
const sorted = [...growthData].reverse();
const labels = sorted.map(r => {
const measured = new Date(r.measured_at);
const birth = new Date(child.birthDate);
const months = Math.floor((measured.getTime() - birth.getTime()) / (1000 * 60 * 60 * 24 * 30));
return `${months}mo`;
});
const childData = sorted.map(r => r[metricKey]).filter(Boolean);
return {
labels,
datasets: [
{
label: child.name,
data: childData,
borderColor: "#fb7185",
backgroundColor: "rgba(251, 113, 133, 0.1)",
fill: true,
tension: 0.3,
pointRadius: 6,
pointBackgroundColor: "#fb7185",
},
// WHO 85th percentile
{
label: "85th",
data: labels.map(() => whoStandard?.[whoKey]?.p85 ?? null),
borderColor: "#fbbf24",
borderDash: [5, 5],
fill: false,
pointRadius: 0,
tension: 0.4,
},
// WHO 50th percentile
{
label: "50th",
data: labels.map(() => whoStandard?.[whoKey]?.p50 ?? null),
borderColor: "#22c55e",
borderDash: [5, 5],
fill: false,
pointRadius: 0,
tension: 0.4,
},
// WHO 15th percentile
{
label: "15th",
data: labels.map(() => whoStandard?.[whoKey]?.p15 ?? null),
borderColor: "#fbbf24",
borderDash: [5, 5],
fill: false,
pointRadius: 0,
tension: 0.4,
},
{
label: "3rd",
data: labels.map(() => whoStandard?.[whoKey]?.p3 ?? null),
borderColor: "#fbbf24",
borderDash: [5, 5],
fill: false,
pointRadius: 0,
tension: 0.4,
},
],
};
})();
const chartOptions = {
responsive: true,
plugins: {
legend: { display: true, position: "top" as const, labels: { boxWidth: 10 } },
title: { display: false },
},
scales: {
y: {
title: { display: true, text: chartMetric === "weight" ? "Weight (kg)" : "cm" },
},
},
};
if (!familyId) {
return <div className="p-4">Please log in to view growth records.</div>;
}
return (
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800 pb-20">
<div className="p-4 flex justify-between items-center">
<div className="flex items-center gap-4">
<a href="/menu" className="p-2"></a>
<h1 className="text-xl font-bold">Growth 📈</h1>
</div>
<div className="flex gap-2">
<Button variant="secondary" size="sm" onClick={exportCSV} title="Export CSV">📥</Button>
<Button variant="secondary" size="sm" onClick={() => setShowGoals(!showGoals)} title="Set goals">🎯</Button>
<Button variant="primary" size="sm" onClick={() => { setShowAdd(!showAdd); setEditingId(null); setWeight(""); setHeight(""); setHeadCircumference(""); setMeasuredAt(new Date().toISOString().split("T")[0]); }}>
+ Add
</Button>
</div>
</div>
{/* Goals Card */}
{showGoals && (
<Card className="mx-4 mb-4">
<h3 className="font-semibold mb-3">Set Growth Goals</h3>
<div className="space-y-3">
<Input label="Target Weight (kg)" type="number" step="0.1" placeholder="e.g., 10"
value={goal.weightKg || ""}
onChange={e => setGoal({ ...goal, weightKg: parseFloat(e.target.value) || undefined })}
/>
<Input label="Target Height (cm)" type="number" step="0.1" placeholder="e.g., 80"
value={goal.heightCm || ""}
onChange={e => setGoal({ ...goal, heightCm: parseFloat(e.target.value) || undefined })}
/>
<div className="flex gap-2">
<Button fullWidth onClick={saveGoal}>Save Goal</Button>
<Button variant="secondary" fullWidth onClick={() => setShowGoals(false)}>Cancel</Button>
</div>
</div>
</Card>
)}
{/* Latest Reading Card */}
{latest && (
<div className="mx-4 mb-4 p-4 bg-gradient-to-r from-rose-50 to-pink-50 dark:from-rose-900 dark:to-pink-900 rounded-xl hover:shadow-lg transition-shadow cursor-pointer">
<div className="flex justify-between items-center mb-3">
<div>
<div className="font-semibold text-rose-600 dark:text-rose-300">Latest Reading</div>
<div className="text-sm text-gray-500">
{new Date(latest.measured_at).toLocaleDateString()} ({child ? formatAge(child.birthDate, latest.measured_at) : ""})
</div>
</div>
{velocity && (
<div className={`text-sm font-medium ${velocity.direction === "up" ? "text-green-500" : "text-amber-500"}`}>
{velocity.weight}kg/mo {velocity.direction === "up" ? "↑" : "↓"}
</div>
)}
</div>
<div className="grid grid-cols-3 gap-3">
{latest.weight_kg && (
<div className="p-3 bg-white dark:bg-gray-800 rounded-lg hover:shadow-md transition-shadow">
<div className="text-xs text-gray-500 mb-1">Weight</div>
<div className="text-xl font-bold">{latest.weight_kg} <span className="text-sm font-normal">kg</span></div>
{weightPercentile && (
<div className={`text-xs font-medium mt-1 ${getPercentileColor(weightPercentile)}`}>
{weightPercentile}
{savedGoals.weightKg && (
<span className="ml-1 text-gray-400">
{((latest.weight_kg / savedGoals.weightKg) * 100).toFixed(0)}%
</span>
)}
</div>
)}
</div>
)}
{latest.height_cm && (
<div className="p-3 bg-white dark:bg-gray-800 rounded-lg hover:shadow-md transition-shadow">
<div className="text-xs text-gray-500 mb-1">Height</div>
<div className="text-xl font-bold">{latest.height_cm} <span className="text-sm font-normal">cm</span></div>
{heightPercentile && (
<div className={`text-xs font-medium mt-1 ${getPercentileColor(heightPercentile)}`}>
{heightPercentile}
{savedGoals.heightCm && (
<span className="ml-1 text-gray-400">
{((latest.height_cm / savedGoals.heightCm) * 100).toFixed(0)}%
</span>
)}
</div>
)}
</div>
)}
{latest.head_circumference_cm && (
<div className="p-3 bg-white dark:bg-gray-800 rounded-lg hover:shadow-md transition-shadow">
<div className="text-xs text-gray-500 mb-1">Head</div>
<div className="text-xl font-bold">{latest.head_circumference_cm} <span className="text-sm font-normal">cm</span></div>
{headPercentile && (
<div className={`text-xs font-medium mt-1 ${getPercentileColor(headPercentile)}`}>
{headPercentile}
</div>
)}
</div>
)}
</div>
</div>
)}
{/* WHO Standards + Chart Card - Collapsible */}
{child && standard && (
<div className="mx-4 mb-4 bg-white dark:bg-gray-800 rounded-xl overflow-hidden">
{/* WHO Standards Header - Clickable */}
<button
onClick={() => setShowWhoStandards(!showWhoStandards)}
className="w-full p-4 flex justify-between items-center hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="text-left">
<div className="font-semibold text-lg">{child.name}</div>
<div className="text-sm text-gray-500">{formatAge(child.birthDate)}</div>
</div>
<div className="flex items-center gap-2">
{savedGoals.weightKg && (
<div className="text-sm text-rose-500 mr-2">Goal: {savedGoals.weightKg}kg</div>
)}
<span className={`transform transition-transform ${showWhoStandards ? "rotate-180" : ""}`}></span>
</div>
</button>
{/* When collapsed - Show recommended values with icons */}
{!showWhoStandards && (
<div className="px-4 pb-4">
<div className="grid grid-cols-3 gap-3">
<div className={`p-3 rounded-lg text-center ${latest?.weight_kg && latest.weight_kg < standard.weight.p3 ? "bg-red-100 dark:bg-red-900" : latest?.weight_kg && latest.weight_kg > standard.weight.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-50 dark:bg-green-900"}`}>
<div className="text-2xl mb-1"></div>
<div className="font-medium">{standard.weight.p50} kg</div>
<div className="text-xs text-gray-500">{standard.weight.p3}-{standard.weight.p97}</div>
</div>
<div className={`p-3 rounded-lg text-center ${latest?.height_cm && latest.height_cm < standard.height.p3 ? "bg-red-100 dark:bg-red-900" : latest?.height_cm && latest.height_cm > standard.height.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-50 dark:bg-green-900"}`}>
<div className="text-2xl mb-1">📏</div>
<div className="font-medium">{standard.height.p50} cm</div>
<div className="text-xs text-gray-500">{standard.height.p3}-{standard.height.p97}</div>
</div>
<div className={`p-3 rounded-lg text-center ${latest?.head_circumference_cm && latest.head_circumference_cm < standard.headCircumference.p3 ? "bg-red-100 dark:bg-red-900" : latest?.head_circumference_cm && latest.head_circumference_cm > standard.headCircumference.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-50 dark:bg-green-900"}`}>
<div className="text-2xl mb-1"></div>
<div className="font-medium">{standard.headCircumference.p50} cm</div>
<div className="text-xs text-gray-500">{standard.headCircumference.p3}-{standard.headCircumference.p97}</div>
</div>
</div>
</div>
)}
{/* When expanded - Show full details + chart */}
{showWhoStandards && (
<div className="px-4 pb-4">
{/* Color-coded percentile zones legend */}
<div className="flex gap-3 mb-3 text-xs">
<span className="flex items-center gap-1">
<span className="w-3 h-3 rounded-full bg-green-500"></span>
Normal
</span>
<span className="flex items-center gap-1">
<span className="w-3 h-3 rounded-full bg-amber-500"></span>
Watch
</span>
<span className="flex items-center gap-1">
<span className="w-3 h-3 rounded-full bg-red-500"></span>
Alert
</span>
</div>
{/* WHO Standards Cards */}
<div className="grid grid-cols-3 gap-3 mb-3">
<div className={`p-3 rounded-lg hover:shadow-md transition-shadow ${latest?.weight_kg && latest.weight_kg < standard.weight.p3 ? "bg-red-100 dark:bg-red-900" : latest?.weight_kg && latest.weight_kg > standard.weight.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-50 dark:bg-green-900"}`}>
<div className="text-gray-500 mb-1 text-xs">Weight</div>
<div className="font-medium text-lg">{standard.weight.p50} kg</div>
<div className="text-xs text-gray-400">{standard.weight.p3}-{standard.weight.p97}</div>
{latest?.weight_kg && (
<div className={`text-xs font-medium mt-1 ${getPercentileColor(weightPercentile)}`}>
{latest.weight_kg}kg ({weightPercentile})
</div>
)}
</div>
<div className={`p-3 rounded-lg hover:shadow-md transition-shadow ${latest?.height_cm && latest.height_cm < standard.height.p3 ? "bg-red-100 dark:bg-red-900" : latest?.height_cm && latest.height_cm > standard.height.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-50 dark:bg-green-900"}`}>
<div className="text-gray-500 mb-1 text-xs">Height</div>
<div className="font-medium text-lg">{standard.height.p50} cm</div>
<div className="text-xs text-gray-400">{standard.height.p3}-{standard.height.p97}</div>
{latest?.height_cm && (
<div className={`text-xs font-medium mt-1 ${getPercentileColor(heightPercentile)}`}>
{latest.height_cm}cm ({heightPercentile})
</div>
)}
</div>
<div className={`p-3 rounded-lg hover:shadow-md transition-shadow ${latest?.head_circumference_cm && latest.head_circumference_cm < standard.headCircumference.p3 ? "bg-red-100 dark:bg-red-900" : latest?.head_circumference_cm && latest.head_circumference_cm > standard.headCircumference.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-50 dark:bg-green-900"}`}>
<div className="text-gray-500 mb-1 text-xs">Head</div>
<div className="font-medium text-lg">{standard.headCircumference.p50} cm</div>
<div className="text-xs text-gray-400">{standard.headCircumference.p3}-{standard.headCircumference.p97}</div>
{latest?.head_circumference_cm && (
<div className={`text-xs font-medium mt-1 ${getPercentileColor(headPercentile)}`}>
{latest.head_circumference_cm}cm ({headPercentile})
</div>
)}
</div>
</div>
{/* Growth velocity indicator */}
{velocity && (
<div className="flex items-center gap-2 text-sm mb-3">
<span className="text-gray-500">Velocity:</span>
<span className={`font-medium ${velocity.direction === "up" ? "text-green-600" : "text-amber-600"}`}>
{velocity.weight} kg/month {velocity.direction === "up" ? "↑" : "↓"}
</span>
</div>
)}
{/* Growth Chart - Collapsible inside expanded view */}
{chartData && growthData.length > 0 && (
<div className="border-t border-gray-200 dark:border-gray-700 pt-3">
<button
onClick={() => setShowChart(!showChart)}
className="w-full flex justify-between items-center mb-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded p-2 -mx-2 transition-colors"
>
<h3 className="font-semibold text-sm">📈 Growth Chart</h3>
<span className={`transform transition-transform ${showChart ? "rotate-180" : ""}`}></span>
</button>
{showChart && <Line data={chartData} options={chartOptions} />}
</div>
)}
</div>
)}
</div>
)}
{/* Add Measurement - Same pattern as Goals */}
{showAdd && latest && (
<div className="mx-4 mb-4 p-4 bg-white dark:bg-gray-800 rounded-xl space-y-3">
<h3 className="font-semibold">{editingId ? "Edit Record" : "Add New Measurement"}</h3>
{saveError && (
<div className="p-2 bg-red-100 dark:bg-red-900 text-red-600 rounded-lg text-sm">{saveError}</div>
)}
<Input type="date" value={measuredAt} onChange={e => setMeasuredAt(e.target.value)} />
<Input type="number" step="0.01" placeholder="Weight (kg)" value={weight} onChange={e => setWeight(e.target.value)} />
<Input type="number" step="0.1" placeholder="Height (cm)" value={height} onChange={e => setHeight(e.target.value)} />
<Input type="number" step="0.1" placeholder="Head circumference (cm)" value={headCircumference} onChange={e => setHeadCircumference(e.target.value)} />
<div className="flex gap-2">
{editingId ? (
<>
<Button fullWidth loading={saving} onClick={() => handleEdit(editingId)}>Update</Button>
<Button variant="secondary" fullWidth onClick={resetForm}>Cancel</Button>
</>
) : (
<>
<Button fullWidth loading={saving} onClick={() => handleAdd()}>Save</Button>
<Button variant="secondary" fullWidth onClick={() => { setShowAdd(false); setWeight(""); setHeight(""); setHeadCircumference(""); }}>Cancel</Button>
</>
)}
</div>
</div>
)}
{/* Empty State */}
{loading ? (
<div className="p-8 text-center text-gray-500">
<p>Loading...</p>
</div>
) : growthData.length === 0 ? (
<Card className="mx-4">
<div className="text-center py-4">
<div className="text-4xl mb-4">📏</div>
<h3 className="text-lg font-semibold mb-2">Track {child?.name}&apos;s Growth</h3>
<p className="text-gray-500 mb-4 text-sm">
Start logging weight, height, and head measurements to see how {child?.name} is growing compared to WHO standards.
</p>
<Button onClick={() => setShowAdd(true)}>Add First Measurement</Button>
</div>
</Card>
) : (
/* History List - Collapsible */
<div className="mx-4 mb-4 bg-white dark:bg-gray-800 rounded-xl overflow-hidden">
<button
onClick={() => setShowHistory(!showHistory)}
className="w-full p-4 flex justify-between items-center hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="font-semibold">History ({growthData.length} records)</div>
<span className={`transform transition-transform ${showHistory ? "rotate-180" : ""}`}></span>
</button>
{showHistory && (
<div className="px-4 pb-4 space-y-2 max-h-96 overflow-y-auto">
{growthData.map((record, i) => (
<div key={i} className="p-3 bg-gray-50 dark:bg-gray-700 rounded-lg hover:shadow-md transition-shadow flex justify-between items-center">
<div>
<div className="text-sm text-gray-500">
{new Date(record.measured_at).toLocaleDateString()} ({child ? formatAge(child.birthDate, record.measured_at) : ""})
</div>
<div className="flex gap-2 mt-1">
{record.weight_kg && (
<span className="px-2 py-0.5 bg-white dark:bg-gray-600 rounded text-xs"> {record.weight_kg}kg</span>
)}
{record.height_cm && (
<span className="px-2 py-0.5 bg-white dark:bg-gray-600 rounded text-xs">📏 {record.height_cm}cm</span>
)}
{record.head_circumference_cm && (
<span className="px-2 py-0.5 bg-white dark:bg-gray-600 rounded text-xs"> {record.head_circumference_cm}cm</span>
)}
</div>
</div>
<div className="flex gap-1">
<button
onClick={() => startEdit(record)}
className="p-2 text-sm text-gray-400 hover:text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-900 rounded-lg transition-colors"
title="Edit"
>
</button>
<button
onClick={() => setConfirmDeleteId(record.id)}
className="p-2 text-sm text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900 rounded-lg transition-colors"
title="Delete"
>
🗑
</button>
</div>
</div>
))}
</div>
)}
</div>
)}
<ConfirmDialog
open={!!confirmDeleteId}
onClose={() => setConfirmDeleteId(null)}
onConfirm={() => confirmDeleteId && handleDelete(confirmDeleteId)}
title="Delete this record?"
description="This measurement will be permanently removed."
confirmLabel="Delete"
variant="danger"
/>
</div>
);
}