diff --git a/src/components/JournalPage.js b/src/components/JournalPage.js index 2a28244..75c5833 100644 --- a/src/components/JournalPage.js +++ b/src/components/JournalPage.js @@ -95,6 +95,60 @@ const JournalPage = ({ authorizedFetch, stores }) => { return parsed.getTime(); }, []); + const getIntervalMonths = useCallback((interval) => { + if (interval === 'monthly') { + return 1; + } + if (interval === 'quarterly') { + return 3; + } + return 12; + }, []); + + const addMonths = useCallback((date, months) => { + const copy = new Date(date.getTime()); + copy.setMonth(copy.getMonth() + months); + return copy; + }, []); + + const startOfDay = useCallback((date) => { + return new Date(date.getFullYear(), date.getMonth(), date.getDate()); + }, []); + + const getNextReminderTimestamp = useCallback( + (entry) => { + if (!entry?.reminder?.enabled || !entry.pickupDate) { + return null; + } + const baseDate = new Date(`${entry.pickupDate}T00:00:00`); + if (Number.isNaN(baseDate.getTime())) { + return null; + } + const intervalMonths = getIntervalMonths(entry.reminder.interval); + const daysBefore = Number.isFinite(entry.reminder.daysBefore) + ? Math.max(0, entry.reminder.daysBefore) + : 42; + const todayStart = startOfDay(new Date()); + let occurrence = startOfDay(baseDate); + const guardYear = todayStart.getFullYear() + 200; + while (occurrence < todayStart && occurrence.getFullYear() < guardYear) { + occurrence = startOfDay(addMonths(occurrence, intervalMonths)); + } + if (occurrence.getFullYear() >= guardYear) { + return null; + } + let reminderDate = new Date(occurrence.getTime()); + reminderDate.setDate(reminderDate.getDate() - daysBefore); + if (reminderDate < todayStart) { + occurrence = startOfDay(addMonths(occurrence, intervalMonths)); + reminderDate = new Date(occurrence.getTime()); + reminderDate.setDate(reminderDate.getDate() - daysBefore); + } + return reminderDate.getTime(); + }, + [addMonths, getIntervalMonths, startOfDay] + ); + const filteredEntries = useMemo(() => { const query = filters.query.trim().toLowerCase(); const fromValue = parseDateValue(filters.dateFrom); @@ -142,11 +196,25 @@ const JournalPage = ({ authorizedFetch, stores }) => { const aLabel = a.storeName || storeLabelMap.get(String(a.storeId)) || ''; const bLabel = b.storeName || storeLabelMap.get(String(b.storeId)) || ''; return bLabel.localeCompare(aLabel); + }, + nextReminderAsc: (a, b) => { + const aNext = getNextReminderTimestamp(a); + const bNext = getNextReminderTimestamp(b); + if (aNext === null && bNext === null) { + return 0; + } + if (aNext === null) { + return 1; + } + if (bNext === null) { + return -1; + } + return aNext - bNext; } }; const sorter = sorters[sortBy] || sorters.pickupDateDesc; return [...results].sort(sorter); - }, [entries, filters, parseDateValue, sortBy, storeLabelMap]); + }, [entries, filters, getNextReminderTimestamp, parseDateValue, sortBy, storeLabelMap]); const releaseEntryImageUrls = useCallback((entryList) => { if (!Array.isArray(entryList)) { @@ -538,6 +606,7 @@ const JournalPage = ({ authorizedFetch, stores }) => { +