From bfd5f62a7a85670c03c3f0f24585c538fbcc3044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rodrigues?= Date: Sat, 13 Jun 2026 16:46:55 +0100 Subject: [PATCH 1/2] feat: show committed goals in fixed costs panel Committed goals now appear in the Fixed Costs panel on the dashboard with a "committed goal" label, and the total line uses TotalCommittedCents (fixed costs + goal contributions) instead of total monthly expenses. Co-Authored-By: Claude Sonnet 4.6 --- apps/finance/services/api/main/handler.go | 12 ++++++++++-- apps/finance/services/api/main/models.go | 8 +++++--- .../services/api/main/templates/dashboard.html | 13 +++++++++---- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/apps/finance/services/api/main/handler.go b/apps/finance/services/api/main/handler.go index b513101..f809c22 100644 --- a/apps/finance/services/api/main/handler.go +++ b/apps/finance/services/api/main/handler.go @@ -324,7 +324,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { // disposable income = income - fixed recurring disposableIncome := thisMonthIncome - totalFixedCents - // deduct committed goal contributions from disposable + // deduct committed goal contributions from disposable and add to fixed costs list committedGoalsCents := int64(0) if goals, err := h.store.getGoals(ctx, a.UserID); err == nil { now2 := time.Now() @@ -340,10 +340,17 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { if ml < 1 { ml = 1 } - committedGoalsCents += remaining / ml + monthly := remaining / ml + committedGoalsCents += monthly + recurringExpenses = append(recurringExpenses, RecurringExpense{ + Category: g.Name, + MonthlyCents: monthly, + IsGoal: true, + }) } } disposableIncome -= committedGoalsCents + totalCommittedCents := totalFixedCents + committedGoalsCents // variable spend so far this month (non-fixed categories, expenses only) variableSpent := int64(0) @@ -450,6 +457,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { RecurringExpenses: recurringExpenses, BankShouldBe: bankShouldBe, SafetyBufferCents: safetyBuffer, + TotalCommittedCents: totalCommittedCents, SavingsRatePct: savingsRatePct, LastMonthSavingsRatePct: lastMonthSavingsRatePct, PortfolioValueCents: portfolioValueCents, diff --git a/apps/finance/services/api/main/models.go b/apps/finance/services/api/main/models.go index 6000ee7..4ff6969 100644 --- a/apps/finance/services/api/main/models.go +++ b/apps/finance/services/api/main/models.go @@ -113,8 +113,9 @@ var FixedCategories = map[string]bool{ } type RecurringExpense struct { - Category string + Category string MonthlyCents int64 + IsGoal bool // true when this entry comes from a committed goal } type DashboardData struct { @@ -140,8 +141,9 @@ type DashboardData struct { MonthSpentPct int // % of disposable already spent RecurringExpenses []RecurringExpense - BankShouldBe int64 // sum of upcoming fixed costs + safety buffer - SafetyBufferCents int64 + BankShouldBe int64 + SafetyBufferCents int64 + TotalCommittedCents int64 // sum of all fixed costs + committed goals SavingsRatePct int // savings / income * 100 this month LastMonthSavingsRatePct int diff --git a/apps/finance/services/api/main/templates/dashboard.html b/apps/finance/services/api/main/templates/dashboard.html index 48fc4de..ff41a45 100644 --- a/apps/finance/services/api/main/templates/dashboard.html +++ b/apps/finance/services/api/main/templates/dashboard.html @@ -236,10 +236,14 @@ {{$color := index $d.CategoryColors .Category}}
- {{if $color}}{{end}} + {{if .IsGoal}} + + {{else if $color}} + + {{end}}
{{.Category}}
-
committed monthly cost
+
{{if .IsGoal}}committed goal{{else}}recurring expense{{end}}
@@ -249,8 +253,9 @@
{{end}}
- Total fixed - − €{{cents $d.BankShouldBe}} + Total committed + €0
From 42f5b0df4d30eac0ee021d014446c74381244c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rodrigues?= Date: Sat, 13 Jun 2026 16:52:57 +0100 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20phase=204=20=E2=80=94=20net=20worth?= =?UTF-8?q?=20page=20+=20dashboard=20card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds /networth page with hero total, cash/portfolio breakdown cards, and a month-by-month historical chart. Also shows net worth as a value card on the dashboard with a link to the full breakdown. Co-Authored-By: Claude Sonnet 4.6 --- apps/finance/services/api/main/handler.go | 91 +++++++++++++ apps/finance/services/api/main/models.go | 27 ++++ .../services/api/main/templates/base.html | 1 + .../api/main/templates/dashboard.html | 7 + .../services/api/main/templates/networth.html | 125 ++++++++++++++++++ 5 files changed, 251 insertions(+) create mode 100644 apps/finance/services/api/main/templates/networth.html diff --git a/apps/finance/services/api/main/handler.go b/apps/finance/services/api/main/handler.go index f809c22..2e05ca5 100644 --- a/apps/finance/services/api/main/handler.go +++ b/apps/finance/services/api/main/handler.go @@ -116,6 +116,7 @@ var ( portfolioTmpl = parseTmpl("templates/base.html", "templates/portfolio.html") sharingTmpl = parseTmpl("templates/base.html", "templates/sharing.html") goalsTmpl = parseTmpl("templates/base.html", "templates/goals.html") + networthTmpl = parseTmpl("templates/base.html", "templates/networth.html") ) type authInfo struct { @@ -464,6 +465,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { PortfolioPCLCents: portfolioPCLCents, PortfolioHoldings: portfolioHoldings, PortfolioPricesAvailable: portfolioPricesAvailable, + NetWorthCents: portfolioValueCents + running, }) } @@ -1543,6 +1545,94 @@ func parseFloat(s string) float64 { return f } +func (h *Handler) NetWorth(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + a := getAuth(r) + + txns, err := h.store.getTransactions(ctx, a.UserID, bson.M{}) + if err != nil { + slog.Error("networth get transactions", "err", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + // build a running total per month for cash (non-credit accounts treated as assets, + // credit accounts as liabilities) + // We don't have account-type info on transactions, so we use signing convention: + // Income category = income (+), everything else = expense (−). + // For a simple net-worth history: sum all transaction amounts cumulatively per month. + type monthKey = string + monthCash := make(map[monthKey]int64) + var months []string + seen := make(map[monthKey]bool) + + // cumulative running balance across all txns sorted by date (store returns desc; reverse) + // running cumulative balance — reset is not possible so we track running sum + runningBalance := int64(0) + // txns are sorted desc by date; reverse to process oldest first + for i := len(txns) - 1; i >= 0; i-- { + t := txns[i] + runningBalance += t.AmountCents + mk := t.Date.Format("2006-01") + monthCash[mk] = runningBalance + if !seen[mk] { + seen[mk] = true + months = append(months, mk) + } + } + sortStrings(months) + + // current cash = running balance at end of all transactions + cashCents := runningBalance + + // portfolio + var portfolioCents int64 + var pricesAvailable bool + if trades, err2 := h.store.getTrades(ctx, a.UserID); err2 == nil && len(trades) > 0 { + prices, _ := fetchPricesByISIN(uniqueISINs(trades)) + holdings := computeHoldings(trades, prices) + pr := aggregatePortfolio(holdings) + for _, p := range prices { + if p > 0 { + pricesAvailable = true + break + } + } + if pricesAvailable { + portfolioCents = pr.TotalVal + } else { + portfolioCents = pr.TotalCost + } + } + + netWorthCents := cashCents + portfolioCents + + // build history points — each month: cash snapshot + portfolio (we only have current portfolio) + var history []NetWorthPoint + for _, m := range months { + cash := monthCash[m] + history = append(history, NetWorthPoint{ + Month: m, + AssetCents: cash + portfolioCents, + LiabCents: 0, + NetCents: cash + portfolioCents, + }) + } + + render(w, networthTmpl, &NetWorthData{ + UserID: a.UserID, + Email: a.Email, + Title: "Net Worth", + Route: "networth", + CashCents: cashCents, + PortfolioCents: portfolioCents, + CreditCents: 0, + NetWorthCents: netWorthCents, + PortfolioPricesAvailable: pricesAvailable, + History: history, + }) +} + func (h *Handler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /{$}", h.Dashboard) mux.HandleFunc("GET /transactions", h.Transactions) @@ -1562,6 +1652,7 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /portfolio", h.Portfolio) mux.HandleFunc("GET /goals", h.Goals) mux.HandleFunc("POST /goals", h.Goals) + mux.HandleFunc("GET /networth", h.NetWorth) mux.HandleFunc("GET /sharing", h.Sharing) mux.HandleFunc("POST /sharing", h.Sharing) mux.HandleFunc("DELETE /sharing/{viewer_id}", h.Sharing) diff --git a/apps/finance/services/api/main/models.go b/apps/finance/services/api/main/models.go index 4ff6969..455530c 100644 --- a/apps/finance/services/api/main/models.go +++ b/apps/finance/services/api/main/models.go @@ -152,6 +152,8 @@ type DashboardData struct { PortfolioPCLCents int64 PortfolioHoldings []Holding PortfolioPricesAvailable bool + + NetWorthCents int64 } type PeriodSummary struct { @@ -217,6 +219,31 @@ type SharingUser struct { Email string } +type NetWorthPoint struct { + Month string // "2025-01" + AssetCents int64 + LiabCents int64 + NetCents int64 +} + +type NetWorthData struct { + UserID string + Email string + Title string + Route string + + // current snapshot + CashCents int64 // running balance of all non-credit accounts + PortfolioCents int64 // market value (or cost basis) + CreditCents int64 // total outstanding on credit accounts (positive = owed) + NetWorthCents int64 // cash + portfolio − credit + + PortfolioPricesAvailable bool + + // month-by-month history + History []NetWorthPoint +} + // GoalType classifies a financial goal for display and calculation purposes. type GoalType string diff --git a/apps/finance/services/api/main/templates/base.html b/apps/finance/services/api/main/templates/base.html index 4f78d27..cd9736f 100644 --- a/apps/finance/services/api/main/templates/base.html +++ b/apps/finance/services/api/main/templates/base.html @@ -400,6 +400,7 @@ Projections Portfolio Goals + Net Worth Sharing {{.Email}} diff --git a/apps/finance/services/api/main/templates/dashboard.html b/apps/finance/services/api/main/templates/dashboard.html index ff41a45..06cf22d 100644 --- a/apps/finance/services/api/main/templates/dashboard.html +++ b/apps/finance/services/api/main/templates/dashboard.html @@ -55,6 +55,13 @@ {{end}} +
+

Net worth

+
€0.00
+

→ full breakdown

+
+

Portfolio today

{{if $d.PortfolioHoldings}} diff --git a/apps/finance/services/api/main/templates/networth.html b/apps/finance/services/api/main/templates/networth.html new file mode 100644 index 0000000..f8de88f --- /dev/null +++ b/apps/finance/services/api/main/templates/networth.html @@ -0,0 +1,125 @@ +{{define "content"}} +{{$d := .}} + +
+

Net Worth

+ {{if $d.Email}}{{$d.Email}}{{end}} +
+ + +
+
+ Total net worth + + cash balance + portfolio + +
+
€0.00
+
+ + +
+ +
+

Cash balance

+
€0.00
+

all transaction history

+
+ +
+

Portfolio{{if not $d.PortfolioPricesAvailable}} (cost basis){{end}}

+
€0.00
+ {{if $d.PortfolioPricesAvailable}} +

market value

+ {{else}} +

prices unavailable · cost basis shown

+ {{end}} +
+ + {{if $d.CreditCents}} +
+

Credit / liabilities

+
€0.00
+

outstanding balance

+
+ {{end}} + +
+ + +{{if $d.History}} +
+
+

Net worth over time

+ cumulative · all time +
+ +
+ + +{{else}} +
+
📊
+

No transaction history yet

+

Import some transactions to see your net worth over time.

+ Import transactions → +
+{{end}} + +{{end}}