Migrate medical data from localStorage to database

- Add database tables: medicines, allergies, doctor_visits, illness_logs
- Create API endpoints: /api/medicines, /api/allergies, /api/visits, /api/illnesses
- Update medical page to use database APIs instead of localStorage
- All medical data now persists across sessions and devices

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-10 21:07:26 +05:30
parent a1b436710f
commit 3e66b259f2
6 changed files with 535 additions and 94 deletions

View file

@ -92,6 +92,44 @@ const { theme, toggle, setMode } = useTheme();
- Model: `minimax-2.7`
- See `/docs/debugging.md` for troubleshooting
## Data Storage Consistency
### RULE: All user data must persist to database, NOT localStorage
| Data Type | Storage | API Key | Persists After Refresh | Persists After Logout |
|----------|---------|--------|------------------------|-------------------|
| Children | Database | `/api/children` | ✅ Yes | ✅ Yes |
| Activity Logs | Database | `/api/logs` | ✅ Yes | ✅ Yes |
| Vaccinations | Database | `/api/vaccinations` | ✅ Yes | ✅ Yes |
| Growth Records | Database | `/api/growth` | ✅ Yes | ✅ Yes |
| User Profile | Database | `/api/auth/profile` | ✅ Yes | ✅ Yes |
| Memories/Photos | Database + R2 | `/api/upload` | ✅ Yes | ✅ Yes |
| **Medicines** | Database | `/api/medicines` | ⚠️ TODO | ⚠️ TODO |
| **Allergies** | Database | `/api/allergies` | ⚠️ TODO | ⚠️ TODO |
| **Doctor Visits** | Database | `/api/visits` | ⚠️ TODO | ⚠️ TODO |
| **Illness Log** | Database | `/api/illnesses` | ⚠️ TODO | ⚠️ TODO |
| Theme | localStorage | `tia_theme` | ✅ Yes | ✅ Yes |
| Chat Sessions | localStorage | `tia_chat_sessions` | ✅ Yes | ❌ No |
| Offline Queue | localStorage | `tia_offline_queue` | ✅ Yes | ❌ No |
### localStorage acceptable for:
- Theme preference (user-specific display only)
- Temporary cache (offline queue for retry)
- Chat sessions (upcoming feature: move to database)
### NEVER use localStorage for:
- Medical/health data (medicines, allergies, visits, illnesses)
- Any data that should persist across devices
- Data important for pediatrician visits
### Audit (2026-05-10)
Current status - needs migration to database:
- `tia_medicines``/api/medicines`
- `tia_allergies``/api/allergies`
- `tia_visits``/api/visits`
- `tia_illnesses``/api/illnesses`
## R2 Storage (Cloudflare)
### Setup

View file

@ -0,0 +1,86 @@
import { NextResponse } from "next/server";
import { sql } from "@/db";
// GET - list allergies for a child
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const childId = searchParams.get("childId") || "default";
try {
const allergies = await sql.unsafe(
`SELECT id, name, severity, notes, created_at as "createdAt"
FROM allergies WHERE child_id = $1 ORDER BY created_at DESC`,
[childId]
);
return NextResponse.json({ allergies: allergies || [] });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// POST - create allergy
export async function POST(request: Request) {
try {
const body = await request.json();
const { childId, name, severity, notes } = body;
if (!childId || !name) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
const [allergy] = await sql.unsafe(
`INSERT INTO allergies (child_id, name, severity, notes)
VALUES ($1, $2, $3, $4)
RETURNING id, name, severity, notes`,
[childId, name, severity || "mild", notes || null]
);
return NextResponse.json({ success: true, allergy });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// PATCH - update allergy
export async function PATCH(request: Request) {
try {
const body = await request.json();
const { id, name, severity, notes } = body;
if (!id || !name) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
const [allergy] = await sql.unsafe(
`UPDATE allergies SET name = $1, severity = $2, notes = $3, updated_at = NOW()
WHERE id = $4
RETURNING id, name, severity, notes`,
[name, severity, notes, id]
);
return NextResponse.json({ success: true, allergy });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// DELETE - delete allergy
export async function DELETE(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (!id) {
return NextResponse.json({ error: "ID required" }, { status: 400 });
}
try {
await sql.unsafe(`DELETE FROM allergies WHERE id = $1`, [id]);
return NextResponse.json({ success: true });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}

View file

@ -0,0 +1,86 @@
import { NextResponse } from "next/server";
import { sql } from "@/db";
// GET - list illness logs for a child
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const childId = searchParams.get("childId") || "default";
try {
const illnesses = await sql.unsafe(
`SELECT id, name, start_date as "startDate", end_date as "endDate", notes, created_at as "createdAt"
FROM illness_logs WHERE child_id = $1 ORDER BY start_date DESC`,
[childId]
);
return NextResponse.json({ illnesses: illnesses || [] });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// POST - create illness log
export async function POST(request: Request) {
try {
const body = await request.json();
const { childId, name, startDate, endDate, notes } = body;
if (!childId || !name || !startDate) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
const [illness] = await sql.unsafe(
`INSERT INTO illness_logs (child_id, name, start_date, end_date, notes)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, name, start_date as "startDate", end_date as "endDate", notes`,
[childId, name, startDate, endDate || null, notes || null]
);
return NextResponse.json({ success: true, illness });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// PATCH - update illness log
export async function PATCH(request: Request) {
try {
const body = await request.json();
const { id, name, startDate, endDate, notes } = body;
if (!id || !name || !startDate) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
const [illness] = await sql.unsafe(
`UPDATE illness_logs SET name = $1, start_date = $2, end_date = $3, notes = $4, updated_at = NOW()
WHERE id = $5
RETURNING id, name, start_date as "startDate", end_date as "endDate", notes`,
[name, startDate, endDate, notes, id]
);
return NextResponse.json({ success: true, illness });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// DELETE - delete illness log
export async function DELETE(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (!id) {
return NextResponse.json({ error: "ID required" }, { status: 400 });
}
try {
await sql.unsafe(`DELETE FROM illness_logs WHERE id = $1`, [id]);
return NextResponse.json({ success: true });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}

View file

@ -0,0 +1,86 @@
import { NextResponse } from "next/server";
import { sql } from "@/db";
// GET - list medicines for a child
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const childId = searchParams.get("childId") || "default";
try {
const medicines = await sql.unsafe(
`SELECT id, name, dose, notes, reminder_time as "reminderTime", created_at as "createdAt"
FROM medicines WHERE child_id = $1 ORDER BY created_at DESC`,
[childId]
);
return NextResponse.json({ medicines: medicines || [] });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// POST - create medicine
export async function POST(request: Request) {
try {
const body = await request.json();
const { childId, name, dose, notes, reminderTime } = body;
if (!childId || !name) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
const [medicine] = await sql.unsafe(
`INSERT INTO medicines (child_id, name, dose, notes, reminder_time)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, name, dose, notes, reminder_time as "reminderTime"`,
[childId, name, dose || null, notes || null, reminderTime || null]
);
return NextResponse.json({ success: true, medicine });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// PATCH - update medicine
export async function PATCH(request: Request) {
try {
const body = await request.json();
const { id, name, dose, notes, reminderTime } = body;
if (!id || !name) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
const [medicine] = await sql.unsafe(
`UPDATE medicines SET name = $1, dose = $2, notes = $3, reminder_time = $4, updated_at = NOW()
WHERE id = $5
RETURNING id, name, dose, notes, reminder_time as "reminderTime"`,
[name, dose, notes, reminderTime, id]
);
return NextResponse.json({ success: true, medicine });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// DELETE - delete medicine
export async function DELETE(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (!id) {
return NextResponse.json({ error: "ID required" }, { status: 400 });
}
try {
await sql.unsafe(`DELETE FROM medicines WHERE id = $1`, [id]);
return NextResponse.json({ success: true });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}

View file

@ -0,0 +1,86 @@
import { NextResponse } from "next/server";
import { sql } from "@/db";
// GET - list doctor visits for a child
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const childId = searchParams.get("childId") || "default";
try {
const visits = await sql.unsafe(
`SELECT id, doctor_name as "doctorName", reason, visit_date as "date", notes, created_at as "createdAt"
FROM doctor_visits WHERE child_id = $1 ORDER BY visit_date DESC`,
[childId]
);
return NextResponse.json({ visits: visits || [] });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// POST - create visit
export async function POST(request: Request) {
try {
const body = await request.json();
const { childId, doctorName, reason, date, notes } = body;
if (!childId || !doctorName || !date) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
const [visit] = await sql.unsafe(
`INSERT INTO doctor_visits (child_id, doctor_name, reason, visit_date, notes)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, doctor_name as "doctorName", reason, visit_date as "date", notes`,
[childId, doctorName, reason || null, date, notes || null]
);
return NextResponse.json({ success: true, visit });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// PATCH - update visit
export async function PATCH(request: Request) {
try {
const body = await request.json();
const { id, doctorName, reason, date, notes } = body;
if (!id || !doctorName || !date) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
const [visit] = await sql.unsafe(
`UPDATE doctor_visits SET doctor_name = $1, reason = $2, visit_date = $3, notes = $4, updated_at = NOW()
WHERE id = $5
RETURNING id, doctor_name as "doctorName", reason, visit_date as "date", notes`,
[doctorName, reason, date, notes, id]
);
return NextResponse.json({ success: true, visit });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}
// DELETE - delete visit
export async function DELETE(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (!id) {
return NextResponse.json({ error: "ID required" }, { status: 400 });
}
try {
await sql.unsafe(`DELETE FROM doctor_visits WHERE id = $1`, [id]);
return NextResponse.json({ success: true });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}

View file

@ -76,8 +76,6 @@ export default function MedicalPage() {
const [tab, setTab] = useState<"vaccinations" | "medicine" | "allergies" | "visits" | "illness">("vaccinations");
const [showAddDate, setShowAddDate] = useState<string | null>(null);
const [givenDate, setGivenDate] = useState("");
const [showReminder, setShowReminder] = useState<string | null>(null);
const [reminderTime, setReminderTime] = useState("08:00");
// CRUD state for medicine, allergies, visits, illness
const [medicines, setMedicines] = useState<Medicine[]>([]);
@ -111,44 +109,85 @@ export default function MedicalPage() {
const [newIllnessEnd, setNewIllnessEnd] = useState("");
const [newIllnessNotes, setNewIllnessNotes] = useState("");
// Load data from localStorage on mount
// Load data from database on mount
useEffect(() => {
// Load medicines
const savedMeds = localStorage.getItem("tia_medicines");
if (savedMeds) setMedicines(JSON.parse(savedMeds));
const savedAllergies = localStorage.getItem("tia_allergies");
if (savedAllergies) setAllergies(JSON.parse(savedAllergies));
const savedVisits = localStorage.getItem("tia_visits");
if (savedVisits) setVisits(JSON.parse(savedVisits));
const savedIllnesses = localStorage.getItem("tia_illnesses");
if (savedIllnesses) setIllnesses(JSON.parse(savedIllnesses));
fetchMedicines();
fetchAllergies();
fetchVisits();
fetchIllnesses();
}, []);
// Medicine CRUD
const saveMedicine = () => {
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;
const newItem: Medicine = {
id: editingMed?.id || Date.now().toString(),
name: newMedName,
dose: newMedDose,
notes: newMedNotes,
reminderTime: reminderTime,
};
const updated = editingMed
? medicines.map((m) => (m.id === editingMed.id ? newItem : m))
: [...medicines, newItem];
setMedicines(updated);
localStorage.setItem("tia_medicines", JSON.stringify(updated));
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 = (id: string) => {
const updated = medicines.filter((m) => m.id !== id);
setMedicines(updated);
localStorage.setItem("tia_medicines", JSON.stringify(updated));
const deleteMedicine = async (id: string) => {
try {
await fetch(`/api/medicines?id=${id}`, { method: "DELETE" });
fetchMedicines();
} catch (err) {
console.error("Failed to delete:", err);
}
};
const editMedicine = (med: Medicine) => {
@ -156,7 +195,6 @@ export default function MedicalPage() {
setNewMedName(med.name);
setNewMedDose(med.dose);
setNewMedNotes(med.notes);
setReminderTime(med.reminderTime || "08:00");
setShowAddMed(true);
};
@ -168,27 +206,37 @@ export default function MedicalPage() {
setShowAddMed(false);
};
// Allergy CRUD
const saveAllergy = () => {
// Allergy CRUD - now using database
const saveAllergy = async () => {
if (!newAllergyName) return;
const newItem: Allergy = {
id: editingAllergy?.id || Date.now().toString(),
name: newAllergyName,
severity: newAllergySeverity,
notes: newAllergyNotes,
};
const updated = editingAllergy
? allergies.map((a) => (a.id === editingAllergy.id ? newItem : a))
: [...allergies, newItem];
setAllergies(updated);
localStorage.setItem("tia_allergies", JSON.stringify(updated));
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 = (id: string) => {
const updated = allergies.filter((a) => a.id !== id);
setAllergies(updated);
localStorage.setItem("tia_allergies", JSON.stringify(updated));
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) => {
@ -207,28 +255,37 @@ export default function MedicalPage() {
setShowAddAllergy(false);
};
// Visit CRUD
const saveVisit = () => {
// Visit CRUD - now using database
const saveVisit = async () => {
if (!newVisitDoctor) return;
const newItem: Visit = {
id: editingVisit?.id || Date.now().toString(),
doctorName: newVisitDoctor,
reason: newVisitReason,
date: newVisitDate || new Date().toISOString().split("T")[0],
notes: newVisitNotes,
};
const updated = editingVisit
? visits.map((v) => (v.id === editingVisit.id ? newItem : v))
: [...visits, newItem];
setVisits(updated);
localStorage.setItem("tia_visits", JSON.stringify(updated));
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 = (id: string) => {
const updated = visits.filter((v) => v.id !== id);
setVisits(updated);
localStorage.setItem("tia_visits", JSON.stringify(updated));
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) => {
@ -249,28 +306,37 @@ export default function MedicalPage() {
setShowAddVisit(false);
};
// Illness CRUD
const saveIllness = () => {
// Illness CRUD - now using database
const saveIllness = async () => {
if (!newIllnessName) return;
const newItem: Illness = {
id: editingIllness?.id || Date.now().toString(),
name: newIllnessName,
startDate: newIllnessStart || new Date().toISOString().split("T")[0],
endDate: newIllnessEnd || undefined,
notes: newIllnessNotes,
};
const updated = editingIllness
? illnesses.map((i) => (i.id === editingIllness.id ? newItem : i))
: [...illnesses, newItem];
setIllnesses(updated);
localStorage.setItem("tia_illnesses", JSON.stringify(updated));
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 = (id: string) => {
const updated = illnesses.filter((i) => i.id !== id);
setIllnesses(updated);
localStorage.setItem("tia_illnesses", JSON.stringify(updated));
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) => {
@ -291,13 +357,6 @@ export default function MedicalPage() {
setShowAddIllness(false);
};
// Reminder save handler
const handleSetReminder = (medName: string) => {
// In production, save to database with reminder time
alert(`Reminder set for ${medName} at ${reminderTime}`);
setShowReminder(null);
};
const birthDate = "2024-01-15";
// Common supplements for babies