tia/src/components/medical/MedicineTab.tsx
Mannu 96d28cbadc refactor: full codebase sweep — shared types, utilities, component splits
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>
2026-05-18 21:37:39 +05:30

247 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<Medicine[]>([]);
const [editingMed, setEditingMed] = useState<Medicine | null>(null);
const [showAddMed, setShowAddMed] = useState(false);
const [name, setName] = useState("");
const [dose, setDose] = useState("");
const [notes, setNotes] = useState("");
const [logDoseMedId, setLogDoseMedId] = useState<string | null>(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<Record<string, Dose[]>>({});
const [showDoseHistory, setShowDoseHistory] = useState<Record<string, boolean>>({});
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 (
<div className="space-y-2">
<h2 className="font-semibold mb-3">Medicine &amp; Supplements</h2>
{showAddMed && (
<div className="p-4 bg-white dark:bg-gray-800 rounded-xl space-y-3">
<Input placeholder="Medicine name" value={name} onChange={e => setName(e.target.value)} />
<Input placeholder="Dose (e.g., 5ml, 1 tablet)" value={dose} onChange={e => setDose(e.target.value)} />
<Input placeholder="Notes" value={notes} onChange={e => setNotes(e.target.value)} />
<div className="flex gap-2">
<Button fullWidth onClick={saveMedicine}>{editingMed ? "Update" : "Add"}</Button>
<Button variant="secondary" fullWidth onClick={resetForm}>Cancel</Button>
</div>
</div>
)}
{medicines.length === 0 && !showAddMed ? (
<div className="p-4 bg-white dark:bg-gray-800 rounded-xl">
<p className="text-gray-500 dark:text-gray-400">No medicines added</p>
</div>
) : (
medicines.map(med => (
<div key={med.id} className="bg-white dark:bg-gray-800 rounded-xl overflow-hidden">
<div className="p-4">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="font-medium dark:text-white">{med.name}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{med.dose}{med.notes && ` · ${med.notes}`}
{med.reminderTime && ` · ⏰ ${med.reminderTime}`}
</div>
</div>
<div className="flex gap-1">
<button onClick={() => openLogDose(med.id)} className="px-2 py-1 text-xs bg-rose-400 text-white rounded-lg">Log Dose</button>
<button onClick={() => editMedicine(med)} className="p-2 text-gray-400"></button>
<button onClick={() => deleteMedicine(med.id)} className="p-2 text-red-400">🗑</button>
</div>
</div>
<button
onClick={() => toggleDoseHistory(med.id)}
className="mt-2 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
{showDoseHistory[med.id] ? "▲ Hide history" : "▼ Dose history"}
</button>
</div>
{showDoseHistory[med.id] && (
<div className="border-t dark:border-gray-700 px-4 pb-3 space-y-2 pt-2">
{!doseHistory[med.id] ? (
<p className="text-xs text-gray-400">Loading</p>
) : doseHistory[med.id].length === 0 ? (
<p className="text-xs text-gray-400">No doses logged yet.</p>
) : 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 (
<div key={d.id} className="text-xs border dark:border-gray-700 rounded-lg p-2">
<div className="flex justify-between items-start">
<div>
<span className="text-gray-600 dark:text-gray-300 font-medium">
{new Date(d.administered_at).toLocaleString()}
</span>
{displayAmount && <span className="ml-2 text-gray-500">· {displayAmount}</span>}
{displayNotes && <span className="ml-1 text-gray-400">· {displayNotes}</span>}
{!!latest && <span className="ml-1 text-amber-500 text-[10px] font-medium">edited</span>}
</div>
<button
onClick={() => { setCorrectDose({ medId: med.id, dose: d }); setCorrectAmount(displayAmount || ""); setCorrectNotes(displayNotes || ""); setCorrectReason(""); }}
className="text-gray-300 dark:text-gray-600 hover:text-amber-500 ml-2"
>
</button>
</div>
</div>
);
})}
</div>
)}
</div>
))
)}
{!showAddMed && (
<button onClick={() => setShowAddMed(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 Medicine
</button>
)}
<Modal open={!!logDoseMedId} onClose={() => setLogDoseMedId(null)} title="Log Dose" maxWidth="sm">
<div className="space-y-3">
<Input placeholder="Amount given (e.g. 5ml)" value={doseAmount} onChange={e => setDoseAmount(e.target.value)} />
<Input type="datetime-local" value={doseTime} onChange={e => setDoseTime(e.target.value)} />
<Input placeholder="Notes (optional)" value={doseNotes} onChange={e => setDoseNotes(e.target.value)} />
<div className="flex gap-2">
<Button fullWidth loading={doseLoading} onClick={submitDose}>Save Dose</Button>
<Button variant="secondary" fullWidth onClick={() => setLogDoseMedId(null)}>Cancel</Button>
</div>
</div>
</Modal>
<Modal open={!!correctDose} onClose={() => setCorrectDose(null)} title="Edit Dose (Correction)" maxWidth="sm">
<div className="space-y-3">
<p className="text-xs text-gray-400">Original is kept. A correction record is added.</p>
{correctDose && (
<div className="text-xs bg-gray-50 dark:bg-gray-700 rounded-lg p-2 text-gray-500 dark:text-gray-400">
Original: {correctDose.dose.amount_given || "—"} · {new Date(correctDose.dose.administered_at).toLocaleString()}
</div>
)}
<Input placeholder="Corrected amount" value={correctAmount} onChange={e => setCorrectAmount(e.target.value)} />
<Input placeholder="Corrected notes" value={correctNotes} onChange={e => setCorrectNotes(e.target.value)} />
<Input placeholder="Reason for correction (optional)" value={correctReason} onChange={e => setCorrectReason(e.target.value)} />
<div className="flex gap-2">
<Button fullWidth loading={correctLoading} onClick={submitCorrection} className="bg-amber-500 hover:bg-amber-600">
Save Correction
</Button>
<Button variant="secondary" fullWidth onClick={() => setCorrectDose(null)}>Cancel</Button>
</div>
</div>
</Modal>
</div>
);
}