import React, { useState, useEffect, useMemo, useCallback, useRef, } from 'react'; import { generateSchedule, generateReminderSchedule } from './utils/schedule'; import { getAppConfig } from './utils/env'; import { Medication, Dose, DoseStatus, HistoricalDose, User, UserSettings, TakenDoses, CustomReminder, ScheduleItem, DailyStat, MedicationStat, } from './types'; // Component imports - organized by feature import { AddMedicationModal, EditMedicationModal, ManageMedicationsModal, DoseCard, } from './components/medication'; import { AuthPage, AvatarDropdown, ChangePasswordModal, } from './components/auth'; import { AdminInterface } from './components/admin'; import { AccountModal, AddReminderModal, EditReminderModal, HistoryModal, ManageRemindersModal, OnboardingModal, StatsModal, } from './components/modals'; import { ReminderCard, ThemeSwitcher } from './components/ui'; // Icon and utility imports import { PillIcon, PlusIcon, MenuIcon, HistoryIcon, SunIcon, SunsetIcon, MoonIcon, SearchIcon, SettingsIcon, BellIcon, BarChartIcon, } from './components/icons/Icons'; import { useUser } from './contexts/UserContext'; import { databaseService } from './services/database'; import { databaseSeeder } from './services/database.seeder'; const Header: React.FC<{ onAdd: () => void; onManage: () => void; onManageReminders: () => void; onHistory: () => void; onStats: () => void; onAccount: () => void; onAdmin: () => void; onChangePassword: () => void; user: User; onLogout: () => void; }> = ({ onAdd, onManage, onManageReminders, onHistory, onStats, onAccount, onAdmin, onChangePassword, user, onLogout, }) => (

Medication Reminder

); const EmptyState: React.FC<{ onAdd: () => void }> = ({ onAdd }) => (

No Medications Scheduled

Get started by adding your first medication.

); const groupDetails: { [key: string]: { icon: React.FC>; iconClass: string; }; } = { Morning: { icon: SunIcon, iconClass: 'text-amber-500' }, Afternoon: { icon: SunIcon, iconClass: 'text-sky-500' }, Evening: { icon: SunsetIcon, iconClass: 'text-indigo-500 dark:text-indigo-400', }, Night: { icon: MoonIcon, iconClass: 'text-slate-500 dark:text-slate-400' }, }; const MedicationScheduleApp: React.FC<{ user: User }> = ({ user }) => { const { logout, updateUser } = useUser(); const [medications, setMedications] = useState([]); const [customReminders, setCustomReminders] = useState([]); const [takenDosesDoc, setTakenDosesDoc] = useState(null); const [settings, setSettings] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [currentTime, setCurrentTime] = useState(() => new Date()); const [isAddModalOpen, setAddModalOpen] = useState(false); const [isManageModalOpen, setManageModalOpen] = useState(false); const [isHistoryModalOpen, setHistoryModalOpen] = useState(false); const [isAccountModalOpen, setAccountModalOpen] = useState(false); const [isStatsModalOpen, setStatsModalOpen] = useState(false); const [editingMedication, setEditingMedication] = useState( null ); const [searchQuery, setSearchQuery] = useState(''); const [isOnboardingOpen, setOnboardingOpen] = useState(false); const [isAdminInterfaceOpen, setAdminInterfaceOpen] = useState(false); const [isChangePasswordOpen, setChangePasswordOpen] = useState(false); const [isManageRemindersOpen, setManageRemindersOpen] = useState(false); const [isAddReminderOpen, setAddReminderOpen] = useState(false); const [editingReminder, setEditingReminder] = useState( null ); const [snoozedDoses, setSnoozedDoses] = useState>({}); const notificationTimers = useRef>({}); const takenDoses = useMemo(() => takenDosesDoc?.doses ?? {}, [takenDosesDoc]); useEffect(() => { // Don't try to fetch data if user._id is not available if (!user._id) { console.warn('Skipping data fetch: user._id is not available'); return; } const fetchData = async () => { try { setIsLoading(true); setError(null); console.warn('Fetching data for user:', user._id); const [medsData, remindersData, takenDosesData, settingsData] = await Promise.all([ databaseService.getMedications(user._id), databaseService.getCustomReminders(user._id), databaseService.getTakenDoses(user._id), databaseService.getUserSettings(user._id), ]); console.warn('Data fetched successfully:', { medications: medsData.length, reminders: remindersData.length, hasTakenDoses: !!takenDosesData, hasSettings: !!settingsData, }); setMedications(medsData); setCustomReminders(remindersData); setTakenDosesDoc(takenDosesData); setSettings(settingsData); if (!settingsData.hasCompletedOnboarding) { setOnboardingOpen(true); } } catch (e) { setError('Failed to load your data. Please try again.'); console.error('Error loading user data:', e); console.error('User object:', user); } finally { setIsLoading(false); } }; // Add a small delay to ensure user state is fully settled const timeoutId = setTimeout(() => { fetchData(); }, 100); return () => clearTimeout(timeoutId); }, [user._id, user]); useEffect(() => { if ( settings?.notificationsEnabled && 'Notification' in window && Notification.permission === 'default' ) { Notification.requestPermission(); } }, [settings?.notificationsEnabled]); useEffect(() => { const timer = setInterval(() => setCurrentTime(new Date()), 60000); return () => clearInterval(timer); }, []); const unifiedSchedule = useMemo(() => { const medSchedule = generateSchedule(medications, currentTime); const reminderSchedule = generateReminderSchedule( customReminders, currentTime ); const combined = [...medSchedule, ...reminderSchedule] as ScheduleItem[]; return combined.sort( (a, b) => a.scheduledTime.getTime() - b.scheduledTime.getTime() ); }, [medications, customReminders, currentTime]); const handleAddMedication = async (med: Omit) => { const newMed = await databaseService.createMedication(user._id, med); setMedications(prev => [...prev, newMed]); setAddModalOpen(false); }; const handleDeleteMedication = async (medToDelete: Medication) => { await databaseService.deleteMedication(medToDelete._id); setMedications(meds => meds.filter(med => med._id !== medToDelete._id)); }; const handleUpdateMedication = async (updatedMed: Medication) => { const savedMed = await databaseService.updateMedication(updatedMed); setMedications(meds => meds.map(m => (m._id === savedMed._id ? savedMed : m)) ); setEditingMedication(null); }; const handleAddReminder = async ( reminder: Omit ) => { const newReminder = await databaseService.createCustomReminder( user._id, reminder ); setCustomReminders(prev => [...prev, newReminder]); setAddReminderOpen(false); }; const handleUpdateReminder = async (updatedReminder: CustomReminder) => { const savedReminder = await databaseService.updateCustomReminder(updatedReminder); setCustomReminders(reminders => reminders.map(r => (r._id === savedReminder._id ? savedReminder : r)) ); setEditingReminder(null); }; const handleDeleteReminder = async (reminderToDelete: CustomReminder) => { await databaseService.deleteCustomReminder(reminderToDelete._id); setCustomReminders(reminders => reminders.filter(r => r._id !== reminderToDelete._id) ); }; const handleOpenEditModal = (med: Medication) => { setEditingMedication(med); setManageModalOpen(false); }; const handleOpenEditReminderModal = (reminder: CustomReminder) => { setEditingReminder(reminder); setManageRemindersOpen(false); }; const handleToggleDose = useCallback( async (doseId: string) => { if (!takenDosesDoc) return; const newDoses = { ...takenDosesDoc.doses }; if (newDoses[doseId]) { delete newDoses[doseId]; } else { newDoses[doseId] = new Date().toISOString(); } const updatedDoc = await databaseService.updateTakenDoses({ ...takenDosesDoc, doses: newDoses, }); setTakenDosesDoc(updatedDoc); }, [takenDosesDoc] ); const handleSnoozeDose = useCallback((doseId: string) => { const SNOOZE_DURATION = 5 * 60 * 1000; // 5 minutes const snoozedUntil = new Date(Date.now() + SNOOZE_DURATION).toISOString(); setSnoozedDoses(prev => ({ ...prev, [doseId]: snoozedUntil })); // Clear existing timer and set a new one if (notificationTimers.current[doseId]) { clearTimeout(notificationTimers.current[doseId]); delete notificationTimers.current[doseId]; } }, []); const getDoseStatus = useCallback( (dose: Dose, doseTime: Date, now: Date): DoseStatus => { if (takenDoses[dose.id]) return DoseStatus.TAKEN; if (snoozedDoses[dose.id] && new Date(snoozedDoses[dose.id]) > now) return DoseStatus.SNOOZED; if (doseTime.getTime() < now.getTime()) return DoseStatus.MISSED; return DoseStatus.UPCOMING; }, [takenDoses, snoozedDoses] ); const scheduleWithStatus = useMemo(() => { return unifiedSchedule .map(item => { if ('medicationId' in item) { // It's a Dose const medication = medications.find(m => m._id === item.medicationId); if (!medication) return null; return { ...item, type: 'dose' as const, medication, status: getDoseStatus(item, item.scheduledTime, currentTime), takenAt: takenDoses[item.id], snoozedUntil: snoozedDoses[item.id] ? new Date(snoozedDoses[item.id]) : undefined, }; } else { // It's a Custom Reminder return { ...item, type: 'reminder' as const, }; } }) .filter((d): d is NonNullable => d !== null); }, [ unifiedSchedule, medications, getDoseStatus, currentTime, takenDoses, snoozedDoses, ]); useEffect(() => { if ( !settings?.notificationsEnabled || !('Notification' in window) || Notification.permission !== 'granted' ) { return; } const now = new Date(); const activeTimers = notificationTimers.current; scheduleWithStatus.forEach(item => { const itemId = item.id; if (activeTimers[itemId]) return; // Timer already set let timeToNotification = -1; let notificationBody = ''; let notificationTitle = ''; if (item.type === 'dose' && item.status === DoseStatus.UPCOMING) { timeToNotification = item.scheduledTime.getTime() - now.getTime(); notificationTitle = 'Time for your medication!'; notificationBody = `${item.medication.name} (${item.medication.dosage})`; } else if ( item.type === 'dose' && item.status === DoseStatus.SNOOZED && item.snoozedUntil ) { timeToNotification = item.snoozedUntil.getTime() - now.getTime(); notificationTitle = 'Snoozed Medication Reminder'; notificationBody = `${item.medication.name} (${item.medication.dosage})`; } else if (item.type === 'reminder' && item.scheduledTime > now) { timeToNotification = item.scheduledTime.getTime() - now.getTime(); notificationTitle = 'Reminder'; notificationBody = item.title; } if (timeToNotification > 0) { activeTimers[itemId] = setTimeout(() => { new Notification(notificationTitle, { body: notificationBody, tag: itemId, }); if (item.type === 'dose' && item.status === DoseStatus.SNOOZED) { setSnoozedDoses(prev => { const newSnoozed = { ...prev }; delete newSnoozed[itemId]; return newSnoozed; }); } delete activeTimers[itemId]; }, timeToNotification) as unknown as number; } }); return () => Object.values(activeTimers).forEach(clearTimeout); }, [scheduleWithStatus, settings?.notificationsEnabled]); const filteredSchedule = useMemo( () => scheduleWithStatus.filter(item => { if (item.type === 'reminder') { return item.title.toLowerCase().includes(searchQuery.toLowerCase()); } return item.medication.name .toLowerCase() .includes(searchQuery.toLowerCase()); }), [scheduleWithStatus, searchQuery] ); const groupedSchedule = useMemo(() => { const groups: { [key: string]: typeof filteredSchedule } = { Morning: [], Afternoon: [], Evening: [], Night: [], }; filteredSchedule.forEach(item => { const hour = item.scheduledTime.getHours(); if (hour >= 5 && hour < 12) groups['Morning'].push(item); else if (hour >= 12 && hour < 17) groups['Afternoon'].push(item); else if (hour >= 17 && hour < 21) groups['Evening'].push(item); else groups['Night'].push(item); }); return groups; }, [filteredSchedule]); const medicationHistory = useMemo(() => { const history: { date: string; doses: HistoricalDose[] }[] = []; const today = new Date(); today.setHours(0, 0, 0, 0); const now = new Date(); for (let i = 0; i < 7; i++) { const date = new Date(today); date.setDate(date.getDate() - i); const daySchedule = generateSchedule(medications, date); if (daySchedule.length === 0 || date.getTime() > now.getTime()) continue; const dosesForDay: HistoricalDose[] = daySchedule .map(dose => { const medication = medications.find(m => m._id === dose.medicationId); return medication ? { id: dose.id, medication, scheduledTime: dose.scheduledTime, status: getDoseStatus(dose, dose.scheduledTime, now), takenAt: takenDoses[dose.id], } : null; }) .filter((d): d is NonNullable => d !== null); if (dosesForDay.length > 0) history.push({ date: date.toISOString().split('T')[0], doses: dosesForDay, }); } return history; }, [medications, takenDoses, getDoseStatus]); const { dailyStats, medicationStats } = useMemo(() => { const today = new Date(); today.setHours(23, 59, 59, 999); const now = new Date(); const daily: DailyStat[] = []; for (let i = 6; i >= 0; i--) { const date = new Date(); date.setHours(0, 0, 0, 0); date.setDate(date.getDate() - i); const daySchedule = generateSchedule(medications, date); const pastDoses = daySchedule.filter(d => d.scheduledTime < now); if (pastDoses.length === 0) { daily.push({ date: date.toISOString().split('T')[0], adherence: 100 }); continue; } let takenCount = 0; pastDoses.forEach(dose => { if (takenDoses[dose.id]) { takenCount++; } }); const adherence = (takenCount / pastDoses.length) * 100; daily.push({ date: date.toISOString().split('T')[0], adherence: Math.round(adherence), }); } const statsByMedId: Record< string, { taken: number; missed: number; upcoming: number; medication: Medication; lastTakenAt?: string; } > = {}; medications.forEach(med => { statsByMedId[med._id] = { taken: 0, missed: 0, upcoming: 0, medication: med, }; }); const sevenDaysAgo = new Date(); sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); medications.forEach(med => { for (let i = 0; i < 7; i++) { const date = new Date(); date.setDate(date.getDate() - i); const daySchedule = generateSchedule([med], date); daySchedule.forEach(dose => { const stat = statsByMedId[dose.medicationId]; if (stat) { const status = getDoseStatus(dose, dose.scheduledTime, now); if (status === DoseStatus.TAKEN) { stat.taken++; const takenAt = takenDoses[dose.id]; if ( takenAt && (!stat.lastTakenAt || new Date(takenAt) > new Date(stat.lastTakenAt)) ) { stat.lastTakenAt = takenAt; } } else if (status === DoseStatus.MISSED) stat.missed++; else if (status === DoseStatus.UPCOMING) stat.upcoming++; } }); } }); const medication: MedicationStat[] = Object.values(statsByMedId) .map(stat => { const totalPast = stat.taken + stat.missed; const adherence = totalPast > 0 ? Math.round((stat.taken / totalPast) * 100) : 100; return { ...stat, adherence }; }) .sort((a, b) => a.medication.name.localeCompare(b.medication.name)); return { dailyStats: daily, medicationStats: medication }; }, [medications, takenDoses, getDoseStatus]); const handleUpdateSettings = async (newSettings: UserSettings) => { const updatedSettings = await databaseService.updateUserSettings(newSettings); setSettings(updatedSettings); }; const handleDeleteAllData = async () => { if ( window.confirm( 'Are you sure you want to delete all your medication data? This action cannot be undone.' ) ) { await databaseService.deleteAllUserData(user._id); setMedications([]); setCustomReminders([]); const updatedTakenDoses = await databaseService.getTakenDoses(user._id); setTakenDosesDoc(updatedTakenDoses); setAccountModalOpen(false); } }; const handleCompleteOnboarding = async () => { if (settings) { try { const updatedSettings = await databaseService.updateUserSettings({ ...settings, hasCompletedOnboarding: true, }); setSettings(updatedSettings); setOnboardingOpen(false); } catch (error) { console.error('Failed to update onboarding status', error); setOnboardingOpen(false); } } }; if (isLoading) { return (
); } if (error) { return (
{error}
); } return (
setAddModalOpen(true)} onManage={() => setManageModalOpen(true)} onManageReminders={() => setManageRemindersOpen(true)} onHistory={() => setHistoryModalOpen(true)} onStats={() => setStatsModalOpen(true)} onAccount={() => setAccountModalOpen(true)} onAdmin={() => setAdminInterfaceOpen(true)} onChangePassword={() => setChangePasswordOpen(true)} user={user} onLogout={logout} />

Today's Schedule

{medications.length > 0 || customReminders.length > 0 ? ( <>
setSearchQuery(e.target.value)} />
{filteredSchedule.length > 0 ? (
{Object.entries(groupedSchedule).map(([groupName, items]) => { const scheduleItems = items as typeof filteredSchedule; if (scheduleItems.length === 0) return null; const Icon = groupDetails[groupName]?.icon; return (
{Icon && (
    {scheduleItems.map(item => item.type === 'dose' ? ( ) : ( ) )}
); })}
) : (

No items found

Your search for "{searchQuery}" did not match any items scheduled for today.

)} ) : ( setAddModalOpen(true)} /> )}
setAddModalOpen(false)} onAdd={handleAddMedication} /> setManageModalOpen(false)} medications={medications} onDelete={handleDeleteMedication} onEdit={handleOpenEditModal} /> setEditingMedication(null)} medication={editingMedication} onUpdate={handleUpdateMedication} /> setHistoryModalOpen(false)} history={medicationHistory} /> setStatsModalOpen(false)} dailyStats={dailyStats} medicationStats={medicationStats} /> {settings && ( setAccountModalOpen(false)} user={user} settings={settings} onUpdateUser={updateUser} onUpdateSettings={handleUpdateSettings} onDeleteAllData={handleDeleteAllData} /> )} setManageRemindersOpen(false)} reminders={customReminders} onAdd={() => { setManageRemindersOpen(false); setAddReminderOpen(true); }} onEdit={handleOpenEditReminderModal} onDelete={handleDeleteReminder} /> setAddReminderOpen(false)} onAdd={handleAddReminder} /> setEditingReminder(null)} reminder={editingReminder} onUpdate={handleUpdateReminder} /> {/* Admin Interface - Only shown when opened */} {isAdminInterfaceOpen && ( setAdminInterfaceOpen(false)} /> )} {/* Password Change Modal - Only shown when opened */} {isChangePasswordOpen && ( setChangePasswordOpen(false)} onSuccess={() => { alert('Password changed successfully!'); setChangePasswordOpen(false); }} /> )}
); }; const App: React.FC = () => { const { user, isLoading } = useUser(); // Run database seeding on app startup useEffect(() => { const runSeeding = async () => { try { console.warn('🌱 Initializing database seeding...'); await databaseSeeder.seedDatabase(); } catch (error) { console.error('❌ Database seeding failed:', error); } }; runSeeding(); }, []); if (isLoading) { return (
); } if (!user) { return ; } return ; }; export default App;