Files
sure/app/views/retirement/_kpis.html.erb
Guillem Arias 174dd66914 feat(retirement): PR3b what-if KPIs + live forecast Turbo Stream
Surfaces the forecast on the page and makes the levers live.

- KPI cards (_kpis): Freedom date, Coast FIRE, Money-lasts-to + terminal
  value, with a "set your birth year" prompt until a plan is projectable.
  Wrapped in #retirement_kpis for Turbo Stream replacement; money carries
  privacy-sensitive.
- What-if form: birth_year / retire_age / target_spend / monthly_savings /
  real_return_pct. On input, retirement_what_if_controller debounces and
  POSTs the current values to PATCH /retirement/forecast, which recomputes
  against transient inputs and streams the KPI cards back WITHOUT
  persisting. "Save plan" submits to #update to persist retirement_params.
- RetirementController gains #update (persist) and #forecast (transient
  recompute → turbo_stream). Both reuse merged_plan_params, which drops
  blank fields so a partial what-if doesn't clobber stored values.

Tests: KPI section renders; update persists params; forecast streams
#retirement_kpis without writing the slider value back. Rubocop +
erb_lint + biome clean.

PR4 replaces this minimal form with the designed slider rail + glide
chart; the #forecast endpoint and the engine stay.
2026-05-29 11:45:31 +02:00

33 lines
1.8 KiB
Plaintext

<%# locals: (plan:) %>
<div id="retirement_kpis" class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<% forecast = plan.forecast %>
<% if forecast.nil? %>
<div class="sm:col-span-3 bg-container shadow-border-xs rounded-xl p-5">
<p class="text-primary text-sm font-medium"><%= t("retirement.kpis.set_birth_year_heading") %></p>
<p class="text-secondary text-sm"><%= t("retirement.kpis.set_birth_year_body") %></p>
</div>
<% else %>
<div class="bg-container shadow-border-xs rounded-xl p-5">
<p class="text-secondary text-xs uppercase tracking-wide"><%= t("retirement.kpis.freedom_date") %></p>
<p class="text-primary text-2xl font-semibold"><%= plan.freedom_date&.year || "—" %></p>
<p class="text-secondary text-xs"><%= t("retirement.kpis.retire_at_age", age: plan.effective_retire_age) %></p>
</div>
<div class="bg-container shadow-border-xs rounded-xl p-5">
<p class="text-secondary text-xs uppercase tracking-wide"><%= t("retirement.kpis.coast_fire") %></p>
<p class="text-primary text-2xl font-semibold"><%= plan.coast_fire_date&.year || t("retirement.kpis.not_yet") %></p>
<p class="text-secondary text-xs"><%= t("retirement.kpis.coast_hint") %></p>
</div>
<div class="bg-container shadow-border-xs rounded-xl p-5">
<p class="text-secondary text-xs uppercase tracking-wide"><%= t("retirement.kpis.money_lasts_to") %></p>
<p class="text-primary text-2xl font-semibold">
<%= forecast.lasts_past_terminal? ? t("retirement.kpis.past_age", age: forecast.terminal_age) : t("retirement.kpis.age", age: forecast.money_lasts_to_age) %>
</p>
<p class="text-secondary text-xs privacy-sensitive">
<%= t("retirement.kpis.terminal", amount: Money.new(forecast.terminal_value, plan.currency).format(precision: 0)) %>
</p>
</div>
<% end %>
</div>