New foundations: - src/types/index.ts — shared domain types (Child, Log, GrowthRecord, Medicine, Dose, Allergy, Visit, Illness, Vaccination, AIChat, ChatSession, Goal) - src/lib/formatting.ts — calculateAge, formatAge, formatTimeAgo (eliminates 3 duplicate implementations spread across page.tsx and growth/page.tsx) - src/lib/api.ts — typed fetch helpers (api.get/post/patch/delete) with consistent error handling; replaces manual fetch boilerplate New shared components: - src/components/PageHeader.tsx — reusable back-link + title header - src/components/TabBar.tsx — horizontal pill tab bar - src/components/CalendarView.tsx — extracted from activity/page.tsx (was ~170 inline lines) - src/components/medical/ — medical page split into 5 focused tab components: VaccineTab, MedicineTab, AllergyTab, VisitTab, IllnessTab Pages updated: - medical/page.tsx: 1029 → 42 lines (thin shell wiring the 5 tab components) - activity/page.tsx: uses CalendarView + shared Log type + api.ts - growth/page.tsx: uses shared GrowthRecord/Goal types + formatAge; fixes `any` catch clauses; fixes undefined → null in Chart.js dataset values - page.tsx (home): uses shared Log/AIChat/ChatSession types + formatTimeAgo/ calculateAge from formatting.ts; removes inline type definitions - ai/page.tsx: uses shared AIChat/ChatSession types - FamilyProvider.tsx: uses shared Child type; fixes `c: any` mapping Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
113 lines
4 KiB
TypeScript
113 lines
4 KiB
TypeScript
"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<Visit[]>([]);
|
||
const [editingVisit, setEditingVisit] = useState<Visit | null>(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 (
|
||
<div className="space-y-2">
|
||
<h2 className="font-semibold mb-3">Doctor Visits</h2>
|
||
|
||
{showAdd && (
|
||
<div className="p-4 bg-white dark:bg-gray-800 rounded-xl space-y-3">
|
||
<Input placeholder="Doctor name" value={doctor} onChange={e => setDoctor(e.target.value)} />
|
||
<Input placeholder="Reason (e.g., Checkup, Fever)" value={reason} onChange={e => setReason(e.target.value)} />
|
||
<Input type="date" value={date} onChange={e => setDate(e.target.value)} />
|
||
<Input placeholder="Notes" value={notes} onChange={e => setNotes(e.target.value)} />
|
||
<div className="flex gap-2">
|
||
<Button fullWidth onClick={save}>{editingVisit ? "Update" : "Add"}</Button>
|
||
<Button variant="secondary" fullWidth onClick={reset}>Cancel</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{visits.length === 0 && !showAdd ? (
|
||
<div className="p-4 bg-white dark:bg-gray-800 rounded-xl">
|
||
<p className="text-gray-500 dark:text-gray-400">No visits recorded</p>
|
||
</div>
|
||
) : (
|
||
visits.map(v => (
|
||
<div key={v.id} className="p-4 bg-white dark:bg-gray-800 rounded-xl">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex-1">
|
||
<div className="font-medium">{v.doctorName}</div>
|
||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||
{new Date(v.date).toLocaleDateString()}
|
||
{v.reason && ` · ${v.reason}`}
|
||
{v.notes && ` · ${v.notes}`}
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button onClick={() => edit(v)} className="p-2 text-gray-400">✏️</button>
|
||
<button onClick={() => remove(v.id)} className="p-2 text-red-400">🗑️</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
|
||
{!showAdd && (
|
||
<button onClick={() => setShowAdd(true)} className="w-full p-4 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-xl text-gray-500 dark:text-gray-400">
|
||
+ Add Visit
|
||
</button>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|