"use client"; import { useState, useEffect } from "react"; import Link from "next/link"; import { useFamily } from "@/app/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 { fmtDate } from "@/lib/date-ist"; import { trackGrowthLogged } from "@/lib/analytics"; 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 "@/app/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([]); 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"); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [confirmDeleteId, setConfirmDeleteId] = useState(null); const [showWhoStandards, setShowWhoStandards] = useState(true); const [showChart, setShowChart] = useState(true); const [showHistory, setShowHistory] = useState(true); // 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)) { 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; } trackGrowthLogged(); 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 => [ fmtDate(r.measured_at), 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
Please log in to view growth records.
; } return (
{/* Header */}

Growth 📈

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

🎯 Growth Goals

setGoal({ ...goal, weightKg: parseFloat(e.target.value) || undefined })} /> setGoal({ ...goal, heightCm: parseFloat(e.target.value) || undefined })} />
)} {/* Add / Edit Measurement — always at the top, right below the header */} {showAdd && (

{editingId ? "✏️ Edit Record" : "📏 New Measurement"}

{saveError && (
{saveError}
)} setMeasuredAt(e.target.value)} />
setWeight(e.target.value)} /> setHeight(e.target.value)} /> setHeadCircumference(e.target.value)} />
{editingId ? ( <> ) : ( <> )}
)} {/* Latest Reading Card */} {latest && (
Latest Reading
{fmtDate(latest.measured_at)} ({child ? formatAge(child.birthDate, latest.measured_at) : ""})
{velocity && (
{velocity.weight}kg/mo {velocity.direction === "up" ? "↑" : "↓"}
)}
{latest.weight_kg && (
Weight
{latest.weight_kg} kg
{weightPercentile && (
{weightPercentile} {savedGoals.weightKg && ( → {((latest.weight_kg / savedGoals.weightKg) * 100).toFixed(0)}% )}
)}
)} {latest.height_cm && (
Height
{latest.height_cm} cm
{heightPercentile && (
{heightPercentile} {savedGoals.heightCm && ( → {((latest.height_cm / savedGoals.heightCm) * 100).toFixed(0)}% )}
)}
)} {latest.head_circumference_cm && (
Head
{latest.head_circumference_cm} cm
{headPercentile && (
{headPercentile}
)}
)}
)} {/* WHO Standards + Chart Card - Collapsible */} {child && standard && (
{/* WHO Standards Header - Clickable */} {/* When collapsed - Show recommended values with icons */} {!showWhoStandards && (
standard.weight.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-50 dark:bg-green-900"}`}>
⚖️
{standard.weight.p50} kg
{standard.weight.p3}-{standard.weight.p97}
standard.height.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-50 dark:bg-green-900"}`}>
📏
{standard.height.p50} cm
{standard.height.p3}-{standard.height.p97}
standard.headCircumference.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-50 dark:bg-green-900"}`}>
{standard.headCircumference.p50} cm
{standard.headCircumference.p3}-{standard.headCircumference.p97}
)} {/* When expanded - Show full details + chart */} {showWhoStandards && (
{/* Color-coded percentile zones legend */}
Normal Watch Alert
{/* WHO Standards Cards */}
standard.weight.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-50 dark:bg-green-900"}`}>
Weight
{standard.weight.p50} kg
{standard.weight.p3}-{standard.weight.p97}
{latest?.weight_kg && (
{latest.weight_kg}kg ({weightPercentile})
)}
standard.height.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-50 dark:bg-green-900"}`}>
Height
{standard.height.p50} cm
{standard.height.p3}-{standard.height.p97}
{latest?.height_cm && (
{latest.height_cm}cm ({heightPercentile})
)}
standard.headCircumference.p85 ? "bg-amber-100 dark:bg-amber-900" : "bg-green-50 dark:bg-green-900"}`}>
Head
{standard.headCircumference.p50} cm
{standard.headCircumference.p3}-{standard.headCircumference.p97}
{latest?.head_circumference_cm && (
{latest.head_circumference_cm}cm ({headPercentile})
)}
{/* Growth velocity indicator */} {velocity && (
Velocity: {velocity.weight} kg/month {velocity.direction === "up" ? "↑" : "↓"}
)} {/* Growth Chart - Collapsible inside expanded view */} {chartData && growthData.length > 0 && (
{showChart && }
)}
)}
)} {/* Empty State */} {loading ? (

Loading...

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

Track {child?.name}'s Growth

Log weight, height, and head measurements to see how {child?.name} compares to WHO standards.

) : ( /* History List - Collapsible */
{showHistory && (
{growthData.map((record, i) => (
{fmtDate(record.measured_at)} ({child ? formatAge(child.birthDate, record.measured_at) : ""})
{record.weight_kg && ( ⚖️ {record.weight_kg}kg )} {record.height_cm && ( 📏 {record.height_cm}cm )} {record.head_circumference_cm && ( ⭕ {record.head_circumference_cm}cm )}
))}
)}
)} setConfirmDeleteId(null)} onConfirm={() => confirmDeleteId && handleDelete(confirmDeleteId)} title="Delete this record?" description="This measurement will be permanently removed." confirmLabel="Delete" variant="danger" />
); }