feat(medication): improve snooze timer handling

This commit is contained in:
William Valentin
2025-09-23 10:45:18 -07:00
parent 9b4ee116e6
commit 2cb56d5f5f
3 changed files with 153 additions and 18 deletions

73
App.tsx
View File

@@ -65,6 +65,7 @@ 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;
@@ -377,16 +378,35 @@ const MedicationScheduleApp: React.FC<{ user: User }> = ({ user }) => {
async (doseId: string) => {
if (!takenDosesDoc) return;
const newDoses = { ...takenDosesDoc.doses };
if (newDoses[doseId]) {
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]
);
@@ -404,13 +424,13 @@ const MedicationScheduleApp: React.FC<{ user: User }> = ({ user }) => {
}, []);
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;
},
(dose: Dose, doseTime: Date, now: Date): DoseStatus =>
determineDoseStatus({
takenAt: takenDoses[dose.id],
snoozedUntil: snoozedDoses[dose.id],
scheduledTime: doseTime,
now,
}),
[takenDoses, snoozedDoses]
);
@@ -422,15 +442,20 @@ const MedicationScheduleApp: React.FC<{ user: User }> = ({ user }) => {
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: snoozedDoses[item.id]
? new Date(snoozedDoses[item.id])
: undefined,
snoozedUntil: validSnooze,
};
} else {
// It's a Custom Reminder
@@ -469,9 +494,10 @@ const MedicationScheduleApp: React.FC<{ user: User }> = ({ user }) => {
let timeToNotification = -1;
let notificationBody = '';
let notificationTitle = '';
let targetTime: Date | null = null;
if (item.type === 'dose' && item.status === DoseStatus.UPCOMING) {
timeToNotification = item.scheduledTime.getTime() - now.getTime();
targetTime = item.snoozedUntil ?? item.scheduledTime;
notificationTitle = 'Time for your medication!';
notificationBody = `${item.medication.name} (${item.medication.dosage})`;
} else if (
@@ -479,17 +505,21 @@ const MedicationScheduleApp: React.FC<{ user: User }> = ({ user }) => {
item.status === DoseStatus.SNOOZED &&
item.snoozedUntil
) {
timeToNotification = item.snoozedUntil.getTime() - now.getTime();
targetTime = item.snoozedUntil;
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();
targetTime = item.scheduledTime;
notificationTitle = 'Reminder';
notificationBody = item.title;
}
if (targetTime) {
timeToNotification = targetTime.getTime() - now.getTime();
}
if (timeToNotification > 0) {
activeTimers[itemId] = setTimeout(() => {
const timerId = window.setTimeout(() => {
new Notification(notificationTitle, {
body: notificationBody,
tag: itemId,
@@ -497,16 +527,23 @@ const MedicationScheduleApp: React.FC<{ user: User }> = ({ user }) => {
if (item.type === 'dose' && item.status === DoseStatus.SNOOZED) {
setSnoozedDoses(prev => {
const newSnoozed = { ...prev };
delete newSnoozed[itemId];
newSnoozed[itemId] = new Date().toISOString();
return newSnoozed;
});
}
delete activeTimers[itemId];
}, timeToNotification) as unknown as number;
}, timeToNotification);
activeTimers[itemId] = timerId;
}
});
return () => Object.values(activeTimers).forEach(clearTimeout);
return () => {
Object.entries(activeTimers).forEach(([id, timer]) => {
clearTimeout(timer);
delete activeTimers[id];
});
};
}, [scheduleWithStatus, settings?.notificationsEnabled]);
const filteredSchedule = useMemo(

View File

@@ -0,0 +1,63 @@
import { determineDoseStatus } from '../doseStatus';
import { DoseStatus } from '../../types';
describe('determineDoseStatus', () => {
const scheduledTime = new Date('2024-05-10T08:00:00.000Z');
const now = new Date('2024-05-10T07:00:00.000Z');
it('returns TAKEN when dose has been recorded as taken', () => {
const status = determineDoseStatus({
takenAt: new Date().toISOString(),
snoozedUntil: undefined,
scheduledTime,
now,
});
expect(status).toBe(DoseStatus.TAKEN);
});
it('returns SNOOZED when snooze time is in the future', () => {
const status = determineDoseStatus({
takenAt: undefined,
snoozedUntil: new Date(now.getTime() + 5 * 60 * 1000).toISOString(),
scheduledTime,
now,
});
expect(status).toBe(DoseStatus.SNOOZED);
});
it('returns UPCOMING when snooze time has expired', () => {
const pastSnooze = new Date(now.getTime() - 5 * 60 * 1000).toISOString();
const status = determineDoseStatus({
takenAt: undefined,
snoozedUntil: pastSnooze,
scheduledTime: new Date(now.getTime() - 60 * 60 * 1000),
now,
});
expect(status).toBe(DoseStatus.UPCOMING);
});
it('returns MISSED when scheduled time is in the past without snooze', () => {
const status = determineDoseStatus({
takenAt: undefined,
snoozedUntil: undefined,
scheduledTime: new Date(now.getTime() - 60 * 60 * 1000),
now,
});
expect(status).toBe(DoseStatus.MISSED);
});
it('returns UPCOMING when scheduled time is in the future without snooze', () => {
const status = determineDoseStatus({
takenAt: undefined,
snoozedUntil: undefined,
scheduledTime: new Date(now.getTime() + 60 * 60 * 1000),
now,
});
expect(status).toBe(DoseStatus.UPCOMING);
});
});

35
utils/doseStatus.ts Normal file
View File

@@ -0,0 +1,35 @@
import { DoseStatus } from '../types';
export interface DoseStatusParams {
takenAt?: string;
snoozedUntil?: string;
scheduledTime: Date;
now: Date;
}
export const determineDoseStatus = ({
takenAt,
snoozedUntil,
scheduledTime,
now,
}: DoseStatusParams): DoseStatus => {
if (takenAt) {
return DoseStatus.TAKEN;
}
if (snoozedUntil) {
const snoozeTime = new Date(snoozedUntil);
if (!Number.isNaN(snoozeTime.getTime())) {
if (snoozeTime.getTime() > now.getTime()) {
return DoseStatus.SNOOZED;
}
return DoseStatus.UPCOMING;
}
}
if (scheduledTime.getTime() < now.getTime()) {
return DoseStatus.MISSED;
}
return DoseStatus.UPCOMING;
};