mirror of
https://github.com/we-promise/sure.git
synced 2026-05-29 23:39:03 +00:00
Each lever (retire age / target spend / save per mo / real return) now pairs a numeric input with a range slider; retirement_what_if_controller mirrors the value across the pair (data-lever) and debounces the live forecast preview. Birth year stays a plain numeric input. (Skinned delete confirmations already render via Sure's global Turbo.config.forms.confirm → DS::Dialog override, so the PR2 turbo_confirm buttons are already styled — no change needed.)
261 lines
13 KiB
Plaintext
261 lines
13 KiB
Plaintext
<% content_for :breadcrumbs do %>
|
|
<%= render "layouts/shared/breadcrumbs", breadcrumbs: @breadcrumbs %>
|
|
<% end %>
|
|
|
|
<div class="space-y-6">
|
|
<header class="flex items-start justify-between gap-4">
|
|
<div class="space-y-2">
|
|
<h1 class="text-primary text-2xl font-semibold"><%= t("retirement.show.title") %></h1>
|
|
<p class="text-secondary text-sm"><%= t("retirement.show.subtitle") %></p>
|
|
<p class="text-subdued text-xs"><%= t("retirement.show.preview_note") %></p>
|
|
</div>
|
|
<% if @glide.present? %>
|
|
<%= render DS::Pill.new(label: t("retirement.show.active_plan"), tone: :success, marker: false, show_dot: true, size: :sm) %>
|
|
<% end %>
|
|
</header>
|
|
|
|
<%= render "retirement/kpis", plan: @plan %>
|
|
|
|
<%# Glide path chart %>
|
|
<% if @glide.present? %>
|
|
<section class="bg-container shadow-border-xs rounded-xl p-5 space-y-3">
|
|
<div>
|
|
<h2 class="text-primary font-medium"><%= t("retirement.glide.title") %></h2>
|
|
<p class="text-secondary text-xs"><%= t("retirement.glide.subtitle") %></p>
|
|
</div>
|
|
<div class="h-72 w-full privacy-sensitive"
|
|
data-controller="retirement-glide-chart"
|
|
data-retirement-glide-chart-data-value="<%= @glide.to_json %>"
|
|
data-retirement-glide-chart-aria-label-value="<%= t("retirement.glide.title") %>"
|
|
data-retirement-glide-chart-covered-label-value="<%= t("retirement.glide.covered") %>"
|
|
data-retirement-glide-chart-portfolio-label-value="<%= t("retirement.glide.portfolio") %>"
|
|
data-retirement-glide-chart-state-label-value="<%= t("retirement.glide.state") %>"
|
|
data-retirement-glide-chart-workplace-label-value="<%= t("retirement.glide.workplace") %>"
|
|
data-retirement-glide-chart-drawdown-label-value="<%= t("retirement.glide.drawdown") %>"
|
|
data-retirement-glide-chart-total-label-value="<%= t("retirement.glide.total") %>"
|
|
data-retirement-glide-chart-retire-label-value="<%= t("retirement.glide.retire") %>"
|
|
data-retirement-glide-chart-coast-label-value="<%= t("retirement.glide.coast") %>"></div>
|
|
</section>
|
|
<% end %>
|
|
|
|
<%# What-if — live recompute on input, persist on save %>
|
|
<section data-controller="retirement-what-if"
|
|
data-retirement-what-if-url-value="<%= forecast_retirement_path %>"
|
|
class="bg-container shadow-border-xs rounded-xl p-5 space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-primary font-medium"><%= t("retirement.what_if.title") %></h2>
|
|
<p class="text-secondary text-xs"><%= t("retirement.what_if.hint") %></p>
|
|
</div>
|
|
|
|
<%= form_with url: retirement_path, method: :patch, data: { retirement_what_if_target: "form" }, class: "space-y-3" do |form| %>
|
|
<% levers = {
|
|
"retire_age" => { min: 40, max: 75, step: 1 },
|
|
"target_spend" => { min: 0, max: 20_000, step: 50 },
|
|
"monthly_savings" => { min: 0, max: 20_000, step: 50 },
|
|
"real_return_pct" => { min: 0, max: 10, step: 0.1 }
|
|
} %>
|
|
<div class="grid grid-cols-1 sm:grid-cols-5 gap-4">
|
|
<label class="text-xs text-secondary space-y-1">
|
|
<span class="block"><%= t("retirement.what_if.fields.birth_year") %></span>
|
|
<%= number_field_tag "retirement[birth_year]", @plan.birth_year, step: 1,
|
|
autocomplete: "off", class: "form-field__input w-full",
|
|
data: { action: "input->retirement-what-if#preview" } %>
|
|
</label>
|
|
|
|
<% levers.each do |field, cfg| %>
|
|
<% value = @plan.public_send(field) %>
|
|
<div class="space-y-1.5">
|
|
<label class="text-xs text-secondary block" for="rwi_<%= field %>"><%= t("retirement.what_if.fields.#{field}") %></label>
|
|
<%= number_field_tag "retirement[#{field}]", value, step: cfg[:step], id: "rwi_#{field}",
|
|
autocomplete: "off", class: "form-field__input w-full",
|
|
data: { lever: field, action: "input->retirement-what-if#sync" } %>
|
|
<%= range_field_tag "rwi_#{field}_slider", value, min: cfg[:min], max: cfg[:max], step: cfg[:step],
|
|
class: "w-full accent-green-600 cursor-pointer",
|
|
data: { lever: field, action: "input->retirement-what-if#sync" } %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<%= form.submit t("retirement.what_if.save"), class: "text-sm font-medium text-primary underline cursor-pointer" %>
|
|
<% end %>
|
|
</section>
|
|
|
|
<%# Why this target? — spending anchor + FI number %>
|
|
<% if @glide.present? %>
|
|
<section class="bg-container shadow-border-xs rounded-xl p-5 space-y-4">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<div>
|
|
<h2 class="text-primary font-medium"><%= t("retirement.why.title") %></h2>
|
|
<p class="text-secondary text-xs"><%= t("retirement.why.subtitle") %></p>
|
|
</div>
|
|
<%= button_to t("retirement.why.use_average"), retirement_path, method: :patch,
|
|
params: { retirement: { target_spend: @baseline.amount.to_i } },
|
|
class: "text-sm text-primary underline shrink-0" %>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<div>
|
|
<p class="text-secondary text-xs uppercase tracking-wide"><%= t("retirement.why.last_12") %></p>
|
|
<p class="text-primary text-xl font-semibold tabular-nums privacy-sensitive">
|
|
<%= @baseline.format(precision: 0) %><span class="text-secondary text-sm"><%= t("retirement.show.per_month") %></span>
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-secondary text-xs uppercase tracking-wide"><%= t("retirement.why.target") %></p>
|
|
<p class="text-primary text-xl font-semibold tabular-nums privacy-sensitive">
|
|
<%= Money.new(@plan.target_spend_monthly, @plan.currency).format(precision: 0) %><span class="text-secondary text-sm"><%= t("retirement.show.per_month") %></span>
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-secondary text-xs uppercase tracking-wide"><%= t("retirement.why.fi_number") %></p>
|
|
<p class="text-primary text-xl font-semibold tabular-nums privacy-sensitive">
|
|
<%= Money.new(@plan.fi_number, @plan.currency).format(precision: 0) %>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<% end %>
|
|
|
|
<%# Pension sources %>
|
|
<section class="bg-container shadow-border-xs rounded-xl p-5 space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-primary font-medium"><%= t("retirement.show.sources_title") %></h2>
|
|
<%= link_to t("retirement.show.add_source"), new_retirement_pension_source_path,
|
|
class: "text-sm font-medium text-primary underline" %>
|
|
</div>
|
|
|
|
<% if @pension_sources.empty? %>
|
|
<p class="text-secondary text-sm"><%= t("retirement.show.no_sources") %></p>
|
|
<% else %>
|
|
<ul class="divide-y divide-tertiary">
|
|
<% @pension_sources.each do |source| %>
|
|
<li class="py-3 flex items-center justify-between gap-3">
|
|
<div>
|
|
<p class="text-primary text-sm font-medium">
|
|
<%= source.name %>
|
|
<span class="text-secondary text-xs">· <%= t("retirement.pension_sources.kinds.#{source.kind}") %></span>
|
|
</p>
|
|
<p class="text-secondary text-xs">
|
|
<%= t("retirement.show.starts_at_age", age: source.start_age) %> ·
|
|
<%= source.country %> ·
|
|
<%= t("retirement.pension_sources.payout_shapes.#{source.payout_shape}") %>
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-3 shrink-0">
|
|
<span class="text-primary text-sm tabular-nums privacy-sensitive">
|
|
<%= source.amount_money&.format %><span class="text-secondary"><%= t("retirement.show.per_month") %></span>
|
|
</span>
|
|
<%= link_to t("retirement.show.edit"), edit_retirement_pension_source_path(source),
|
|
class: "text-xs text-secondary underline" %>
|
|
<%= button_to t("retirement.show.delete"), retirement_pension_source_path(source),
|
|
method: :delete, class: "text-xs text-destructive underline",
|
|
form: { data: { turbo_confirm: t("retirement.show.delete_confirm") } } %>
|
|
</div>
|
|
</li>
|
|
<% end %>
|
|
</ul>
|
|
<% end %>
|
|
</section>
|
|
|
|
<%# Adjustments %>
|
|
<section class="bg-container shadow-border-xs rounded-xl p-5 space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-primary font-medium"><%= t("retirement.show.adjustments_title") %></h2>
|
|
<%= link_to t("retirement.show.add_adjustment"), new_retirement_adjustment_path,
|
|
class: "text-sm font-medium text-primary underline" %>
|
|
</div>
|
|
|
|
<% if @adjustments.empty? %>
|
|
<p class="text-secondary text-sm"><%= t("retirement.show.no_adjustments") %></p>
|
|
<% else %>
|
|
<ul class="divide-y divide-tertiary">
|
|
<% @adjustments.each do |adjustment| %>
|
|
<li class="py-3 flex items-center justify-between gap-3">
|
|
<div>
|
|
<p class="text-primary text-sm font-medium"><%= adjustment.label %></p>
|
|
<p class="text-secondary text-xs"><%= t("retirement.show.from_age", age: adjustment.from_age) %></p>
|
|
</div>
|
|
<div class="flex items-center gap-3 shrink-0">
|
|
<span class="text-primary text-sm tabular-nums privacy-sensitive"><%= adjustment.amount_today_money&.format %></span>
|
|
<%= link_to t("retirement.show.edit"), edit_retirement_adjustment_path(adjustment),
|
|
class: "text-xs text-secondary underline" %>
|
|
<%= button_to t("retirement.show.delete"), retirement_adjustment_path(adjustment),
|
|
method: :delete, class: "text-xs text-destructive underline",
|
|
form: { data: { turbo_confirm: t("retirement.show.delete_confirm") } } %>
|
|
</div>
|
|
</li>
|
|
<% end %>
|
|
</ul>
|
|
<% end %>
|
|
</section>
|
|
|
|
<%# Retirement bucket %>
|
|
<section class="bg-container shadow-border-xs rounded-xl p-5 space-y-4">
|
|
<div>
|
|
<h2 class="text-primary font-medium"><%= t("retirement.show.bucket_title") %></h2>
|
|
<p class="text-secondary text-xs"><%= t("retirement.show.bucket_hint") %></p>
|
|
</div>
|
|
|
|
<%= form_with url: retirement_bucket_path, method: :patch, class: "space-y-3" do |form| %>
|
|
<div class="space-y-2">
|
|
<% @bucket_candidates.each do |account| %>
|
|
<%= render DS::SelectableCard.new(
|
|
name: "bucket[account_ids][]",
|
|
value: account.id,
|
|
title: account.name,
|
|
subtitle: account.accountable_type&.underscore&.humanize,
|
|
amount: account.balance_money&.format,
|
|
checked: @bucket_account_ids.include?(account.id)
|
|
) %>
|
|
<% end %>
|
|
</div>
|
|
<%= form.submit t("retirement.show.save_bucket"), class: "text-sm font-medium text-primary underline cursor-pointer" %>
|
|
<% end %>
|
|
</section>
|
|
|
|
<%# Statement journal %>
|
|
<section class="bg-container shadow-border-xs rounded-xl p-5 space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h2 class="text-primary font-medium"><%= t("retirement.show.statements_title") %></h2>
|
|
<p class="text-secondary text-xs"><%= t("retirement.show.statements_subtitle") %></p>
|
|
</div>
|
|
<%= link_to t("retirement.show.log_statement"), new_retirement_statement_path,
|
|
class: "text-sm font-medium text-primary underline" %>
|
|
</div>
|
|
|
|
<% if @statements.empty? %>
|
|
<p class="text-secondary text-sm"><%= t("retirement.show.no_statements") %></p>
|
|
<% else %>
|
|
<table class="w-full text-sm">
|
|
<thead class="text-secondary text-xs text-left">
|
|
<tr>
|
|
<th class="py-2"><%= t("retirement.statements.table.date") %></th>
|
|
<th class="py-2"><%= t("retirement.statements.table.source") %></th>
|
|
<th class="py-2"><%= t("retirement.statements.table.points") %></th>
|
|
<th class="py-2"><%= t("retirement.statements.table.amount") %></th>
|
|
<th class="py-2"><%= t("retirement.statements.table.age") %></th>
|
|
<th class="py-2"><%= t("retirement.statements.table.notes") %></th>
|
|
<th class="py-2"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-tertiary">
|
|
<% @statements.each do |statement| %>
|
|
<tr>
|
|
<td class="py-2 text-primary tabular-nums"><%= I18n.l(statement.received_on) %></td>
|
|
<td class="py-2 text-secondary"><%= statement.pension_source.name %></td>
|
|
<td class="py-2 text-secondary tabular-nums"><%= statement.current_points %></td>
|
|
<td class="py-2 text-primary tabular-nums privacy-sensitive"><%= statement.projected_monthly_amount_money&.format %></td>
|
|
<td class="py-2 text-secondary tabular-nums"><%= statement.projected_at_age %></td>
|
|
<td class="py-2 text-secondary"><%= statement.raw_source_doc %></td>
|
|
<td class="py-2 text-right">
|
|
<%= button_to t("retirement.show.delete"), retirement_statement_path(statement),
|
|
method: :delete, class: "text-xs text-destructive underline",
|
|
form: { data: { turbo_confirm: t("retirement.show.delete_confirm") } } %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
<% end %>
|
|
</section>
|
|
</div>
|