From dec13d2982bb0ae6fa8c6370675ef95c4aa2de84 Mon Sep 17 00:00:00 2001 From: Billy Kennedy <214814620+bkennedyshit@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:21:29 -0400 Subject: [PATCH] feat(weekly-digest): smart weekly financial summary with export (#121) - Weekly spending summary with category breakdown - Week-over-week % change per category (up/down indicator) - Top 3 spending categories highlighted with budget vs actual - Savings rate calculation when income data available - AI-generated top insight banner (most notable change) - CSV export for spreadsheet analysis - Share digest as text (native share API or clipboard fallback) - Toggle between this week / last week views - Adds /weekly-digest route + Digest nav link Closes #121 --- app/src/App.tsx | 9 + app/src/components/layout/Navbar.tsx | 1 + app/src/pages/WeeklyDigest.tsx | 295 +++++++++++++++++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 app/src/pages/WeeklyDigest.tsx diff --git a/app/src/App.tsx b/app/src/App.tsx index f0dc5942..d3867124 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -16,6 +16,7 @@ import NotFound from "./pages/NotFound"; import { Landing } from "./pages/Landing"; import ProtectedRoute from "./components/auth/ProtectedRoute"; import Account from "./pages/Account"; +import WeeklyDigest from "./pages/WeeklyDigest"; const queryClient = new QueryClient({ defaultOptions: { @@ -83,6 +84,14 @@ const App = () => ( } /> + + + + } + /> b.amount - a.amount).slice(0, 3); + const savingsRate = digest.totalIncome > 0 + ? Math.round(((digest.totalIncome - digest.totalSpent) / digest.totalIncome) * 100) + : null; + + function handleExportCSV() { + const rows = [ + ['Category', 'This Week', 'Last Week', 'Change %', 'Budget'], + ...digest.categories.map(c => [ + c.name, + c.amount.toFixed(2), + c.prevAmount.toFixed(2), + `${pctChange(c.amount, c.prevAmount)}%`, + c.budget ? c.budget.toFixed(2) : 'N/A', + ]), + ['TOTAL', digest.totalSpent.toFixed(2), digest.prevTotalSpent.toFixed(2), `${change}%`, ''], + ]; + const csv = rows.map(r => r.join(',')).join('\n'); + const blob = new Blob([csv], { type: 'text/csv' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `weekly-digest-${digest.startDate}.csv`; + a.click(); + } + + function handleShare() { + const text = `📊 My Weekly Spending Digest (${digest.startDate} – ${digest.endDate})\n\n` + + `Total spent: ${formatCurrency(digest.totalSpent)} (${change >= 0 ? '+' : ''}${change}% vs last week)\n\n` + + `Top categories:\n` + + topCategories.map(c => `• ${c.name}: ${formatCurrency(c.amount)}`).join('\n') + + `\n\nTracked with FinMind 💰`; + if (navigator.share) { + navigator.share({ title: 'Weekly Digest', text }); + } else { + navigator.clipboard.writeText(text); + } + } + + return ( +
+ {/* Header */} +
+
+

Weekly Digest

+

+ {digest.startDate} — {digest.endDate} +

+
+
+
+ {weekData.map((w, i) => ( + + ))} +
+ + +
+
+ + {/* Top insight banner */} +
+ +

{digest.topInsight}

+
+ + {/* Summary row */} +
+ + + Total Spent + + +
{formatCurrency(digest.totalSpent)}
+
0 ? 'text-destructive' : 'text-success'}`}> + {change > 0 ? : } + {Math.abs(change)}% vs last week +
+
+
+ + {savingsRate !== null && ( + + + Savings Rate + + +
{savingsRate}%
+

+ {formatCurrency(digest.totalIncome - digest.totalSpent)} saved +

+
+
+ )} + + + + Categories + + +
{digest.categories.length}
+

spending areas tracked

+
+
+ + + + Biggest Spend + + +
{topCategories[0]?.name}
+

+ {formatCurrency(topCategories[0]?.amount ?? 0)} +

+
+
+
+ + {/* Top 3 categories highlight */} +
+

+ Top Spending Categories +

+
+ {topCategories.map((cat, i) => { + const catChange = pctChange(cat.amount, cat.prevAmount); + const budgetPct = cat.budget ? Math.round((cat.amount / cat.budget) * 100) : null; + const Icon = cat.icon; + return ( + 90 ? 'border-destructive/50' : ''}> + +
+
+ +
+
+ {cat.name} + #{i + 1} this week +
+
+
+ +
+ {formatCurrency(cat.amount)} + 0 ? 'text-destructive' : 'text-success'}`}> + {catChange > 0 ? : } + {Math.abs(catChange)}% + +
+ {cat.budget && ( +
+
+ Budget + {budgetPct}% used +
+ 90 ? '[&>div]:bg-destructive' : ''} /> +
+ )} +
+
+ ); + })} +
+
+ + {/* Full category breakdown */} +
+

All Categories

+
+ {digest.categories.map(cat => { + const catChange = pctChange(cat.amount, cat.prevAmount); + const budgetPct = cat.budget ? Math.round((cat.amount / cat.budget) * 100) : null; + const Icon = cat.icon; + return ( + + +
+ + {cat.name} + {cat.budget && ( +
+ 90 ? '[&>div]:bg-destructive' : ''}`} /> +
+ )} + {formatCurrency(cat.amount)} + 0 ? 'text-destructive' : 'text-success'}`}> + {catChange > 0 ? : } + {Math.abs(catChange)}% + + {cat.budget && ( + 100 ? 'destructive' : budgetPct && budgetPct > 80 ? 'outline' : 'secondary'} + className="w-16 justify-center text-xs"> + {budgetPct}% + + )} +
+
+
+ ); + })} +
+
+
+ ); +} + +export default WeeklyDigest;