"use client"; import { useState, useEffect, useRef } from "react"; import { useFamily } from "../FamilyProvider"; import { WHO_BOY_WEIGHT, WHO_GIRL_WEIGHT, getAgeInMonthsFromBirth, getPercentile, type GrowthStandard } from "@/lib/growth-standards"; 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 GrowthRecord { id: string; child_id: string; measured_at: string; weight_kg: number | null; height_cm: number | null; head_circumference_cm: number | null; notes: string | null; } interface Goal { weightKg?: number; heightCm?: number; targetDate?: string; } export default function GrowthPage() { const { childId, child, familyId } = useFamily(); const { theme } = useTheme(); const isDark = theme === "dark"; const [growthData, setGrowthData] = useState([]); const [whoStandard, setWhoStandard] = useState(null); const [loading, setLoading] = useState(true); const [showAdd, setShowAdd] = useState(false); const [showGoals, setShowGoals] = useState(false); const [editingId, setEditingId] = useState(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"); // Goals state const [goal, setGoal] = useState({}); // Saved goals from localStorage const [savedGoals, setSavedGoals] = useState({}); 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)) return; 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, }), }); resetForm(); fetchGrowthData(); }; const handleEdit = async (id: string) => { 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, }), }); setEditingId(null); resetForm(); fetchGrowthData(); }; const handleDelete = async (id: string) => { if (!confirm("Delete this record?")) return; await fetch(`/api/growth?id=${id}`, { method: "DELETE" }); 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), borderColor: "#fbbf24", borderDash: [5, 5], fill: false, pointRadius: 0, tension: 0.4, }, // WHO 50th percentile { label: "50th", data: labels.map(() => whoStandard?.[whoKey]?.p50), borderColor: "#22c55e", borderDash: [5, 5], fill: false, pointRadius: 0, tension: 0.4, }, // WHO 15th percentile { label: "15th", data: labels.map(() => whoStandard?.[whoKey]?.p15), borderColor: "#fbbf24", borderDash: [5, 5], fill: false, pointRadius: 0, tension: 0.4, }, // WHO 3rd-97th band { label: "3rd-97th", data: labels.map(() => [whoStandard?.[whoKey]?.p3, whoStandard?.[whoKey]?.p97]), borderColor: "transparent", backgroundColor: "rgba(34, 197, 94, 0.1)", fill: "+1", pointRadius: 0, }, ], }; })(); 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
Please log in to view growth records.
; } return (

Growth 📈

{/* Goals Card */} {showGoals && (

Set Growth Goals

setGoal({ ...goal, weightKg: parseFloat(e.target.value) || undefined })} className="w-full p-2 border rounded-lg dark:bg-gray-700" />
setGoal({ ...goal, heightCm: parseFloat(e.target.value) || undefined })} className="w-full p-2 border rounded-lg dark:bg-gray-700" />
)} {/* WHO Standards Card - Enhanced with age-wise targets */} {child && standard && (
{child.name}
{ageMonths} months old
{savedGoals.weightKg && (
Goal
{savedGoals.weightKg}kg
)}
{/* Color-coded percentile zones legend */}
Normal (15th-85th) Watch (<15th or >85th) Alert (<3rd or >97th)
standard.weight.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-100 dark:bg-green-900"}`}>
Weight
{standard.weight.p50} kg
Target: {standard.weight.p3}-{standard.weight.p97}
{latest?.weight_kg && (
Actual: {latest.weight_kg}kg ({weightPercentile})
)}
standard.height.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-100 dark:bg-green-900"}`}>
Height
{standard.height.p50} cm
Target: {standard.height.p3}-{standard.height.p97}
{latest?.height_cm && (
Actual: {latest.height_cm}cm ({heightPercentile})
)}
standard.headCircumference.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-100 dark:bg-green-900"}`}>
Head
{standard.headCircumference.p50} cm
Target: {standard.headCircumference.p3}-{standard.headCircumference.p97}
{latest?.head_circumference_cm && (
Actual: {latest.head_circumference_cm}cm ({headPercentile})
)}
{/* Growth velocity indicator */} {velocity && (
Growth velocity: {velocity.weight} kg/month {velocity.direction === "up" ? "↑" : "↓"}
)}
)} {/* Growth Chart */} {chartData && growthData.length > 0 && (

Growth Chart

)} {/* Latest Reading Card */} {latest && (
Latest: {new Date(latest.measured_at).toLocaleDateString()} {velocity && ( ({velocity.weight}kg/mo {velocity.direction === "up" ? "↑" : "↓"}) )}
{latest.weight_kg && (
Weight
{latest.weight_kg} kg
{weightPercentile && (
{weightPercentile} percentile {savedGoals.weightKg && ( → {((latest.weight_kg / savedGoals.weightKg) * 100).toFixed(0)}% of goal )}
)}
)} {latest.height_cm && (
Height
{latest.height_cm} cm
{heightPercentile && (
{heightPercentile} percentile {savedGoals.heightCm && ( → {((latest.height_cm / savedGoals.heightCm) * 100).toFixed(0)}% of goal )}
)}
)} {latest.head_circumference_cm && (
Head
{latest.head_circumference_cm} cm
{headPercentile && (
{headPercentile} percentile
)}
)}
)} {/* Add/Edit Form */} {showAdd && (

{editingId ? "Edit Record" : "Add New Measurement"}

setMeasuredAt(e.target.value)} className="w-full p-3 border rounded-xl dark:bg-gray-700" /> setWeight(e.target.value)} className="w-full p-3 border rounded-xl dark:bg-gray-700" /> setHeight(e.target.value)} className="w-full p-3 border rounded-xl dark:bg-gray-700" /> setHeadCircumference(e.target.value)} className="w-full p-3 border rounded-xl dark:bg-gray-700" />
{editingId ? ( <> ) : ( )}
)} {/* Empty State */} {loading ? (

Loading...

) : growthData.length === 0 ? (
📏

Track {child?.name}'s Growth

Start logging weight, height, and head measurements to see how {child?.name} is growing compared to WHO standards.

) : ( /* History List */

History

{growthData.map((record: any, i: number) => (
{new Date(record.measured_at).toLocaleDateString()}
{record.weight_kg && (
⚖️ {record.weight_kg} kg
)} {record.height_cm && (
📏 {record.height_cm} cm
)} {record.head_circumference_cm && (
⭕ {record.head_circumference_cm} cm
)}
))}
)}
); }