diff --git a/src/app/FamilyProvider.tsx b/src/app/FamilyProvider.tsx index 64bc7d9..5bc3b4d 100644 --- a/src/app/FamilyProvider.tsx +++ b/src/app/FamilyProvider.tsx @@ -3,13 +3,7 @@ import { useState, useEffect, createContext, useContext } from "react"; import { ReactNode } from "react"; import { useRouter, usePathname } from "next/navigation"; - -interface Child { - id: string; - name: string; - birthDate: string; - sex: string; -} +import type { Child } from "@/types"; interface FamilyContextType { familyId: string | null; @@ -87,7 +81,7 @@ export function FamilyProvider({ children: providerChildren }: { children: React } if (data.children?.length > 0) { - const childList = data.children.map((c: any) => ({ + const childList: Child[] = data.children.map((c: Child) => ({ id: c.id, name: c.name, birthDate: c.birthDate, diff --git a/src/app/activity/page.tsx b/src/app/activity/page.tsx index 5d75136..582dda9 100644 --- a/src/app/activity/page.tsx +++ b/src/app/activity/page.tsx @@ -1,34 +1,21 @@ "use client"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect } from "react"; import Link from "next/link"; import { useFamily } from "../FamilyProvider"; import { getGuideline, getAgeInMonths } from "@/lib/guidelines"; +import { api } from "@/lib/api"; +import { CalendarView } from "@/components/CalendarView"; import { LogModal, type LogType as ModalLogType } from "@/components/LogModal"; +import type { Log, LogType } from "@/types"; type ViewMode = "timeline" | "calendar"; -type LogType = "feed" | "sleep" | "diaper"; - -interface Log { - id: string; - type: LogType; - subType?: string; - amount?: number; - notes?: string; - loggedAt: string; -} interface DayLogs { date: string; logs: Log[]; } -const TYPE_COLORS: Record = { - feed: "bg-rose-400", - sleep: "bg-blue-400", - diaper: "bg-amber-400", -}; - function getIcon(type: LogType) { if (type === "feed") return "🍼"; if (type === "sleep") return "😴"; @@ -36,193 +23,17 @@ function getIcon(type: LogType) { return "πŸ“"; } -// ─── Calendar view ──────────────────────────────────────────────────────────── - -function CalendarView({ logs, filter }: { logs: Log[]; filter: LogType | "all" }) { - const now = new Date(); - const [calMonth, setCalMonth] = useState(new Date(now.getFullYear(), now.getMonth(), 1)); - const [selectedDay, setSelectedDay] = useState( - now.toISOString().slice(0, 10) - ); - - const year = calMonth.getFullYear(); - const month = calMonth.getMonth(); - const today = now.toISOString().slice(0, 10); - const isCurrentMonth = year === now.getFullYear() && month === now.getMonth(); - - const filteredLogs = filter === "all" ? logs : logs.filter(l => l.type === filter); - - // Group logs by YYYY-MM-DD - const logsByDate = useMemo(() => { - const map: Record = {}; - filteredLogs.forEach(log => { - const key = new Date(log.loggedAt).toISOString().slice(0, 10); - if (!map[key]) map[key] = []; - map[key].push(log); - }); - return map; - }, [filteredLogs]); - - // Build grid: leading nulls for DOW offset, then day numbers - const firstDow = new Date(year, month, 1).getDay(); - const daysInMonth = new Date(year, month + 1, 0).getDate(); - const cells: (number | null)[] = [ - ...Array(firstDow).fill(null), - ...Array.from({ length: daysInMonth }, (_, i) => i + 1), - ]; - - const dayKey = (d: number) => - `${year}-${String(month + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`; - - const selectedLogs = useMemo( - () => (selectedDay ? (logsByDate[selectedDay] || []) - .slice() - .sort((a, b) => new Date(b.loggedAt).getTime() - new Date(a.loggedAt).getTime()) - : []), - [selectedDay, logsByDate] - ); - - return ( -
- {/* Month grid */} -
- {/* Month nav */} -
- - - {calMonth.toLocaleDateString("en-US", { month: "long", year: "numeric" })} - - -
- - {/* DOW headers */} -
- {["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map(d => ( -
{d}
- ))} -
- - {/* Day cells */} -
- {cells.map((day, i) => { - if (!day) return
; - const key = dayKey(day); - const dayLogs = logsByDate[key] || []; - const isToday = key === today; - const isSel = key === selectedDay; - const types = [...new Set(dayLogs.map(l => l.type))] as LogType[]; - - return ( - - ); - })} -
- - {/* Legend */} -
- {(["feed", "sleep", "diaper"] as LogType[]).map(t => ( -
- - {t} -
- ))} -
-
- - {/* Selected day detail */} - {selectedDay && ( -
-
-

- {new Date(selectedDay + "T12:00:00").toLocaleDateString("en-US", { - weekday: "long", month: "short", day: "numeric", - })} -

- - {selectedLogs.length} {selectedLogs.length === 1 ? "entry" : "entries"} - -
- - {selectedLogs.length === 0 ? ( -

No activity logged

- ) : ( -
- {selectedLogs.map(log => ( -
- {getIcon(log.type)} -
-
{log.type}
-
- {[ - log.subType?.replace(/_/g, " "), - log.amount ? `${log.amount}ml` : null, - log.notes, - ].filter(Boolean).join(" Β· ")} -
-
-
- {new Date(log.loggedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} -
-
- ))} -
- )} -
- )} -
- ); -} - -// ─── Main page ──────────────────────────────────────────────────────────────── - export default function ActivityPage() { const { child, childId: providerChildId } = useFamily(); - const [view, setView] = useState("timeline"); - const [logs, setLogs] = useState([]); - const [loading, setLoading] = useState(true); - const [filter, setFilter] = useState("all"); + const [view, setView] = useState("timeline"); + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState("all"); const [showSuggested, setShowSuggested] = useState(true); const [generating, setGenerating] = useState(false); const [fabOpen, setFabOpen] = useState(false); const [modalType, setModalType] = useState(null); - const childId = providerChildId || ""; + const childId = providerChildId ?? ""; useEffect(() => { if (providerChildId) fetchLogs(); @@ -231,11 +42,10 @@ export default function ActivityPage() { const fetchLogs = async () => { if (!childId) return; try { - const res = await fetch(`/api/logs?childId=${childId}&limit=200`); - const data = await res.json(); + const data = await api.get<{ entries: Log[] }>(`/api/logs?childId=${childId}&limit=200`); setLogs(data.entries || []); } catch (err) { - console.error("Failed to fetch:", err); + console.error("Failed to fetch logs:", err); } setLoading(false); }; @@ -244,22 +54,17 @@ export default function ActivityPage() { if (!child) return; setGenerating(true); try { - const res = await fetch("/api/history", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ childId: child.id, birthDate: child.birthDate }), - }); - const data = await res.json(); + const data = await api.post<{ success: boolean }>("/api/history", { childId: child.id, birthDate: child.birthDate }); if (data.success) fetchLogs(); } catch (err) { - console.error("Failed to generate:", err); + console.error("Failed to generate history:", err); } setGenerating(false); }; const filteredLogs = filter === "all" ? logs : logs.filter(l => l.type === filter); - const groupedByDay = filteredLogs.reduce((acc: DayLogs[], log) => { + const groupedByDay = filteredLogs.reduce((acc, log) => { const date = new Date(log.loggedAt).toDateString(); const existing = acc.find(d => d.date === date); if (existing) existing.logs.push(log); @@ -286,22 +91,17 @@ export default function ActivityPage() {
{/* View toggle */}
- - + {(["timeline", "calendar"] as const).map(v => ( + + ))}
@@ -321,38 +121,36 @@ export default function ActivityPage() { {/* Guidelines card */} - {child && showSuggested && ( -
- {(() => { - const guide = getGuideline(child.birthDate); - const ageMonths = getAgeInMonths(child.birthDate); - return ( -
-
-
- {child.name} Β· {ageMonths} months old -
- + {child && showSuggested && (() => { + const guide = getGuideline(child.birthDate); + const ageMonths = getAgeInMonths(child.birthDate); + return ( +
+
+
+
+ {child.name} Β· {ageMonths} months old
-
-
-
{guide.feeds.times}
-
feeds/day
-
-
-
{guide.sleep.totalHours}h
-
sleep/day
-
-
-
{guide.diapers.count}
-
diapers/day
-
+ +
+
+
+
{guide.feeds.times}
+
feeds/day
+
+
+
{guide.sleep.totalHours}h
+
sleep/day
+
+
+
{guide.diapers.count}
+
diapers/day
- ); - })()} -
- )} +
+
+ ); + })()} {/* Content */}
@@ -370,9 +168,7 @@ export default function ActivityPage() { {groupedByDay.map(day => (
- {new Date(day.date).toLocaleDateString("en-US", { - weekday: "long", month: "short", day: "numeric", - })} + {new Date(day.date).toLocaleDateString("en-US", { weekday: "long", month: "short", day: "numeric" })}
{day.logs @@ -404,20 +200,16 @@ export default function ActivityPage() { {/* FAB */}
- {fabOpen && ( - <> - {(["feed", "sleep", "diaper"] as ModalLogType[]).map(t => ( - - ))} - - )} + {fabOpen && (["feed", "sleep", "diaper"] as ModalLogType[]).map(t => ( + + ))}
+ {fabOpen &&
setFabOpen(false)} />} + - - {fabOpen && ( -
setFabOpen(false)} /> - )}
); } diff --git a/src/app/ai/page.tsx b/src/app/ai/page.tsx index 788809a..469af9b 100644 --- a/src/app/ai/page.tsx +++ b/src/app/ai/page.tsx @@ -3,21 +3,7 @@ import { useState, useEffect } from "react"; import { useFamily } from "../FamilyProvider"; import { Button, Input, ConfirmDialog } from "@/components/ui"; - -interface AIChat { - id: string; - role: "user" | "assistant"; - content: string; - createdAt: string; -} - -interface ChatSession { - id: string; - title: string; - messages: AIChat[]; - createdAt: string; - updatedAt: string; -} +import type { AIChat, ChatSession } from "@/types"; export default function AIChatPage() { const { childId } = useFamily(); diff --git a/src/app/growth/page.tsx b/src/app/growth/page.tsx index bbf2c60..72da19d 100644 --- a/src/app/growth/page.tsx +++ b/src/app/growth/page.tsx @@ -1,9 +1,11 @@ "use client"; -import { useState, useEffect, useRef } from "react"; +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, type GrowthStandard } from "@/lib/growth-standards"; +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, @@ -18,57 +20,21 @@ import { import { Line } from "react-chartjs-2"; import { useTheme } from "../ThemeProvider"; -ChartJS.register( - CategoryScale, - LinearScale, - PointElement, - LineElement, - Title, - Tooltip, - Legend, - Filler -); +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; -} - -function formatAge(birthDate: string, measurementDate?: string): string { - const birth = new Date(birthDate); - const now = measurementDate ? new Date(measurementDate) : new Date(); - const years = Math.floor((now.getTime() - birth.getTime()) / (1000 * 60 * 60 * 24 * 365)); - const months = Math.floor(((now.getTime() - birth.getTime()) % (1000 * 60 * 60 * 24 * 365)) / (1000 * 60 * 60 * 24 * 30)); - - if (years > 0 && months > 0) { - return `${years}y ${months}mo`; - } else if (years > 0) { - return `${years}y`; - } else if (months > 0) { - return `${months}mo`; - } else { - return "Newborn"; - } +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(); - const { theme } = useTheme(); - const isDark = theme === "dark"; + useTheme(); // keep theme context alive for dark mode CSS const [growthData, setGrowthData] = useState([]); - const [whoStandard, setWhoStandard] = useState(null); + const [whoStandard, setWhoStandard] = useState(null); const [loading, setLoading] = useState(true); const [showAdd, setShowAdd] = useState(false); const [showGoals, setShowGoals] = useState(false); @@ -150,8 +116,8 @@ export default function GrowthPage() { } resetForm(); fetchGrowthData(); - } catch (e: any) { - setSaveError(e.message || "Failed to save"); + } catch (e) { + setSaveError(e instanceof Error ? e.message : "Failed to save"); } finally { setSaving(false); } @@ -312,7 +278,7 @@ export default function GrowthPage() { // WHO 85th percentile { label: "85th", - data: labels.map(() => whoStandard?.[whoKey]?.p85), + data: labels.map(() => whoStandard?.[whoKey]?.p85 ?? null), borderColor: "#fbbf24", borderDash: [5, 5], fill: false, @@ -322,7 +288,7 @@ export default function GrowthPage() { // WHO 50th percentile { label: "50th", - data: labels.map(() => whoStandard?.[whoKey]?.p50), + data: labels.map(() => whoStandard?.[whoKey]?.p50 ?? null), borderColor: "#22c55e", borderDash: [5, 5], fill: false, @@ -332,21 +298,21 @@ export default function GrowthPage() { // WHO 15th percentile { label: "15th", - data: labels.map(() => whoStandard?.[whoKey]?.p15), + data: labels.map(() => whoStandard?.[whoKey]?.p15 ?? null), 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", + label: "3rd", + data: labels.map(() => whoStandard?.[whoKey]?.p3 ?? null), + borderColor: "#fbbf24", + borderDash: [5, 5], + fill: false, pointRadius: 0, + tension: 0.4, }, ], }; diff --git a/src/app/medical/page.tsx b/src/app/medical/page.tsx index e759496..a6a8eec 100644 --- a/src/app/medical/page.tsx +++ b/src/app/medical/page.tsx @@ -1,1030 +1,46 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { useFamily } from "../FamilyProvider"; -import { Button, Card, Modal, Input, Select, Textarea, Badge } from "@/components/ui"; +import { PageHeader } from "@/components/PageHeader"; +import { TabBar } from "@/components/TabBar"; +import { VaccineTab } from "@/components/medical/VaccineTab"; +import { MedicineTab } from "@/components/medical/MedicineTab"; +import { AllergyTab } from "@/components/medical/AllergyTab"; +import { VisitTab } from "@/components/medical/VisitTab"; +import { IllnessTab } from "@/components/medical/IllnessTab"; -interface Medicine { - id: string; - name: string; - dose: string; - notes: string; - reminderTime?: string; -} +type MedTab = "vaccinations" | "medicine" | "allergies" | "visits" | "illness"; -interface Dose { - id: string; - administered_at: string; - amount_given: string | null; - notes: string | null; - corrections?: { id: string; corrected_value: Record; reason: string | null; created_at: string }[]; -} - -interface Allergy { - id: string; - name: string; - severity: string; - notes: string; -} - -interface Visit { - id: string; - doctorName: string; - reason: string; - date: string; - notes: string; -} - -interface Illness { - id: string; - name: string; - startDate: string; - endDate?: string; - notes: string; -} - -// IAP Vaccination Schedule (India) -const IAP_SCHEDULE = [ - { name: "BCG", weeks: 0 }, - { name: "OPV-0", weeks: 0 }, - { name: "HepB-1", weeks: 0 }, - { name: "OPV-1", weeks: 6 }, - { name: "Pentavalent-1", weeks: 6 }, - { name: "PCV-1", weeks: 6 }, - { name: "Rota-1", weeks: 6 }, - { name: "OPV-2", weeks: 10 }, - { name: "Pentavalent-2", weeks: 10 }, - { name: "PCV-2", weeks: 10 }, - { name: "Rota-2", weeks: 10 }, - { name: "OPV-3", weeks: 14 }, - { name: "Pentavalent-3", weeks: 14 }, - { name: "PCV-3", weeks: 14 }, - { name: "Rota-3", weeks: 14 }, - { name: "MR-1", weeks: 48 }, // 9 months - { name: "JE-1", weeks: 48 }, - { name: "Vitamin A-1", weeks: 48 }, - { name: "OPV-4", weeks: 48 }, - { name: "MR-2", weeks: 96 }, // 18 months - { name: "JE-2", weeks: 96 }, - { name: "DPT-Booster-1", weeks: 96 }, - { name: "Vitamin A-2", weeks: 96 }, - { name: "OPV-5", weeks: 96 }, - { name: "DPT-Booster-2", weeks: 208 }, // 4 years - { name: "Tetanus and adult diphtheria (Td)", weeks: 208 }, +const TABS = [ + { value: "vaccinations", label: "Vaccines" }, + { value: "medicine", label: "Medicine" }, + { value: "allergies", label: "Allergies" }, + { value: "visits", label: "Doctor Visit" }, + { value: "illness", label: "Illness" }, ]; -function calculateDueDate(birthDate: string, weeks: number): string { - const birth = new Date(birthDate); - birth.setDate(birth.getDate() + weeks * 7); - return birth.toISOString().split("T")[0]; -} - export default function MedicalPage() { - const [vaccinations, setVaccinations] = useState([]); - const [loading, setLoading] = useState(true); - const { childId: sessionChildId, child, loading: loadingChild } = useFamily(); - const [tab, setTab] = useState<"vaccinations" | "medicine" | "allergies" | "visits" | "illness">("vaccinations"); - const [vaccineTab, setVaccineTab] = useState<"upcoming" | "completed" | "overdue">("upcoming"); - const [showAddDate, setShowAddDate] = useState(null); - const [givenDate, setGivenDate] = useState(""); + const { childId, child } = useFamily(); + const [tab, setTab] = useState("vaccinations"); - // CRUD state for medicine, allergies, visits, illness - const [medicines, setMedicines] = useState([]); - const [allergies, setAllergies] = useState([]); - const [visits, setVisits] = useState([]); - const [illnesses, setIllnesses] = useState([]); - - // Add/Edit mode - const [editingMed, setEditingMed] = useState(null); - // Dose log state - const [logDoseMedId, setLogDoseMedId] = useState(null); - const [doseAmount, setDoseAmount] = useState(""); - const [doseNotes, setDoseNotes] = useState(""); - const [doseTime, setDoseTime] = useState(() => new Date().toISOString().slice(0, 16)); - const [doseLoading, setDoseLoading] = useState(false); - const [doseHistory, setDoseHistory] = useState>({}); - const [showDoseHistory, setShowDoseHistory] = useState>({}); - const [correctDose, setCorrectDose] = useState<{ medId: string; dose: Dose } | null>(null); - const [correctAmount, setCorrectAmount] = useState(""); - const [correctNotes, setCorrectNotes] = useState(""); - const [correctReason, setCorrectReason] = useState(""); - const [correctLoading, setCorrectLoading] = useState(false); - const [editingAllergy, setEditingAllergy] = useState(null); - const [editingVisit, setEditingVisit] = useState(null); - const [editingIllness, setEditingIllness] = useState(null); - const [showAddMed, setShowAddMed] = useState(false); - const [showAddAllergy, setShowAddAllergy] = useState(false); - const [showAddVisit, setShowAddVisit] = useState(false); - const [showAddIllness, setShowAddIllness] = useState(false); - - // Form state for new items - const [newMedName, setNewMedName] = useState(""); - const [newMedDose, setNewMedDose] = useState(""); - const [newMedNotes, setNewMedNotes] = useState(""); - const [newAllergyName, setNewAllergyName] = useState(""); - const [newAllergySeverity, setNewAllergySeverity] = useState("mild"); - const [newAllergyNotes, setNewAllergyNotes] = useState(""); - const [newVisitDoctor, setNewVisitDoctor] = useState(""); - const [newVisitReason, setNewVisitReason] = useState(""); - const [newVisitDate, setNewVisitDate] = useState(""); - const [newVisitNotes, setNewVisitNotes] = useState(""); - const [newIllnessName, setNewIllnessName] = useState(""); - const [newIllnessStart, setNewIllnessStart] = useState(""); - const [newIllnessEnd, setNewIllnessEnd] = useState(""); - const [newIllnessNotes, setNewIllnessNotes] = useState(""); - - // Load data from database on mount - useEffect(() => { - fetchMedicines(); - fetchAllergies(); - fetchVisits(); - fetchIllnesses(); - }, []); - - const fetchMedicines = async () => { - try { - const res = await fetch(`/api/medicines?childId=${childId}`); - const data = await res.json(); - setMedicines(data.medicines || []); - } catch (err) { - console.error("Failed to fetch medicines:", err); - } - }; - - const fetchAllergies = async () => { - try { - const res = await fetch(`/api/allergies?childId=${childId}`); - const data = await res.json(); - setAllergies(data.allergies || []); - } catch (err) { - console.error("Failed to fetch allergies:", err); - } - }; - - const fetchVisits = async () => { - try { - const res = await fetch(`/api/visits?childId=${childId}`); - const data = await res.json(); - setVisits(data.visits || []); - } catch (err) { - console.error("Failed to fetch visits:", err); - } - }; - - const fetchIllnesses = async () => { - try { - const res = await fetch(`/api/illnesses?childId=${childId}`); - const data = await res.json(); - setIllnesses(data.illnesses || []); - } catch (err) { - console.error("Failed to fetch illnesses:", err); - } - }; - - // Medicine CRUD - now using database - const saveMedicine = async () => { - if (!newMedName) return; - try { - if (editingMed) { - await fetch(`/api/medicines`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ id: editingMed.id, name: newMedName, dose: newMedDose, notes: newMedNotes }), - }); - } else { - await fetch("/api/medicines", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ childId, name: newMedName, dose: newMedDose, notes: newMedNotes }), - }); - } - fetchMedicines(); - } catch (err) { - console.error("Failed to save:", err); - } - resetMedForm(); - }; - - const deleteMedicine = async (id: string) => { - try { - await fetch(`/api/medicines?id=${id}`, { method: "DELETE" }); - fetchMedicines(); - } catch (err) { - console.error("Failed to delete:", err); - } - }; - - const openLogDose = (medId: string) => { - setLogDoseMedId(medId); - setDoseAmount(""); - setDoseNotes(""); - setDoseTime(new Date().toISOString().slice(0, 16)); - }; - - const submitDose = async () => { - if (!logDoseMedId) return; - setDoseLoading(true); - try { - await fetch(`/api/medicines/${logDoseMedId}/doses`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ amountGiven: doseAmount, notes: doseNotes, administeredAt: new Date(doseTime).toISOString() }), - }); - setLogDoseMedId(null); - if (showDoseHistory[logDoseMedId]) fetchDoseHistory(logDoseMedId); - } finally { - setDoseLoading(false); - } - }; - - const fetchDoseHistory = async (medId: string) => { - const res = await fetch(`/api/medicines/${medId}/doses`); - const data = await res.json(); - setDoseHistory(prev => ({ ...prev, [medId]: data.doses || [] })); - }; - - const toggleDoseHistory = async (medId: string) => { - const next = !showDoseHistory[medId]; - setShowDoseHistory(prev => ({ ...prev, [medId]: next })); - if (next && !doseHistory[medId]) fetchDoseHistory(medId); - }; - - const submitCorrection = async () => { - if (!correctDose) return; - setCorrectLoading(true); - try { - await fetch(`/api/medicines/${correctDose.medId}/doses/${correctDose.dose.id}/correct`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ amountGiven: correctAmount, notes: correctNotes, reason: correctReason }), - }); - setCorrectDose(null); - fetchDoseHistory(correctDose.medId); - } finally { - setCorrectLoading(false); - } - }; - - const editMedicine = (med: Medicine) => { - setEditingMed(med); - setNewMedName(med.name); - setNewMedDose(med.dose); - setNewMedNotes(med.notes); - setShowAddMed(true); - }; - - const resetMedForm = () => { - setEditingMed(null); - setNewMedName(""); - setNewMedDose(""); - setNewMedNotes(""); - setShowAddMed(false); - }; - - // Allergy CRUD - now using database - const saveAllergy = async () => { - if (!newAllergyName) return; - try { - if (editingAllergy) { - await fetch(`/api/allergies`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ id: editingAllergy.id, name: newAllergyName, severity: newAllergySeverity, notes: newAllergyNotes }), - }); - } else { - await fetch("/api/allergies", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ childId, name: newAllergyName, severity: newAllergySeverity, notes: newAllergyNotes }), - }); - } - fetchAllergies(); - } catch (err) { - console.error("Failed to save:", err); - } - resetAllergyForm(); - }; - - const deleteAllergy = async (id: string) => { - try { - await fetch(`/api/allergies?id=${id}`, { method: "DELETE" }); - fetchAllergies(); - } catch (err) { - console.error("Failed to delete:", err); - } - }; - - const editAllergy = (allergy: Allergy) => { - setEditingAllergy(allergy); - setNewAllergyName(allergy.name); - setNewAllergySeverity(allergy.severity); - setNewAllergyNotes(allergy.notes); - setShowAddAllergy(true); - }; - - const resetAllergyForm = () => { - setEditingAllergy(null); - setNewAllergyName(""); - setNewAllergySeverity("mild"); - setNewAllergyNotes(""); - setShowAddAllergy(false); - }; - - // Visit CRUD - now using database - const saveVisit = async () => { - if (!newVisitDoctor) return; - try { - if (editingVisit) { - await fetch(`/api/visits`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ id: editingVisit.id, doctorName: newVisitDoctor, reason: newVisitReason, date: newVisitDate, notes: newVisitNotes }), - }); - } else { - await fetch("/api/visits", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ childId, doctorName: newVisitDoctor, reason: newVisitReason, date: newVisitDate, notes: newVisitNotes }), - }); - } - fetchVisits(); - } catch (err) { - console.error("Failed to save:", err); - } - resetVisitForm(); - }; - - const deleteVisit = async (id: string) => { - try { - await fetch(`/api/visits?id=${id}`, { method: "DELETE" }); - fetchVisits(); - } catch (err) { - console.error("Failed to delete:", err); - } - }; - - const editVisit = (visit: Visit) => { - setEditingVisit(visit); - setNewVisitDoctor(visit.doctorName); - setNewVisitReason(visit.reason); - setNewVisitDate(visit.date); - setNewVisitNotes(visit.notes); - setShowAddVisit(true); - }; - - const resetVisitForm = () => { - setEditingVisit(null); - setNewVisitDoctor(""); - setNewVisitReason(""); - setNewVisitDate(""); - setNewVisitNotes(""); - setShowAddVisit(false); - }; - - // Illness CRUD - now using database - const saveIllness = async () => { - if (!newIllnessName) return; - try { - if (editingIllness) { - await fetch(`/api/illnesses`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ id: editingIllness.id, name: newIllnessName, startDate: newIllnessStart, endDate: newIllnessEnd, notes: newIllnessNotes }), - }); - } else { - await fetch("/api/illnesses", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ childId, name: newIllnessName, startDate: newIllnessStart, endDate: newIllnessEnd, notes: newIllnessNotes }), - }); - } - fetchIllnesses(); - } catch (err) { - console.error("Failed to save:", err); - } - resetIllnessForm(); - }; - - const deleteIllness = async (id: string) => { - try { - await fetch(`/api/illnesses?id=${id}`, { method: "DELETE" }); - fetchIllnesses(); - } catch (err) { - console.error("Failed to delete:", err); - } - }; - - const editIllness = (illness: Illness) => { - setEditingIllness(illness); - setNewIllnessName(illness.name); - setNewIllnessStart(illness.startDate); - setNewIllnessEnd(illness.endDate || ""); - setNewIllnessNotes(illness.notes); - setShowAddIllness(true); - }; - - const resetIllnessForm = () => { - setEditingIllness(null); - setNewIllnessName(""); - setNewIllnessStart(""); - setNewIllnessEnd(""); - setNewIllnessNotes(""); - setShowAddIllness(false); - }; - - const childId = sessionChildId; - const birthDate = child?.birthDate || "2024-01-15"; - -// Common supplements for babies -const SUPPLEMENTS = [ - { name: "Vitamin D3", dose: "400 IU daily", notes: "For bone health" }, - { name: "Iron", dose: "1 mg/kg daily", notes: "As prescribed" }, - { name: "Calcium", dose: "500 mg daily", notes: "With food" }, - { name: "Zinc", dose: "5 mg daily", notes: "Immune support" }, - { name: "Omega-3", dose: "DHA 100mg", notes: "Brain development" }, - { name: "Probiotics", dose: "1 shot daily", notes: "Gut health" }, - { name: "Multivitamin", dose: "As directed", notes: "Daily vitamin" }, -]; - - useEffect(() => { - fetch(`/api/vaccinations?childId=${childId}`) - .then((res) => res.json()) - .then((data) => { - setVaccinations(data.vaccinations || []); - setLoading(false); - }) - .catch(() => setLoading(false)); - }, [childId]); - - 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", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - childId, - vaccineName, - scheduledDate: dueDate, - givenDate: dateToSave, - 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); - - // Get vaccines by status - const getVaccinesByStatus = (status: "upcoming" | "completed" | "overdue") => { - const today = new Date(); - const todayStr = today.toISOString().split("T")[0]; - - return IAP_SCHEDULE.filter((v) => { - const dueDate = calculateDueDate(birthDate, v.weeks); - const given = isGiven(v.name); - - if (status === "completed") return given; - if (status === "overdue") return !given && dueDate < todayStr; - if (status === "upcoming") return !given && dueDate >= todayStr; - return false; - }).sort((a, b) => a.weeks - b.weeks); - }; - - const getVaccineStatus = (name: string) => { - const dueDate = calculateDueDate(birthDate, IAP_SCHEDULE.find((v) => v.name === name)?.weeks || 0); - const today = new Date(); - const todayStr = today.toISOString().split("T")[0]; - const given = isGiven(name); - - if (given) return "completed"; - if (dueDate < todayStr) return "overdue"; - return "upcoming"; - }; + if (!childId || !child) { + return
Loading...
; + } return ( -
-
-
- ← -

Medical

-
- -
- - - - - -
- - {tab === "vaccinations" && ( -
-
- - - -
- -

IAP Schedule

- {loading ? ( -

Loading...

- ) : ( - getVaccinesByStatus(vaccineTab).map((vaccine) => { - const given = isGiven(vaccine.name); - const actualDate = getGivenDate(vaccine.name); - const dueDate = calculateDueDate(birthDate, vaccine.weeks); - const status = getVaccineStatus(vaccine.name); - const daysOverdue = status === "overdue" ? Math.floor((new Date().getTime() - new Date(dueDate).getTime()) / (1000 * 60 * 60 * 24)) : 0; - - return ( -
-
-
-
{vaccine.name}
-
- Due: {new Date(dueDate).toLocaleDateString()} - {actualDate && ` Β· Given: ${new Date(actualDate).toLocaleDateString()}`} -
- {status === "overdue" && ( -
{daysOverdue} days overdue
- )} -
- {given ? ( - βœ“ - ) : showAddDate === vaccine.name ? ( -
- setGivenDate(e.target.value)} - className="p-1 text-sm border dark:border-gray-600 rounded dark:bg-gray-700 dark:text-white" - placeholder="Date given" - /> - - -
- ) : ( - - )} -
-
- ); - }) - )} -
- )} - - {tab === "medicine" && ( -
-

Medicine & Supplements

- - {/* Add Form */} - {showAddMed && ( -
- setNewMedName(e.target.value)} - placeholder="Medicine name" - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" - /> - setNewMedDose(e.target.value)} - placeholder="Dose (e.g., 5ml, 1 tablet)" - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" - /> - setNewMedNotes(e.target.value)} - placeholder="Notes" - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" - /> -
- - -
-
- )} - - {medicines.length === 0 && !showAddMed ? ( -
-

No medicines added

-
- ) : ( - medicines.map((med) => ( -
-
-
-
-
{med.name}
-
- {med.dose} {med.notes && `· ${med.notes}`} - {med.reminderTime && ` · ⏰ ${med.reminderTime}`} -
-
-
- - - -
-
- -
- {showDoseHistory[med.id] && ( -
- {!doseHistory[med.id] ? ( -

Loading…

- ) : doseHistory[med.id].length === 0 ? ( -

No doses logged yet.

- ) : doseHistory[med.id].map(dose => { - const latest = dose.corrections?.[0]?.corrected_value; - const isEdited = !!latest; - const displayAmount = latest?.amountGiven || dose.amount_given; - const displayNotes = latest?.notes || dose.notes; - return ( -
-
-
- - {new Date(dose.administered_at).toLocaleString()} - - {displayAmount && Β· {displayAmount}} - {displayNotes && Β· {displayNotes}} - {isEdited && ( - edited - )} -
- -
-
- ); - })} -
- )} -
- )) - )} - - {!showAddMed && ( - - )} -
- )} - - {tab === "allergies" && ( -
-

Known Allergies

- - {showAddAllergy && ( -
- setNewAllergyName(e.target.value)} - placeholder="Allergy name (e.g., Peanut, Milk)" - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" - /> - - setNewAllergyNotes(e.target.value)} - placeholder="Notes" - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" - /> -
- - -
-
- )} - - {allergies.length === 0 && !showAddAllergy ? ( -
-

No allergies recorded

-
- ) : ( - allergies.map((allergy) => ( -
-
-
-
{allergy.name}
-
- - {allergy.severity.toUpperCase()} - - {allergy.notes && ` Β· ${allergy.notes}`} -
-
-
- - -
-
-
- )) - )} - - {!showAddAllergy && ( - - )} -
- )} - - {tab === "visits" && ( -
-

Doctor Visits

- - {showAddVisit && ( -
- setNewVisitDoctor(e.target.value)} - placeholder="Doctor name" - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" - /> - setNewVisitReason(e.target.value)} - placeholder="Reason (e.g., Checkup, Fever)" - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" - /> - setNewVisitDate(e.target.value)} - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white" - /> - setNewVisitNotes(e.target.value)} - placeholder="Notes" - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" - /> -
- - -
-
- )} - - {visits.length === 0 && !showAddVisit ? ( -
-

No visits recorded

-
- ) : ( - visits.map((visit) => ( -
-
-
-
{visit.doctorName}
-
- {new Date(visit.date).toLocaleDateString()} - {visit.reason && ` Β· ${visit.reason}`} - {visit.notes && ` Β· ${visit.notes}`} -
-
-
- - -
-
-
- )) - )} - - {!showAddVisit && ( - - )} -
- )} - - {tab === "illness" && ( -
-

Illness Log

- - {showAddIllness && ( -
- setNewIllnessName(e.target.value)} - placeholder="Illness (e.g., Cold, Fever, Flu)" - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" - /> -
- setNewIllnessStart(e.target.value)} - placeholder="Start date" - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white" - /> - setNewIllnessEnd(e.target.value)} - placeholder="End date" - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white" - /> -
- setNewIllnessNotes(e.target.value)} - placeholder="Notes" - className="w-full p-2 border dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-white dark:placeholder-gray-400" - /> -
- - -
-
- )} - - {illnesses.length === 0 && !showAddIllness ? ( -
-

No illnesses recorded

-
- ) : ( - illnesses.map((illness) => ( -
-
-
-
{illness.name}
-
- {new Date(illness.startDate).toLocaleDateString()} - {illness.endDate && ` - ${new Date(illness.endDate).toLocaleDateString()}`} - {illness.notes && ` Β· ${illness.notes}`} -
-
-
- - -
-
-
- )) - )} - - {!showAddIllness && ( - - )} -
- )} +
+ +
+ setTab(v as MedTab)} /> +
+
+ {tab === "vaccinations" && } + {tab === "medicine" && } + {tab === "allergies" && } + {tab === "visits" && } + {tab === "illness" && }
- - {/* Log Dose Modal */} - setLogDoseMedId(null)} title="Log Dose" maxWidth="sm"> -
- setDoseAmount(e.target.value)} /> - setDoseTime(e.target.value)} /> - setDoseNotes(e.target.value)} /> -
- - -
-
-
- - {/* Correction Modal */} - setCorrectDose(null)} title="Edit Dose (Correction)" maxWidth="sm"> -
-

Original is kept. A correction record is added.

- {correctDose && ( -
- Original: {correctDose.dose.amount_given || "β€”"} Β· {new Date(correctDose.dose.administered_at).toLocaleString()} -
- )} - setCorrectAmount(e.target.value)} /> - setCorrectNotes(e.target.value)} /> - setCorrectReason(e.target.value)} /> -
- - -
-
-
); -} \ No newline at end of file +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 3b13bfb..291b4ca 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,23 +7,9 @@ import { useFamily } from "./FamilyProvider"; import { useStageCheck, type BabyStage } from "@/hooks/useStageCheck"; import { Button } from "@/components/ui"; import { LogModal, type LogType } from "@/components/LogModal"; -import { getOfflineQueue, addToOfflineQueue, processOfflineQueue } from "@/lib/offline-queue"; - -interface AIChat { - id: string; - role: "user" | "assistant"; - content: string; - createdAt: string; -} - -interface ChatSession { - id: string; - title: string; - messages: AIChat[]; - createdAt: string; - updatedAt: string; -} - +import { getOfflineQueue, processOfflineQueue } from "@/lib/offline-queue"; +import { calculateAge, formatTimeAgo } from "@/lib/formatting"; +import type { Log, AIChat, ChatSession } from "@/types"; async function getSessions(cid: string): Promise { try { @@ -46,25 +32,6 @@ async function createSession(cid: string): Promise { } -function calculateAge(birthDate: string) { - if (!birthDate) return ""; - const birth = new Date(birthDate); - const now = new Date(); - let years = now.getFullYear() - birth.getFullYear(); - let months = now.getMonth() - birth.getMonth(); - let days = now.getDate() - birth.getDate(); - if (days < 0) { - months--; - days += new Date(now.getFullYear(), now.getMonth(), 0).getDate(); - } - if (months < 0) { years--; months += 12; } - const parts = []; - if (years > 0) parts.push(`${years} year${years > 1 ? "s" : ""}`); - if (months > 0) parts.push(`${months} month${months > 1 ? "s" : ""}`); - if (days > 0) parts.push(`${days} day${days > 1 ? "s" : ""}`); - return parts.length > 0 ? parts.join(", ") : "Newborn"; -} - function getGreeting() { const hour = new Date().getHours(); if (hour < 12) return "Good morning"; @@ -72,21 +39,7 @@ function getGreeting() { return "Good evening"; } -function formatTimeAgo(dateStr: string | null | undefined) { - if (!dateStr) return null; - const date = new Date(dateStr); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - if (diffMins < 1) return "just now"; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - return `${diffDays}d ago`; -} - -function TodaySummary({ logs }: { logs: any[] }) { +function TodaySummary({ logs }: { logs: Log[] }) { const todayStr = new Date().toDateString(); const today = logs.filter(l => new Date(l.loggedAt).toDateString() === todayStr); const counts = { @@ -132,7 +85,7 @@ export default function HomePage() { const [aiLoading, setAiLoading] = useState(false); const [homeSessionId, setHomeSessionId] = useState(null); const [pendingCount, setPendingCount] = useState(0); - const [recentLogs, setRecentLogs] = useState([]); + const [recentLogs, setRecentLogs] = useState([]); const [logsLoading, setLogsLoading] = useState(true); const [vaccineReminders, setVaccineReminders] = useState([]); const { theme, toggle: toggleTheme } = useTheme(); @@ -336,7 +289,7 @@ export default function HomePage() {

Recent Activity

- {logsLoading ?

Loading...

: recentLogs.length === 0 ?

No logs yet today

: recentLogs.slice(0, 5).map((log: any) => ( + {logsLoading ?

Loading...

: recentLogs.length === 0 ?

No logs yet today

: recentLogs.slice(0, 5).map(log => (
{log.type === "feed" && "🍼"}{log.type === "sleep" && "😴"}{log.type === "diaper" && "🚼"} @@ -355,7 +308,7 @@ export default function HomePage() { onSaved={fetchRecentLogs} smartDefault={modalType ? (() => { const last = recentLogs.find(l => l.type === modalType); - return last ? { subType: last.subType, amountMl: last.amount ?? undefined } : null; + return last?.subType ? { subType: last.subType, amountMl: last.amount ?? undefined } : null; })() : null} /> diff --git a/src/components/CalendarView.tsx b/src/components/CalendarView.tsx new file mode 100644 index 0000000..edea165 --- /dev/null +++ b/src/components/CalendarView.tsx @@ -0,0 +1,186 @@ +"use client"; + +import { useState, useMemo } from "react"; +import type { Log, LogType } from "@/types"; + +const TYPE_COLORS: Record = { + feed: "bg-rose-400", + sleep: "bg-blue-400", + diaper: "bg-amber-400", +}; + +function getIcon(type: LogType) { + if (type === "feed") return "🍼"; + if (type === "sleep") return "😴"; + if (type === "diaper") return "🚼"; + return "πŸ“"; +} + +interface Props { + logs: Log[]; + filter: LogType | "all"; +} + +export function CalendarView({ logs, filter }: Props) { + const now = new Date(); + const [calMonth, setCalMonth] = useState(new Date(now.getFullYear(), now.getMonth(), 1)); + const [selectedDay, setSelectedDay] = useState(now.toISOString().slice(0, 10)); + + const year = calMonth.getFullYear(); + const month = calMonth.getMonth(); + const today = now.toISOString().slice(0, 10); + const isCurrentMonth = year === now.getFullYear() && month === now.getMonth(); + + const filteredLogs = filter === "all" ? logs : logs.filter(l => l.type === filter); + + const logsByDate = useMemo(() => { + const map: Record = {}; + filteredLogs.forEach(log => { + const key = new Date(log.loggedAt).toISOString().slice(0, 10); + if (!map[key]) map[key] = []; + map[key].push(log); + }); + return map; + }, [filteredLogs]); + + const firstDow = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const cells: (number | null)[] = [ + ...Array(firstDow).fill(null), + ...Array.from({ length: daysInMonth }, (_, i) => i + 1), + ]; + + const dayKey = (d: number) => + `${year}-${String(month + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`; + + const selectedLogs = useMemo( + () => + selectedDay + ? (logsByDate[selectedDay] || []) + .slice() + .sort((a, b) => new Date(b.loggedAt).getTime() - new Date(a.loggedAt).getTime()) + : [], + [selectedDay, logsByDate], + ); + + return ( +
+
+ {/* Month nav */} +
+ + + {calMonth.toLocaleDateString("en-US", { month: "long", year: "numeric" })} + + +
+ + {/* DOW headers */} +
+ {["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map(d => ( +
{d}
+ ))} +
+ + {/* Day cells */} +
+ {cells.map((day, i) => { + if (!day) return
; + const key = dayKey(day); + const dayLogs = logsByDate[key] || []; + const isToday = key === today; + const isSel = key === selectedDay; + const types = [...new Set(dayLogs.map(l => l.type))] as LogType[]; + + return ( + + ); + })} +
+ + {/* Legend */} +
+ {(["feed", "sleep", "diaper"] as LogType[]).map(t => ( +
+ + {t} +
+ ))} +
+
+ + {/* Selected day detail */} + {selectedDay && ( +
+
+

+ {new Date(selectedDay + "T12:00:00").toLocaleDateString("en-US", { + weekday: "long", month: "short", day: "numeric", + })} +

+ + {selectedLogs.length} {selectedLogs.length === 1 ? "entry" : "entries"} + +
+ + {selectedLogs.length === 0 ? ( +

No activity logged

+ ) : ( +
+ {selectedLogs.map(log => ( +
+ {getIcon(log.type)} +
+
{log.type}
+
+ {[ + log.subType?.replace(/_/g, " "), + log.amount ? `${log.amount}ml` : null, + log.notes, + ].filter(Boolean).join(" Β· ")} +
+
+
+ {new Date(log.loggedAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} +
+
+ ))} +
+ )} +
+ )} +
+ ); +} diff --git a/src/components/LogModal.tsx b/src/components/LogModal.tsx index 4aa23e8..3ccf8e0 100644 --- a/src/components/LogModal.tsx +++ b/src/components/LogModal.tsx @@ -3,8 +3,9 @@ import { useState, useEffect } from "react"; import { Button, Modal, Select, Input } from "@/components/ui"; import { addToOfflineQueue } from "@/lib/offline-queue"; +import type { LogType } from "@/types"; -export type LogType = "feed" | "diaper" | "sleep"; +export type { LogType }; export interface SmartDefault { subType: string; diff --git a/src/components/PageHeader.tsx b/src/components/PageHeader.tsx new file mode 100644 index 0000000..f1a298d --- /dev/null +++ b/src/components/PageHeader.tsx @@ -0,0 +1,24 @@ +import { ReactNode } from "react"; +import Link from "next/link"; + +interface Props { + title: string; + subtitle?: string; + backHref?: string; + actions?: ReactNode; +} + +export function PageHeader({ title, subtitle, backHref = "/menu", actions }: Props) { + return ( +
+
+ ← +
+

{title}

+ {subtitle &&

{subtitle}

} +
+
+ {actions &&
{actions}
} +
+ ); +} diff --git a/src/components/TabBar.tsx b/src/components/TabBar.tsx new file mode 100644 index 0000000..3e35e90 --- /dev/null +++ b/src/components/TabBar.tsx @@ -0,0 +1,37 @@ +interface Tab { + value: string; + label: string; +} + +interface Props { + tabs: Tab[]; + value: string; + onChange: (value: string) => void; + danger?: string; +} + +export function TabBar({ tabs, value, onChange, danger }: Props) { + return ( +
+ {tabs.map(tab => { + const isActive = tab.value === value; + const isDanger = danger && tab.value === danger; + return ( + + ); + })} +
+ ); +} diff --git a/src/components/medical/AllergyTab.tsx b/src/components/medical/AllergyTab.tsx new file mode 100644 index 0000000..b4808ac --- /dev/null +++ b/src/components/medical/AllergyTab.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button, Input, Select } from "@/components/ui"; +import { api } from "@/lib/api"; +import type { Allergy } from "@/types"; + +interface Props { childId: string } + +export function AllergyTab({ childId }: Props) { + const [allergies, setAllergies] = useState([]); + const [editingAllergy, setEditingAllergy] = useState(null); + const [showAdd, setShowAdd] = useState(false); + const [name, setName] = useState(""); + const [severity, setSeverity] = useState("mild"); + const [notes, setNotes] = useState(""); + + useEffect(() => { fetchAllergies(); }, [childId]); + + const fetchAllergies = () => + api.get<{ allergies: Allergy[] }>(`/api/allergies?childId=${childId}`) + .then(d => setAllergies(d.allergies || [])) + .catch(console.error); + + const save = async () => { + if (!name) return; + try { + if (editingAllergy) { + await api.patch("/api/allergies", { id: editingAllergy.id, name, severity, notes }); + } else { + await api.post("/api/allergies", { childId, name, severity, notes }); + } + fetchAllergies(); + } catch (err) { console.error("Failed to save allergy:", err); } + reset(); + }; + + const remove = async (id: string) => { + try { + await api.delete(`/api/allergies?id=${id}`); + fetchAllergies(); + } catch (err) { console.error("Failed to delete allergy:", err); } + }; + + const edit = (a: Allergy) => { + setEditingAllergy(a); + setName(a.name); + setSeverity(a.severity); + setNotes(a.notes); + setShowAdd(true); + }; + + const reset = () => { + setEditingAllergy(null); + setName(""); + setSeverity("mild"); + setNotes(""); + setShowAdd(false); + }; + + return ( +
+

Known Allergies

+ + {showAdd && ( +
+ setName(e.target.value)} /> + + setNotes(e.target.value)} /> +
+ + +
+
+ )} + + {allergies.length === 0 && !showAdd ? ( +
+

No allergies recorded

+
+ ) : ( + allergies.map(a => ( +
+
+
+
{a.name}
+
+ + {a.severity.toUpperCase()} + + {a.notes && ` Β· ${a.notes}`} +
+
+
+ + +
+
+
+ )) + )} + + {!showAdd && ( + + )} +
+ ); +} diff --git a/src/components/medical/IllnessTab.tsx b/src/components/medical/IllnessTab.tsx new file mode 100644 index 0000000..9bce7a8 --- /dev/null +++ b/src/components/medical/IllnessTab.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button, Input } from "@/components/ui"; +import { api } from "@/lib/api"; +import type { Illness } from "@/types"; + +interface Props { childId: string } + +export function IllnessTab({ childId }: Props) { + const [illnesses, setIllnesses] = useState([]); + const [editingIllness, setEditingIllness] = useState(null); + const [showAdd, setShowAdd] = useState(false); + const [name, setName] = useState(""); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + const [notes, setNotes] = useState(""); + + useEffect(() => { fetchIllnesses(); }, [childId]); + + const fetchIllnesses = () => + api.get<{ illnesses: Illness[] }>(`/api/illnesses?childId=${childId}`) + .then(d => setIllnesses(d.illnesses || [])) + .catch(console.error); + + const save = async () => { + if (!name) return; + try { + if (editingIllness) { + await api.patch("/api/illnesses", { id: editingIllness.id, name, startDate, endDate, notes }); + } else { + await api.post("/api/illnesses", { childId, name, startDate, endDate, notes }); + } + fetchIllnesses(); + } catch (err) { console.error("Failed to save illness:", err); } + reset(); + }; + + const remove = async (id: string) => { + try { + await api.delete(`/api/illnesses?id=${id}`); + fetchIllnesses(); + } catch (err) { console.error("Failed to delete illness:", err); } + }; + + const edit = (ill: Illness) => { + setEditingIllness(ill); + setName(ill.name); + setStartDate(ill.startDate); + setEndDate(ill.endDate || ""); + setNotes(ill.notes); + setShowAdd(true); + }; + + const reset = () => { + setEditingIllness(null); + setName(""); + setStartDate(""); + setEndDate(""); + setNotes(""); + setShowAdd(false); + }; + + return ( +
+

Illness Log

+ + {showAdd && ( +
+ setName(e.target.value)} /> +
+ setStartDate(e.target.value)} /> + setEndDate(e.target.value)} /> +
+ setNotes(e.target.value)} /> +
+ + +
+
+ )} + + {illnesses.length === 0 && !showAdd ? ( +
+

No illnesses recorded

+
+ ) : ( + illnesses.map(ill => ( +
+
+
+
{ill.name}
+
+ {new Date(ill.startDate).toLocaleDateString()} + {ill.endDate && ` - ${new Date(ill.endDate).toLocaleDateString()}`} + {ill.notes && ` Β· ${ill.notes}`} +
+
+
+ + +
+
+
+ )) + )} + + {!showAdd && ( + + )} +
+ ); +} diff --git a/src/components/medical/MedicineTab.tsx b/src/components/medical/MedicineTab.tsx new file mode 100644 index 0000000..9a7a980 --- /dev/null +++ b/src/components/medical/MedicineTab.tsx @@ -0,0 +1,247 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button, Input, Modal } from "@/components/ui"; +import { api } from "@/lib/api"; +import type { Medicine, Dose } from "@/types"; + +interface Props { childId: string } + +export function MedicineTab({ childId }: Props) { + const [medicines, setMedicines] = useState([]); + const [editingMed, setEditingMed] = useState(null); + const [showAddMed, setShowAddMed] = useState(false); + const [name, setName] = useState(""); + const [dose, setDose] = useState(""); + const [notes, setNotes] = useState(""); + + const [logDoseMedId, setLogDoseMedId] = useState(null); + const [doseAmount, setDoseAmount] = useState(""); + const [doseNotes, setDoseNotes] = useState(""); + const [doseTime, setDoseTime] = useState(() => new Date().toISOString().slice(0, 16)); + const [doseLoading, setDoseLoading] = useState(false); + + const [doseHistory, setDoseHistory] = useState>({}); + const [showDoseHistory, setShowDoseHistory] = useState>({}); + + const [correctDose, setCorrectDose] = useState<{ medId: string; dose: Dose } | null>(null); + const [correctAmount, setCorrectAmount] = useState(""); + const [correctNotes, setCorrectNotes] = useState(""); + const [correctReason, setCorrectReason] = useState(""); + const [correctLoading, setCorrectLoading] = useState(false); + + useEffect(() => { fetchMedicines(); }, [childId]); + + const fetchMedicines = () => + api.get<{ medicines: Medicine[] }>(`/api/medicines?childId=${childId}`) + .then(d => setMedicines(d.medicines || [])) + .catch(console.error); + + const saveMedicine = async () => { + if (!name) return; + try { + if (editingMed) { + await api.patch("/api/medicines", { id: editingMed.id, name, dose, notes }); + } else { + await api.post("/api/medicines", { childId, name, dose, notes }); + } + fetchMedicines(); + } catch (err) { console.error("Failed to save medicine:", err); } + resetForm(); + }; + + const deleteMedicine = async (id: string) => { + try { + await api.delete(`/api/medicines?id=${id}`); + fetchMedicines(); + } catch (err) { console.error("Failed to delete medicine:", err); } + }; + + const editMedicine = (med: Medicine) => { + setEditingMed(med); + setName(med.name); + setDose(med.dose); + setNotes(med.notes); + setShowAddMed(true); + }; + + const resetForm = () => { + setEditingMed(null); + setName(""); + setDose(""); + setNotes(""); + setShowAddMed(false); + }; + + const openLogDose = (medId: string) => { + setLogDoseMedId(medId); + setDoseAmount(""); + setDoseNotes(""); + setDoseTime(new Date().toISOString().slice(0, 16)); + }; + + const submitDose = async () => { + if (!logDoseMedId) return; + setDoseLoading(true); + try { + await api.post(`/api/medicines/${logDoseMedId}/doses`, { + amountGiven: doseAmount, + notes: doseNotes, + administeredAt: new Date(doseTime).toISOString(), + }); + const medId = logDoseMedId; + setLogDoseMedId(null); + if (showDoseHistory[medId]) fetchDoseHistory(medId); + } catch (err) { console.error("Failed to log dose:", err); } + setDoseLoading(false); + }; + + const fetchDoseHistory = (medId: string) => + api.get<{ doses: Dose[] }>(`/api/medicines/${medId}/doses`) + .then(d => setDoseHistory(prev => ({ ...prev, [medId]: d.doses || [] }))) + .catch(console.error); + + const toggleDoseHistory = (medId: string) => { + const next = !showDoseHistory[medId]; + setShowDoseHistory(prev => ({ ...prev, [medId]: next })); + if (next && !doseHistory[medId]) fetchDoseHistory(medId); + }; + + const submitCorrection = async () => { + if (!correctDose) return; + setCorrectLoading(true); + try { + await api.post(`/api/medicines/${correctDose.medId}/doses/${correctDose.dose.id}/correct`, { + amountGiven: correctAmount, + notes: correctNotes, + reason: correctReason, + }); + fetchDoseHistory(correctDose.medId); + setCorrectDose(null); + } catch (err) { console.error("Failed to submit correction:", err); } + setCorrectLoading(false); + }; + + return ( +
+

Medicine & Supplements

+ + {showAddMed && ( +
+ setName(e.target.value)} /> + setDose(e.target.value)} /> + setNotes(e.target.value)} /> +
+ + +
+
+ )} + + {medicines.length === 0 && !showAddMed ? ( +
+

No medicines added

+
+ ) : ( + medicines.map(med => ( +
+
+
+
+
{med.name}
+
+ {med.dose}{med.notes && ` · ${med.notes}`} + {med.reminderTime && ` · ⏰ ${med.reminderTime}`} +
+
+
+ + + +
+
+ +
+ + {showDoseHistory[med.id] && ( +
+ {!doseHistory[med.id] ? ( +

Loading…

+ ) : doseHistory[med.id].length === 0 ? ( +

No doses logged yet.

+ ) : doseHistory[med.id].map(d => { + const latest = d.corrections?.[0]?.corrected_value; + const displayAmount = latest?.amountGiven ?? d.amount_given; + const displayNotes = latest?.notes ?? d.notes; + return ( +
+
+
+ + {new Date(d.administered_at).toLocaleString()} + + {displayAmount && Β· {displayAmount}} + {displayNotes && Β· {displayNotes}} + {!!latest && edited} +
+ +
+
+ ); + })} +
+ )} +
+ )) + )} + + {!showAddMed && ( + + )} + + setLogDoseMedId(null)} title="Log Dose" maxWidth="sm"> +
+ setDoseAmount(e.target.value)} /> + setDoseTime(e.target.value)} /> + setDoseNotes(e.target.value)} /> +
+ + +
+
+
+ + setCorrectDose(null)} title="Edit Dose (Correction)" maxWidth="sm"> +
+

Original is kept. A correction record is added.

+ {correctDose && ( +
+ Original: {correctDose.dose.amount_given || "β€”"} Β· {new Date(correctDose.dose.administered_at).toLocaleString()} +
+ )} + setCorrectAmount(e.target.value)} /> + setCorrectNotes(e.target.value)} /> + setCorrectReason(e.target.value)} /> +
+ + +
+
+
+
+ ); +} diff --git a/src/components/medical/VaccineTab.tsx b/src/components/medical/VaccineTab.tsx new file mode 100644 index 0000000..95412d8 --- /dev/null +++ b/src/components/medical/VaccineTab.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { TabBar } from "@/components/TabBar"; +import { api } from "@/lib/api"; +import type { Vaccination } from "@/types"; + +interface Props { + childId: string; + birthDate: string; +} + +type VaccineStatus = "upcoming" | "completed" | "overdue"; + +const IAP_SCHEDULE = [ + { name: "BCG", weeks: 0 }, + { name: "OPV-0", weeks: 0 }, + { name: "HepB-1", weeks: 0 }, + { name: "OPV-1", weeks: 6 }, + { name: "Pentavalent-1", weeks: 6 }, + { name: "PCV-1", weeks: 6 }, + { name: "Rota-1", weeks: 6 }, + { name: "OPV-2", weeks: 10 }, + { name: "Pentavalent-2", weeks: 10 }, + { name: "PCV-2", weeks: 10 }, + { name: "Rota-2", weeks: 10 }, + { name: "OPV-3", weeks: 14 }, + { name: "Pentavalent-3", weeks: 14 }, + { name: "PCV-3", weeks: 14 }, + { name: "Rota-3", weeks: 14 }, + { name: "MR-1", weeks: 48 }, + { name: "JE-1", weeks: 48 }, + { name: "Vitamin A-1", weeks: 48 }, + { name: "OPV-4", weeks: 48 }, + { name: "MR-2", weeks: 96 }, + { name: "JE-2", weeks: 96 }, + { name: "DPT-Booster-1", weeks: 96 }, + { name: "Vitamin A-2", weeks: 96 }, + { name: "OPV-5", weeks: 96 }, + { name: "DPT-Booster-2", weeks: 208 }, + { name: "Td", weeks: 208 }, +]; + +function calculateDueDate(birthDate: string, weeks: number): string { + const d = new Date(birthDate); + d.setDate(d.getDate() + weeks * 7); + return d.toISOString().split("T")[0]; +} + +const STATUS_TABS = [ + { value: "upcoming", label: "" }, + { value: "completed", label: "" }, + { value: "overdue", label: "" }, +]; + +export function VaccineTab({ childId, birthDate }: Props) { + const [vaccinations, setVaccinations] = useState([]); + const [loading, setLoading] = useState(true); + const [vaccineTab, setVaccineTab] = useState("upcoming"); + const [showAddDate, setShowAddDate] = useState(null); + const [givenDate, setGivenDate] = useState(""); + + useEffect(() => { + api.get<{ vaccinations: Vaccination[] }>(`/api/vaccinations?childId=${childId}`) + .then(d => setVaccinations(d.vaccinations || [])) + .catch(console.error) + .finally(() => setLoading(false)); + }, [childId]); + + 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 todayStr = new Date().toISOString().split("T")[0]; + + const getVaccineStatus = (name: string): VaccineStatus => { + const due = calculateDueDate(birthDate, IAP_SCHEDULE.find(v => v.name === name)?.weeks ?? 0); + if (isGiven(name)) return "completed"; + if (due < todayStr) return "overdue"; + return "upcoming"; + }; + + const getByStatus = (status: VaccineStatus) => + IAP_SCHEDULE.filter(v => { + const due = calculateDueDate(birthDate, v.weeks); + if (status === "completed") return isGiven(v.name); + if (status === "overdue") return !isGiven(v.name) && due < todayStr; + return !isGiven(v.name) && due >= todayStr; + }).sort((a, b) => a.weeks - b.weeks); + + const handleMarkGiven = async (vaccineName: string) => { + const due = calculateDueDate(birthDate, IAP_SCHEDULE.find(v => v.name === vaccineName)?.weeks ?? 0); + const dateToSave = givenDate || todayStr; + try { + await api.post("/api/vaccinations", { childId, vaccineName, scheduledDate: due, givenDate: dateToSave, status: "given" }); + setVaccinations(prev => [...prev, { vaccine_name: vaccineName, given_date: dateToSave, status: "given" }]); + } catch (err) { + console.error("Failed to mark given:", err); + } + setShowAddDate(null); + setGivenDate(""); + }; + + const counts = { + upcoming: getByStatus("upcoming").length, + completed: getByStatus("completed").length, + overdue: getByStatus("overdue").length, + }; + + const statusTabs = STATUS_TABS.map(t => ({ + value: t.value, + label: `${t.value.charAt(0).toUpperCase() + t.value.slice(1)} (${counts[t.value as VaccineStatus]})`, + })); + + return ( +
+ setVaccineTab(v as VaccineStatus)} danger="overdue" /> + +

IAP Schedule

+ + {loading ? ( +

Loading...

+ ) : ( + getByStatus(vaccineTab).map(vaccine => { + const due = calculateDueDate(birthDate, vaccine.weeks); + const status = getVaccineStatus(vaccine.name); + const actualDate = getGivenDate(vaccine.name); + const daysOverdue = status === "overdue" + ? Math.floor((Date.now() - new Date(due).getTime()) / 86_400_000) + : 0; + + return ( +
+
+
+
{vaccine.name}
+
+ Due: {new Date(due).toLocaleDateString()} + {actualDate && ` Β· Given: ${new Date(actualDate).toLocaleDateString()}`} +
+ {status === "overdue" && ( +
{daysOverdue} days overdue
+ )} +
+ + {isGiven(vaccine.name) ? ( + βœ“ + ) : showAddDate === vaccine.name ? ( +
+ setGivenDate(e.target.value)} + className="p-1 text-sm border dark:border-gray-600 rounded dark:bg-gray-700 dark:text-white" + /> + + +
+ ) : ( + + )} +
+
+ ); + }) + )} +
+ ); +} diff --git a/src/components/medical/VisitTab.tsx b/src/components/medical/VisitTab.tsx new file mode 100644 index 0000000..1d185ac --- /dev/null +++ b/src/components/medical/VisitTab.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button, Input } from "@/components/ui"; +import { api } from "@/lib/api"; +import type { Visit } from "@/types"; + +interface Props { childId: string } + +export function VisitTab({ childId }: Props) { + const [visits, setVisits] = useState([]); + const [editingVisit, setEditingVisit] = useState(null); + const [showAdd, setShowAdd] = useState(false); + const [doctor, setDoctor] = useState(""); + const [reason, setReason] = useState(""); + const [date, setDate] = useState(""); + const [notes, setNotes] = useState(""); + + useEffect(() => { fetchVisits(); }, [childId]); + + const fetchVisits = () => + api.get<{ visits: Visit[] }>(`/api/visits?childId=${childId}`) + .then(d => setVisits(d.visits || [])) + .catch(console.error); + + const save = async () => { + if (!doctor) return; + try { + if (editingVisit) { + await api.patch("/api/visits", { id: editingVisit.id, doctorName: doctor, reason, date, notes }); + } else { + await api.post("/api/visits", { childId, doctorName: doctor, reason, date, notes }); + } + fetchVisits(); + } catch (err) { console.error("Failed to save visit:", err); } + reset(); + }; + + const remove = async (id: string) => { + try { + await api.delete(`/api/visits?id=${id}`); + fetchVisits(); + } catch (err) { console.error("Failed to delete visit:", err); } + }; + + const edit = (v: Visit) => { + setEditingVisit(v); + setDoctor(v.doctorName); + setReason(v.reason); + setDate(v.date); + setNotes(v.notes); + setShowAdd(true); + }; + + const reset = () => { + setEditingVisit(null); + setDoctor(""); + setReason(""); + setDate(""); + setNotes(""); + setShowAdd(false); + }; + + return ( +
+

Doctor Visits

+ + {showAdd && ( +
+ setDoctor(e.target.value)} /> + setReason(e.target.value)} /> + setDate(e.target.value)} /> + setNotes(e.target.value)} /> +
+ + +
+
+ )} + + {visits.length === 0 && !showAdd ? ( +
+

No visits recorded

+
+ ) : ( + visits.map(v => ( +
+
+
+
{v.doctorName}
+
+ {new Date(v.date).toLocaleDateString()} + {v.reason && ` Β· ${v.reason}`} + {v.notes && ` Β· ${v.notes}`} +
+
+
+ + +
+
+
+ )) + )} + + {!showAdd && ( + + )} +
+ ); +} diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..bf76ecf --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,30 @@ +async function request(url: string, init?: RequestInit): Promise { + const res = await fetch(url, init); + if (!res.ok) { + const body = await res.json().catch(() => ({})) as { error?: string }; + throw new Error(body.error ?? `HTTP ${res.status}`); + } + return res.json() as Promise; +} + +export const api = { + get: (url: string) => + request(url), + + post: (url: string, body: unknown) => + request(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }), + + patch: (url: string, body: unknown) => + request(url, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }), + + delete: (url: string) => + request(url, { method: "DELETE" }), +}; diff --git a/src/lib/formatting.ts b/src/lib/formatting.ts new file mode 100644 index 0000000..3b874b0 --- /dev/null +++ b/src/lib/formatting.ts @@ -0,0 +1,42 @@ +export function calculateAge(birthDate: string): string { + if (!birthDate) return ""; + const birth = new Date(birthDate); + const now = new Date(); + let years = now.getFullYear() - birth.getFullYear(); + let months = now.getMonth() - birth.getMonth(); + let days = now.getDate() - birth.getDate(); + if (days < 0) { + months--; + days += new Date(now.getFullYear(), now.getMonth(), 0).getDate(); + } + if (months < 0) { years--; months += 12; } + const parts: string[] = []; + if (years > 0) parts.push(`${years} year${years > 1 ? "s" : ""}`); + if (months > 0) parts.push(`${months} month${months > 1 ? "s" : ""}`); + if (days > 0) parts.push(`${days} day${days > 1 ? "s" : ""}`); + return parts.length > 0 ? parts.join(", ") : "Newborn"; +} + +export function formatAge(birthDate: string, measurementDate?: string): string { + const birth = new Date(birthDate); + const ref = measurementDate ? new Date(measurementDate) : new Date(); + const totalDays = (ref.getTime() - birth.getTime()) / (1000 * 60 * 60 * 24); + const years = Math.floor(totalDays / 365); + const months = Math.floor((totalDays % 365) / 30); + if (years > 0 && months > 0) return `${years}y ${months}mo`; + if (years > 0) return `${years}y`; + if (months > 0) return `${months}mo`; + return "Newborn"; +} + +export function formatTimeAgo(dateStr: string | null | undefined): string | null { + if (!dateStr) return null; + const diffMs = Date.now() - new Date(dateStr).getTime(); + const mins = Math.floor(diffMs / 60_000); + const hours = Math.floor(mins / 60); + const days = Math.floor(hours / 24); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + if (hours < 24) return `${hours}h ago`; + return `${days}d ago`; +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..d80c779 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,102 @@ +// Shared domain types β€” import from here rather than defining inline in each page + +export interface Child { + id: string; + name: string; + birthDate: string; + sex: string; +} + +export type LogType = "feed" | "diaper" | "sleep"; + +export interface Log { + id: string; + type: LogType; + subType?: string; + amount?: number | null; + notes?: string | null; + loggedAt: string; +} + +export 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; +} + +export interface Goal { + weightKg?: number; + heightCm?: number; + targetDate?: string; +} + +export interface Medicine { + id: string; + name: string; + dose: string; + notes: string; + reminderTime?: string; +} + +export interface DoseCorrection { + id: string; + corrected_value: Record; + reason: string | null; + created_at: string; +} + +export interface Dose { + id: string; + administered_at: string; + amount_given: string | null; + notes: string | null; + corrections?: DoseCorrection[]; +} + +export interface Allergy { + id: string; + name: string; + severity: string; + notes: string; +} + +export interface Visit { + id: string; + doctorName: string; + reason: string; + date: string; + notes: string; +} + +export interface Illness { + id: string; + name: string; + startDate: string; + endDate?: string; + notes: string; +} + +export interface Vaccination { + vaccine_name: string; + given_date: string; + status: "given" | "pending"; +} + +export interface AIChat { + id: string; + role: "user" | "assistant"; + content: string; + createdAt: string; +} + +export interface ChatSession { + id: string; + title: string; + messages: AIChat[]; + createdAt: string; + updatedAt: string; +}