ux(goals/show): balance-sheet-style funding widget; drop redundant stat row

Lower half of the goal detail used to be: (stat row: monthly pace +
total contributions) + (bottom row: contributions list + funding
breakdown card). Two of those four pieces were redundant:

- Total Contributions stat duplicated the count badge that already
  sits beside the Contributions heading below.
- Monthly Pace stat repeated the same numbers the catch-up alert
  surfaces above and the chart subtitle reads.

Adopt the dashboard Balance Sheet pattern (app/views/pages/dashboard/_
balance_sheet.html.erb) for the funding widget: inline header with
total ("Funding accounts · $13,250"), thin gap-separated segment bar,
color-dot legend with percent, and a bg-container-inset table with the
shared `pages/dashboard/group_weight` 5-stick weight indicator + value
column.

New show.html.erb bottom: just two full-width sections — funding
widget, then chronological contributions list. Both rendered only when
the goal has contributions (matches the empty-state branch added
earlier).

Locale: goals.show.funding_table.{name, weight, value}.
This commit is contained in:
Guillem Arias
2026-05-11 21:18:41 +02:00
parent 57b7848eec
commit 4bcca3e4af
3 changed files with 68 additions and 64 deletions

View File

@@ -1,27 +1,61 @@
<% if total.zero? %>
<p class="text-sm text-secondary"><%= t("goals.show.no_contributions_yet") %></p>
<% else %>
<div class="flex h-2 rounded-full overflow-hidden mb-4">
<% rows.each do |row| %>
<% next if row[:amount].to_d.zero? %>
<div style="width: <%= percent_for(row[:amount]) %>%; background-color: <%= Goals::AvatarComponent.color_for(row[:account].name) %>;"
title="<%= row[:account].name %>"></div>
<% end %>
</div>
<div class="space-y-4">
<h2 class="text-lg font-medium inline-flex items-center gap-1.5">
<%= t("goals.show.funding_accounts_heading") %>
<span class="text-secondary">·</span>
<span class="text-secondary font-medium text-lg privacy-sensitive tabular-nums"><%= Money.new(total, goal.currency).format(precision: 0) %></span>
</h2>
<ul class="space-y-3">
<% rows.each do |row| %>
<li class="flex items-center gap-3">
<%= render Goals::AvatarComponent.new(name: row[:account].name, color: Goals::AvatarComponent.color_for(row[:account].name), size: "sm") %>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-primary truncate"><%= row[:account].name %></p>
<p class="text-[11px] text-subdued"><%= row[:account].subtype&.titleize || row[:account].accountable_type %> · <%= t("goals.show.funding_balance", amount: Money.new(row[:account].balance, row[:account].currency).format) %></p>
<div class="flex gap-1">
<% rows.each do |row| %>
<% next if row[:amount].to_d.zero? %>
<div class="h-1.5 rounded-sm" style="width: <%= percent_for(row[:amount]) %>%; background-color: <%= Goals::AvatarComponent.color_for(row[:account].name) %>;"></div>
<% end %>
</div>
<div class="flex flex-wrap gap-4">
<% rows.each do |row| %>
<% next if row[:amount].to_d.zero? %>
<div class="flex items-center gap-2 text-sm">
<div class="h-2.5 w-2.5 rounded-full" style="background-color: <%= Goals::AvatarComponent.color_for(row[:account].name) %>;"></div>
<p class="text-secondary"><%= row[:account].name %></p>
<p class="text-primary font-mono privacy-sensitive tabular-nums"><%= percent_for(row[:amount]) %>%</p>
</div>
<div class="text-right">
<p class="text-sm font-medium text-primary tabular-nums"><%= row[:money].format %></p>
<p class="text-[10px] text-subdued tabular-nums"><%= percent_for(row[:amount]) %>% <%= t("goals.show.of_saved") %></p>
<% end %>
</div>
<div class="bg-container-inset rounded-xl p-1 overflow-x-auto">
<div class="px-4 py-2 flex items-center uppercase text-xs font-medium text-secondary">
<div class="flex-1 min-w-0"><%= t("goals.show.funding_table.name") %></div>
<div class="ml-auto text-right flex items-center gap-2">
<div class="w-20 shrink-0"><p><%= t("goals.show.funding_table.weight") %></p></div>
<div class="w-24 shrink-0"><p><%= t("goals.show.funding_table.value") %></p></div>
</div>
</li>
<% end %>
</ul>
</div>
<div class="rounded-lg bg-container font-medium text-sm">
<% rows.each_with_index do |row, idx| %>
<div class="p-4 flex items-center justify-between gap-3">
<div class="flex-1 min-w-0 flex items-center gap-2">
<%= render Goals::AvatarComponent.new(name: row[:account].name, color: Goals::AvatarComponent.color_for(row[:account].name), size: "sm") %>
<p class="truncate"><%= row[:account].name %></p>
</div>
<div class="flex items-center justify-between text-right gap-2 shrink-0">
<div class="w-20 shrink-0 flex items-center justify-end gap-2">
<%= render "pages/dashboard/group_weight", weight: percent_for(row[:amount]), color: Goals::AvatarComponent.color_for(row[:account].name) %>
</div>
<div class="w-24 shrink-0">
<p class="privacy-sensitive tabular-nums"><%= row[:money].format(precision: 0) %></p>
</div>
</div>
</div>
<% if idx < rows.size - 1 %>
<%= render "shared/ruler", classes: "mx-4" %>
<% end %>
<% end %>
</div>
</div>
</div>
<% end %>

View File

@@ -269,44 +269,15 @@
<% end %>
</section>
<%# Stat row — combo pace card + contributions count. Reached, paused,
or archived goals hide the pace combo since the comparison is moot
or misleading. %>
<% goal_reached = @goal.completed? || @goal.status == :reached %>
<% hide_pace = goal_reached || @goal.archived? || @goal.paused? %>
<section class="grid grid-cols-1 <%= hide_pace ? "" : "md:grid-cols-3" %> gap-3">
<% unless hide_pace %>
<%# Combo: Avg vs Target pace %>
<div class="md:col-span-2 bg-container rounded-xl shadow-border-xs px-5 py-4">
<p class="text-[11px] text-secondary mb-2"><%= t(".stats.monthly_pace") %></p>
<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>
</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 %>
<% if delta.positive? %>
<p class="text-xs text-subdued mt-1 tabular-nums"><%= t(".stats.behind_by", amount: Money.new(delta, @goal.currency).format) %></p>
<% else %>
<p class="text-xs text-subdued mt-1 tabular-nums"><%= t(".stats.above_target_pace") %></p>
<% end %>
<% else %>
<p class="text-xs text-subdued mt-1"><%= t(".stats.no_required_pace") %></p>
<% end %>
</div>
<% end %>
<% unless @contributions.empty? %>
<%# Funding breakdown — balance-sheet-style widget (heading · total /
thin bar / dot legend / weight table). %>
<section class="bg-container rounded-xl shadow-border-xs p-5">
<%= render Goals::FundingAccountsBreakdownComponent.new(goal: @goal, rows: @funding_breakdown) %>
</section>
<%# Total contributions %>
<div class="bg-container rounded-xl shadow-border-xs px-5 py-4">
<p class="text-[11px] text-secondary mb-1"><%= t(".stats.total_contributions") %></p>
<p class="text-lg font-medium text-primary tabular-nums"><%= @stats[:contributions_count] %></p>
<p class="text-[11px] text-subdued mt-1"><%= t(".stats.across_all_accounts") %></p>
</div>
</section>
<%# Bottom row: contributions + funding accounts %>
<section class="grid grid-cols-1 lg:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)] gap-3">
<div class="bg-container rounded-xl shadow-border-xs p-5">
<%# Contributions — chronological list, full width. %>
<section class="bg-container rounded-xl shadow-border-xs p-5">
<div class="flex items-center mb-4">
<h2 class="text-sm font-medium text-primary"><%= t(".contributions_heading") %></h2>
<span class="ml-2 text-xs text-subdued tabular-nums"><%= @contributions.size %></span>
@@ -314,13 +285,8 @@
<div class="max-h-[420px] overflow-y-auto overflow-x-hidden scrollbar">
<%= render "contributions_list", contributions: @contributions %>
</div>
</div>
<div class="bg-container rounded-xl shadow-border-xs p-5">
<h2 class="text-sm font-medium text-primary mb-3"><%= t(".funding_accounts_heading") %></h2>
<%= render Goals::FundingAccountsBreakdownComponent.new(goal: @goal, rows: @funding_breakdown) %>
</div>
</section>
</section>
<% end %>
<% if @goal.notes.present? %>
<section class="bg-container rounded-xl shadow-border-xs p-5">

View File

@@ -94,6 +94,10 @@ en:
contributions_heading: Contributions
add_contribution: Add contribution
funding_accounts_heading: Funding accounts
funding_table:
name: Name
weight: Weight
value: Value
no_contributions_yet: No contributions yet.
delete_contribution: Delete contribution
confirm_delete_contribution: Delete this contribution?