mirror of
https://github.com/we-promise/sure.git
synced 2026-05-30 07:49:01 +00:00
The dashboard centerpiece. Goal::Retirement#glide_payload derives, from the forecast, the active-plan series + a zero-savings (Walletburst) shadow + a ±1pp real-return band + the per-age income breakdown for the hover tooltip + lump markers + the retire/Coast crossover points (three extra deterministic Forecast runs; cheap). retirement_glide_chart_controller (D3, mirrors goal_projection_chart's import / ResizeObserver / theme-observer idiom): portfolio-by-age line + area, accumulation/drawdown phase shading, the ±1pp band cone, the dashed Walletburst shadow, a "Retire · age N / $X" chip on the retire line, a blue Coast crossover ring, purple lump bars, and a hover tooltip (PR #2029 bg-container/rounded-xl/shadow style) showing the monthly State / Workplace / Drawdown breakdown + Total-vs-target with a Covered badge. Wired into the show page above the what-if; container is privacy-sensitive. Browser-verified: renders the band, shading, retire chip ($571K), Coast dot, and shadow against the demo plan. glide_payload + lump_markers unit-tested. Rubocop + erb_lint + biome clean. Remaining for PR4: DS::SelectableCard bucket, "Why this target?" anchor card, skinned DS::Dialog deletes, DE locale, demo seed, system test.
42 lines
1.4 KiB
Ruby
42 lines
1.4 KiB
Ruby
class RetirementController < ApplicationController
|
|
include RetirementScoped
|
|
|
|
def show
|
|
@glide = @plan.glide_payload
|
|
@pension_sources = @plan.pension_sources.order(:start_age)
|
|
@adjustments = @plan.adjustments.ordered
|
|
@statements = @plan.statements.chronological.reverse
|
|
@bucket_account_ids = @plan.retirement_bucket_entries.pluck(:account_id).to_set
|
|
@bucket_candidates = Current.family.accounts.visible.alphabetically
|
|
@breadcrumbs = [
|
|
[ t("breadcrumbs.home"), root_path ],
|
|
[ t("breadcrumbs.retirement"), nil ]
|
|
]
|
|
end
|
|
|
|
def update
|
|
if @plan.update(retirement_params: merged_plan_params)
|
|
redirect_to retirement_path, notice: t(".updated")
|
|
else
|
|
redirect_to retirement_path, alert: @plan.errors.full_messages.to_sentence
|
|
end
|
|
end
|
|
|
|
# Live what-if: recompute against transient inputs WITHOUT persisting, and
|
|
# stream the KPI cards back. The plan is only saved via #update.
|
|
def forecast
|
|
@plan.assign_attributes(retirement_params: merged_plan_params)
|
|
render turbo_stream: turbo_stream.replace(
|
|
"retirement_kpis", partial: "retirement/kpis", locals: { plan: @plan }
|
|
)
|
|
end
|
|
|
|
private
|
|
def merged_plan_params
|
|
raw = params.fetch(:retirement, {}).permit(
|
|
:birth_year, :retire_age, :target_spend, :monthly_savings, :real_return_pct
|
|
).to_h
|
|
(@plan.retirement_params || {}).merge(raw.reject { |_, v| v.to_s.strip.empty? })
|
|
end
|
|
end
|