Gonçalo Rodrigues 7a2cb10c79 feat(finance): dark mode UI overhaul + admin seed data
UI — full dark/light theme system:
- CSS custom-property token system (--bg, --surface, --accent, --green, --red, etc.)
  with complete light-mode overrides via [data-theme="light"]
- Sticky frosted-glass nav with animated brand icon, theme toggle persisted to localStorage,
  respects prefers-color-scheme on first visit
- Cards with layered shadows, glass backdrop-filter, shimmer accent stripe on value cards
- Glowing category color dots, colored P&L badges, budget bars with glow effect
- All Chart.js instances use CSS-variable-aware grid/text colours
- Scroll-reveal animations and animated money counters on every KPI card
- 3-D donut portfolio chart recoloured to match palette; hover lifts the hovered slice
- Accounts page shows type emoji icons; delete removes row in-place
- Sharing page search dropdown themed with var() colours
- Import preview: colour-coded left border on category select driven by category colour
- Projections: second KPI card (monthly avg) + pace bars per category

Seed data (seed.go):
- SeedAdmin() runs in a goroutine at startup; idempotent (skips if transactions exist)
- Resolves admin user ID via internal users service GET /admin/users?search=<email>
  (SEED_USER_EMAIL env var, defaults to admin@homelab.local)
- Seeds 4 accounts (CGD Checking, CGD Savings, Visa Credit, Trade Republic)
- Seeds 14 categories with colours and monthly budgets
- Seeds ~65 realistic Portuguese household transactions spread across 6 months
- Seeds 7 ETF buy trades across VWCE, SXR8 (S&P 500), EUNL (MSCI World)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 12:16:23 +01:00

93 lines
3.1 KiB
HTML

{{define "content"}}
{{$d := .}}
<h1 style="margin-bottom:24px;">Projections</h1>
<div class="grid">
<div class="card value-card animate-on-scroll">
<h2>Projected Annual Spend</h2>
<div class="value negative animate-counter" data-target="{{$d.AnnualTotal}}" data-prefix="€">€0.00</div>
<p class="text-muted" style="margin-top:8px;">Based on 6-month average</p>
</div>
<div class="card value-card animate-on-scroll">
<h2>Projected Monthly Spend</h2>
{{$monthly := div $d.AnnualTotal 12}}
<div class="value negative animate-counter" data-target="{{$monthly}}" data-prefix="€">€0.00</div>
<p class="text-muted" style="margin-top:8px;">Average across categories</p>
</div>
</div>
<div class="card animate-on-scroll">
<h2 style="margin-bottom:12px;">Monthly Average by Category — Last 6 Months</h2>
<div style="padding-top:4px;">
<canvas id="projChart" height="260"></canvas>
</div>
</div>
<div class="card animate-on-scroll">
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Category</th>
<th class="text-right">Monthly Avg</th>
<th class="text-right">Projected Annual</th>
<th style="width:200px;">Pace</th>
</tr>
</thead>
<tbody>
{{range $cat, $avg := $d.MonthlyAvg}}
<td style="font-weight:500;">{{$cat}}</td>
<td class="cents negative">€{{printf "%.2f" $avg}}</td>
<td class="cents negative">€{{printf "%.2f" (mul $avg 12)}}</td>
<td>
<div style="background:var(--bg3); border-radius:6px; height:6px; overflow:hidden;">
<div style="height:100%; border-radius:6px; background:var(--accent);
width:{{round (mul (div (round (mul $avg 100)) (div $d.AnnualTotal 12)) 100)}}%;
max-width:100%;"></div>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<script>
const isDark = () => document.documentElement.getAttribute('data-theme') === 'dark';
const gridC = () => isDark() ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.06)';
const textC = () => isDark() ? '#5c6585' : '#8a92b0';
const catColors = [
'#6979f8','#f87171','#fbbf24','#34d399','#a78bfa',
'#f472b6','#38bdf8','#fb923c','#4ade80','#e879f9',
'#94a3b8','#22d3ee','#f97316','#a3e635',
];
new Chart(document.getElementById('projChart'), {
type: 'bar',
data: {
labels: [{{range $cat, $_ := $d.MonthlyAvg}}"{{$cat}}",{{end}}],
datasets: [{
label: 'Monthly Avg (€)',
data: [{{range $_, $avg := $d.MonthlyAvg}}{{$avg}},{{end}}],
backgroundColor: catColors.map(c => c + '99'),
borderColor: catColors,
borderWidth: 1.5,
borderRadius: 6,
borderSkipped: false,
}]
},
options: {
responsive: true,
animation: { duration: 900, easing: 'easeOutQuart' },
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, grid: { color: gridC() }, ticks: { color: textC(), callback: v => '€' + v } },
x: { grid: { display: false }, ticks: { color: textC() } }
}
}
});
</script>
{{end}}