ux(goals/show): catch-up consolidation + confirm dialogs on archive / mark complete

A — Catch-up math triple-encoding. The catch-up amount (e.g. $1,531/mo)
was rendered verbatim in three places: the banner title, the projection
card subtitle, and the pace stat ("target $X/mo"). Only new information
anywhere was the buried "Behind by $Y/mo" delta in pace-card subtext.

- Banner body now carries the delta: "...currently $Y/mo behind."
- Projection sentence drops the "Bump to %{required}/mo" restatement;
  reduces to "At %{current}/mo you'll miss your target date." Chart
  aria-description benefits from the simpler phrasing too.
- Pace stat drops the "· target $X/mo" sub-line. Pair becomes
  "$avg/mo" + "Behind by $delta/mo" — same delta now in the banner,
  surfaced twice intentionally (alert vs at-a-glance stat).

K — Destructive transition confirms. Pause / Resume stay no-confirm
(recoverable). Mark complete (irreversible via UI; no may_uncomplete?
event exists) and Archive (goal disappears from active list) now wear
CustomConfirm. New locale keys: goals.show.confirm_{complete,archive}_
{title,body,cta}.

Locale catch_up body strings now interpolate %{delta} alongside %{date};
projection.behind drops %{required}. Controller#projection_summary still
passes both keys — extras are ignored by I18n.
This commit is contained in:
Guillem Arias
2026-05-11 20:22:45 +02:00
parent 8fc26b0666
commit 4be2ca2eeb
2 changed files with 37 additions and 10 deletions

View File

@@ -57,10 +57,32 @@
<% menu.with_item(variant: "button", text: t(".resume"), icon: "play", href: resume_goal_path(@goal), method: :patch) %>
<% end %>
<% if @goal.may_complete? %>
<% menu.with_item(variant: "button", text: t(".complete"), icon: "circle-check-big", href: complete_goal_path(@goal), method: :patch) %>
<% menu.with_item(
variant: "button",
text: t(".complete"),
icon: "circle-check-big",
href: complete_goal_path(@goal),
method: :patch,
confirm: CustomConfirm.new(
title: t(".confirm_complete_title"),
body: t(".confirm_complete_body"),
btn_text: t(".confirm_complete_cta")
)
) %>
<% end %>
<% if @goal.may_archive? %>
<% menu.with_item(variant: "button", text: t(".archive"), icon: "archive", href: archive_goal_path(@goal), method: :patch) %>
<% menu.with_item(
variant: "button",
text: t(".archive"),
icon: "archive",
href: archive_goal_path(@goal),
method: :patch,
confirm: CustomConfirm.new(
title: t(".confirm_archive_title"),
body: t(".confirm_archive_body"),
btn_text: t(".confirm_archive_cta")
)
) %>
<% end %>
<% if @goal.may_unarchive? %>
<% menu.with_item(variant: "button", text: t(".unarchive"), icon: "archive-restore", href: unarchive_goal_path(@goal), method: :patch) %>
@@ -113,12 +135,14 @@
<% elsif @goal.status == :behind && @goal.monthly_target_amount %>
<%# Catch-up callout %>
<% catch_up_money = Money.new(@goal.monthly_target_amount, @goal.currency) %>
<% catch_up_delta = @goal.monthly_target_amount.to_d - @stats[:avg_monthly].to_d %>
<% catch_up_delta_money = Money.new(catch_up_delta, @goal.currency) %>
<%= render DS::Alert.new(variant: "warning", title: t("goals.show.catch_up.title", amount: catch_up_money.format)) do %>
<p class="text-secondary">
<% if @goal.target_date %>
<%= t("goals.show.catch_up.body_with_date", date: I18n.l(@goal.target_date, format: :long)) %>
<%= t("goals.show.catch_up.body_with_date", date: I18n.l(@goal.target_date, format: :long), delta: catch_up_delta_money.format) %>
<% else %>
<%= t("goals.show.catch_up.body") %>
<%= t("goals.show.catch_up.body", delta: catch_up_delta_money.format) %>
<% end %>
</p>
<div class="mt-2">
@@ -240,9 +264,6 @@
<div class="flex items-baseline gap-2">
<p class="text-2xl font-medium text-primary tabular-nums"><%= Money.new(@stats[:avg_monthly], @goal.currency).format %></p>
<p class="text-sm text-subdued tabular-nums">/mo</p>
<% if @goal.monthly_target_amount && @goal.monthly_target_amount.to_d.positive? %>
<p class="text-sm text-subdued tabular-nums">· <%= t(".stats.target_of", amount: Money.new(@goal.monthly_target_amount, @goal.currency).format) %></p>
<% end %>
</div>
<% if @goal.monthly_target_amount && @goal.monthly_target_amount.to_d.positive? %>
<% delta = @goal.monthly_target_amount.to_d - @stats[:avg_monthly].to_d %>

View File

@@ -119,14 +119,20 @@ en:
reached: Goal reached. Nice work.
no_target_date: No target date set. Set one to project a finish line.
no_pace: No contributions yet. Add some to start a projection.
behind: At %{current}/mo you'll fall short. Bump to <strong class="text-primary">%{required}/mo</strong> to hit it on time.
behind: At %{current}/mo you'll miss your target date.
on_track: At your current pace, you'll reach this goal around <strong class="text-primary">%{date}</strong>.
aria_label: "Projection chart for %{name}"
catch_up:
title: "Save %{amount}/mo to catch up"
body_with_date: "Bump your monthly contribution to stay on track for %{date}."
body: Bump your monthly contribution to stay on track.
body_with_date: "Bump your monthly contribution to stay on track for %{date}. Currently %{delta}/mo behind."
body: "Bump your monthly contribution to stay on track. Currently %{delta}/mo behind."
cta: "Add %{amount}"
confirm_complete_title: Mark this goal complete?
confirm_complete_body: It leaves the Ongoing list. You can still archive or restore it later.
confirm_complete_cta: Mark complete
confirm_archive_title: Archive this goal?
confirm_archive_body: Archived goals disappear from the main list. You can restore them later.
confirm_archive_cta: Archive
paused_banner:
title: This goal is paused
body: Resume it to keep tracking your progress.