Major feature release: security hardening + Goals, Rules, search, charts
Security: - Replace SHA-256 password hashing with bcrypt (cost 12) + timing-safe compare - Introduce server-side session tokens (x-session-token header); drop x-user-id - Add /api/auth/logout endpoint to invalidate sessions - Enforce 8-char minimum password server-side - Fix password reset: now uses real signed tokens with 1-hour TTL Features: - Goals: savings goals with progress bars, target dates, add/withdraw funds - Auto-categorization Rules: keyword → envelope mapping applied on transaction add - Transaction search/filter: description, account, envelope, date range - Transaction table: shows envelope color dot, running balance column - Monthly Spending History chart (last 6 months income vs expenses bar chart) - Spending Insights: month-over-month comparison + overspent envelope callouts - Budget progress expanded: per-envelope bars with rollover indicator - Envelope rollover toggle: unspent funds can carry forward - Recurring transactions auto-applied on login - Save indicator in nav (Saving… / Saved ✓ / Save failed) - Nav overflow fixed: all tabs scroll horizontally instead of clipping - PWA manifest added for home-screen install support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
8814423fa6
commit
20ea25f4de
19 files changed
+1685
-1486
No files matched your search
@@ -301,6 +301,56 @@ export default function DashboardCharts({ accounts, envelopes, transactions, vis
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Monthly Spending History (Last 6 Months) */}
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6 lg:col-span-2">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">Monthly Spending History (Last 6 Months)</h3>
|
||||
{(() => {
|
||||
const months = Array.from({ length: 6 }, (_, i) => {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - (5 - i), 1);
|
||||
return { year: d.getFullYear(), month: d.getMonth(), label: d.toLocaleDateString('en-US', { month: 'short', year: '2-digit' }) };
|
||||
});
|
||||
const monthlyData = months.map(m => {
|
||||
const start = new Date(m.year, m.month, 1);
|
||||
const end = new Date(m.year, m.month + 1, 0, 23, 59, 59);
|
||||
const income = transactions.filter(t => {
|
||||
const d = t.date instanceof Date ? t.date : new Date(t.date);
|
||||
return t.amount > 0 && !t.envelopeId && d >= start && d <= end;
|
||||
}).reduce((s, t) => s + t.amount, 0);
|
||||
const spent = transactions.filter(t => {
|
||||
const d = t.date instanceof Date ? t.date : new Date(t.date);
|
||||
return t.amount < 0 && d >= start && d <= end;
|
||||
}).reduce((s, t) => s + Math.abs(t.amount), 0);
|
||||
return { ...m, income, spent };
|
||||
});
|
||||
const maxVal = Math.max(...monthlyData.map(m => Math.max(m.income, m.spent)), 1);
|
||||
return monthlyData.some(m => m.income > 0 || m.spent > 0) ? (
|
||||
<div>
|
||||
<div className="flex items-end justify-between h-40 gap-3">
|
||||
{monthlyData.map(m => (
|
||||
<div key={m.label} className="flex-1 flex flex-col items-center gap-1">
|
||||
<div className="w-full flex items-end gap-0.5 h-32">
|
||||
<div className="flex-1 bg-green-400 rounded-t hover:bg-green-500 transition-colors relative group"
|
||||
style={{ height: `${(m.income / maxVal) * 100}%`, minHeight: m.income > 0 ? '4px' : '0' }}>
|
||||
{m.income > 0 && <div className="absolute -top-6 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs py-0.5 px-1 rounded opacity-0 group-hover:opacity-100 whitespace-nowrap">${m.income.toFixed(0)}</div>}
|
||||
</div>
|
||||
<div className="flex-1 bg-red-400 rounded-t hover:bg-red-500 transition-colors relative group"
|
||||
style={{ height: `${(m.spent / maxVal) * 100}%`, minHeight: m.spent > 0 ? '4px' : '0' }}>
|
||||
{m.spent > 0 && <div className="absolute -top-6 left-1/2 -translate-x-1/2 bg-gray-800 text-white text-xs py-0.5 px-1 rounded opacity-0 group-hover:opacity-100 whitespace-nowrap">${m.spent.toFixed(0)}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">{m.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-4 mt-3 justify-center text-xs text-gray-600">
|
||||
<span className="flex items-center gap-1"><span className="w-3 h-3 rounded bg-green-400 inline-block" />Income</span>
|
||||
<span className="flex items-center gap-1"><span className="w-3 h-3 rounded bg-red-400 inline-block" />Expenses</span>
|
||||
</div>
|
||||
</div>
|
||||
) : <p className="text-gray-500 text-sm">No data yet. Add transactions to see history.</p>;
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Daily Spending Chart (Last 7 Days) */}
|
||||
{visibleCharts.dailySpending && (
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6 lg:col-span-2">
|
||||
|
||||
Reference in new issue
Block a user