Gonçalo Rodrigues be0c2bd89e feat: phase 2 — goals explore mode
Adds a Goals page where users can plan financial goals before committing
to them. Each goal shows:

- Required monthly contribution to hit the deadline
- Months remaining vs months at current savings rate
- Disposable income impact (what's left after the contribution)
- Feasibility banner (green if on track, red with month delta if not)
- Progress bar once savings are tracked

Goal types: one-off purchase, deposit/down-payment, emergency fund,
recurring investment — each with a description hint in the creation modal.

Data: Goal model + store CRUD in finance_goals collection.
Nav: Goals tab added between Portfolio and Sharing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 16:20:46 +01:00

199 lines
9.7 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>Goals</h1>
<button onclick="document.getElementById('new-goal-modal').style.display='flex'"
class="btn btn-primary btn-sm">+ New goal</button>
</div>
{{if $d.AvgMonthlySavings}}
<div style="display:flex; gap:10px; margin-bottom:20px; flex-wrap:wrap;">
<div class="card value-card animate-on-scroll" style="flex:1; min-width:160px;">
<h2>Avg monthly savings</h2>
<div class="value positive animate-counter" data-target="{{$d.AvgMonthlySavings}}" data-prefix="€">€0</div>
<p style="font-size:12px; color:var(--text3); margin-top:4px;">last 3 months</p>
</div>
<div class="card value-card animate-on-scroll" style="flex:1; min-width:160px;">
<h2>Disposable income</h2>
<div class="value animate-counter" data-target="{{$d.DisposableIncome}}" data-prefix="€" style="color:var(--text);">€0</div>
<p style="font-size:12px; color:var(--text3); margin-top:4px;">this month</p>
</div>
</div>
{{end}}
{{if $d.Goals}}
<div style="display:flex; flex-direction:column; gap:14px;">
{{range $d.Goals}}
<div class="card animate-on-scroll">
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; flex-wrap:wrap;">
<!-- left: name + type + progress -->
<div style="flex:1; min-width:200px;">
<div style="display:flex; align-items:center; gap:10px; margin-bottom:4px;">
<span style="font-size:18px;">{{if eq .Type "once"}}🎯{{else if eq .Type "deposit"}}🏠{{else if eq .Type "emergency"}}🛡️{{else}}📈{{end}}</span>
<div>
<div style="font-size:15px; font-weight:600; color:var(--text);">{{.Name}}</div>
<div style="font-size:11px; color:var(--text3); text-transform:uppercase; letter-spacing:.4px;">
{{if eq .Type "once"}}One-off purchase{{else if eq .Type "deposit"}}Deposit / down-payment{{else if eq .Type "emergency"}}Emergency fund{{else}}Recurring investment{{end}}
</div>
</div>
</div>
<div style="margin-top:12px;">
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
<span style="font-size:12px; color:var(--text2);">€{{cents .SavedCents}} saved of €{{cents .TargetCents}}</span>
<span style="font-size:12px; color:var(--text2);">{{.ProgressPct}}%</span>
</div>
<div style="background:var(--bg3); border-radius:99px; height:6px; 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>
</div>
<!-- right: projection numbers -->
<div style="display:flex; gap:24px; flex-wrap:wrap; align-items:flex-start;">
<div style="text-align:center;">
<div style="font-size:11px; color:var(--text3); margin-bottom:3px;">Need per month</div>
<div class="animate-counter" style="font-size:18px; font-weight:600; color:var(--text);"
data-target="{{.MonthlyCents}}" data-prefix="€">€0</div>
</div>
<div style="text-align:center;">
<div style="font-size:11px; color:var(--text3); margin-bottom:3px;">Months left</div>
<div style="font-size:18px; font-weight:600; color:var(--text);">{{.MonthsLeft}}</div>
</div>
<div style="text-align:center;">
<div style="font-size:11px; color:var(--text3); margin-bottom:3px;">At current rate</div>
{{if gt .MonthsAtCurrentRate 0}}
<div style="font-size:18px; font-weight:600;
{{if .Feasible}}color:var(--green){{else}}color:var(--red){{end}};">
{{.MonthsAtCurrentRate}}mo
</div>
{{else}}
<div style="font-size:14px; color:var(--text3);"></div>
{{end}}
</div>
<div style="text-align:center;">
<div style="font-size:11px; color:var(--text3); margin-bottom:3px;">Disposable after</div>
<div class="animate-counter" style="font-size:18px; font-weight:600;
{{if ge .ImpactOnDisposable 0}}color:var(--green){{else}}color:var(--red){{end}};"
data-target="{{.ImpactOnDisposable}}" data-prefix="€">€0</div>
</div>
</div>
</div>
<!-- feasibility banner -->
<div style="margin-top:14px; padding:10px 14px; border-radius:8px; font-size:13px;
background:{{if .Feasible}}rgba(74,222,128,0.08){{else}}rgba(248,113,113,0.08){{end}};
border:1px solid {{if .Feasible}}rgba(74,222,128,0.2){{else}}rgba(248,113,113,0.2){{end}};
color:{{if .Feasible}}var(--green){{else}}var(--red){{end}};">
{{if .Feasible}}
✓ On track — your current savings rate covers the required €{{cents .MonthlyCents}}/month with {{.MonthsLeft}} months to go.
{{else}}
⚠ At your current savings rate (€{{cents $d.AvgMonthlySavings}}/mo) you'd reach this in {{.MonthsAtCurrentRate}} months, missing the deadline by {{sub .MonthsAtCurrentRate .MonthsLeft}} months.
{{end}}
</div>
<!-- delete -->
<form method="POST" action="/goals" style="margin-top:10px; text-align:right;">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="{{.ID}}">
<button type="submit" class="btn btn-outline btn-sm" style="color:var(--red); border-color:var(--red)33;"
onclick="return confirm('Remove this goal?')">Remove</button>
</form>
</div>
{{end}}
</div>
{{else}}
<div class="card empty-state animate-on-scroll">
<div style="font-size:48px; margin-bottom:16px;">🎯</div>
<h3>No goals yet</h3>
<p style="margin-bottom:20px;">Plan a purchase, a house deposit, or an emergency fund.<br>See exactly how much you need to save each month to get there.</p>
<button onclick="document.getElementById('new-goal-modal').style.display='flex'" class="btn btn-primary">
Add your first goal
</button>
</div>
{{end}}
<!-- New goal modal -->
<div id="new-goal-modal" style="display:none; position:fixed; inset:0; z-index:1000;
background:rgba(0,0,0,0.6); backdrop-filter:blur(4px);
align-items:center; justify-content:center; padding:20px;">
<div style="background:var(--bg2); border:1px solid var(--border); border-radius:var(--radius);
padding:28px; width:100%; max-width:460px; position:relative;">
<h2 style="margin-bottom:20px;">New goal</h2>
<form method="POST" action="/goals" style="display:flex; flex-direction:column; gap:16px;">
<div>
<label style="font-size:12px; color:var(--text2); display:block; margin-bottom:6px;">Goal name</label>
<input name="name" required placeholder="e.g. Nintendo Switch, House deposit…"
style="width:100%; padding:9px 12px; background:var(--bg3); border:1px solid var(--border);
border-radius:8px; color:var(--text); font-size:14px;">
</div>
<div>
<label style="font-size:12px; color:var(--text2); display:block; margin-bottom:6px;">Type</label>
<select name="type" id="goal-type" onchange="updateTypeHint(this.value)"
style="width:100%; padding:9px 12px; background:var(--bg3); border:1px solid var(--border);
border-radius:8px; color:var(--text); font-size:14px;">
<option value="once">🎯 One-off purchase</option>
<option value="deposit">🏠 Deposit / down-payment</option>
<option value="emergency">🛡️ Emergency fund</option>
<option value="investment">📈 Recurring investment</option>
</select>
<p id="type-hint" style="font-size:12px; color:var(--text3); margin-top:5px;">
A specific purchase you want to save up for.
</p>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
<div>
<label style="font-size:12px; color:var(--text2); display:block; margin-bottom:6px;">Target amount (€)</label>
<input name="target_euros" type="number" min="1" step="0.01" required placeholder="500"
style="width:100%; padding:9px 12px; background:var(--bg3); border:1px solid var(--border);
border-radius:8px; color:var(--text); font-size:14px;">
</div>
<div>
<label style="font-size:12px; color:var(--text2); display:block; margin-bottom:6px;">Target date</label>
<input name="deadline" type="month" required
style="width:100%; padding:9px 12px; background:var(--bg3); border:1px solid var(--border);
border-radius:8px; color:var(--text); font-size:14px;">
</div>
</div>
<div style="display:flex; gap:10px; justify-content:flex-end; margin-top:4px;">
<button type="button" onclick="document.getElementById('new-goal-modal').style.display='none'"
class="btn btn-outline">Cancel</button>
<button type="submit" class="btn btn-primary">Save goal</button>
</div>
</form>
</div>
</div>
<script>
const typeHints = {
once: 'A specific purchase you want to save up for.',
deposit: 'A lump sum needed for a house deposit or down-payment.',
emergency: 'A safety net covering several months of expenses.',
investment: 'A recurring investment target (e.g. ETF monthly contribution).',
};
function updateTypeHint(v) {
document.getElementById('type-hint').textContent = typeHints[v] || '';
}
// close modal on backdrop click
document.getElementById('new-goal-modal').addEventListener('click', function(e) {
if (e.target === this) this.style.display = 'none';
});
</script>
{{end}}