From 7a2cb10c79cf4ae48c736bb7d6f541b7a1e04ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rodrigues?= Date: Sat, 13 Jun 2026 12:16:23 +0100 Subject: [PATCH 1/4] feat(finance): dark mode UI overhaul + admin seed data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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= (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 --- apps/finance/services/api/main/main.go | 3 + apps/finance/services/api/main/seed.go | 314 ++++++++++ .../services/api/main/templates/accounts.html | 45 +- .../services/api/main/templates/base.html | 584 +++++++++++++----- .../api/main/templates/categories.html | 47 +- .../api/main/templates/dashboard.html | 146 +++-- .../services/api/main/templates/import.html | 188 +++--- .../api/main/templates/portfolio.html | 381 +++++------- .../api/main/templates/projections.html | 92 ++- .../services/api/main/templates/reports.html | 81 +-- .../services/api/main/templates/sharing.html | 160 +++-- .../api/main/templates/transactions.html | 156 ++--- 12 files changed, 1439 insertions(+), 758 deletions(-) create mode 100644 apps/finance/services/api/main/seed.go diff --git a/apps/finance/services/api/main/main.go b/apps/finance/services/api/main/main.go index e525a71..bb3fce4 100644 --- a/apps/finance/services/api/main/main.go +++ b/apps/finance/services/api/main/main.go @@ -32,6 +32,9 @@ func main() { defer db.Close(ctx) store := NewStore(db) + + go SeedAdmin(ctx, store) + handler := NewHandler(store) mux := http.NewServeMux() diff --git a/apps/finance/services/api/main/seed.go b/apps/finance/services/api/main/seed.go new file mode 100644 index 0000000..a491c80 --- /dev/null +++ b/apps/finance/services/api/main/seed.go @@ -0,0 +1,314 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "time" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// SeedAdmin looks up the admin user by email (via the internal users service) +// and seeds demo data if the account has no existing transactions. +func SeedAdmin(ctx context.Context, store *Store) { + email := os.Getenv("SEED_USER_EMAIL") + if email == "" { + email = "admin@homelab.local" + } + + userID, err := lookupUserByEmail(email) + if err != nil { + slog.Warn("seed: could not resolve admin user, skipping", "email", email, "err", err) + return + } + + // idempotent — skip if any transactions already exist + existing, err := store.getTransactions(ctx, userID, bson.M{}) + if err == nil && len(existing) > 0 { + slog.Info("seed: data already present, skipping", "user_id", userID) + return + } + + slog.Info("seed: seeding demo data", "user_id", userID, "email", email) + + if err := seedAll(ctx, store, userID); err != nil { + slog.Error("seed: failed", "err", err) + } else { + slog.Info("seed: done") + } +} + +func lookupUserByEmail(email string) (string, error) { + usersURL := os.Getenv("USERS_SERVICE_URL") + if usersURL == "" { + usersURL = "http://users" + } + resp, err := http.Get(fmt.Sprintf("%s/admin/users?search=%s", usersURL, email)) + if err != nil { + return "", fmt.Errorf("users service unreachable: %w", err) + } + defer resp.Body.Close() + + var users []struct { + ID string `json:"id"` + Email string `json:"email"` + } + if err := json.NewDecoder(resp.Body).Decode(&users); err != nil { + return "", fmt.Errorf("decode users response: %w", err) + } + for _, u := range users { + if u.Email == email { + return u.ID, nil + } + } + return "", fmt.Errorf("user %q not found", email) +} + +func seedAll(ctx context.Context, store *Store, userID string) error { + // ── Accounts ───────────────────────────────────────────────────────── + checkingID := bson.NewObjectID().Hex() + savingsID := bson.NewObjectID().Hex() + creditID := bson.NewObjectID().Hex() + investID := bson.NewObjectID().Hex() + + accounts := []*Account{ + {ID: checkingID, UserID: userID, Name: "CGD Checking", Type: "checking"}, + {ID: savingsID, UserID: userID, Name: "CGD Savings", Type: "savings"}, + {ID: creditID, UserID: userID, Name: "Visa Credit", Type: "credit"}, + {ID: investID, UserID: userID, Name: "Trade Republic", Type: "securities"}, + } + for _, a := range accounts { + if err := store.createAccount(ctx, a); err != nil { + return fmt.Errorf("create account: %w", err) + } + } + + // ── Categories with budgets ─────────────────────────────────────────── + type catDef struct { + Name string + Color string + BudgetCents int64 + } + catDefs := []catDef{ + {"Groceries", "#4caf50", 30000}, + {"Food", "#ff9800", 20000}, + {"Transport", "#2196f3", 8000}, + {"Housing", "#9c27b0", 80000}, + {"Utilities", "#607d8b", 10000}, + {"Health", "#f44336", 5000}, + {"Clothing", "#e91e63", 10000}, + {"Games", "#673ab7", 3000}, + {"Entertainment", "#ff5722", 5000}, + {"Subscriptions", "#795548", 4000}, + {"Shopping", "#ff6f00", 15000}, + {"Income", "#2e7d32", 0}, + {"Investments", "#1565c0", 20000}, + {"Others", "#9e9e9e", 5000}, + } + catIDByName := make(map[string]string) + for _, cd := range catDefs { + id := bson.NewObjectID().Hex() + catIDByName[cd.Name] = id + cat := &Category{ + ID: id, + UserID: userID, + Name: cd.Name, + Color: cd.Color, + BudgetCents: cd.BudgetCents, + } + if err := store.createCategory(ctx, cat); err != nil { + return fmt.Errorf("create category %s: %w", cd.Name, err) + } + } + + // ── Transactions — 6 months of realistic Portuguese household spend ─── + now := time.Now() + var txns []Transaction + + type txDef struct { + daysAgo int + desc string + amountCents int64 + cat string + accountID string + } + + rawTxns := []txDef{ + // ── Month 0 (current month) ────────────────────────────────────── + {0, "Salary — Homelab Corp", 230000, "Income", checkingID}, + {1, "Continente Supermercado", -4230, "Groceries", checkingID}, + {2, "MEO Internet", -3999, "Utilities", checkingID}, + {3, "Glovo — Sushi House", -2150, "Food", creditID}, + {4, "Uber Lisboa", -850, "Transport", creditID}, + {5, "Steam — Elden Ring DLC", -2999, "Games", creditID}, + {6, "Pingo Doce", -3120, "Groceries", checkingID}, + {7, "Farmácia Saúde", -1540, "Health", checkingID}, + {8, "Spotify Premium", -999, "Subscriptions", creditID}, + {9, "Netflix", -1599, "Subscriptions", creditID}, + {10, "Lidl Supermercado", -5640, "Groceries", checkingID}, + {11, "Restaurante O Barrigas", -3280, "Food", creditID}, + {12, "Trade Republic — VWCE Buy", -50000, "Investments", investID}, + {13, "CP — Lisboa Cascais", -310, "Transport", creditID}, + {14, "Decathlon", -4990, "Shopping", creditID}, + {15, "Rendimento subarrendamento", 30000, "Income", checkingID}, + // ── Month 1 ────────────────────────────────────────────────────── + {30, "Salary — Homelab Corp", 230000, "Income", checkingID}, + {31, "Auchan Supermercado", -6780, "Groceries", checkingID}, + {32, "EDP Energia", -6200, "Utilities", checkingID}, + {33, "Renda apartamento", -80000, "Housing", checkingID}, + {34, "McDonald's Marquês", -1190, "Food", creditID}, + {35, "Zara — Coleção Verão", -8990, "Clothing", creditID}, + {36, "Bolt ride", -620, "Transport", creditID}, + {37, "NOS Telemóvel", -1799, "Utilities", checkingID}, + {38, "Intermarché", -4420, "Groceries", checkingID}, + {39, "Ginásio Holmes Place", -4900, "Health", checkingID}, + {40, "Amazon Prime", -799, "Subscriptions", creditID}, + {41, "Glovo — Burger King", -1890, "Food", creditID}, + {42, "Trade Republic — SXR8 Buy", -30000, "Investments", investID}, + {43, "Via Verde portagens", -920, "Transport", checkingID}, + {44, "Fnac — Livros", -2350, "Shopping", creditID}, + {45, "H&M online", -5490, "Clothing", creditID}, + // ── Month 2 ────────────────────────────────────────────────────── + {60, "Salary — Homelab Corp", 230000, "Income", checkingID}, + {61, "Continente Supermercado", -5120, "Groceries", checkingID}, + {62, "MEO Internet", -3999, "Utilities", checkingID}, + {63, "Renda apartamento", -80000, "Housing", checkingID}, + {64, "Pastelaria Batalha", -480, "Food", creditID}, + {65, "Uber Lisboa", -1240, "Transport", creditID}, + {66, "Epic Games — Fortnite", -1999, "Games", creditID}, + {67, "Lidl Supermercado", -4890, "Groceries", checkingID}, + {68, "Farmácia da Baixa", -2310, "Health", checkingID}, + {69, "Spotify Premium", -999, "Subscriptions", creditID}, + {70, "Netflix", -1599, "Subscriptions", creditID}, + {71, "Restaurante Eleven", -9400, "Food", creditID}, + {72, "Trade Republic — VWCE Buy", -50000, "Investments", investID}, + {73, "Teatro Nacional", -2500, "Entertainment", creditID}, + {74, "IKEA Lisboa", -14900, "Shopping", creditID}, + {75, "Pingo Doce", -3670, "Groceries", checkingID}, + // ── Month 3 ────────────────────────────────────────────────────── + {90, "Salary — Homelab Corp", 230000, "Income", checkingID}, + {91, "Auchan Supermercado", -7230, "Groceries", checkingID}, + {92, "EDP Energia", -5800, "Utilities", checkingID}, + {93, "Renda apartamento", -80000, "Housing", checkingID}, + {94, "KFC Colombo", -1440, "Food", creditID}, + {95, "Bolt ride", -740, "Transport", creditID}, + {96, "NOS Telemóvel", -1799, "Utilities", checkingID}, + {97, "Intermarché", -5560, "Groceries", checkingID}, + {98, "Consulta médica particular", -8000, "Health", checkingID}, + {99, "Disney+", -799, "Subscriptions", creditID}, + {100, "Amazon Prime", -799, "Subscriptions", creditID}, + {101, "Restaurante A Cevicheria", -6200, "Food", creditID}, + {102, "Trade Republic — SXR8 Buy", -30000, "Investments", investID}, + {103, "CP — InterCity Porto", -2450, "Transport", checkingID}, + {104, "Livraria Bertrand", -1890, "Shopping", creditID}, + // ── Month 4 ────────────────────────────────────────────────────── + {120, "Salary — Homelab Corp", 230000, "Income", checkingID}, + {121, "Continente Supermercado", -6010, "Groceries", checkingID}, + {122, "MEO Internet", -3999, "Utilities", checkingID}, + {123, "Renda apartamento", -80000, "Housing", checkingID}, + {124, "Glovo — Pizza Hut", -2340, "Food", creditID}, + {125, "Uber Lisboa", -990, "Transport", creditID}, + {126, "PlayStation Store", -2999, "Games", creditID}, + {127, "Lidl Supermercado", -5210, "Groceries", checkingID}, + {128, "Farmácia Saúde", -890, "Health", checkingID}, + {129, "Spotify Premium", -999, "Subscriptions", creditID}, + {130, "Netflix", -1599, "Subscriptions", creditID}, + {131, "Tasca do Chico — jantar", -5400, "Food", creditID}, + {132, "Trade Republic — VWCE Buy", -50000, "Investments", investID}, + {133, "Fnac — AirPods", -17900, "Shopping", creditID}, + {134, "Pingo Doce", -4120, "Groceries", checkingID}, + // ── Month 5 ────────────────────────────────────────────────────── + {150, "Salary — Homelab Corp", 230000, "Income", checkingID}, + {151, "Auchan Supermercado", -5890, "Groceries", checkingID}, + {152, "EDP Energia", -6400, "Utilities", checkingID}, + {153, "Renda apartamento", -80000, "Housing", checkingID}, + {154, "Nando's Lisboa", -1980, "Food", creditID}, + {155, "Bolt ride", -510, "Transport", creditID}, + {156, "NOS Telemóvel", -1799, "Utilities", checkingID}, + {157, "Intermarché", -4780, "Groceries", checkingID}, + {158, "Óculos — Ótica Avenida", -12000, "Health", checkingID}, + {159, "Disney+", -799, "Subscriptions", creditID}, + {160, "Amazon Prime", -799, "Subscriptions", creditID}, + {161, "Cinemateca Portuguesa", -600, "Entertainment", creditID}, + {162, "Trade Republic — VWCE Buy", -50000, "Investments", investID}, + {163, "Zara — Outono", -12490, "Clothing", creditID}, + {164, "Worten — Monitor", -34900, "Shopping", creditID}, + } + + for _, td := range rawTxns { + date := now.AddDate(0, 0, -td.daysAgo).Truncate(24 * time.Hour) + txns = append(txns, Transaction{ + ID: bson.NewObjectID().Hex(), + UserID: userID, + AccountID: td.accountID, + Date: date, + Description: td.desc, + AmountCents: td.amountCents, + Category: td.cat, + CreatedAt: time.Now(), + }) + } + + if err := store.createTransactions(ctx, txns); err != nil { + return fmt.Errorf("create transactions: %w", err) + } + + // ── Portfolio trades ───────────────────────────────────────────────── + trades := []Trade{ + // VWCE — Vanguard FTSE All-World + { + ID: bson.NewObjectID().Hex(), UserID: userID, + ISIN: "IE00B3RBWM25", Name: "VWCE - Vanguard All-World", + Type: "buy", Quantity: 12, PriceCents: 11820, TotalCents: 141840, + Date: now.AddDate(0, -5, 5), CreatedAt: time.Now(), + }, + { + ID: bson.NewObjectID().Hex(), UserID: userID, + ISIN: "IE00B3RBWM25", Name: "VWCE - Vanguard All-World", + Type: "buy", Quantity: 8, PriceCents: 11960, TotalCents: 95680, + Date: now.AddDate(0, -3, 12), CreatedAt: time.Now(), + }, + { + ID: bson.NewObjectID().Hex(), UserID: userID, + ISIN: "IE00B3RBWM25", Name: "VWCE - Vanguard All-World", + Type: "buy", Quantity: 6, PriceCents: 12100, TotalCents: 72600, + Date: now.AddDate(0, -1, 12), CreatedAt: time.Now(), + }, + // SXR8 — iShares Core S&P 500 + { + ID: bson.NewObjectID().Hex(), UserID: userID, + ISIN: "IE00B5BMR087", Name: "SXR8 - iShares S&P 500", + Type: "buy", Quantity: 15, PriceCents: 53200, TotalCents: 798000, + Date: now.AddDate(0, -4, 8), CreatedAt: time.Now(), + }, + { + ID: bson.NewObjectID().Hex(), UserID: userID, + ISIN: "IE00B5BMR087", Name: "SXR8 - iShares S&P 500", + Type: "buy", Quantity: 10, PriceCents: 55100, TotalCents: 551000, + Date: now.AddDate(0, -2, 3), CreatedAt: time.Now(), + }, + // EUNL — iShares Core MSCI World + { + ID: bson.NewObjectID().Hex(), UserID: userID, + ISIN: "IE00B4L5Y983", Name: "EUNL - iShares MSCI World", + Type: "buy", Quantity: 20, PriceCents: 8950, TotalCents: 179000, + Date: now.AddDate(0, -5, 20), CreatedAt: time.Now(), + }, + { + ID: bson.NewObjectID().Hex(), UserID: userID, + ISIN: "IE00B4L5Y983", Name: "EUNL - iShares MSCI World", + Type: "buy", Quantity: 10, PriceCents: 9210, TotalCents: 92100, + Date: now.AddDate(0, -2, 18), CreatedAt: time.Now(), + }, + } + + if err := store.createTrades(ctx, trades); err != nil { + return fmt.Errorf("create trades: %w", err) + } + + return nil +} diff --git a/apps/finance/services/api/main/templates/accounts.html b/apps/finance/services/api/main/templates/accounts.html index fb7a42d..8aac44c 100644 --- a/apps/finance/services/api/main/templates/accounts.html +++ b/apps/finance/services/api/main/templates/accounts.html @@ -1,14 +1,15 @@ {{define "content"}} {{$d := .}} -

Accounts

+

Accounts

-
-
-
+
+

Add Account

+ +
- +
-
+
- +
-
+
- + + + + + {{range $d.Accounts}} - - - + + + {{else}} - + + + {{end}}
NameType
NameType
{{.Name}}{{.Type}}
{{.Name}} + {{$icon := "🏦"}} + {{if eq .Type "savings"}}{{$icon = "🏧"}}{{end}} + {{if eq .Type "credit"}}{{$icon = "💳"}}{{end}} + {{if eq .Type "securities"}}{{$icon = "📈"}}{{end}} + + {{$icon}} {{.Type}} + +
No accounts yet.
No accounts yet.
@@ -47,7 +62,9 @@ {{end}} diff --git a/apps/finance/services/api/main/templates/base.html b/apps/finance/services/api/main/templates/base.html index 57f7bf7..110e52d 100644 --- a/apps/finance/services/api/main/templates/base.html +++ b/apps/finance/services/api/main/templates/base.html @@ -1,214 +1,474 @@ - + {{if .Title}}{{.Title}} — {{end}}Finance +
{{block "content" .}}{{end}}
- diff --git a/apps/finance/services/api/main/templates/categories.html b/apps/finance/services/api/main/templates/categories.html index 6746d77..48dd82b 100644 --- a/apps/finance/services/api/main/templates/categories.html +++ b/apps/finance/services/api/main/templates/categories.html @@ -4,14 +4,14 @@

Add Category

-
+
- +
@@ -22,7 +22,7 @@ - + @@ -32,28 +32,35 @@ {{range $d.Categories}} {{else}} - + + + {{end}}
Color Name Monthly Budget
- + {{.Name}} - - {{if gt .BudgetCents 0}}€{{cents .BudgetCents}}{{else}}No budget set{{end}} + + {{if gt .BudgetCents 0}}€{{cents .BudgetCents}}{{else}}No budget{{end}} -
No categories yet. Add one above.
+ No categories yet. Add one above. +
@@ -64,20 +71,17 @@ function editBudget(id) { document.getElementById('budget-display-' + id).style.display = 'none'; document.getElementById('budget-btn-' + id).style.display = 'none'; - document.getElementById('budget-edit-' + id).style.display = 'inline-flex'; - document.getElementById('budget-edit-' + id).style.gap = '6px'; - document.getElementById('budget-edit-' + id).style.alignItems = 'center'; + const edit = document.getElementById('budget-edit-' + id); + edit.style.display = 'inline-flex'; document.getElementById('budget-input-' + id).focus(); } - function cancelBudget(id) { document.getElementById('budget-display-' + id).style.display = 'inline'; - document.getElementById('budget-btn-' + id).style.display = 'inline-block'; + document.getElementById('budget-btn-' + id).style.display = 'inline-flex'; document.getElementById('budget-edit-' + id).style.display = 'none'; } - function saveBudget(id) { - const val = parseFloat(document.getElementById('budget-input-' + id).value.replace(',', '.')); + const val = parseFloat(document.getElementById('budget-input-' + id).value.replace(',', '.')); if (isNaN(val) || val < 0) return; const cents = Math.round(val * 100); fetch('/categories/' + id, { @@ -86,14 +90,15 @@ function saveBudget(id) { body: JSON.stringify({budget_cents: cents}) }).then(r => { if (!r.ok) return; - const display = document.getElementById('budget-display-' + id); - display.innerHTML = cents > 0 ? '€' + (cents / 100).toFixed(2) : 'No budget set'; + const d = document.getElementById('budget-display-' + id); + d.innerHTML = cents > 0 + ? '€' + (cents / 100).toLocaleString('pt-PT', {minimumFractionDigits:2}) + : 'No budget'; cancelBudget(id); }); } - function delCat(id) { - if (!confirm('Delete this category? Existing transactions will keep their category label.')) return; + if (!confirm('Delete this category? Transactions keep their label.')) return; fetch('/categories/' + id, {method: 'DELETE'}).then(r => { if (r.ok) document.getElementById('cat-row-' + id).remove(); }); diff --git a/apps/finance/services/api/main/templates/dashboard.html b/apps/finance/services/api/main/templates/dashboard.html index 6bd111a..a9ec5ce 100644 --- a/apps/finance/services/api/main/templates/dashboard.html +++ b/apps/finance/services/api/main/templates/dashboard.html @@ -1,48 +1,56 @@ {{define "content"}} {{$d := .}} {{$change := sub $d.ThisMonth.TotalCents $d.LastMonth.TotalCents}} -

Dashboard

- -
+
+

Dashboard

+ {{if $d.Email}}{{$d.Email}}{{end}} +
+ + +
-

This Month (Net)

+

Net This Month

€0
+ data-target="{{$d.ThisMonth.TotalCents}}" data-prefix="€">€0.00

Income

€0
+ data-target="{{$d.ThisMonthIncome}}" data-prefix="€">€0.00

Expenses

€0
+ data-target="{{$d.ThisMonthExpense}}" data-prefix="€">€0.00

vs Last Month

€0
+ data-target="{{$change}}" data-prefix="€">€0.00
- -
+ +
-

Spending by Category (This Month)

+

Spending by Category — This Month

{{if $d.ThisMonth.ByCategory}} - +
+ +
{{else}}
No spending data this month.
{{end}}
-

Balance Trend (90 days)

+

Balance Trend — 90 Days

{{if $d.BalanceTrend}} - +
+ +
{{else}} -
No transactions yet. Import some!
+
No transactions yet. Import some!
{{end}}
@@ -50,36 +58,42 @@ {{if $d.CategoryBudgets}}
-

Budget vs Actual (This Month)

-
+
+

Budget vs Actual — This Month

+ Manage budgets +
+
{{range $cat, $budget := $d.CategoryBudgets}} {{$spent := index $d.ThisMonth.ByCategory $cat}} - {{$color := index $d.CategoryColors $cat}} {{$spentAbs := centsAbs $spent}} + {{$color := index $d.CategoryColors $cat}} + {{$over := isOver $spentAbs $budget}} + {{$pct := clampPct $spentAbs $budget}}
-
- - {{if $color}}{{end}} +
+ + {{if $color}}{{end}} {{$cat}} - - €{{cents $spentAbs}} / €{{cents $budget}} + + €{{cents $spentAbs}} / €{{cents $budget}} + {{if $over}} ⚠ over budget{{end}}
-
-
+
+
{{end}}
-

Set budgets in Categories.

{{end}}
-
+

Recent Transactions

View all
@@ -97,23 +111,27 @@ {{range $d.RecentTxns}} {{$color := index $d.CategoryColors .Category}} - {{dateShort .Date}} - {{.Description}} + {{dateShort .Date}} + {{.Description}} - - {{if $color}}{{end}} + + {{if $color}}{{end}} {{.Category}} - + {{if lt .AmountCents 0}}−{{else}}+{{end}}€{{cents (centsAbs .AmountCents)}} {{else}} - No transactions yet. Import some! + + + No transactions yet. Import some! + + {{end}} @@ -121,69 +139,75 @@
+ +{{else}} + +
+
+

+ Bank Transactions +

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

+ Securities Trades +

+

+ Upload your Trade Republic securities CSV to import buy/sell trades into your portfolio. +

+
+
+ + +
+ +
+ +
+

+ After importing, visit Portfolio to see live prices and P&L. +

+
+
+
+{{end}} {{end}} diff --git a/apps/finance/services/api/main/templates/portfolio.html b/apps/finance/services/api/main/templates/portfolio.html index 296cab7..c538ba2 100644 --- a/apps/finance/services/api/main/templates/portfolio.html +++ b/apps/finance/services/api/main/templates/portfolio.html @@ -1,69 +1,84 @@ {{define "content"}} {{$d := .}} -

Portfolio

+

Portfolio

{{if $d.Holdings}} +
-
+

Total Value

-
- €{{cents $d.TotalValueCents}} -
+
€0.00
-
+

Total Cost

-
€{{cents $d.TotalCostCents}}
+
€0.00
-
-

Unrealized P&L

-
- {{pctSign $d.TotalPCLPct}}€{{cents $d.TotalPCLCents}} - ({{pctSign $d.TotalPCLPct}}{{printf "%.2f" $d.TotalPCLPct}}%) +
+

Unrealized P&L

+
€0.00
+

+ {{pctSign $d.TotalPCLPct}}{{printf "%.2f" $d.TotalPCLPct}}% +

+
+
+ +
+ +
+

Allocation

+
+
+ + +
+

Holdings

+
+ + + + + + + + + + + + + {{range $i, $h := $d.Holdings}} + + + + + + + + + {{end}} + +
AssetSharesAvg CostPriceValueP&L
+
{{.Name}}
+
{{.ISIN}}
+
{{printf "%.4f" .SharesOwned}}€{{cents .AvgEntryCents}}€{{cents .CurrentPriceCents}}€{{cents .CurrentValueCents}} +
+ {{pctSign .UnrealizedPCLPct}}{{printf "%.2f" .UnrealizedPCLPct}}% +
+
+ {{if ge .UnrealizedPCLCents 0}}+{{else}}−{{end}}€{{cents (centsAbs .UnrealizedPCLCents)}} +
+
+
+ +
+ Add trades via + Import CSV
-
-

Allocation

-
- -
-
-
- -
- - - - - - - - - - - - - - {{range $i, $h := $d.Holdings}} - - - - - - - - - - {{end}} - -
StockSharesAvg EntryCurrent PriceValueP&LReturn
{{.Name}}
{{.ISIN}}
{{printf "%.4f" .SharesOwned}}€{{cents .AvgEntryCents}}€{{cents .CurrentPriceCents}}€{{cents .CurrentValueCents}} - €{{cents .UnrealizedPCLCents}} - - {{pctSign .UnrealizedPCLPct}}{{printf "%.2f" .UnrealizedPCLPct}}% -
-
- + {{else}} -
-

No trades imported yet

-

Go to Import and upload your Trade Republic securities CSV.

+
+
📈
+

No trades yet

+

Import your Trade Republic securities CSV to see your portfolio.

+ Import Trades
{{end}} {{end}} diff --git a/apps/finance/services/api/main/templates/projections.html b/apps/finance/services/api/main/templates/projections.html index a3163a0..b48983a 100644 --- a/apps/finance/services/api/main/templates/projections.html +++ b/apps/finance/services/api/main/templates/projections.html @@ -1,62 +1,90 @@ {{define "content"}} {{$d := .}} -

Projections

+

Projections

-
+

Projected Annual Spend

-
€{{cents $d.AnnualTotal}}
+
€0.00
+

Based on 6-month average

+
+
+

Projected Monthly Spend

+ {{$monthly := div $d.AnnualTotal 12}} +
€0.00
+

Average across categories

-

Monthly Average Spend (Last 6 Months)

- +

Monthly Average by Category — Last 6 Months

+
+ +
-
- - - - - - - - - - {{range $cat, $avg := $d.MonthlyAvg}} - - - - - - {{end}} - -
CategoryMonthly AvgProjected Annual
{{$cat}}€{{printf "%.2f" $avg}}€{{printf "%.2f" (mul $avg 12)}}
+
+
+ + + + + + + + + + + {{range $cat, $avg := $d.MonthlyAvg}} + + + + + + {{end}} + +
CategoryMonthly AvgProjected AnnualPace
{{$cat}}€{{printf "%.2f" $avg}}€{{printf "%.2f" (mul $avg 12)}} +
+
+
+
+
- {{end}} diff --git a/apps/finance/services/api/main/templates/transactions.html b/apps/finance/services/api/main/templates/transactions.html index e8df6c0..3893f9a 100644 --- a/apps/finance/services/api/main/templates/transactions.html +++ b/apps/finance/services/api/main/templates/transactions.html @@ -6,9 +6,9 @@
-
+
- +
-
- +
+
- - + +
{{if or $.Cat $.Search $.Days}} @@ -56,26 +56,36 @@ {{range $d.Txns}} {{$color := index $d.CategoryColors .Category}} - {{dateShort .Date}} - {{.Description}} - + {{dateShort .Date}} + {{.Description}} + {{$name := index $d.AccountNames .AccountID}} - {{if $name}}{{$name}}{{else}}{{end}} + {{if $name}}{{$name}}{{else}}{{end}} - - {{if $color}}{{end}}{{.Category}} + + + {{if $color}}{{end}} + {{.Category}} - {{range $d.Categories}} - + {{end}} - + - + {{if lt .AmountCents 0}}−{{else}}+{{end}}€{{cents (centsAbs .AmountCents)}} @@ -83,7 +93,13 @@ {{else}} - No transactions found. Import some or . + + + No transactions found. + Import some or + . + + {{end}} @@ -91,9 +107,17 @@
-