Files
rxminder/App.tsx
2025-09-23 10:45:18 -07:00

995 lines
32 KiB
TypeScript

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,
ResetPasswordPage,
} 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';
import { logger } from './services/logging';
import { normalizeError } from './utils/error';
import { determineDoseStatus } from './utils/doseStatus';
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,
}) => (
<header className='bg-white dark:bg-slate-800 shadow-md sticky top-0 z-20 border-b border-slate-200 dark:border-slate-700'>
<div className='container mx-auto px-4 py-3 flex justify-between items-center'>
<div className='flex items-center space-x-3'>
<div className='bg-indigo-600 p-2 rounded-lg'>
<PillIcon className='w-6 h-6 text-white' />
</div>
<h1 className='text-xl md:text-2xl font-bold text-slate-800 dark:text-slate-100'>
Medication Reminder
</h1>
</div>
<div className='flex items-center space-x-2'>
<button
onClick={onStats}
className='hidden sm:flex items-center space-x-2 px-4 py-2 text-sm font-medium text-slate-700 bg-slate-100 rounded-lg hover:bg-slate-200 transition-colors dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600'
>
<BarChartIcon className='w-4 h-4' aria-hidden='true' />
<span>Stats</span>
</button>
<button
onClick={onHistory}
className='hidden sm:flex items-center space-x-2 px-4 py-2 text-sm font-medium text-slate-700 bg-slate-100 rounded-lg hover:bg-slate-200 transition-colors dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600'
>
<HistoryIcon className='w-4 h-4' aria-hidden='true' />
<span>History</span>
</button>
<button
onClick={onManage}
className='hidden sm:flex items-center space-x-2 px-4 py-2 text-sm font-medium text-slate-700 bg-slate-100 rounded-lg hover:bg-slate-200 transition-colors dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600'
>
<MenuIcon className='w-4 h-4' aria-hidden='true' />
<span>{getAppConfig().name}</span>
</button>
<button
onClick={onManageReminders}
className='flex items-center space-x-2 px-4 py-2 text-sm font-medium text-slate-700 bg-slate-100 rounded-lg hover:bg-slate-200 transition-colors dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600'
>
<BellIcon className='w-4 h-4' aria-hidden='true' />
<span className='hidden sm:inline'>Reminders</span>
</button>
<button
onClick={onAdd}
className='flex items-center space-x-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-slate-900'
>
<PlusIcon className='w-4 h-4' aria-hidden='true' />
<span className='hidden sm:inline'>Add Med</span>
</button>
<div className='border-l border-slate-200 dark:border-slate-600 h-8 mx-2'></div>
<ThemeSwitcher />
<button
onClick={onAccount}
className='flex items-center justify-center w-10 h-10 rounded-lg bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 transition-colors'
aria-label='Account settings'
>
<SettingsIcon className='w-5 h-5 text-slate-700 dark:text-slate-200' />
</button>
<div className='border-l border-slate-200 dark:border-slate-600 h-8 ml-2'></div>
<AvatarDropdown
user={user}
onLogout={onLogout}
onAdmin={onAdmin}
onChangePassword={onChangePassword}
/>
</div>
</div>
</header>
);
const EmptyState: React.FC<{ onAdd: () => void }> = ({ onAdd }) => (
<div className='text-center py-20 px-4'>
<div className='mx-auto bg-slate-200 dark:bg-slate-700 rounded-full h-16 w-16 flex items-center justify-center animate-float'>
<PillIcon className='w-8 h-8 text-slate-500 dark:text-slate-400' />
</div>
<h3 className='mt-4 text-lg font-semibold text-slate-800 dark:text-slate-100'>
No Medications Scheduled
</h3>
<p className='mt-1 text-slate-500 dark:text-slate-400'>
Get started by adding your first medication.
</p>
<div className='mt-6'>
<button
type='button'
onClick={onAdd}
className='inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-slate-900'
>
<PlusIcon className='-ml-1 mr-2 h-5 w-5' />
Add Medication
</button>
</div>
</div>
);
const groupDetails: {
[key: string]: {
icon: React.FC<React.SVGProps<SVGSVGElement>>;
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<Medication[]>([]);
const [customReminders, setCustomReminders] = useState<CustomReminder[]>([]);
const [takenDosesDoc, setTakenDosesDoc] = useState<TakenDoses | null>(null);
const [settings, setSettings] = useState<UserSettings | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(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<Medication | null>(
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<CustomReminder | null>(
null
);
const [snoozedDoses, setSnoozedDoses] = useState<Record<string, string>>({});
const notificationTimers = useRef<Record<string, number>>({});
const takenDoses = useMemo(() => takenDosesDoc?.doses ?? {}, [takenDosesDoc]);
useEffect(() => {
// Don't try to fetch data if user._id is not available
if (!user._id) {
logger.ui.action('Skipping data fetch because user id is missing', {
userId: user._id,
});
return;
}
const fetchData = async () => {
try {
setIsLoading(true);
setError(null);
logger.db.query('Fetching medication data for user', {
userId: 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),
]);
logger.db.query('Fetched user data successfully', {
userId: user._id,
medicationCount: medsData.length,
reminderCount: 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.');
logger.db.error('Error loading user data', normalizeError(e), {
userId: user._id,
});
} 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<Medication, '_id' | '_rev'>) => {
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<CustomReminder, '_id' | '_rev'>
) => {
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 };
const wasTaken = Boolean(newDoses[doseId]);
if (wasTaken) {
delete newDoses[doseId];
} else {
newDoses[doseId] = new Date().toISOString();
}
const updatedDoc = await databaseService.updateTakenDoses({
...takenDosesDoc,
doses: newDoses,
});
setTakenDosesDoc(updatedDoc);
if (!wasTaken) {
setSnoozedDoses(prev => {
if (!prev[doseId]) {
return prev;
}
const updated = { ...prev };
delete updated[doseId];
return updated;
});
if (notificationTimers.current[doseId]) {
clearTimeout(notificationTimers.current[doseId]);
delete notificationTimers.current[doseId];
}
}
},
[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 =>
determineDoseStatus({
takenAt: takenDoses[dose.id],
snoozedUntil: snoozedDoses[dose.id],
scheduledTime: doseTime,
now,
}),
[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;
const snoozeString = snoozedDoses[item.id];
const snoozeDate = snoozeString ? new Date(snoozeString) : undefined;
const validSnooze =
snoozeDate && !Number.isNaN(snoozeDate.getTime())
? snoozeDate
: undefined;
return {
...item,
type: 'dose' as const,
medication,
status: getDoseStatus(item, item.scheduledTime, currentTime),
takenAt: takenDoses[item.id],
snoozedUntil: validSnooze,
};
} else {
// It's a Custom Reminder
return {
...item,
type: 'reminder' as const,
};
}
})
.filter((d): d is NonNullable<typeof d> => 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 = '';
let targetTime: Date | null = null;
if (item.type === 'dose' && item.status === DoseStatus.UPCOMING) {
targetTime = item.snoozedUntil ?? item.scheduledTime;
notificationTitle = 'Time for your medication!';
notificationBody = `${item.medication.name} (${item.medication.dosage})`;
} else if (
item.type === 'dose' &&
item.status === DoseStatus.SNOOZED &&
item.snoozedUntil
) {
targetTime = item.snoozedUntil;
notificationTitle = 'Snoozed Medication Reminder';
notificationBody = `${item.medication.name} (${item.medication.dosage})`;
} else if (item.type === 'reminder' && item.scheduledTime > now) {
targetTime = item.scheduledTime;
notificationTitle = 'Reminder';
notificationBody = item.title;
}
if (targetTime) {
timeToNotification = targetTime.getTime() - now.getTime();
}
if (timeToNotification > 0) {
const timerId = window.setTimeout(() => {
new Notification(notificationTitle, {
body: notificationBody,
tag: itemId,
});
if (item.type === 'dose' && item.status === DoseStatus.SNOOZED) {
setSnoozedDoses(prev => {
const newSnoozed = { ...prev };
newSnoozed[itemId] = new Date().toISOString();
return newSnoozed;
});
}
delete activeTimers[itemId];
}, timeToNotification);
activeTimers[itemId] = timerId;
}
});
return () => {
Object.entries(activeTimers).forEach(([id, timer]) => {
clearTimeout(timer);
delete activeTimers[id];
});
};
}, [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<typeof d> => 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) {
logger.ui.error(
'Failed to update onboarding status',
normalizeError(error),
{ userId: user._id }
);
setOnboardingOpen(false);
}
}
};
if (isLoading) {
return (
<div className='min-h-screen flex items-center justify-center'>
<PillIcon className='w-12 h-12 text-indigo-500 animate-spin' />
</div>
);
}
if (error) {
return (
<div className='min-h-screen flex items-center justify-center text-red-500'>
{error}
</div>
);
}
return (
<div className='min-h-screen text-slate-800 dark:text-slate-200'>
<Header
onAdd={() => 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}
/>
<main className='container mx-auto p-4 md:p-6'>
<div className='flex items-center justify-between mb-6'>
<h2 className='text-2xl font-bold dark:text-slate-100'>
Today's Schedule
</h2>
<time className='font-medium text-slate-500 dark:text-slate-400'>
{currentTime.toLocaleDateString(undefined, {
weekday: 'long',
month: 'long',
day: 'numeric',
})}
</time>
</div>
{medications.length > 0 || customReminders.length > 0 ? (
<>
<div className='relative mb-6'>
<div className='pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3'>
<SearchIcon
className='h-5 w-5 text-slate-400'
aria-hidden='true'
/>
</div>
<input
type='search'
name='search'
id='search'
className='block w-full pl-10 pr-4 py-2 border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm bg-white dark:bg-slate-700 dark:border-slate-600 dark:placeholder-slate-400 dark:text-white'
placeholder='Search schedule...'
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
</div>
{filteredSchedule.length > 0 ? (
<div className='space-y-8'>
{Object.entries(groupedSchedule).map(([groupName, items]) => {
const scheduleItems = items as typeof filteredSchedule;
if (scheduleItems.length === 0) return null;
const Icon = groupDetails[groupName]?.icon;
return (
<section key={groupName}>
<div className='flex items-center space-x-3 mb-3 pb-2 border-b border-slate-200 dark:border-slate-700'>
{Icon && (
<Icon
className={`w-6 h-6 ${groupDetails[groupName].iconClass}`}
aria-hidden='true'
/>
)}
<h3 className='text-lg font-semibold text-slate-600 dark:text-slate-300'>
{groupName}
</h3>
</div>
<ul className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'>
{scheduleItems.map(item =>
item.type === 'dose' ? (
<DoseCard
key={item.id}
dose={item}
medication={item.medication}
status={item.status}
onToggleDose={handleToggleDose}
onSnooze={handleSnoozeDose}
snoozedUntil={item.snoozedUntil}
/>
) : (
<ReminderCard key={item.id} reminder={item} />
)
)}
</ul>
</section>
);
})}
</div>
) : (
<div className='text-center py-10 px-4'>
<SearchIcon className='mx-auto h-12 w-12 text-slate-400' />
<h3 className='mt-2 text-sm font-semibold text-slate-900 dark:text-slate-100'>
No items found
</h3>
<p className='mt-1 text-sm text-slate-500 dark:text-slate-400'>
Your search for "{searchQuery}" did not match any items
scheduled for today.
</p>
</div>
)}
</>
) : (
<EmptyState onAdd={() => setAddModalOpen(true)} />
)}
</main>
<AddMedicationModal
isOpen={isAddModalOpen}
onClose={() => setAddModalOpen(false)}
onAdd={handleAddMedication}
/>
<ManageMedicationsModal
isOpen={isManageModalOpen}
onClose={() => setManageModalOpen(false)}
medications={medications}
onDelete={handleDeleteMedication}
onEdit={handleOpenEditModal}
/>
<EditMedicationModal
isOpen={editingMedication !== null}
onClose={() => setEditingMedication(null)}
medication={editingMedication}
onUpdate={handleUpdateMedication}
/>
<HistoryModal
isOpen={isHistoryModalOpen}
onClose={() => setHistoryModalOpen(false)}
history={medicationHistory}
/>
<StatsModal
isOpen={isStatsModalOpen}
onClose={() => setStatsModalOpen(false)}
dailyStats={dailyStats}
medicationStats={medicationStats}
/>
{settings && (
<AccountModal
isOpen={isAccountModalOpen}
onClose={() => setAccountModalOpen(false)}
user={user}
settings={settings}
onUpdateUser={updateUser}
onUpdateSettings={handleUpdateSettings}
onDeleteAllData={handleDeleteAllData}
/>
)}
<OnboardingModal
isOpen={isOnboardingOpen}
onComplete={handleCompleteOnboarding}
/>
<ManageRemindersModal
isOpen={isManageRemindersOpen}
onClose={() => setManageRemindersOpen(false)}
reminders={customReminders}
onAdd={() => {
setManageRemindersOpen(false);
setAddReminderOpen(true);
}}
onEdit={handleOpenEditReminderModal}
onDelete={handleDeleteReminder}
/>
<AddReminderModal
isOpen={isAddReminderOpen}
onClose={() => setAddReminderOpen(false)}
onAdd={handleAddReminder}
/>
<EditReminderModal
isOpen={editingReminder !== null}
onClose={() => setEditingReminder(null)}
reminder={editingReminder}
onUpdate={handleUpdateReminder}
/>
{/* Admin Interface - Only shown when opened */}
{isAdminInterfaceOpen && (
<AdminInterface onClose={() => setAdminInterfaceOpen(false)} />
)}
{/* Password Change Modal - Only shown when opened */}
{isChangePasswordOpen && (
<ChangePasswordModal
onClose={() => setChangePasswordOpen(false)}
onSuccess={() => {
alert('Password changed successfully!');
setChangePasswordOpen(false);
}}
/>
)}
</div>
);
};
const App: React.FC = () => {
const { user, isLoading } = useUser();
// Run database seeding on app startup
useEffect(() => {
const runSeeding = async () => {
try {
logger.db.query('Initializing database seeding');
await databaseSeeder.seedDatabase();
} catch (error) {
logger.db.error('Database seeding failed', normalizeError(error));
}
};
runSeeding();
}, []);
if (isLoading) {
return (
<div className='min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900'>
<PillIcon className='w-12 h-12 text-indigo-500 animate-spin' />
</div>
);
}
if (!user) {
if (
typeof window !== 'undefined' &&
window.location.pathname === '/reset-password'
) {
return <ResetPasswordPage />;
}
return <AuthPage />;
}
return <MedicationScheduleApp user={user} />;
};
export default App;