Gonçalo Rodrigues 5412dda2ac fix: dashboard investment display and budget health
- Exclude FixedCategories (Housing, Utilities, Subscriptions, Investments)
  from budget health panel — they are committed costs, not variable spend
- Portfolio card and stocks panel now degrade gracefully to cost basis
  when Yahoo Finance prices are unavailable, instead of showing €0
- Stocks panel shows "cost basis" label per holding when live prices
  are not available

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

228 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.PortfolioHoldings}}
<div class="value animate-counter" data-target="{{$d.PortfolioValueCents}}" data-prefix="€" style="color:var(--text);">€0.00</div>
{{if $d.PortfolioPricesAvailable}}
<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}}
<p style="font-size:12px; color:var(--text3); margin-top:6px;">cost basis · prices unavailable</p>
{{end}}
{{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);">{{printf "%.4f" .SharesOwned}} shares</div>
</div>
<div style="text-align:right;">
{{if $d.PortfolioPricesAvailable}}
<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>
{{else}}
<div style="font-size:13px; font-weight:500; color:var(--text);">€{{cents .TotalCostCents}}</div>
<div style="font-size:11px; color:var(--text3);">cost basis</div>
{{end}}
</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{{if not $d.PortfolioPricesAvailable}} invested{{end}}</span>
<span class="animate-counter" style="font-size:15px; font-weight:600; color:var(--text);"
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}}