* feat(property): Layer 3 — Dream House Simulator Add /dream page with a four-phase simulation engine: Phase 1 — Save the down payment (uses current property equity) Phase 2 — Construction period (both loans running simultaneously) Phase 3 — Sell current house, apply proceeds to construction loan Phase 4 — Final state: just the construction loan remaining Inputs: dream cost, down payment %, construction loan rate/term, build duration, monthly savings, expected sale price. All pre-filled from existing property/loan data when available. Output: per-phase timeline cards, monthly cost bar chart, total interest, final payoff date, and a key levers section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(plan): rename Dream House to Goal Planner at /plan - Route /dream → /plan - Nav label "Dream House" → "Goal Planner" - Template dream.html → plan.html - All user-facing labels generalised (construction loan → new loan, build duration → acquisition/build period, current property → current asset, dream house cost → new goal cost, etc.) - Empty state updated with generic copy and 🎯 icon Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(goals): merge Goal Planner into /goals as a second tab - /goals now has two tabs: "Committed goals" and "Goal Planner" - Goal creation only happens from the Planner tab (simulate first, then "Save as goal" → creates an uncommitted goal) - Commitment, deadline adjustment, and deletion stay on the Goals tab - Off-track goals show an "Adjust deadline →" button that pushes the deadline to the realistic date based on current savings rate - /plan and /dream both redirect to /goals?tab=planner (301) - "Goal Planner" nav link removed; plan.html kept for redirect compat - GoalsData gains Tab, PlanProperties, PlanLoans, HasPlanResult, PlanResult, PlanForm fields Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(goals): type-driven planner — Save for a purchase vs Sell & upgrade Goal Planner tab now opens with two goal type cards: 🛒 Save for a purchase — name, target, monthly savings, optional deadline. Shows time-to-reach at current rate, monthly needed to hit the deadline, and a feasibility banner. 🔄 Sell & upgrade — the full four-phase transition simulator (existing asset + loan → acquire new → sell old → payoff). Each type has its own focused form and result section. Selecting a type highlights the card and loads the matching form. Results include a "Save as goal" action that drops an uncommitted goal into the Goals tab. Also adds runPurchaseSim() and PurchaseSimResult model. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Gonçalo Rodrigues <guga@Goncalos-MacBook-Pro.local> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
580 lines
34 KiB
HTML
580 lines
34 KiB
HTML
{{define "content"}}
|
|
{{$d := .}}
|
|
|
|
<div style="display:flex; align-items:baseline; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:8px;">
|
|
<h1>Goals</h1>
|
|
</div>
|
|
|
|
<!-- Tab bar -->
|
|
<div style="display:flex; gap:4px; margin-bottom:24px; border-bottom:1px solid var(--border); padding-bottom:0;">
|
|
<a href="/goals?tab=goals"
|
|
style="padding:8px 16px; font-size:14px; font-weight:500; border-radius:6px 6px 0 0; text-decoration:none;
|
|
{{if eq $d.Tab "goals"}}background:var(--bg2); color:var(--text1); border:1px solid var(--border); border-bottom:1px solid var(--bg2); margin-bottom:-1px;{{else}}color:var(--text3);{{end}}">
|
|
Committed goals
|
|
</a>
|
|
<a href="/goals?tab=planner"
|
|
style="padding:8px 16px; font-size:14px; font-weight:500; border-radius:6px 6px 0 0; text-decoration:none;
|
|
{{if eq $d.Tab "planner"}}background:var(--bg2); color:var(--text1); border:1px solid var(--border); border-bottom:1px solid var(--bg2); margin-bottom:-1px;{{else}}color:var(--text3);{{end}}">
|
|
Goal Planner
|
|
</a>
|
|
</div>
|
|
|
|
{{if eq $d.Tab "goals"}}
|
|
{{/* ─── GOALS TAB ─────────────────────────────────────────────────────── */}}
|
|
|
|
{{if $d.ConflictWarning}}
|
|
<div style="padding:14px 18px; border-radius:10px; margin-bottom:16px; font-size:13px;
|
|
background:rgba(248,113,113,0.08); border:1px solid rgba(248,113,113,0.25); color:var(--red);">
|
|
⚠ {{$d.ConflictWarning}}
|
|
</div>
|
|
{{end}}
|
|
|
|
{{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;">before goals</p>
|
|
</div>
|
|
{{if $d.CommittedMonthlyCents}}
|
|
<div class="card value-card animate-on-scroll" style="flex:1; min-width:160px;">
|
|
<h2>Reserved for goals</h2>
|
|
<div class="value negative animate-counter" data-target="{{$d.CommittedMonthlyCents}}" data-prefix="€">€0</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:4px;">per month</p>
|
|
</div>
|
|
<div class="card value-card animate-on-scroll" style="flex:1; min-width:160px;">
|
|
<h2>Free to spend</h2>
|
|
<div class="value animate-counter {{if lt $d.RemainingDisposable 0}}negative{{else}}positive{{end}}"
|
|
data-target="{{$d.RemainingDisposable}}" data-prefix="€">€0</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:4px;">after goals</p>
|
|
</div>
|
|
{{end}}
|
|
</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;">
|
|
|
|
<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>
|
|
|
|
<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}}; display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
|
<span>
|
|
{{if .Feasible}}
|
|
✓ On track — your current savings rate covers €{{cents .MonthlyCents}}/month with {{.MonthsLeft}} months to go.
|
|
{{else}}
|
|
⚠ At your current rate (€{{cents $d.AvgMonthlySavings}}/mo) you'd reach this in {{.MonthsAtCurrentRate}} months — {{sub .MonthsAtCurrentRate .MonthsLeft}} months late.
|
|
{{end}}
|
|
</span>
|
|
{{if and (not .Feasible) (gt .MonthsAtCurrentRate 0)}}
|
|
<form method="POST" action="/goals" style="margin:0;">
|
|
<input type="hidden" name="action" value="adjust_deadline">
|
|
<input type="hidden" name="id" value="{{.ID}}">
|
|
<input type="hidden" name="months" value="{{.MonthsAtCurrentRate}}">
|
|
<button type="submit" class="btn btn-outline btn-sm" style="color:var(--red); border-color:var(--red)44; white-space:nowrap;">
|
|
Adjust deadline →
|
|
</button>
|
|
</form>
|
|
{{end}}
|
|
</div>
|
|
|
|
<!-- commit / uncommit / delete -->
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:14px; flex-wrap:wrap; gap:8px;">
|
|
<form method="POST" action="/goals">
|
|
<input type="hidden" name="action" value="{{if .Committed}}uncommit{{else}}commit{{end}}">
|
|
<input type="hidden" name="id" value="{{.ID}}">
|
|
{{if .Committed}}
|
|
<button type="submit" class="btn btn-outline btn-sm" style="color:var(--green); border-color:var(--green)55;">
|
|
✓ Committed — click to uncommit
|
|
</button>
|
|
{{else}}
|
|
<button type="submit" class="btn btn-primary btn-sm">Commit to this goal</button>
|
|
{{end}}
|
|
</form>
|
|
<form method="POST" action="/goals">
|
|
<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>
|
|
</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;">Use the <strong>Goal Planner</strong> tab to simulate a goal and save it here.</p>
|
|
<a href="/goals?tab=planner" class="btn btn-primary">Open Goal Planner →</a>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{else}}
|
|
{{/* ─── PLANNER TAB ─────────────────────────────────────────────────────── */}}
|
|
{{$r := $d.PlanResult}}
|
|
{{$pr := $d.PurchaseResult}}
|
|
|
|
<!-- Type selector -->
|
|
<div class="card animate-on-scroll" style="margin-bottom:20px;">
|
|
<h2 style="margin-bottom:14px;">What kind of goal?</h2>
|
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
|
|
<a href="/goals?tab=planner&planner_type=purchase"
|
|
style="display:block; padding:16px 18px; border-radius:var(--radius); border:2px solid {{if eq $d.PlannerType "purchase"}}var(--accent){{else}}var(--border){{end}}; text-decoration:none; transition:border-color 0.15s;">
|
|
<div style="font-size:22px; margin-bottom:8px;">🛒</div>
|
|
<div style="font-weight:600; font-size:14px; color:var(--text1); margin-bottom:4px;">Save for a purchase</div>
|
|
<div style="font-size:12px; color:var(--text3);">Car, trip, gadget, fund — save up to a target by a date.</div>
|
|
</a>
|
|
<a href="/goals?tab=planner&planner_type=transition"
|
|
style="display:block; padding:16px 18px; border-radius:var(--radius); border:2px solid {{if eq $d.PlannerType "transition"}}var(--accent){{else}}var(--border){{end}}; text-decoration:none; transition:border-color 0.15s;">
|
|
<div style="font-size:22px; margin-bottom:8px;">🔄</div>
|
|
<div style="font-weight:600; font-size:14px; color:var(--text1); margin-bottom:4px;">Sell & upgrade</div>
|
|
<div style="font-size:12px; color:var(--text3);">Own an asset with a loan, acquire something new, sell the old to fund it.</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{{if eq $d.PlannerType "purchase"}}
|
|
<!-- ── PURCHASE FORM ──────────────────────────────────────────────────────── -->
|
|
<div class="card animate-on-scroll" style="margin-bottom:20px;">
|
|
<h2 style="margin-bottom:16px;">Your purchase goal</h2>
|
|
<form method="GET" action="/goals">
|
|
<input type="hidden" name="tab" value="planner">
|
|
<input type="hidden" name="planner_type" value="purchase">
|
|
<input type="hidden" name="run" value="1">
|
|
<div style="display:flex; flex-direction:column; gap:14px; margin-bottom:16px;">
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">Goal name</label>
|
|
<input name="name" placeholder="e.g. New car, Europe trip, Emergency fund…"
|
|
value="{{if $d.HasPurchaseResult}}{{$pr.Name}}{{end}}"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
|
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">Target amount (€)</label>
|
|
<input type="number" name="target" min="0" step="100"
|
|
value="{{if $d.HasPurchaseResult}}{{div $pr.TargetCents 100}}{{end}}"
|
|
placeholder="e.g. 12000"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">Monthly savings (€)</label>
|
|
<input type="number" name="monthly_savings" min="0" step="50"
|
|
value="{{if $d.HasPurchaseResult}}{{div $pr.MonthlySavingsCents 100}}{{end}}"
|
|
placeholder="e.g. 400"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">Target date (optional — leave blank to see when you'll get there)</label>
|
|
<input type="month" name="deadline"
|
|
value="{{if and $d.HasPurchaseResult $pr.HasDeadline}}{{$pr.DeadlineDate.Format "2006-01"}}{{end}}"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
|
|
</div>
|
|
<button type="submit" class="btn btn-primary" style="width:100%;">Calculate →</button>
|
|
</form>
|
|
</div>
|
|
|
|
{{if $d.HasPurchaseResult}}
|
|
<!-- Purchase result -->
|
|
<div class="grid" style="margin-bottom:20px;">
|
|
<div class="card value-card animate-on-scroll">
|
|
<h2>At your savings rate</h2>
|
|
{{if gt $pr.MonthsNeeded 0}}
|
|
<div class="value positive">{{$pr.YearsNeeded}}y {{$pr.RemMonths}}m</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:6px;">reach {{dateShort $pr.ReachDate}}</p>
|
|
{{else}}
|
|
<div class="value" style="color:var(--text3); font-size:18px;">—</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:6px;">enter monthly savings</p>
|
|
{{end}}
|
|
</div>
|
|
<div class="card value-card animate-on-scroll">
|
|
<h2>Monthly needed</h2>
|
|
{{if $pr.HasDeadline}}
|
|
<div class="value {{if $pr.Feasible}}positive{{else}}negative{{end}}">€{{cents $pr.MonthlyNeededForDeadline}}</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:6px;">to hit {{dateShort $pr.DeadlineDate}}</p>
|
|
{{else}}
|
|
<div class="value" style="color:var(--text3); font-size:18px;">—</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:6px;">set a target date above</p>
|
|
{{end}}
|
|
</div>
|
|
<div class="card value-card animate-on-scroll">
|
|
<h2>Target</h2>
|
|
<div class="value positive">€{{cents $pr.TargetCents}}</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:6px;">your goal amount</p>
|
|
</div>
|
|
</div>
|
|
|
|
{{if $pr.HasDeadline}}
|
|
<div style="padding:12px 16px; border-radius:var(--radius); margin-bottom:20px; font-size:13px;
|
|
background:{{if $pr.Feasible}}rgba(74,222,128,0.08){{else}}rgba(248,113,113,0.08){{end}};
|
|
border:1px solid {{if $pr.Feasible}}rgba(74,222,128,0.25){{else}}rgba(248,113,113,0.25){{end}};
|
|
color:{{if $pr.Feasible}}var(--green){{else}}var(--red){{end}};">
|
|
{{if $pr.Feasible}}
|
|
✓ On track — €{{cents $pr.MonthlySavingsCents}}/mo covers the required €{{cents $pr.MonthlyNeededForDeadline}}/mo.
|
|
{{else}}
|
|
⚠ You need €{{cents $pr.MonthlyNeededForDeadline}}/mo to hit the deadline but you're saving €{{cents $pr.MonthlySavingsCents}}/mo. At your current rate you'll get there in {{$pr.MonthsNeeded}} months ({{dateShort $pr.ReachDate}}).
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Save as goal -->
|
|
<div class="card animate-on-scroll">
|
|
<h2 style="margin-bottom:4px;">Save as a goal</h2>
|
|
<p style="font-size:13px; color:var(--text3); margin-bottom:14px;">Adds this to your Goals tab so you can commit to it.</p>
|
|
<form method="POST" action="/goals" style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end;">
|
|
<input type="hidden" name="type" value="once">
|
|
<input type="hidden" name="target_euros" value="{{div $pr.TargetCents 100}}">
|
|
<input type="hidden" name="deadline" value="{{if $pr.HasDeadline}}{{$pr.DeadlineDate.Format "2006-01"}}{{else}}{{$pr.ReachDate.Format "2006-01"}}{{end}}">
|
|
<div style="flex:1; min-width:200px;">
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">Goal name</label>
|
|
<input name="name" required value="{{$pr.Name}}" placeholder="e.g. New car"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
<button type="submit" class="btn btn-primary" style="white-space:nowrap;">Save goal →</button>
|
|
</form>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{else if eq $d.PlannerType "transition"}}
|
|
<!-- ── TRANSITION FORM ───────────────────────────────────────────────────── -->
|
|
<div class="card animate-on-scroll" style="margin-bottom:20px;">
|
|
<h2 style="margin-bottom:16px;">Your transition scenario</h2>
|
|
<form method="GET" action="/goals">
|
|
<input type="hidden" name="tab" value="planner">
|
|
<input type="hidden" name="planner_type" value="transition">
|
|
<input type="hidden" name="run" value="1">
|
|
|
|
<div class="grid" style="gap:14px; margin-bottom:16px;">
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">Current asset (optional)</label>
|
|
<select name="property_id" style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px;">
|
|
<option value="">— none selected —</option>
|
|
{{range $d.PlanProperties}}
|
|
<option value="{{.ID}}" {{if and $d.HasPlanResult (eq $d.PlanForm.PropertyID .ID)}}selected{{end}}>{{.Name}} (€{{cents .CurrentValueCents}})</option>
|
|
{{end}}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">Current loan (optional)</label>
|
|
<select name="loan_id" style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px;">
|
|
<option value="">— none selected —</option>
|
|
{{range $d.PlanLoans}}
|
|
<option value="{{.ID}}" {{if and $d.HasPlanResult (eq $d.PlanForm.LoanID .ID)}}selected{{end}}>{{.Name}} (€{{cents .BalanceCents}} left)</option>
|
|
{{end}}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">New goal cost (€)</label>
|
|
<input type="number" name="dream_cost" min="0" step="1000"
|
|
value="{{if $d.HasPlanResult}}{{div $d.PlanForm.DreamCostCents 100}}{{end}}"
|
|
placeholder="e.g. 350000"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">Down payment (%)</label>
|
|
<input type="number" name="down_pct" min="0" max="100" step="1"
|
|
value="{{if $d.HasPlanResult}}{{round $d.PlanForm.DownPaymentPct}}{{else}}20{{end}}"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">New loan rate (% annual)</label>
|
|
<input type="number" name="const_rate" min="0" max="30" step="0.1"
|
|
value="{{if $d.HasPlanResult}}{{$d.PlanForm.ConstructionRatePct}}{{else}}4.0{{end}}"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">New loan term (years)</label>
|
|
<input type="number" name="const_term" min="1" max="40" step="1"
|
|
value="{{if $d.HasPlanResult}}{{$d.PlanForm.ConstructionTermYears}}{{else}}30{{end}}"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">Acquisition / build period (months)</label>
|
|
<input type="number" name="build_months" min="1" max="60" step="1"
|
|
value="{{if $d.HasPlanResult}}{{$d.PlanForm.BuildMonths}}{{else}}18{{end}}"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">Monthly savings available (€)</label>
|
|
<input type="number" name="monthly_savings" min="0" step="100"
|
|
value="{{if $d.HasPlanResult}}{{div $d.PlanForm.MonthlySavingsCents 100}}{{end}}"
|
|
placeholder="e.g. 800"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
|
|
<div>
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">Expected sale price of current asset (€)</label>
|
|
<input type="number" name="sale_price" min="0" step="1000"
|
|
value="{{if $d.HasPlanResult}}{{div $d.PlanForm.ExpectedSalePriceCents 100}}{{end}}"
|
|
placeholder="leave blank to use current value"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary" style="width:100%;">Run simulation →</button>
|
|
</form>
|
|
</div>
|
|
|
|
{{if $d.HasPlanResult}}
|
|
|
|
{{if $r.Warning}}
|
|
<div style="background:rgba(251,191,36,0.12); border:1px solid rgba(251,191,36,0.4); border-radius:var(--radius); padding:12px 16px; margin-bottom:16px; font-size:13px; color:var(--text2);">
|
|
⚠️ {{$r.Warning}}
|
|
</div>
|
|
{{end}}
|
|
|
|
<!-- Summary cards -->
|
|
<div class="grid" style="margin-bottom:20px;">
|
|
<div class="card value-card animate-on-scroll">
|
|
<h2>Total timeline</h2>
|
|
<div class="value positive">{{$r.TotalYears}}y {{$r.TotalRemMonths}}m</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:6px;">until goal is fully paid off</p>
|
|
</div>
|
|
<div class="card value-card animate-on-scroll">
|
|
<h2>Final monthly cost</h2>
|
|
<div class="value positive">€{{cents $r.Phase4MonthlyCents}}</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:6px;">after selling current asset</p>
|
|
</div>
|
|
<div class="card value-card animate-on-scroll">
|
|
<h2>Total interest</h2>
|
|
<div class="value" style="color:var(--red);">€{{cents $r.TotalInterestCents}}</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:6px;">across both loans</p>
|
|
</div>
|
|
<div class="card value-card animate-on-scroll">
|
|
<h2>Free by</h2>
|
|
<div class="value positive" style="font-size:24px;">{{dateShort $r.FinalDate}}</div>
|
|
<p style="font-size:12px; color:var(--text3); margin-top:6px;">fully paid off</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Phase timeline -->
|
|
<div class="card animate-on-scroll" style="margin-bottom:20px;">
|
|
<h2 style="margin-bottom:20px;">Your roadmap</h2>
|
|
<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:0; position:relative;">
|
|
<div style="position:absolute; top:20px; left:12.5%; right:12.5%; height:3px; background:var(--border); z-index:0; border-radius:2px;"></div>
|
|
|
|
<!-- Phase 1 -->
|
|
<div style="text-align:center; position:relative; padding:0 8px;">
|
|
<div style="width:40px; height:40px; border-radius:50%; background:var(--accent); color:#fff; display:flex; align-items:center; justify-content:center; font-size:18px; margin:0 auto 12px; position:relative; z-index:1;">1</div>
|
|
<div style="font-weight:600; font-size:13px; color:var(--text1); margin-bottom:6px;">Save down payment</div>
|
|
{{if gt $r.Phase1Months 0}}
|
|
<div style="font-size:22px; font-weight:500; color:var(--accent); margin-bottom:4px;">{{$r.Phase1Months}}mo</div>
|
|
<div style="font-size:11px; color:var(--text3);">until {{dateShort $r.Phase1EndDate}}</div>
|
|
<div style="margin-top:10px; font-size:12px; color:var(--text2); background:var(--bg3); border-radius:var(--radius-sm); padding:8px 10px; text-align:left;">
|
|
<div>Target: <strong>€{{cents $r.DownPaymentCents}}</strong></div>
|
|
<div>Already have: <strong>€{{cents $r.AlreadyHaveCents}}</strong></div>
|
|
<div>Still need: <strong>€{{cents $r.StillNeededCents}}</strong></div>
|
|
<div>Saving: <strong>€{{cents $r.Form.MonthlySavingsCents}}/mo</strong></div>
|
|
</div>
|
|
{{else}}
|
|
<div style="font-size:16px; font-weight:600; color:var(--green); margin-bottom:4px;">Ready now!</div>
|
|
<div style="font-size:11px; color:var(--text3);">equity covers down payment</div>
|
|
<div style="margin-top:10px; font-size:12px; color:var(--text2); background:var(--bg3); border-radius:var(--radius-sm); padding:8px 10px; text-align:left;">
|
|
<div>Down payment: <strong>€{{cents $r.DownPaymentCents}}</strong></div>
|
|
<div>Your equity: <strong style="color:var(--green);">€{{cents $r.AlreadyHaveCents}}</strong></div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<!-- Phase 2 -->
|
|
<div style="text-align:center; position:relative; padding:0 8px;">
|
|
<div style="width:40px; height:40px; border-radius:50%; background:#f97316; color:#fff; display:flex; align-items:center; justify-content:center; font-size:18px; margin:0 auto 12px; position:relative; z-index:1;">2</div>
|
|
<div style="font-weight:600; font-size:13px; color:var(--text1); margin-bottom:6px;">Acquire / build</div>
|
|
<div style="font-size:22px; font-weight:500; color:#f97316; margin-bottom:4px;">{{$r.Phase2Months}}mo</div>
|
|
<div style="font-size:11px; color:var(--text3);">until {{dateShort $r.Phase2EndDate}}</div>
|
|
<div style="margin-top:10px; font-size:12px; color:var(--text2); background:var(--bg3); border-radius:var(--radius-sm); padding:8px 10px; text-align:left;">
|
|
<div>New loan: <strong>€{{cents $r.ConstructionLoanCents}}</strong></div>
|
|
{{if $r.CurrentLoan}}<div>Existing loan: <strong>€{{cents $r.CurrentMonthlyCents}}/mo</strong></div>{{end}}
|
|
<div>New EMI: <strong>€{{cents $r.ConstructionMonthly}}/mo</strong></div>
|
|
<div style="border-top:1px solid var(--border); margin-top:6px; padding-top:6px;">Total burden: <strong style="color:#f97316;">€{{cents $r.Phase2MonthlyCents}}/mo</strong></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Phase 3 -->
|
|
<div style="text-align:center; position:relative; padding:0 8px;">
|
|
<div style="width:40px; height:40px; border-radius:50%; background:#14b8a6; color:#fff; display:flex; align-items:center; justify-content:center; font-size:18px; margin:0 auto 12px; position:relative; z-index:1;">3</div>
|
|
<div style="font-weight:600; font-size:13px; color:var(--text1); margin-bottom:6px;">Sell & transition</div>
|
|
<div style="font-size:16px; font-weight:600; color:#14b8a6; margin-bottom:4px;">One-time event</div>
|
|
<div style="font-size:11px; color:var(--text3);">after acquisition completes</div>
|
|
<div style="margin-top:10px; font-size:12px; color:var(--text2); background:var(--bg3); border-radius:var(--radius-sm); padding:8px 10px; text-align:left;">
|
|
<div>Sale price: <strong>€{{cents $r.SalePriceCents}}</strong></div>
|
|
<div>Pay off loan: <strong style="color:var(--red);">-€{{cents $r.MortgagePayoffCents}}</strong></div>
|
|
<div style="border-top:1px solid var(--border); margin-top:6px; padding-top:6px;">Net proceeds: <strong style="color:var(--green);">€{{cents $r.NetProceedsCents}}</strong></div>
|
|
<div>Applied to new loan</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Phase 4 -->
|
|
<div style="text-align:center; position:relative; padding:0 8px;">
|
|
<div style="width:40px; height:40px; border-radius:50%; background:var(--green); color:#fff; display:flex; align-items:center; justify-content:center; font-size:18px; margin:0 auto 12px; position:relative; z-index:1;">4</div>
|
|
<div style="font-weight:600; font-size:13px; color:var(--text1); margin-bottom:6px;">Goal achieved</div>
|
|
{{if gt $r.Phase4Months 0}}
|
|
<div style="font-size:22px; font-weight:500; color:var(--green); margin-bottom:4px;">{{$r.Phase4Months}}mo</div>
|
|
<div style="font-size:11px; color:var(--text3);">paid off {{dateShort $r.Phase4EndDate}}</div>
|
|
<div style="margin-top:10px; font-size:12px; color:var(--text2); background:var(--bg3); border-radius:var(--radius-sm); padding:8px 10px; text-align:left;">
|
|
<div>Remaining loan: <strong>€{{cents $r.RemainingBalanceCents}}</strong></div>
|
|
<div>Monthly: <strong style="color:var(--green);">€{{cents $r.Phase4MonthlyCents}}/mo</strong></div>
|
|
</div>
|
|
{{else}}
|
|
<div style="font-size:16px; font-weight:600; color:var(--green); margin-bottom:4px;">Fully paid!</div>
|
|
<div style="font-size:11px; color:var(--text3);">sale proceeds cleared the loan</div>
|
|
<div style="margin-top:10px; font-size:12px; color:var(--text2); background:var(--bg3); border-radius:var(--radius-sm); padding:8px 10px; text-align:left;">
|
|
<div style="color:var(--green); font-weight:600;">No remaining loan!</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Monthly cost chart -->
|
|
<div class="card animate-on-scroll" style="margin-bottom:20px;">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
|
|
<h2>Monthly cost over time</h2>
|
|
<span style="font-size:11px; color:var(--text3);">what you pay each month</span>
|
|
</div>
|
|
<canvas id="cost-chart" height="160"></canvas>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
const style = getComputedStyle(document.documentElement);
|
|
const accent = style.getPropertyValue('--accent').trim() || '#6979f8';
|
|
const green = style.getPropertyValue('--green').trim() || '#4ade80';
|
|
|
|
const phase1Months = {{$r.Phase1Months}};
|
|
const phase2Months = {{$r.Phase2Months}};
|
|
const phase4Months = {{$r.Phase4Months}};
|
|
const existingMonthly = {{$r.CurrentMonthlyCents}} / 100;
|
|
const phase2Monthly = {{$r.Phase2MonthlyCents}} / 100;
|
|
const phase4Monthly = {{$r.Phase4MonthlyCents}} / 100;
|
|
|
|
const labels = [], costs = [], colors = [];
|
|
for (let i = 0; i < phase1Months; i++) { labels.push('Save '+(i+1)); costs.push(existingMonthly); colors.push(accent); }
|
|
for (let i = 0; i < phase2Months; i++) { labels.push('Acquire '+(i+1)); costs.push(phase2Monthly); colors.push('#f97316'); }
|
|
for (let i = 0; i < phase4Months; i++) { labels.push('Goal '+(i+1)); costs.push(phase4Monthly); colors.push(green); }
|
|
|
|
const step = Math.ceil(labels.length / 24);
|
|
const displayLabels = labels.map((l, i) => i % step === 0 ? l : '');
|
|
|
|
new Chart(document.getElementById('cost-chart').getContext('2d'), {
|
|
type: 'bar',
|
|
data: { labels: displayLabels, datasets: [{ data: costs, backgroundColor: colors, borderRadius: 3, borderSkipped: false }] },
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: { callbacks: { title: i => labels[i[0].dataIndex], label: c => '€'+c.parsed.y.toLocaleString('pt-PT',{minimumFractionDigits:2})+'/mo' } }
|
|
},
|
|
scales: {
|
|
x: { grid: { display: false }, ticks: { color: isDark?'#5c6585':'#9fa8c7', font:{size:10} } },
|
|
y: { grid: { color: isDark?'rgba(255,255,255,0.04)':'rgba(0,0,0,0.05)' }, ticks: { color: isDark?'#5c6585':'#9fa8c7', callback: v=>'€'+(v/1000).toFixed(0)+'k' } }
|
|
}
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<!-- Save as goal (transition) -->
|
|
<div class="card animate-on-scroll">
|
|
<h2 style="margin-bottom:4px;">Save as a goal</h2>
|
|
<p style="font-size:13px; color:var(--text3); margin-bottom:14px;">Adds this to your Goals tab so you can commit to it.</p>
|
|
<form method="POST" action="/goals" style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end;">
|
|
<input type="hidden" name="type" value="deposit">
|
|
<input type="hidden" name="target_euros" value="{{div $r.Form.DreamCostCents 100}}">
|
|
<input type="hidden" name="deadline" value="{{$r.FinalDate.Format "2006-01"}}">
|
|
<div style="flex:1; min-width:200px;">
|
|
<label style="font-size:12px; color:var(--text3); display:block; margin-bottom:5px;">Goal name</label>
|
|
<input name="name" required placeholder="e.g. New property, Upgrade car…"
|
|
style="width:100%; padding:8px 10px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--bg2); color:var(--text1); font-size:14px; box-sizing:border-box;">
|
|
</div>
|
|
<button type="submit" class="btn btn-primary" style="white-space:nowrap;">Save goal →</button>
|
|
</form>
|
|
</div>
|
|
|
|
{{end}}{{/* end HasPlanResult */}}
|
|
|
|
{{end}}{{/* end planner_type transition */}}
|
|
|
|
{{end}}{{/* end tab */}}
|
|
{{end}}
|