'use client'; import { useEffect, useState, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import api from '@/lib/api'; import { PlusIcon, CheckIcon, PillIcon, ClockIcon, TrashIcon, EditIcon } from '@/components/ui/Icons'; import Link from 'next/link'; import PushNotificationToggle from '@/components/notifications/PushNotificationToggle'; interface Medication { id: string; name: string; dosage: string; unit: string; frequency: string; times: string[]; days_of_week?: string[]; interval_days?: number; start_date?: string; next_dose_date?: string; notes?: string; active: boolean; quantity_remaining?: number; } interface TodaysMedication { medication: { id: string; name: string; dosage: string; unit: string; }; scheduled_times: string[]; taken_times: string[]; skipped_times?: string[]; is_prn?: boolean; is_next_day?: boolean; is_previous_day?: boolean; } interface AdherenceEntry { medication_id: string; name: string; adherence_percent: number | null; is_prn?: boolean; } type TimeStatus = 'overdue' | 'due_now' | 'upcoming' | 'taken' | 'skipped'; function getTimeStatus(scheduledTime: string, takenTimes: string[], skippedTimes: string[], now: Date): TimeStatus { if (takenTimes.includes(scheduledTime)) return 'taken'; if (skippedTimes.includes(scheduledTime)) return 'skipped'; const [h, m] = scheduledTime.split(':').map(Number); const scheduled = new Date(now); scheduled.setHours(h, m, 0, 0); const diffMs = now.getTime() - scheduled.getTime(); const diffMin = diffMs / 60000; if (diffMin > 15) return 'overdue'; if (diffMin >= -15) return 'due_now'; return 'upcoming'; } const formatSchedule = (med: Medication): string => { if (med.frequency === 'specific_days' && med.days_of_week?.length) { return med.days_of_week.map(d => d.charAt(0).toUpperCase() + d.slice(1)).join(', '); } if (med.frequency === 'every_n_days' && med.interval_days) { return `Every ${med.interval_days} days`; } if (med.frequency === 'as_needed') return 'As needed'; const timesCount = med.times?.length || 0; if (med.frequency === 'twice_daily' || timesCount === 2) return 'Twice daily'; if (timesCount >= 3) return `${timesCount}x daily`; return 'Daily'; }; interface DueEntry { item: TodaysMedication; time: string; status: TimeStatus; } export default function MedicationsPage() { const router = useRouter(); const [medications, setMedications] = useState([]); const [todayMeds, setTodayMeds] = useState([]); const [adherence, setAdherence] = useState([]); const [isLoading, setIsLoading] = useState(true); const [tick, setTick] = useState(0); const fetchData = async () => { try { const [medsData, todayData, adherenceData] = await Promise.all([ api.medications.list(), api.medications.getToday().catch(() => []), api.medications.getAdherence(30).catch(() => []), ]); setMedications(medsData); setTodayMeds(todayData); setAdherence(adherenceData); } catch (err) { console.error('Failed to fetch medications:', err); } }; useEffect(() => { fetchData().finally(() => setIsLoading(false)); }, []); // Re-fetch when tab becomes visible or every 60s useEffect(() => { const onVisible = () => { if (document.visibilityState === 'visible') fetchData(); }; document.addEventListener('visibilitychange', onVisible); const poll = setInterval(fetchData, 60_000); return () => { document.removeEventListener('visibilitychange', onVisible); clearInterval(poll); }; }, []); // Auto-refresh grouping every 60s useEffect(() => { const interval = setInterval(() => setTick(t => t + 1), 60000); return () => clearInterval(interval); }, []); const { dueEntries, upcomingEntries, prnEntries } = useMemo(() => { const now = new Date(); const due: DueEntry[] = []; const upcoming: DueEntry[] = []; const prn: TodaysMedication[] = []; for (const item of todayMeds) { if (item.is_prn) { prn.push(item); continue; } // Next-day meds are always upcoming if (item.is_next_day) { for (const time of item.scheduled_times) { upcoming.push({ item, time, status: 'upcoming' }); } continue; } for (const time of item.scheduled_times) { const status = getTimeStatus(time, item.taken_times, item.skipped_times || [], now); if (status === 'upcoming') { upcoming.push({ item, time, status }); } else { due.push({ item, time, status }); } } } // Sort due: overdue first, then due_now, then by time const statusOrder: Record = { overdue: 0, due_now: 1, taken: 2, skipped: 3, upcoming: 4 }; due.sort((a, b) => statusOrder[a.status] - statusOrder[b.status] || a.time.localeCompare(b.time)); upcoming.sort((a, b) => a.time.localeCompare(b.time)); return { dueEntries: due, upcomingEntries: upcoming, prnEntries: prn }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [todayMeds, tick]); const [error, setError] = useState(null); const handleTake = async (medId: string, time?: string) => { try { setError(null); await api.medications.take(medId, time); window.location.reload(); } catch (err) { console.error('Failed to log medication:', err); setError(err instanceof Error ? err.message : 'Failed to log medication'); } }; const handleSkip = async (medId: string, time?: string) => { try { setError(null); await api.medications.skip(medId, time); window.location.reload(); } catch (err) { console.error('Failed to skip medication:', err); setError(err instanceof Error ? err.message : 'Failed to skip medication'); } }; const handleDelete = async (medId: string) => { try { await api.medications.delete(medId); setMedications(medications.filter(m => m.id !== medId)); } catch (err) { console.error('Failed to delete medication:', err); } }; const getAdherenceForMed = (medId: string) => { const entry = adherence.find(a => a.medication_id === medId); if (!entry) return { percent: 0, isPrn: false }; return { percent: entry.adherence_percent, isPrn: entry.is_prn || false }; }; if (isLoading) { return (
); } const borderColor = (status: TimeStatus) => { if (status === 'overdue') return 'border-l-4 border-l-red-500'; if (status === 'due_now') return 'border-l-4 border-l-amber-500'; if (status === 'taken') return 'border-l-4 border-l-green-500'; if (status === 'skipped') return 'border-l-4 border-l-gray-400'; return ''; }; return (

Medications

{/* Push Notification Toggle */} {error && (
{error}
)} {/* Due Now Section */} {dueEntries.length > 0 && (

Due

{dueEntries.map((entry) => (

{entry.item.medication.name}

{entry.item.is_previous_day && ( Yesterday )}

{entry.item.medication.dosage} {entry.item.medication.unit}

{entry.time} {entry.status === 'overdue' && ( Overdue )}
{entry.status === 'taken' ? ( Taken ) : entry.status === 'skipped' ? ( Skipped ) : (
)}
))}
)} {/* PRN Section */} {prnEntries.length > 0 && (

As Needed

{prnEntries.map((item) => (

{item.medication.name}

{item.medication.dosage} {item.medication.unit}

As needed
))}
)} {/* Upcoming Section */} {upcomingEntries.length > 0 && (

Upcoming

{upcomingEntries.map((entry) => (

{entry.item.medication.name}

{entry.item.is_next_day && ( Tomorrow )}

{entry.item.medication.dosage} {entry.item.medication.unit}

{entry.time}
))}
)} {/* All Medications */}

All Medications

{medications.length === 0 ? (

No medications yet

Add your medications to track them

Add Medication
) : (
{medications.map((med) => { const { percent: adherencePercent, isPrn } = getAdherenceForMed(med.id); return (

{med.name}

{!med.active && ( Inactive )}

{med.dosage} {med.unit} · {formatSchedule(med)}

{med.times.length > 0 && (

Times: {med.times.join(', ')}

)}
{/* Adherence */}
{isPrn || adherencePercent === null ? ( PRN — no adherence tracking ) : ( <>
30-day adherence = 80 ? 'text-green-600 dark:text-green-400' : adherencePercent >= 50 ? 'text-yellow-600 dark:text-yellow-400' : 'text-red-600 dark:text-red-400'}`}> {adherencePercent}%
= 80 ? 'bg-green-500' : adherencePercent >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`} style={{ width: `${adherencePercent}%` }} />
)}
); })}
)}
); }