Gonçalo Rodrigues 1d3aa764cb feat: phase 1 dashboard redesign — disposable income + smart panels
Replaces the old KPI/chart dashboard with a focused layout that
answers the three key questions immediately:

- Hero block: "Available to spend" = income − fixed costs − spent so far,
  with a progress bar showing % of disposable used vs month elapsed
- Bank math panel: detects recurring fixed expenses (Housing, Utilities,
  Subscriptions, Investments) from last 3 months and shows the minimum
  bank balance needed right now including a 2-week safety buffer
- Savings rate card with month-over-month delta
- Portfolio snapshot card with total value and P&L
- Stocks at a glance panel: per-holding value and P&L inline
- Budget health: thin bars per category, red when over limit
- Recent activity: last 5 transactions with category color dots

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 15:51:46 +01:00

219 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{define "content"}}
{{$d := .}}
<div style="display:flex; align-items:baseline; justify-content:space-between; margin-bottom:24px; flex-wrap:wrap; gap:8px;">
<h1>Dashboard</h1>
<span class="text-muted">{{if $d.Email}}{{$d.Email}}{{end}}</span>
</div>
<!-- HERO: available to spend -->
<div class="card animate-on-scroll" style="margin-bottom:16px; padding:24px 28px;">
<div style="font-size:12px; color:var(--text2); text-transform:uppercase; letter-spacing:.5px; margin-bottom:10px; display:flex; align-items:center; gap:8px;">
Available to spend this month
<span style="font-size:11px; background:var(--bg3); color:var(--text3); padding:2px 8px; border-radius:99px; font-weight:400; text-transform:none; letter-spacing:0;">
income fixed costs spent so far
</span>
</div>
<div style="display:flex; align-items:baseline; gap:20px; flex-wrap:wrap; margin-bottom:16px;">
<div class="animate-counter {{if lt $d.AvailableToSpend 0}}negative{{else}}positive{{end}}"
style="font-size:42px; font-weight:500; letter-spacing:-1.5px; line-height:1;"
data-target="{{$d.AvailableToSpend}}" data-prefix="€">€0.00</div>
<div style="font-size:13px; color:var(--text2);">
of <span style="color:var(--text); font-weight:500;" class="animate-counter" data-target="{{$d.DisposableIncome}}" data-prefix="€">€0.00</span> disposable
&nbsp;·&nbsp; <span style="color:var(--text2);">{{$d.MonthSpentPct}}% used</span>
</div>
</div>
<div style="background:var(--bg3); border-radius:99px; height:6px; overflow:hidden; margin-bottom:6px;">
<div style="height:100%; border-radius:99px; width:{{$d.MonthSpentPct}}%;
background:{{if gt $d.MonthSpentPct 90}}var(--red){{else if gt $d.MonthSpentPct 70}}#f59e0b{{else}}var(--green){{end}};
transition:width 1s ease;"></div>
</div>
<div style="display:flex; justify-content:space-between;">
<span style="font-size:11px; color:var(--text3);">Month progress: {{$d.MonthProgressPct}}%</span>
<span style="font-size:11px; color:var(--text3);">Spent: {{$d.MonthSpentPct}}%</span>
</div>
</div>
<!-- 3 diagnostic cards -->
<div class="grid" style="margin-bottom:16px;">
<div class="card value-card animate-on-scroll">
<h2>Bank balance should be</h2>
<div class="value animate-counter" data-target="{{$d.BankShouldBe}}" data-prefix="€" style="color:var(--text);">€0.00</div>
<p style="font-size:12px; color:var(--text3); margin-top:6px;">upcoming fixed + safety buffer</p>
</div>
<div class="card value-card animate-on-scroll">
<h2>Savings rate</h2>
<div class="value {{if gt $d.SavingsRatePct 0}}positive{{else}}negative{{end}}">{{$d.SavingsRatePct}}%</div>
{{if $d.LastMonthSavingsRatePct}}
<p style="font-size:12px; margin-top:6px; {{if gt $d.SavingsRatePct $d.LastMonthSavingsRatePct}}color:var(--green){{else}}color:var(--red){{end}};">
{{if gt $d.SavingsRatePct $d.LastMonthSavingsRatePct}}↑{{else}}↓{{end}} vs last month ({{$d.LastMonthSavingsRatePct}}%)
</p>
{{end}}
</div>
<div class="card value-card animate-on-scroll">
<h2>Portfolio today</h2>
{{if $d.PortfolioValueCents}}
<div class="value animate-counter" data-target="{{$d.PortfolioValueCents}}" data-prefix="€" style="color:var(--text);">€0.00</div>
<p style="font-size:12px; margin-top:6px; {{if ge $d.PortfolioPCLCents 0}}color:var(--green){{else}}color:var(--red){{end}};">
{{if ge $d.PortfolioPCLCents 0}}+{{else}}{{end}}€{{cents (centsAbs $d.PortfolioPCLCents)}} total P&L
</p>
{{else}}
<div class="value" style="color:var(--text3); font-size:18px;">No trades yet</div>
<p style="font-size:12px; color:var(--text3); margin-top:6px;"><a href="/import" style="color:var(--accent);">Import trades →</a></p>
{{end}}
</div>
</div>
<!-- Bank math + stocks -->
<div class="grid-2" style="margin-bottom:16px;">
<div class="card animate-on-scroll">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<h2>What should be in your bank</h2>
<span style="font-size:11px; color:var(--text3);">right now</span>
</div>
{{if $d.RecurringExpenses}}
<div style="display:flex; flex-direction:column;">
{{range $d.RecurringExpenses}}
{{$color := index $d.CategoryColors .Category}}
<div style="display:flex; justify-content:space-between; align-items:center; padding:9px 0; border-bottom:1px solid var(--border);">
<span style="font-size:13px; color:var(--text2); display:flex; align-items:center; gap:7px;">
{{if $color}}<span style="width:7px;height:7px;border-radius:50%;background:{{$color}};display:inline-block;"></span>{{end}}
{{.Category}}
</span>
<span style="font-size:13px; font-weight:500; color:var(--red);"> €{{cents .MonthlyCents}}</span>
</div>
{{end}}
{{if $d.SafetyBufferCents}}
<div style="display:flex; justify-content:space-between; align-items:center; padding:9px 0; border-bottom:1px solid var(--border);">
<span style="font-size:13px; color:var(--text2);">Safety buffer (2 weeks)</span>
<span style="font-size:13px; font-weight:500; color:var(--red);"> €{{cents $d.SafetyBufferCents}}</span>
</div>
{{end}}
<div style="display:flex; justify-content:space-between; align-items:center; padding:12px 0 0 0;">
<span style="font-size:13px; font-weight:500; color:var(--text);">Minimum recommended</span>
<span class="animate-counter positive" style="font-size:16px; font-weight:600;"
data-target="{{$d.BankShouldBe}}" data-prefix="€">€0.00</span>
</div>
</div>
{{else}}
<div class="empty-state" style="padding:24px;">
<p>No recurring expenses detected yet.<br>Import a few months of transactions.</p>
</div>
{{end}}
</div>
<div class="card animate-on-scroll">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<h2>Stocks at a glance</h2>
<a href="/portfolio" style="font-size:12px; color:var(--text3);">→ portfolio</a>
</div>
{{if $d.PortfolioHoldings}}
<div style="display:flex; flex-direction:column;">
{{range $d.PortfolioHoldings}}
<div style="display:flex; align-items:center; justify-content:space-between; padding:9px 0; border-bottom:1px solid var(--border);">
<div>
<div style="font-size:13px; font-weight:600; color:var(--text);">{{.Name}}</div>
<div style="font-size:11px; color:var(--text3);">{{.ISIN}} · {{printf "%.2f" .SharesOwned}} shares</div>
</div>
<div style="text-align:right;">
<div style="font-size:13px; font-weight:500; color:var(--text);">€{{cents .CurrentValueCents}}</div>
<div style="font-size:12px; {{if ge .UnrealizedPCLCents 0}}color:var(--green){{else}}color:var(--red){{end}};">
{{pctSign .UnrealizedPCLPct}}{{printf "%.1f" .UnrealizedPCLPct}}%
</div>
</div>
</div>
{{end}}
<div style="display:flex; justify-content:space-between; align-items:center; padding:12px 0 0 0;">
<span style="font-size:13px; font-weight:500; color:var(--text);">Total</span>
<span class="animate-counter" style="font-size:15px; font-weight:600; {{if ge $d.PortfolioPCLCents 0}}color:var(--green){{else}}color:var(--red){{end}};"
data-target="{{$d.PortfolioValueCents}}" data-prefix="€">€0.00</span>
</div>
</div>
{{else}}
<div class="empty-state" style="padding:24px;">
<p>No holdings yet.<br><a href="/import" style="color:var(--accent);">Import trades →</a></p>
</div>
{{end}}
</div>
</div>
<!-- Budget health + recent activity -->
<div class="grid-2">
{{if $d.CategoryBudgets}}
<div class="card animate-on-scroll">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<h2>Budget health</h2>
<a href="/categories" style="font-size:12px; color:var(--text3);">→ categories</a>
</div>
<div style="display:flex; flex-direction:column; gap:10px;">
{{range $cat, $budget := $d.CategoryBudgets}}
{{$spent := index $d.ThisMonth.ByCategory $cat}}
{{$spentAbs := centsAbs $spent}}
{{$color := index $d.CategoryColors $cat}}
{{$over := isOver $spentAbs $budget}}
{{$pct := clampPct $spentAbs $budget}}
<div>
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
<span style="font-size:12px; color:var(--text2); display:flex; align-items:center; gap:6px;">
{{if $color}}<span style="width:7px;height:7px;border-radius:50%;background:{{$color}};display:inline-block;"></span>{{end}}
{{$cat}}
</span>
<span style="font-size:11px; {{if $over}}color:var(--red); font-weight:600;{{else}}color:var(--text3);{{end}}">
{{$pct}}%{{if $over}} ⚠{{end}}
</span>
</div>
<div style="background:var(--bg3); border-radius:99px; height:5px; overflow:hidden;">
<div style="height:100%; border-radius:99px; width:{{$pct}}%;
background:{{if $over}}var(--red){{else if $color}}{{$color}}{{else}}var(--accent){{end}};
transition:width 1s ease;"></div>
</div>
</div>
{{end}}
</div>
</div>
{{end}}
<div class="card animate-on-scroll">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<h2>Recent activity</h2>
<a href="/transactions" style="font-size:12px; color:var(--text3);">→ all transactions</a>
</div>
{{if $d.RecentTxns}}
<div style="display:flex; flex-direction:column;">
{{range $d.RecentTxns}}
{{$color := index $d.CategoryColors .Category}}
<div style="display:flex; align-items:center; justify-content:space-between; padding:9px 0; border-bottom:1px solid var(--border);">
<div style="display:flex; align-items:center; gap:10px; min-width:0;">
<div style="width:8px; height:8px; border-radius:50%; flex-shrink:0;
background:{{if $color}}{{$color}}{{else}}var(--text3){{end}};"></div>
<div style="min-width:0;">
<div style="font-size:13px; color:var(--text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:180px;">{{.Description}}</div>
<div style="font-size:11px; color:var(--text3);">{{dateShort .Date}}</div>
</div>
</div>
<div style="font-size:13px; font-weight:500; white-space:nowrap; margin-left:12px;
{{if lt .AmountCents 0}}color:var(--red){{else}}color:var(--green){{end}};">
{{if lt .AmountCents 0}}{{else}}+{{end}}€{{cents (centsAbs .AmountCents)}}
</div>
</div>
{{end}}
</div>
{{else}}
<div class="empty-state" style="padding:24px;">
No transactions yet. <a href="/import" style="color:var(--accent);">Import some!</a>
</div>
{{end}}
</div>
</div>
{{end}}