From bfc1543b1cb6a7bb8c40dcb6d9fd60f7d15ccedd Mon Sep 17 00:00:00 2001 From: Mannu Date: Sun, 10 May 2026 16:17:57 +0530 Subject: [PATCH] Add administered date for vaccinations and WHO growth benchmarks Vaccinations: - Allow custom date input when marking vaccine as given - Show actual administered date alongside scheduled due date Growth: - Add WHO child growth standards data (boys/girls) - Show age-based benchmarks on Growth page - Display percentile ranges for weight and height - Show latest measurement compared to standards Co-Authored-By: Claude Opus 4.7 --- src/app/growth/page.tsx | 77 ++++++++++++++++++++++++++++++++++++- src/app/medical/page.tsx | 66 +++++++++++++++++++++++-------- src/lib/growth-standards.ts | 72 ++++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 18 deletions(-) create mode 100644 src/lib/growth-standards.ts diff --git a/src/app/growth/page.tsx b/src/app/growth/page.tsx index 8f25402..dd4421e 100644 --- a/src/app/growth/page.tsx +++ b/src/app/growth/page.tsx @@ -1,6 +1,14 @@ "use client"; import { useState, useEffect } from "react"; +import { WHO_BOY_WEIGHT, WHO_GIRL_WEIGHT, getAgeInMonthsFromBirth, getPercentile } from "@/lib/growth-standards"; + +interface Child { + id: string; + name: string; + birthDate: string; + sex: string; +} export default function GrowthPage() { const [childId] = useState("5ad3b16a-1e0d-45ab-bc91-038397d75d0a"); @@ -9,6 +17,7 @@ export default function GrowthPage() { const [showAdd, setShowAdd] = useState(false); const [weight, setWeight] = useState(""); const [height, setHeight] = useState(""); + const [child, setChild] = useState(null); useEffect(() => { fetch(`/api/growth?childId=${childId}`) @@ -18,6 +27,15 @@ export default function GrowthPage() { setLoading(false); }) .catch(() => setLoading(false)); + + // Fetch child info + fetch("/api/children?familyId=default") + .then(r => r.json()) + .then(data => { + if (data.children?.length > 0) { + setChild(data.children[0]); + } + }); }, [childId]); const handleAdd = async () => { @@ -32,9 +50,21 @@ export default function GrowthPage() { }), }); setShowAdd(false); - window.location.reload(); + setWeight(""); + setHeight(""); + fetch(`/api/growth?childId=${childId}`) + .then(r => r.json()) + .then(data => setGrowthData(data.growth || [])); }; + // Get latest measurement + const latest = growthData[0]; + + // Get standard data for comparison + 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]; + return (
@@ -47,6 +77,51 @@ export default function GrowthPage() {
+ {/* Benchmark Card */} + {child && standard && ( +
+
+
{child.name}
+
{ageMonths} months old
+
+
+
+
Weight (50th percentile)
+
{standard.weight.p50} kg
+
Range: {standard.weight.p3}-{standard.weight.p97} kg
+
+
+
Height (50th percentile)
+
{standard.height.p50} cm
+
Range: {standard.height.p3}-{standard.height.p97} cm
+
+
+
+ )} + + {/* Latest Reading */} + {latest && ( +
+
+ Latest: {new Date(latest.measured_at).toLocaleDateString()} +
+
+ {latest.weight_kg && ( +
+
Weight
+
{latest.weight_kg} kg
+
+ )} + {latest.height_cm && ( +
+
Height
+
{latest.height_cm} cm
+
+ )} +
+
+ )} + {showAdd && (
([]); const [loading, setLoading] = useState(true); const [tab, setTab] = useState<"vaccinations" | "growth">("vaccinations"); + const [showAddDate, setShowAddDate] = useState(null); + const [givenDate, setGivenDate] = useState(""); const birthDate = "2024-01-15"; @@ -58,6 +60,7 @@ export default function MedicalPage() { const handleMarkGiven = async (vaccineName: string) => { const dueDate = calculateDueDate(birthDate, IAP_SCHEDULE.find((v) => v.name === vaccineName)?.weeks || 0); + const dateToSave = givenDate || new Date().toISOString().split("T")[0]; await fetch("/api/vaccinations", { method: "POST", @@ -66,15 +69,18 @@ export default function MedicalPage() { childId, vaccineName, scheduledDate: dueDate, - givenDate: new Date().toISOString().split("T")[0], + givenDate: dateToSave, status: "given", }), }); - setVaccinations((prev) => [...prev, { vaccine_name: vaccineName, status: "given" }]); + setVaccinations((prev) => [...prev, { vaccine_name: vaccineName, given_date: dateToSave, status: "given" }]); + setShowAddDate(null); + setGivenDate(""); }; const isGiven = (name: string) => vaccinations.some((v) => v.vaccine_name === name && v.status === "given"); + const getGivenDate = (name: string) => vaccinations.find((v) => v.vaccine_name === name && v.status === "given")?.given_date; const isPending = (name: string) => !isGiven(name); return ( @@ -110,29 +116,55 @@ export default function MedicalPage() { .sort((a, b) => a.weeks - b.weeks) .map((vaccine) => { const given = isGiven(vaccine.name); + const actualDate = getGivenDate(vaccine.name); const dueDate = calculateDueDate(birthDate, vaccine.weeks); return (
-
-
{vaccine.name}
-
- Due: {new Date(dueDate).toLocaleDateString()} +
+
+
{vaccine.name}
+
+ Due: {new Date(dueDate).toLocaleDateString()} + {actualDate && ` · Given: ${new Date(actualDate).toLocaleDateString()}`} +
+ {given ? ( + + ) : showAddDate === vaccine.name ? ( +
+ setGivenDate(e.target.value)} + className="p-1 text-sm border rounded" + placeholder="Date given" + /> + + +
+ ) : ( + + )}
- {given ? ( - ✓ Given - ) : ( - - )}
); }) diff --git a/src/lib/growth-standards.ts b/src/lib/growth-standards.ts new file mode 100644 index 0000000..7c71fb6 --- /dev/null +++ b/src/lib/growth-standards.ts @@ -0,0 +1,72 @@ +// WHO Child Growth Standards +// Weight-for-age, Length/Height-for-age, Head circumference-for-age +// Source: WHO Child Growth Standards (2006) + +export interface GrowthStandard { + ageMonths: number; + weight: { p50: number; p3: number; p15: number; p85: number; p97: number }; + height: { p50: number; p3: number; p15: number; p85: number; p97: number }; +} + +// Boys weight (kg) by month +export const WHO_BOY_WEIGHT: GrowthStandard[] = [ + { ageMonths: 0, weight: { p50: 3.3, p3: 2.5, p15: 2.9, p85: 3.9, p97: 4.4 }, height: { p50: 49.9, p3: 46.3, p15: 47.9, p85: 51.8, p97: 53.4 } }, + { ageMonths: 1, weight: { p50: 4.5, p3: 3.4, p15: 3.9, p85: 5.2, p97: 5.8 }, height: { p50: 54.7, p3: 50.7, p15: 52.4, p85: 57.0, p97: 58.7 } }, + { ageMonths: 2, weight: { p50: 5.6, p3: 4.3, p15: 4.9, p85: 6.4, p97: 7.1 }, height: { p50: 58.4, p3: 54.1, p15: 55.8, p85: 61.0, p97: 62.7 } }, + { ageMonths: 3, weight: { p50: 6.4, p3: 4.9, p15: 5.6, p85: 7.3, p97: 8.1 }, height: { p50: 61.4, p3: 56.8, p15: 58.6, p85: 64.3, p97: 66.1 } }, + { ageMonths: 4, weight: { p50: 7.0, p3: 5.4, p15: 6.1, p85: 8.0, p97: 8.9 }, height: { p50: 63.9, p3: 59.0, p15: 60.9, p85: 66.8, p97: 68.7 } }, + { ageMonths: 5, weight: { p50: 7.5, p3: 5.8, p15: 6.5, p85: 8.6, p97: 9.5 }, height: { p50: 65.9, p3: 60.8, p15: 62.8, p85: 69.1, p97: 71.0 } }, + { ageMonths: 6, weight: { p50: 7.9, p3: 6.1, p15: 6.9, p85: 9.1, p97: 10.0 }, height: { p50: 67.6, p3: 62.4, p15: 64.5, p85: 70.8, p97: 72.8 } }, + { ageMonths: 7, weight: { p50: 8.3, p3: 6.4, p15: 7.2, p85: 9.5, p97: 10.5 }, height: { p50: 69.2, p3: 63.8, p15: 66.0, p85: 72.4, p97: 74.5 } }, + { ageMonths: 8, weight: { p50: 8.6, p3: 6.6, p15: 7.5, p85: 9.9, p97: 10.9 }, height: { p50: 70.6, p3: 65.1, p15: 67.4, p85: 73.9, p97: 76.0 } }, + { ageMonths: 9, weight: { p50: 8.9, p3: 6.8, p15: 7.7, p85: 10.2, p97: 11.3 }, height: { p50: 72.0, p3: 66.3, p15: 68.7, p85: 75.3, p97: 77.5 } }, + { ageMonths: 10, weight: { p50: 9.2, p3: 7.0, p15: 7.9, p85: 10.5, p97: 11.6 }, height: { p50: 73.3, p3: 67.5, p15: 70.0, p85: 76.7, p97: 78.9 } }, + { ageMonths: 11, weight: { p50: 9.4, p3: 7.1, p15: 8.1, p85: 10.8, p97: 12.0 }, height: { p50: 74.5, p3: 68.7, p15: 71.2, p85: 77.9, p97: 80.2 } }, + { ageMonths: 12, weight: { p50: 9.6, p3: 7.3, p15: 8.3, p85: 11.1, p97: 12.3 }, height: { p50: 75.7, p3: 69.8, p15: 72.4, p85: 79.1, p97: 81.4 } }, + { ageMonths: 15, weight: { p50: 10.3, p3: 7.8, p15: 8.9, p85: 11.9, p97: 13.2 }, height: { p50: 78.8, p3: 72.3, p15: 75.1, p85: 82.4, p97: 84.9 } }, + { ageMonths: 18, weight: { p50: 10.9, p3: 8.3, p15: 9.4, p85: 12.6, p97: 14.0 }, height: { p50: 81.7, p3: 74.7, p15: 77.6, p85: 85.7, p97: 88.3 } }, + { ageMonths: 21, weight: { p50: 11.5, p3: 8.7, p15: 9.9, p85: 13.3, p97: 14.8 }, height: { p50: 84.1, p3: 76.9, p15: 79.9, p85: 88.4, p97: 91.0 } }, + { ageMonths: 24, weight: { p50: 12.2, p3: 9.2, p15: 10.4, p85: 14.0, p97: 15.6 }, height: { p50: 86.4, p3: 78.9, p15: 82.0, p85: 90.7, p97: 93.4 } }, +]; + +// Girls weight (kg) by month +export const WHO_GIRL_WEIGHT: GrowthStandard[] = [ + { ageMonths: 0, weight: { p50: 3.2, p3: 2.4, p15: 2.8, p85: 3.7, p97: 4.2 }, height: { p50: 49.1, p3: 45.4, p15: 47.1, p85: 51.1, p97: 52.7 } }, + { ageMonths: 1, weight: { p50: 4.2, p3: 3.2, p15: 3.6, p85: 4.8, p97: 5.5 }, height: { p50: 53.7, p3: 49.6, p15: 51.3, p85: 56.1, p97: 57.8 } }, + { ageMonths: 2, weight: { p50: 5.1, p3: 3.9, p15: 4.5, p85: 5.9, p97: 6.6 }, height: { p50: 57.1, p3: 52.7, p15: 54.4, p85: 59.7, p97: 61.4 } }, + { ageMonths: 3, weight: { p50: 5.8, p3: 4.5, p15: 5.1, p85: 6.7, p97: 7.5 }, height: { p50: 59.8, p3: 55.1, p15: 57.0, p85: 62.7, p97: 64.5 } }, + { ageMonths: 4, weight: { p50: 6.4, p3: 4.9, p15: 5.6, p85: 7.3, p97: 8.2 }, height: { p50: 62.1, p3: 57.2, p15: 59.1, p85: 65.2, p97: 67.1 } }, + { ageMonths: 5, weight: { p50: 6.9, p3: 5.3, p15: 6.0, p85: 7.9, p97: 8.8 }, height: { p50: 64.0, p3: 58.9, p15: 60.9, p85: 67.2, p97: 69.1 } }, + { ageMonths: 6, weight: { p50: 7.3, p3: 5.6, p15: 6.4, p85: 8.4, p97: 9.3 }, height: { p50: 65.7, p3: 60.4, p15: 62.5, p85: 68.9, p97: 70.9 } }, + { ageMonths: 7, weight: { p50: 7.6, p3: 5.9, p15: 6.7, p85: 8.8, p97: 9.8 }, height: { p50: 67.3, p3: 61.8, p15: 64.0, p85: 70.5, p97: 72.6 } }, + { ageMonths: 8, weight: { p50: 7.9, p3: 6.1, p15: 6.9, p85: 9.1, p97: 10.2 }, height: { p50: 68.7, p3: 63.1, p15: 65.4, p85: 72.0, p97: 74.2 } }, + { ageMonths: 9, weight: { p50: 8.2, p3: 6.3, p15: 7.1, p85: 9.5, p97: 10.5 }, height: { p50: 70.1, p3: 64.4, p15: 66.7, p85: 73.5, p97: 75.7 } }, + { ageMonths: 10, weight: { p50: 8.4, p3: 6.5, p15: 7.4, p85: 9.8, p97: 10.9 }, height: { p50: 71.5, p3: 65.6, p15: 68.0, p85: 75.0, p97: 77.2 } }, + { ageMonths: 11, weight: { p50: 8.7, p3: 6.7, p15: 7.6, p85: 10.1, p97: 11.2 }, height: { p50: 72.8, p3: 66.7, p15: 69.2, p85: 76.4, p97: 78.7 } }, + { ageMonths: 12, weight: { p50: 8.9, p3: 6.8, p15: 7.8, p85: 10.3, p97: 11.5 }, height: { p50: 74.0, p3: 67.7, p15: 70.3, p85: 77.7, p97: 80.1 } }, + { ageMonths: 15, weight: { p50: 9.7, p3: 7.4, p15: 8.4, p85: 11.2, p97: 12.5 }, height: { p50: 77.2, p3: 70.3, p15: 73.0, p85: 81.0, p97: 83.5 } }, + { ageMonths: 18, weight: { p50: 10.3, p3: 7.8, p15: 8.9, p85: 12.0, p97: 13.4 }, height: { p50: 80.2, p3: 72.9, p15: 75.7, p85: 84.3, p97: 86.9 } }, + { ageMonths: 21, weight: { p50: 10.9, p3: 8.3, p15: 9.5, p85: 12.7, p97: 14.2 }, height: { p50: 82.7, p3: 75.3, p15: 78.0, p85: 87.1, p97: 89.8 } }, + { ageMonths: 24, weight: { p50: 11.5, p3: 8.7, p15: 10.0, p85: 13.4, p97: 15.0 }, height: { p50: 85.0, p3: 77.4, p15: 80.2, p85: 89.8, p97: 92.5 } }, +]; + +export function getAgeInMonthsFromBirth(birthDate: string): number { + const birth = new Date(birthDate); + const now = new Date(); + return Math.floor((now.getTime() - birth.getTime()) / (1000 * 60 * 60 * 24 * 30)); +} + +export function getStandard(birthDate: string, sex: string): GrowthStandard { + const ageMonths = getAgeInMonthsFromBirth(birthDate); + const data = sex === "male" ? WHO_BOY_WEIGHT : WHO_GIRL_WEIGHT; + return data.find((d) => d.ageMonths === ageMonths) || data[data.length - 1]; +} + +export function getPercentile(value: number, p3: number, p15: number, p50: number, p85: number, p97: number): string { + if (value < p3) return "Below 3rd"; + if (value < p15) return "3rd-15th"; + if (value < p50) return "15th-50th"; + if (value < p85) return "50th-85th"; + if (value < p97) return "85th-97th"; + return "Above 97th"; +} \ No newline at end of file