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,
}) => (
);
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 (
);
};
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;