Goals are now funded entirely through tagged transactions — no more manually-maintained saved_cents. Free cash waterfall (income → living → goals → free cash) is the single source of truth for where money goes. Core changes: - Transaction.GoalID field links outflows to goals; SavedCents is derived via MongoDB aggregation (getGoalFundedCentsAll) instead of stored - Waterfall on dashboard and goals page splits outflows into living vs goal-funded using GoalID presence - ImpactOnDisposable fixed: uses income−living−monthlyCents instead of waterfallFreeCash−monthlyCents (was double-subtracting goal spend) - avgMonthlySavings fixed: divides by positive-saving months only, and uses year+month key to avoid Dec cross-year collision Interactive waterfall drill-down: - Click Income / Living / Goals rows to expand category breakdown - Click a category to reveal individual transactions inline - All rendered server-side (instant, no extra API call) - New WaterfallRow type + IncomeCats/LivingCats/IncomeCatTxns/LivingCatTxns on DashboardData Goals page: - Summary cards switched from heuristic disposable/committed to waterfall - Each goal card shows funding history (last 5 tagged transactions) - "Fund this goal" button links to /transactions?fund_goal=<id> Transactions page: - Add Transaction modal has goal picker dropdown - submitAdd() includes goal_id in POST body - Auto-opens modal pre-selected when arriving from goals page Seed: - seedGoalTransactions() back-fills tagged contributions for all 4 demo goals (Emergency fund, House down payment, Japan trip, MacBook Pro) - Idempotent — skips if goal-tagged transactions already exist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
409 lines
20 KiB
HTML
409 lines
20 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>{{$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>
|
||
</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);
|
||
}
|
||
</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:14px;">
|
||
{{range $d.DashGoals}}
|
||
<div>
|
||
<div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:6px;">
|
||
<div style="display:flex; align-items:center; gap:8px;">
|
||
<span style="font-size:15px;">{{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);">{{.Name}}</span>
|
||
</div>
|
||
<div style="display:flex; align-items:center; gap:12px;">
|
||
<span style="font-size:12px; 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>
|
||
<div style="background:var(--bg3); border-radius:99px; height:5px; overflow:hidden;">
|
||
<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="display:flex; justify-content:space-between; margin-top:4px; font-size:11px; color:var(--text3);">
|
||
<span>€{{cents .SavedCents}} of €{{cents .TargetCents}}</span>
|
||
<span style="color:{{if .Feasible}}var(--green){{else}}var(--red){{end}};">€{{cents .MonthlyCents}}{{$d.T.Get "dashboard.goals.per_month_needed"}}</span>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
{{end}}
|