259 lines
13 KiB
HTML
259 lines
13 KiB
HTML
{{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
|
||
· <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>
|
||
|
||
{{if $d.RecurringExpenses}}
|
||
<div class="card animate-on-scroll" style="margin-top:16px;">
|
||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
|
||
<h2>Fixed costs</h2>
|
||
<span style="font-size:11px; color:var(--text3);">auto-detected · 3-month average</span>
|
||
</div>
|
||
<div style="display:flex; flex-direction:column;">
|
||
{{range $d.RecurringExpenses}}
|
||
{{$color := index $d.CategoryColors .Category}}
|
||
<div style="display:flex; align-items:center; justify-content:space-between; padding:10px 0; border-bottom:1px solid var(--border);">
|
||
<div style="display:flex; align-items:center; gap:10px;">
|
||
{{if $color}}<span style="width:9px; height:9px; border-radius:50%; background:{{$color}}; flex-shrink:0; display:inline-block;"></span>{{end}}
|
||
<div>
|
||
<div style="font-size:13px; font-weight:500; color:var(--text);">{{.Category}}</div>
|
||
<div style="font-size:11px; color:var(--text3);">committed monthly cost</div>
|
||
</div>
|
||
</div>
|
||
<div style="text-align:right;">
|
||
<div style="font-size:14px; font-weight:600; color:var(--red);">− €{{cents .MonthlyCents}}</div>
|
||
<div style="font-size:11px; color:var(--text3);">/ month</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 fixed</span>
|
||
<span style="font-size:15px; font-weight:600; color:var(--red);">− €{{cents $d.BankShouldBe}}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
{{end}}
|