Gonçalo Rodrigues e93cb38756 feat(finance): interactive waterfall + goal auto-tag + free cash prompt
- Waterfall now drills down: click Income/Living/Goals to expand
  category breakdown, click a category to see its transactions
- Goal contributions are now transaction-backed (GoalID on Transaction,
  SavedCents derived from MongoDB aggregation)
- Dashboard goals widget shows this-month funding status per goal
- Goals page lists funding history transactions per goal
- Transactions modal accepts a goal pre-selection (?fund_goal=<id>)
- Categories can auto-tag a linked goal on expense creation
- Settings → categories shows linked goal column and edit modal
- Free cash "what now?" section lists underfunded committed goals
  with shortfall and Fund → links; shows success state when all met
- i18n: full EN/PT coverage for all new keys
- Seed data includes goal-tagged transactions so progress is non-zero
- Bug fixes: ImpactOnDisposable double-subtraction, avgMonthlySavings
  denominator using only positive-savings months, cross-year month key

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 22:35:29 +01:00

474 lines
24 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>{{$d.T.Get "dashboard.title"}}</h1>
<span class="text-muted">{{if $d.Email}}{{$d.Email}}{{end}}</span>
</div>
{{if $d.Alerts}}
<div style="display:flex; flex-direction:column; gap:8px; margin-bottom:16px;">
{{range $d.Alerts}}
<div style="display:flex; align-items:flex-start; gap:10px; padding:12px 16px; border-radius:10px; font-size:13px;
{{if eq .Level "danger"}}background:rgba(248,113,113,0.08); border:1px solid rgba(248,113,113,0.25); color:var(--red);
{{else if eq .Level "warn"}}background:rgba(245,158,11,0.08); border:1px solid rgba(245,158,11,0.25); color:#f59e0b;
{{else}}background:rgba(105,121,248,0.08); border:1px solid rgba(105,121,248,0.25); color:var(--accent);{{end}}">
<span style="flex-shrink:0; font-size:15px;">{{if eq .Level "danger"}}🔴{{else if eq .Level "warn"}}⚠{{else}}{{end}}</span>
<span>{{.Message}}</span>
</div>
{{end}}
</div>
{{end}}
<!-- HERO: interactive cash flow waterfall -->
<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:4px;">
{{$d.T.Get "dashboard.waterfall.title"}}
</div>
<!-- Income row -->
{{if $d.IncomeCats}}
<div class="wf-section">
<button class="wf-row" onclick="wfToggle('income')" aria-expanded="false">
<span class="wf-chevron" id="wf-chev-income"></span>
<span class="wf-row-label">{{$d.T.Get "dashboard.waterfall.income"}}</span>
<span class="wf-row-amt positive">+€{{cents $d.WaterfallIncome}}</span>
</button>
<div class="wf-detail" id="wf-income" style="display:none;">
{{range $i, $row := $d.IncomeCats}}
<div class="wf-cat-section">
<button class="wf-cat-row" onclick="wfToggleCat('ic{{$i}}')">
{{if $row.Color}}<span class="cat-dot" style="background:{{$row.Color}};"></span>{{end}}
<span style="flex:1; font-size:12px;">{{$row.Name}}</span>
<span class="wf-cat-chev" id="wf-cc-ic{{$i}}"></span>
<span style="font-size:12px; font-weight:500; color:var(--green);">+€{{cents $row.Cents}}</span>
</button>
<div class="wf-txn-list" id="wf-ic{{$i}}" style="display:none;">
{{range index $d.IncomeCatTxns $row.Name}}
<div class="wf-txn-row">
<span class="wf-txn-date">{{dateShort .Date}}</span>
<span class="wf-txn-desc">{{.Description}}</span>
<span class="wf-txn-amt positive">+€{{cents .AmountCents}}</span>
</div>
{{end}}
</div>
</div>
{{end}}
</div>
</div>
{{else}}
<div class="wf-row" style="cursor:default;">
<span class="wf-row-label" style="padding-left:20px;">{{$d.T.Get "dashboard.waterfall.income"}}</span>
<span class="wf-row-amt positive">+€{{cents $d.WaterfallIncome}}</span>
</div>
{{end}}
<!-- Living expenses row -->
{{if $d.LivingCats}}
<div class="wf-section">
<button class="wf-row" onclick="wfToggle('living')" aria-expanded="false">
<span class="wf-chevron" id="wf-chev-living"></span>
<span class="wf-row-label">{{$d.T.Get "dashboard.waterfall.living"}}</span>
<span class="wf-row-amt" style="color:var(--red);">−€{{cents $d.WaterfallLiving}}</span>
</button>
<div class="wf-detail" id="wf-living" style="display:none;">
{{range $i, $row := $d.LivingCats}}
<div class="wf-cat-section">
<button class="wf-cat-row" onclick="wfToggleCat('lc{{$i}}')">
{{if $row.Color}}<span class="cat-dot" style="background:{{$row.Color}};"></span>{{end}}
<span style="flex:1; font-size:12px;">{{$row.Name}}</span>
<span class="wf-cat-chev" id="wf-cc-lc{{$i}}"></span>
<span style="font-size:12px; font-weight:500; color:var(--red);">−€{{cents $row.Cents}}</span>
</button>
<div class="wf-txn-list" id="wf-lc{{$i}}" style="display:none;">
{{range index $d.LivingCatTxns $row.Name}}
<div class="wf-txn-row">
<span class="wf-txn-date">{{dateShort .Date}}</span>
<span class="wf-txn-desc">{{.Description}}</span>
<span class="wf-txn-amt" style="color:var(--red);">−€{{cents (abs .AmountCents)}}</span>
</div>
{{end}}
</div>
</div>
{{end}}
</div>
</div>
{{else if gt $d.WaterfallLiving 0}}
<div class="wf-row" style="cursor:default;">
<span class="wf-row-label" style="padding-left:20px;">{{$d.T.Get "dashboard.waterfall.living"}}</span>
<span class="wf-row-amt" style="color:var(--red);">−€{{cents $d.WaterfallLiving}}</span>
</div>
{{end}}
<!-- Goal contributions row -->
{{if gt $d.WaterfallGoals 0}}
<div class="wf-section">
{{if $d.DashGoals}}
<button class="wf-row" onclick="wfToggle('goals')" aria-expanded="false">
<span class="wf-chevron" id="wf-chev-goals"></span>
<span class="wf-row-label">{{$d.T.Get "dashboard.waterfall.goals"}}</span>
<span class="wf-row-amt" style="color:var(--accent);">−€{{cents $d.WaterfallGoals}}</span>
</button>
<div class="wf-detail" id="wf-goals" style="display:none;">
{{range $d.DashGoals}}
{{$funded := index $d.GoalFundedThisMonth .ID}}
{{if gt $funded 0}}
<div class="wf-cat-row" style="cursor:default;">
<span style="font-size:11px;">{{if eq .Type "once"}}🎯{{else if eq .Type "deposit"}}🏠{{else if eq .Type "emergency"}}🛡️{{else}}📈{{end}}</span>
<span style="flex:1; font-size:12px;">{{.Name}}</span>
<a href="/goals" style="font-size:11px; color:var(--accent); text-decoration:none; margin-right:8px;">{{$d.T.Get "dashboard.waterfall.goals_link"}}</a>
<span style="font-size:12px; font-weight:500; color:var(--accent);">−€{{cents $funded}}</span>
</div>
{{end}}
{{end}}
</div>
{{else}}
<div class="wf-row" style="cursor:default;">
<span class="wf-row-label" style="padding-left:20px; display:flex; align-items:center; gap:8px;">
{{$d.T.Get "dashboard.waterfall.goals"}}
<a href="/goals" style="font-size:11px; color:var(--accent); text-decoration:none;">{{$d.T.Get "dashboard.waterfall.goals_link"}}</a>
</span>
<span class="wf-row-amt" style="color:var(--accent);">−€{{cents $d.WaterfallGoals}}</span>
</div>
{{end}}
</div>
{{end}}
<!-- Free cash total -->
<div style="display:flex; justify-content:space-between; align-items:center; padding:14px 0 0 0; margin-top:4px; border-top:1px solid var(--border);">
<span style="font-size:14px; font-weight:600; color:var(--text);">{{$d.T.Get "dashboard.waterfall.free_cash"}}</span>
<div class="animate-counter {{if lt $d.WaterfallFreeCash 0}}negative{{else}}positive{{end}}"
style="font-size:32px; font-weight:500; letter-spacing:-1px; line-height:1;"
data-target="{{$d.WaterfallFreeCash}}" data-prefix="€">€0</div>
</div>
<!-- Month progress bar -->
<div style="margin-top:16px; padding-top:14px; border-top:1px solid var(--border);">
<div style="display:flex; justify-content:space-between; font-size:11px; color:var(--text3); margin-bottom:5px;">
<span>{{$d.T.Get "dashboard.waterfall.month_progress"}}</span>
<span>{{$d.MonthProgressPct}}%</span>
</div>
<div style="background:var(--bg3); border-radius:99px; height:4px; overflow:hidden;">
<div style="height:100%; border-radius:99px; width:{{$d.MonthProgressPct}}%;
background:var(--text3); transition:width 1s ease;"></div>
</div>
</div>
<!-- What now? prompt — underfunded committed goals -->
{{if $d.DashGoals}}
<div style="margin-top:16px; padding-top:14px; border-top:1px solid var(--border);">
<div style="font-size:11px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:0.06em; margin-bottom:8px;">{{$d.T.Get "dashboard.waterfall.what_now"}}</div>
<div id="wf-what-now-list">
{{range $d.DashGoals}}
{{if .Committed}}
{{$funded := index $d.GoalFundedThisMonth .ID}}
{{$needed := .MonthlyCents}}
{{if lt $funded $needed}}
{{$shortfall := sub $needed $funded}}
<div style="display:flex; justify-content:space-between; align-items:center; padding:5px 0; font-size:13px;">
<span style="color:var(--text2);">{{.Name}} <span style="color:var(--red);">−€{{cents $shortfall}}</span></span>
<a href="/transactions?fund_goal={{.ID}}" style="font-size:12px; color:var(--accent); text-decoration:none; font-weight:600;">{{$d.T.Get "dashboard.waterfall.fund_link"}} →</a>
</div>
{{end}}
{{end}}
{{end}}
</div>
<div id="wf-all-funded" style="display:none; font-size:13px; color:var(--green);">{{$d.T.Get "dashboard.waterfall.all_funded"}}</div>
</div>
{{end}}
</div>
<style>
.wf-section { border-bottom: 1px solid var(--border); }
.wf-section:last-of-type { border-bottom: none; }
.wf-row {
display: flex; align-items: center; gap: 8px;
width: 100%; padding: 10px 0;
background: none; border: none; cursor: pointer; text-align: left;
color: var(--text);
}
.wf-row:hover .wf-row-label { color: var(--text); }
.wf-chevron {
font-size: 14px; color: var(--text3); width: 14px; flex-shrink: 0;
transition: transform 0.18s ease; display: inline-block;
transform-origin: center 45%;
}
.wf-chevron.open { transform: rotate(90deg); }
.wf-row-label { flex: 1; font-size: 13px; color: var(--text2); }
.wf-row-amt { font-size: 15px; font-weight: 600; white-space: nowrap; }
.wf-detail { padding: 4px 0 8px 22px; }
.wf-cat-section { }
.wf-cat-row {
display: flex; align-items: center; gap: 7px;
width: 100%; padding: 6px 0;
background: none; border: none; cursor: pointer; text-align: left;
color: var(--text2);
}
.wf-cat-chev {
font-size: 11px; color: var(--text3); width: 10px;
transition: transform 0.15s ease; display: inline-block;
transform-origin: center 45%;
}
.wf-cat-chev.open { transform: rotate(90deg); }
.cat-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.wf-txn-list { padding: 2px 0 6px 14px; }
.wf-txn-row {
display: flex; align-items: center; gap: 8px;
padding: 5px 0; border-bottom: 1px solid var(--border);
font-size: 12px;
}
.wf-txn-row:last-child { border-bottom: none; }
.wf-txn-date { color: var(--text3); white-space: nowrap; min-width: 44px; }
.wf-txn-desc { flex: 1; color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wf-txn-amt { white-space: nowrap; font-weight: 500; }
</style>
<script>
function wfToggle(id) {
const detail = document.getElementById('wf-' + id);
const chev = document.getElementById('wf-chev-' + id);
const open = detail.style.display !== 'none';
detail.style.display = open ? 'none' : 'block';
if (chev) chev.classList.toggle('open', !open);
}
function wfToggleCat(id) {
const list = document.getElementById('wf-' + id);
const chev = document.getElementById('wf-cc-' + id);
const open = list.style.display !== 'none';
list.style.display = open ? 'none' : 'block';
if (chev) chev.classList.toggle('open', !open);
}
(function() {
const list = document.getElementById('wf-what-now-list');
const allFunded = document.getElementById('wf-all-funded');
if (list && allFunded && !list.children.length) allFunded.style.display = '';
})();
</script>
<!-- 3 diagnostic cards -->
<div class="grid" style="margin-bottom:16px;">
<div class="card value-card animate-on-scroll">
<h2>{{$d.T.Get "dashboard.cards.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}} {{$d.T.Get "dashboard.alerts.vs_last_month_up"}} ({{$d.LastMonthSavingsRatePct}}%)
</p>
{{end}}
</div>
<div class="card value-card animate-on-scroll">
<h2>{{$d.T.Get "dashboard.cards.net_worth"}}</h2>
<div class="value animate-counter {{if lt $d.NetWorthCents 0}}negative{{else}}positive{{end}}"
data-target="{{$d.NetWorthCents}}" data-prefix="€">€0.00</div>
<p style="font-size:12px; color:var(--text3); margin-top:6px;"><a href="/networth" style="color:var(--accent);">{{$d.T.Get "dashboard.cards.net_worth_link"}}</a></p>
</div>
<div class="card value-card animate-on-scroll">
<h2>{{$d.T.Get "dashboard.cards.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;">{{$d.T.Get "dashboard.cards.portfolio_cost_basis"}}</p>
{{end}}
{{else}}
<div class="value" style="color:var(--text3); font-size:18px;">{{$d.T.Get "dashboard.cards.portfolio_no_trades"}}</div>
<p style="font-size:12px; color:var(--text3); margin-top:6px;"><a href="/import" style="color:var(--accent);">{{$d.T.Get "dashboard.cards.portfolio_import_link"}}</a></p>
{{end}}
</div>
</div>
<!-- Stocks + spending breakdown -->
<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>{{$d.T.Get "dashboard.stocks.section_title"}}</h2>
<a href="/portfolio" style="font-size:12px; color:var(--text3);">{{$d.T.Get "dashboard.stocks.portfolio_link"}}</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}} {{$d.T.Get "dashboard.stocks.shares_label"}}</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);">{{$d.T.Get "dashboard.stocks.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);">{{$d.T.Get "dashboard.stocks.total_label"}}{{if not $d.PortfolioPricesAvailable}} {{$d.T.Get "dashboard.stocks.total_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>{{$d.T.Get "dashboard.stocks.no_holdings_msg"}}<br><a href="/import" style="color:var(--accent);">{{$d.T.Get "dashboard.stocks.import_link"}}</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>{{$d.T.Get "dashboard.budget_health.section_title"}}</h2>
<a href="/categories" style="font-size:12px; color:var(--text3);">{{$d.T.Get "dashboard.budget_health.categories_link"}}</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>{{$d.T.Get "dashboard.recent.section_title"}}</h2>
<a href="/transactions" style="font-size:12px; color:var(--text3);">{{$d.T.Get "dashboard.recent.all_txns_link"}}</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;">
{{$d.T.Get "dashboard.recent.no_txns_msg"}} <a href="/import" style="color:var(--accent);">{{$d.T.Get "dashboard.recent.import_link"}}</a>
</div>
{{end}}
</div>
</div>
{{if $d.DashGoals}}
<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>{{$d.T.Get "dashboard.goals.section_title"}}</h2>
<a href="/goals" style="font-size:12px; color:var(--text3);">{{$d.T.Get "dashboard.goals.all_goals_link"}}</a>
</div>
<div style="display:flex; flex-direction:column; gap:18px;">
{{range $d.DashGoals}}
{{$funded := index $d.GoalFundedThisMonth .ID}}
{{$needed := .MonthlyCents}}
{{$met := ge $funded $needed}}
{{$partial := and (gt $funded 0) (not $met)}}
{{$monthPct := 0}}
{{if gt $needed 0}}{{$monthPct = clampPct $funded $needed}}{{end}}
<div>
<!-- name + overall progress -->
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:5px;">
<div style="display:flex; align-items:center; gap:8px; min-width:0;">
<span style="font-size:14px; flex-shrink:0;">{{if eq .Type "once"}}🎯{{else if eq .Type "deposit"}}🏠{{else if eq .Type "emergency"}}🛡️{{else}}📈{{end}}</span>
<span style="font-size:13px; font-weight:500; color:var(--text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{.Name}}</span>
</div>
<div style="display:flex; align-items:center; gap:10px; flex-shrink:0; margin-left:12px;">
<span style="font-size:11px; color:var(--text3);">{{.MonthsLeft}}{{$d.T.Get "dashboard.goals.months_left"}}</span>
<span style="font-size:12px; font-weight:600; color:{{if .Feasible}}var(--green){{else}}var(--red){{end}};">{{.ProgressPct}}%</span>
</div>
</div>
<!-- overall progress bar -->
<div style="background:var(--bg3); border-radius:99px; height:4px; overflow:hidden; margin-bottom:3px;">
<div style="height:100%; border-radius:99px; width:{{.ProgressPct}}%;
background:{{if .Feasible}}var(--green){{else}}var(--accent){{end}};
transition:width 1s ease;"></div>
</div>
<div style="font-size:11px; color:var(--text3); margin-bottom:10px;">
€{{cents .SavedCents}} {{$d.T.Get "dashboard.goals.of"}} €{{cents .TargetCents}}
</div>
<!-- this-month funding status -->
{{if gt $needed 0}}
<div style="background:var(--bg2); border-radius:8px; padding:8px 12px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
<span style="font-size:11px; color:var(--text3); text-transform:uppercase; letter-spacing:.04em;">{{$d.T.Get "dashboard.goals.this_month"}}</span>
{{if $met}}
<span style="font-size:11px; font-weight:600; color:var(--green);">✓ {{$d.T.Get "dashboard.goals.funded"}}</span>
{{else if $partial}}
<a href="/transactions?fund_goal={{.ID}}" style="font-size:11px; color:var(--accent); text-decoration:none;">{{$d.T.Get "dashboard.goals.fund_link"}} →</a>
{{else}}
<a href="/transactions?fund_goal={{.ID}}" style="font-size:11px; color:var(--red); text-decoration:none; font-weight:500;">{{$d.T.Get "dashboard.goals.fund_link"}} →</a>
{{end}}
</div>
<div style="background:var(--bg3); border-radius:99px; height:3px; overflow:hidden; margin-bottom:5px;">
<div style="height:100%; border-radius:99px; width:{{$monthPct}}%;
background:{{if $met}}var(--green){{else if $partial}}var(--accent){{else}}var(--red){{end}};
transition:width 1s ease;"></div>
</div>
<div style="font-size:11px; {{if $met}}color:var(--green){{else if $partial}}color:var(--text2){{else}}color:var(--red){{end}};">
{{if $met}}
€{{cents $funded}} {{$d.T.Get "dashboard.goals.of"}} €{{cents $needed}} {{$d.T.Get "dashboard.goals.needed"}}
{{else if $partial}}
€{{cents $funded}} {{$d.T.Get "dashboard.goals.of"}} €{{cents $needed}} — €{{cents (sub $needed $funded)}} {{$d.T.Get "dashboard.goals.left_to_fund"}}
{{else}}
€{{cents $needed}} {{$d.T.Get "dashboard.goals.needed_this_month"}}
{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
</div>
</div>
{{end}}
{{end}}