diff --git a/client/pages/SettingsPage.tsx b/client/pages/SettingsPage.tsx index aeb51f4..33deb62 100644 --- a/client/pages/SettingsPage.tsx +++ b/client/pages/SettingsPage.tsx @@ -316,6 +316,7 @@ interface SettingsState { date_format: string; week_start: string; grace_period_days: number | string; + due_soon_days: number | string; drift_threshold_pct: string; tracker_show_bank_projection_banner: string; tracker_bank_projection_banner_snoozed_until: string; @@ -335,6 +336,7 @@ export default function SettingsPage() { date_format: 'MM/DD/YYYY', week_start: '0', grace_period_days: 5, + due_soon_days: 3, drift_threshold_pct: '5', tracker_show_bank_projection_banner: 'true', tracker_bank_projection_banner_snoozed_until: '', @@ -367,6 +369,7 @@ export default function SettingsPage() { date_format: s.date_format, week_start: s.week_start, grace_period_days: s.grace_period_days, + due_soon_days: s.due_soon_days, drift_threshold_pct: s.drift_threshold_pct, tracker_show_bank_projection_banner: s.tracker_show_bank_projection_banner, tracker_bank_projection_banner_snoozed_until: s.tracker_bank_projection_banner_snoozed_until || '', @@ -561,7 +564,7 @@ export default function SettingsPage() { Grace period} description="Days after the due date before a bill is marked overdue." >
@@ -577,7 +580,23 @@ export default function SettingsPage() {
Due soon window} + description="How many days ahead a bill is flagged as due soon." + > +
+ setTyped('due_soon_days', Math.min(Math.max(parseInt(e.target.value, 10) || 0, 0), 31))} + className="w-20" + /> + days +
+
+ Price change sensitivity} description="Flag a bill when recent payments differ from the expected amount by at least this percentage." >
diff --git a/routes/calendar.js b/routes/calendar.js index a29f903..38ff2d5 100644 --- a/routes/calendar.js +++ b/routes/calendar.js @@ -110,7 +110,7 @@ router.get('/', (req, res) => { const today = localDateString(now); const userSettings = getUserSettings(req.user.id); - const rowOptions = { gracePeriodDays: userSettings.grace_period_days }; + const rowOptions = { gracePeriodDays: userSettings.grace_period_days, dueSoonDays: userSettings.due_soon_days }; const daysInMonth = new Date(year, month, 0).getDate(); const { start, end } = getCycleRange(year, month); const days = Array.from({ length: daysInMonth }, (_, index) => emptyDay(year, month, index + 1)); diff --git a/services/statusService.js b/services/statusService.js index 0771a92..3c89fa8 100644 --- a/services/statusService.js +++ b/services/statusService.js @@ -25,6 +25,14 @@ function resolveGracePeriodDays(value) { return Number.isInteger(parsed) && parsed >= 0 ? parsed : 5; } +// How many days before the due date a bill is flagged "due soon". Configurable +// like the grace period; clamped to a sane 0–31 with a fallback of 3. +function resolveDueSoonDays(value) { + const parsed = parseInt(value ?? getSetting('due_soon_days') ?? '3', 10); + if (!Number.isInteger(parsed) || parsed < 0) return 3; + return Math.min(parsed, 31); +} + function pad(value) { return String(value).padStart(2, '0'); } @@ -201,6 +209,7 @@ function calculateStatus(bill, payments, dueDate, today, options = {}) { if (!dueDate) return 'inactive_cycle'; const gracePeriodDays = resolveGracePeriodDays(options.gracePeriodDays); + const dueSoonDays = resolveDueSoonDays(options.dueSoonDays); const safePayments = Array.isArray(payments) ? payments : []; const expectedAmount = Number(bill.expected_amount) || 0; const totalPaid = safePayments.reduce((sum, p) => sum + (Number(p.amount) || 0), 0); @@ -215,7 +224,7 @@ function calculateStatus(bill, payments, dueDate, today, options = {}) { const todayDate = new Date(today + 'T00:00:00'); const diffDays = Math.floor((due - todayDate) / 86400000); - if (diffDays > 3) return 'upcoming'; + if (diffDays > dueSoonDays) return 'upcoming'; if (diffDays >= 0) return 'due_soon'; if (Math.abs(diffDays) <= gracePeriodDays) return 'late'; return 'missed'; @@ -296,4 +305,5 @@ module.exports = { resolveBucket, resolveDueDate, resolveGracePeriodDays, + resolveDueSoonDays, }; diff --git a/services/trackerService.js b/services/trackerService.js index 83e8ad1..e934a41 100644 --- a/services/trackerService.js +++ b/services/trackerService.js @@ -547,7 +547,7 @@ function getTracker(userId, query = {}, now = new Date()) { const { year, month } = parsed; const todayStr = localDateString(now); const userSettings = getUserSettings(userId); - const rowOptions = { gracePeriodDays: userSettings.grace_period_days }; + const rowOptions = { gracePeriodDays: userSettings.grace_period_days, dueSoonDays: userSettings.due_soon_days }; const { start, end } = getCycleRange(year, month); const previousMonth = previousMonthFor(year, month); const prevMonthRange = getCycleRange(previousMonth.year, previousMonth.month); @@ -767,7 +767,7 @@ function getUpcomingBills(userId, query = {}, now = new Date()) { const days = Math.max(1, Math.min(parseInt(query.days || '30', 10) || 30, 365)); const todayStr = localDateString(now); const userSettings = getUserSettings(userId); - const rowOptions = { gracePeriodDays: userSettings.grace_period_days }; + const rowOptions = { gracePeriodDays: userSettings.grace_period_days, dueSoonDays: userSettings.due_soon_days }; const bills = fetchActiveBills(db, userId, 'id'); const cutoff = new Date(now); diff --git a/services/userSettings.js b/services/userSettings.js index d896229..455979b 100644 --- a/services/userSettings.js +++ b/services/userSettings.js @@ -7,6 +7,7 @@ const USER_SETTING_KEYS = [ 'date_format', 'week_start', 'grace_period_days', + 'due_soon_days', 'notify_days_before', 'drift_threshold_pct', 'bank_tracking_enabled', @@ -31,6 +32,7 @@ const USER_SETTING_KEYS = [ const USER_SETTING_DEFAULTS = { week_start: '0', + due_soon_days: '3', search_bars_collapsed: 'false', bank_auto_categorize_merchants: 'true', tracker_show_bank_projection_banner: 'true',