diff --git a/DEVELOPMENT_LOG.md b/DEVELOPMENT_LOG.md index 51af7cd..08d58e1 100644 --- a/DEVELOPMENT_LOG.md +++ b/DEVELOPMENT_LOG.md @@ -6,6 +6,32 @@ --- +### v0.21.0 — 3-Month Trend Indicator +**Status:** 🔄 IN PROGRESS +**Date:** 2026-05-10 +**Priority:** MEDIUM + +| Agent | Status | Time | Notes | +|-------|--------|------|-------| +| Neo | ✅ COMPLETED | 19m | Backend trend calculation, TrendIndicator + TrendCard components | +| Ripley | ✅ COMPLETED | — | Fixed duplicate TrendIndicator build error, version bump 0.20.9 → 0.21.0 | +| Bishop | ⏳ PENDING | — | Verification | +| Hudson | ⏳ PENDING | — | Security audit | + +**Files modified:** `routes/tracker.js`, `client/pages/TrackerPage.jsx`, `client/lib/version.js`, `package.json` + +**Work Completed:** +- [x] Backend: 3-month trend calculation with year-wrapping +- [x] Backend: trend object in API response (direction, percent_change, 3_month_avg) +- [x] Frontend: TrendIndicator component (arrow + percentage + label) +- [x] Frontend: TrendCard component (purple gradient card) +- [x] Bug fix: removed duplicate TrendIndicator definition +- [x] Version bumped to 0.21.0 + +**Security Audit (Hudson):** Pending + +--- + ### v0.20.9 — Previous Month Paid on Tracker **Status:** 🔄 IN PROGRESS **Date:** 2026-05-10 diff --git a/FUTURE.md b/FUTURE.md index ee6ac71..5b11f34 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -3,7 +3,7 @@ **This document tracks potential future enhancements for Bill Tracker.** **Last Updated:** 2026-05-10 -**Current Version:** v0.20.9 +**Current Version:** v0.21.0 ## How to Use This Document @@ -39,25 +39,6 @@ Items are grouped under their priority section heading (`## 🔴 CRITICAL`, `## ### 🟡 MEDIUM -### 3-Month Trend Indicator with Up/Down Arrows -**Priority:** MEDIUM -**Added:** 2026-05-08 by _null - -**Description:** -Add trend indicators showing whether the last 3 months of payments went up or down compared to current month. Display as up/down arrow with percentage change. - -**Rationale:** -Visual trend indicator helps users identify spending patterns without navigating to Analytics page. - -**Implementation Notes:** -- Calculate 3-month rolling average -- Compare current month vs. previous 3-month average -- Show green up arrow if trending up (more paid), red down arrow if trending down -- Display percentage change -- Position in Tracker header or Summary card -- Files likely to be modified: `routes/analytics.js` (new endpoint), `client/pages/TrackerPage.jsx` or `client/pages/SummaryPage.jsx` -- Estimated effort: 4 hours - ### Add loading skeletons and better async state management **Priority:** MEDIUM **Added:** 2026-05-08 by Scarlett diff --git a/HISTORY.md b/HISTORY.md index 038ac30..26bc48d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,12 @@ # Bill Tracker — Changelog +## v0.21.0 + +### Added +- **3-Month Trend Indicator** — Tracker shows up/down/flat trend vs 3-month average with percentage change (↑ green, ↓ red, → gray) +- Trend card with purple gradient header and TrendingUp icon +- Backend: 3-month payment aggregation with year-wrapping, ±2% threshold for "flat" + ## v0.20.9 ### Added diff --git a/client/lib/version.js b/client/lib/version.js index d8e9d8d..50049d5 100644 --- a/client/lib/version.js +++ b/client/lib/version.js @@ -1,10 +1,10 @@ -export const APP_VERSION = '0.20.9'; +export const APP_VERSION = '0.21.0'; export const APP_NAME = 'BillTracker'; export const RELEASE_NOTES = { - version: '0.20.9', + version: '0.21.0', date: '2026-05-10', highlights: [ - { icon: '📊', title: 'Previous Month Paid', desc: '"Last Month" column on Tracker shows last month\'s paid amount for comparison.' }, + { icon: '📈', title: '3-Month Trend Indicator', desc: 'Tracker shows up/down trend vs 3-month average with percentage change.' }, ], }; \ No newline at end of file diff --git a/client/pages/TrackerPage.jsx b/client/pages/TrackerPage.jsx index 96657c4..2db174a 100644 --- a/client/pages/TrackerPage.jsx +++ b/client/pages/TrackerPage.jsx @@ -96,6 +96,41 @@ const CARD_DEFS = { }, }; +function TrendIndicator({ trend }) { + if (!trend) return null; + + const { direction, percent_change } = trend; + + let icon, color, text; + switch (direction) { + case 'up': + icon = '↑'; + color = 'text-emerald-500'; + text = `${icon} ${percent_change}%`; + break; + case 'down': + icon = '↓'; + color = 'text-red-500'; + text = `${icon} ${Math.abs(percent_change)}%`; + break; + default: + icon = '→'; + color = 'text-muted-foreground'; + text = `${icon} ${percent_change}%`; + } + + return ( +
+ + {text} + + + vs 3-mo avg + +
+ ); +} + function SummaryCard({ type, value, onEdit, hint }) { const def = CARD_DEFS[type]; const isActive = def.activateWhen(value || 0); @@ -140,6 +175,25 @@ function SummaryCard({ type, value, onEdit, hint }) { ); } +function TrendCard({ trend }) { + if (!trend) return null; + + return ( +
+
+
+ +

+ 3-Month Trend +

+
+
+ +
+
+ ); +} + // ── Status badge ─────────────────────────────────────────────────────────── const StatusBadge = React.memo(function StatusBadge({ status, clickable, onClick, loading }) { const meta = useMemo(() => STATUS_META[status] || STATUS_META.upcoming, [status]); @@ -1356,6 +1410,7 @@ export default function TrackerPage() { + {summary.trend && }
{/* ── Empty state ── */} diff --git a/package.json b/package.json index 31afb6b..664986e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bill-tracker", - "version": "0.20.9", + "version": "0.21.0", "description": "Monthly bill tracking system", "main": "server.js", "scripts": { diff --git a/routes/tracker.js b/routes/tracker.js index b2d6423..9febed6 100644 --- a/routes/tracker.js +++ b/routes/tracker.js @@ -24,6 +24,13 @@ router.get('/', (req, res) => { const prevYear = month === 1 ? year - 1 : year; const prevMonthRange = getCycleRange(prevYear, prevMonth); + // Calculate 3-month range for trend analysis + const threeMonthsAgo = (() => { + let y = year, m = month - 2; + while (m <= 0) { m += 12; y -= 1; } + return { year: y, month: m }; + })(); + const bills = db.prepare(` SELECT b.*, c.name AS category_name FROM bills b @@ -44,6 +51,15 @@ router.get('/', (req, res) => { AND deleted_at IS NULL `); + // Prepare statement for 3-month trend calculations + const threeMonthPaymentsStmt = db.prepare(` + SELECT SUM(amount) as total_paid, strftime('%Y-%m', paid_date) as month_key + FROM payments + WHERE bill_id = ? AND paid_date BETWEEN ? AND ? + AND deleted_at IS NULL + GROUP BY strftime('%Y-%m', paid_date) + `); + const rows = bills.map(bill => { // Only count non-deleted payments for status/totals const payments = db.prepare(` @@ -89,6 +105,67 @@ router.get('/', (req, res) => { // Calculate previous month total const previousMonthTotal = activeRows.reduce((s, r) => s + r.previous_month_paid, 0); + // Calculate 3-month trend data + const threeMonthStart = getCycleRange(threeMonthsAgo.year, threeMonthsAgo.month).start; + const currentMonthEnd = end; + + // Get all payments for the last 3 months for this user + // Join through bills to get user_id since payments table doesn't have user_id + const threeMonthPayments = db.prepare(` + SELECT SUM(p.amount) as total_paid, strftime('%Y-%m', p.paid_date) as month_key + FROM payments p + JOIN bills b ON p.bill_id = b.id + WHERE b.user_id = ? AND p.paid_date BETWEEN ? AND ? + AND p.deleted_at IS NULL + GROUP BY strftime('%Y-%m', p.paid_date) + `).all(req.user.id, threeMonthStart, currentMonthEnd); + + // Create a map of month payments for easier access + const monthlyPaymentsMap = new Map(); + threeMonthPayments.forEach(payment => { + monthlyPaymentsMap.set(payment.month_key, payment.total_paid); + }); + + // Calculate payments for each of the last 3 months + const months = []; + for (let i = 2; i >= 0; i--) { + const date = new Date(year, month - 1 - i); + const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; + months.push({ + year: date.getFullYear(), + month: date.getMonth() + 1, + key: monthKey, + payment: parseFloat(monthlyPaymentsMap.get(monthKey) || 0) + }); + } + + // Calculate 3-month average + const threeMonthTotal = months.reduce((sum, m) => sum + m.payment, 0); + const threeMonthAvg = threeMonthTotal / 3; + + // Calculate current month paid (sum of all bills) + const currentMonthPaid = activeTotalPaid; + + // Calculate percentage change + let percentChange = 0; + let direction = 'flat'; + + if (threeMonthAvg > 0) { + percentChange = ((currentMonthPaid - threeMonthAvg) / threeMonthAvg) * 100; + + // Determine direction based on percentage change + if (percentChange > 2) { + direction = 'up'; + } else if (percentChange < -2) { + direction = 'down'; + } else { + direction = 'flat'; + } + } + + // Ensure percentChange is a number with 1 decimal place + percentChange = parseFloat(percentChange.toFixed(1)); + res.json({ year, month, today: todayStr, summary: { @@ -103,6 +180,12 @@ router.get('/', (req, res) => { count_late: activeRows.filter(r => r.status === 'late' || r.status === 'missed').length, count_autodraft: activeRows.filter(r => r.status === 'autodraft').length, previous_month_total: previousMonthTotal, + trend: { + three_month_avg: parseFloat(threeMonthAvg.toFixed(2)), + current_month_paid: parseFloat(currentMonthPaid.toFixed(2)), + percent_change: percentChange, + direction: direction + } }, rows, });