const express = require('express'); const router = express.Router(); const { getDb } = require('../db/database'); const { buildTrackerRow, getCycleRange, resolveDueDate } = require('../services/statusService'); // GET /api/tracker?year=2026&month=5 router.get('/', (req, res) => { const db = getDb(); const now = new Date(); const year = parseInt(req.query.year || now.getFullYear(), 10); const month = parseInt(req.query.month || now.getMonth() + 1, 10); if (isNaN(year) || year < 2000 || year > 2100) return res.status(400).json({ error: 'year must be a 4-digit integer between 2000 and 2100' }); if (isNaN(month) || month < 1 || month > 12) return res.status(400).json({ error: 'month must be an integer between 1 and 12' }); const todayStr = now.toISOString().slice(0, 10); const { start, end } = getCycleRange(year, month); const bills = db.prepare(` SELECT b.*, c.name AS category_name FROM bills b LEFT JOIN categories c ON b.category_id = c.id WHERE b.active = 1 AND b.user_id = ? ORDER BY b.due_day ASC, b.name ASC `).all(req.user.id); const mbsStmt = db.prepare( 'SELECT actual_amount, notes, is_skipped FROM monthly_bill_state WHERE bill_id=? AND year=? AND month=?' ); const rows = bills.map(bill => { // Only count non-deleted payments for status/totals const payments = db.prepare(` SELECT * FROM payments WHERE bill_id = ? AND paid_date BETWEEN ? AND ? AND deleted_at IS NULL ORDER BY paid_date DESC `).all(bill.id, start, end); const row = buildTrackerRow(bill, payments, year, month, todayStr); // Overlay monthly state overrides const mbs = mbsStmt.get(bill.id, year, month); row.actual_amount = mbs?.actual_amount ?? null; row.monthly_notes = mbs?.notes ?? null; row.is_skipped = !!(mbs?.is_skipped); return row; }); const totalExpected = rows.reduce((s, r) => s + r.expected_amount, 0); const totalPaid = rows.reduce((s, r) => s + r.total_paid, 0); const totalOverdue = rows .filter(r => !r.is_skipped && (r.status === 'late' || r.status === 'missed')) .reduce((s, r) => s + r.balance, 0); const activeRows = rows.filter(r => !r.is_skipped); res.json({ year, month, today: todayStr, summary: { total_expected: activeRows.reduce((s, r) => s + r.expected_amount, 0), total_paid: activeRows.reduce((s, r) => s + r.total_paid, 0), remaining: Math.max(0, activeRows.reduce((s, r) => s + r.expected_amount, 0) - activeRows.reduce((s, r) => s + r.total_paid, 0)), overdue: totalOverdue, count_paid: activeRows.filter(r => r.status === 'paid').length, count_upcoming: activeRows.filter(r => r.status === 'upcoming' || r.status === 'due_soon').length, count_late: activeRows.filter(r => r.status === 'late' || r.status === 'missed').length, count_autodraft: activeRows.filter(r => r.status === 'autodraft').length, }, rows, }); }); // GET /api/tracker/upcoming?days=30 // Returns active bills with a due date in the next N days, sorted by due_date asc. router.get('/upcoming', (req, res) => { const db = getDb(); const days = Math.min(parseInt(req.query.days || '30', 10), 365); const now = new Date(); const todayStr = now.toISOString().slice(0, 10); const year = now.getFullYear(); const month = now.getMonth() + 1; const { start, end } = getCycleRange(year, month); const bills = db.prepare(` SELECT b.*, c.name AS category_name FROM bills b LEFT JOIN categories c ON b.category_id = c.id WHERE b.active = 1 AND b.user_id = ? `).all(req.user.id); const cutoff = new Date(now); cutoff.setDate(cutoff.getDate() + days); const cutoffStr = cutoff.toISOString().slice(0, 10); const upcoming = []; for (const bill of bills) { const dueDate = resolveDueDate(bill, year, month); if (dueDate < todayStr || dueDate > cutoffStr) continue; const payments = db.prepare(` SELECT * FROM payments WHERE bill_id = ? AND paid_date BETWEEN ? AND ? AND deleted_at IS NULL ORDER BY paid_date DESC `).all(bill.id, start, end); const row = buildTrackerRow(bill, payments, year, month, todayStr); if (row.status === 'paid') continue; // skip already paid upcoming.push({ id: bill.id, name: bill.name, category_name: bill.category_name, due_date: dueDate, expected_amount: bill.expected_amount, status: row.status, days_until_due: Math.floor((new Date(dueDate) - now) / 86400000), }); } upcoming.sort((a, b) => a.due_date.localeCompare(b.due_date)); res.json({ days, today: todayStr, upcoming }); }); module.exports = router;