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>
This commit is contained in:
parent
b27268febe
commit
5412dda2ac
@ -223,7 +223,8 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
for _, c := range cats {
|
||||
catNames[c.Name] = c.Name
|
||||
catColors[c.Name] = c.Color
|
||||
if c.BudgetCents > 0 {
|
||||
// exclude fixed categories from budget health — they're committed costs, not variable spend
|
||||
if c.BudgetCents > 0 && !FixedCategories[c.Name] {
|
||||
catBudgets[c.Name] = c.BudgetCents
|
||||
}
|
||||
}
|
||||
@ -379,15 +380,30 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
lastMonthSavingsRatePct = int(float64(lastMonthSavings) / float64(lastMonthIncome) * 100)
|
||||
}
|
||||
|
||||
// portfolio snapshot (best-effort, ignore errors)
|
||||
// portfolio snapshot — degrade to cost basis if live prices are unavailable
|
||||
var portfolioValueCents, portfolioPCLCents int64
|
||||
var portfolioHoldings []Holding
|
||||
var portfolioPricesAvailable bool
|
||||
if trades, err := h.store.getTrades(ctx, a.UserID); err == nil && len(trades) > 0 {
|
||||
if prices, err := fetchPricesByISIN(uniqueISINs(trades)); err == nil {
|
||||
pr := aggregatePortfolio(computeHoldings(trades, prices))
|
||||
prices, _ := fetchPricesByISIN(uniqueISINs(trades))
|
||||
holdings := computeHoldings(trades, prices)
|
||||
pr := aggregatePortfolio(holdings)
|
||||
portfolioHoldings = pr.Holdings
|
||||
|
||||
// check whether any prices came back
|
||||
for _, p := range prices {
|
||||
if p > 0 {
|
||||
portfolioPricesAvailable = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if portfolioPricesAvailable {
|
||||
portfolioValueCents = pr.TotalVal
|
||||
portfolioPCLCents = pr.TotalPCL
|
||||
portfolioHoldings = pr.Holdings
|
||||
} else {
|
||||
// fall back to cost basis so the card is still useful
|
||||
portfolioValueCents = pr.TotalCost
|
||||
}
|
||||
}
|
||||
|
||||
@ -417,6 +433,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
PortfolioValueCents: portfolioValueCents,
|
||||
PortfolioPCLCents: portfolioPCLCents,
|
||||
PortfolioHoldings: portfolioHoldings,
|
||||
PortfolioPricesAvailable: portfolioPricesAvailable,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -149,6 +149,7 @@ type DashboardData struct {
|
||||
PortfolioValueCents int64
|
||||
PortfolioPCLCents int64
|
||||
PortfolioHoldings []Holding
|
||||
PortfolioPricesAvailable bool
|
||||
}
|
||||
|
||||
type PeriodSummary struct {
|
||||
|
||||
@ -57,12 +57,16 @@
|
||||
|
||||
<div class="card value-card animate-on-scroll">
|
||||
<h2>Portfolio today</h2>
|
||||
{{if $d.PortfolioValueCents}}
|
||||
{{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}}
|
||||
@ -120,19 +124,24 @@
|
||||
<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 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</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}};"
|
||||
<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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user