mirror of
https://github.com/we-promise/sure.git
synced 2026-05-30 07:49:01 +00:00
fix(savings_goals): use display_status for inactive goals; hide pace + projection
- SavingsGoal#display_status returns :archived / :paused before falling
through to the visualization status. Memoized like #status. The plain
#status method keeps its meaning (visualization vs. target/pace) so
callers that genuinely want "is this on track" — KPI sort, goal-card
ring color, projection_payload — keep working unchanged.
- Savings::StatusPillComponent: status_key uses display_status; new
:archived variant (bg-surface-inset / text-gray-700 / archive icon).
Previously an archived goal showed "Behind" on the detail page while
the archived banner said the goal was archived — conflicting signal.
- show.html.erb: paused/archived goals render a static recap card
(current saved vs target) instead of the projection chart. Pace stat
(avg vs required monthly) is also hidden — extrapolating "Behind by
$X/mo" against a goal that isn't accepting contributions is misleading.
- New locale keys: savings_goals.status.archived,
savings_goals.show.inactive.{heading_paused, heading_archived, body}.
- Tests cover display_status for archived / paused / active goals.
This commit is contained in:
@@ -9,7 +9,8 @@ class Savings::StatusPillComponent < ApplicationComponent
|
||||
behind: { classes: "bg-yellow-500/10 text-yellow-700", icon: "triangle-alert" },
|
||||
reached: { classes: "bg-green-500/10 text-green-700", icon: "star" },
|
||||
no_target_date: { classes: "bg-surface-inset text-gray-700", icon: "infinity" },
|
||||
paused: { classes: "bg-surface-inset text-gray-700", icon: "pause" }
|
||||
paused: { classes: "bg-surface-inset text-gray-700", icon: "pause" },
|
||||
archived: { classes: "bg-surface-inset text-gray-700", icon: "archive" }
|
||||
}.freeze
|
||||
|
||||
def initialize(goal:)
|
||||
@@ -17,8 +18,7 @@ class Savings::StatusPillComponent < ApplicationComponent
|
||||
end
|
||||
|
||||
def status_key
|
||||
return :paused if @goal.paused?
|
||||
@goal.status
|
||||
@goal.display_status
|
||||
end
|
||||
|
||||
def variant
|
||||
|
||||
@@ -162,6 +162,21 @@ class SavingsGoal < ApplicationRecord
|
||||
}
|
||||
end
|
||||
|
||||
# Display-layer status. Prefers AASM state for inactive goals so the UI
|
||||
# doesn't compute a misleading "Behind / On track" verdict against a goal
|
||||
# that isn't accepting contributions anymore.
|
||||
def display_status
|
||||
return @display_status if defined?(@display_status)
|
||||
|
||||
@display_status = if archived?
|
||||
:archived
|
||||
elsif paused?
|
||||
:paused
|
||||
else
|
||||
status
|
||||
end
|
||||
end
|
||||
|
||||
# :reached → progress_percent >= 100
|
||||
# :on_track → has target_date and current pace >= required monthly pace
|
||||
# :behind → has target_date and current pace < required monthly pace
|
||||
|
||||
@@ -145,7 +145,20 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<% if @savings_goal.completed? || @savings_goal.status == :reached %>
|
||||
<% if @savings_goal.archived? || @savings_goal.paused? %>
|
||||
<%# Paused / archived: pace + projection are misleading. Show a static recap card. %>
|
||||
<div class="bg-container rounded-xl shadow-border-xs p-5 flex flex-col items-center justify-center text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-surface-inset inline-flex items-center justify-center text-secondary mb-3">
|
||||
<%= icon(@savings_goal.archived? ? "archive" : "pause", size: "2xl") %>
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-primary">
|
||||
<%= t(@savings_goal.archived? ? ".inactive.heading_archived" : ".inactive.heading_paused") %>
|
||||
</h2>
|
||||
<p class="text-sm text-secondary mt-1 max-w-md tabular-nums">
|
||||
<%= t(".inactive.body", saved: @savings_goal.current_balance_money.format, target: @savings_goal.target_amount_money.format) %>
|
||||
</p>
|
||||
</div>
|
||||
<% elsif @savings_goal.completed? || @savings_goal.status == :reached %>
|
||||
<%# Reached celebration card %>
|
||||
<div class="bg-container rounded-xl shadow-border-xs p-5 flex flex-col items-center justify-center text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-green-500/10 inline-flex items-center justify-center text-success mb-3">
|
||||
@@ -212,11 +225,13 @@
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<%# Stat row — combo pace card + contributions count. Reached goals
|
||||
hide the pace combo since the comparison is moot. %>
|
||||
<%# 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 = @savings_goal.completed? || @savings_goal.status == :reached %>
|
||||
<section class="grid grid-cols-1 <%= goal_reached ? "" : "md:grid-cols-3" %> gap-3">
|
||||
<% unless goal_reached %>
|
||||
<% hide_pace = goal_reached || @savings_goal.archived? || @savings_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>
|
||||
|
||||
@@ -137,6 +137,10 @@ en:
|
||||
heading: Goal reached. Nice work.
|
||||
body: "You hit your %{amount} target. Keep the goal as a record, or archive it now."
|
||||
archive_cta: Archive goal
|
||||
inactive:
|
||||
heading_paused: This goal is paused
|
||||
heading_archived: This goal is archived
|
||||
body: "%{saved} of %{target} saved so far."
|
||||
no_target_date:
|
||||
heading: Add a target date
|
||||
body: Set a deadline to project a finish line and track required pace.
|
||||
@@ -168,6 +172,7 @@ en:
|
||||
reached: Reached
|
||||
no_target_date: No date
|
||||
paused: Paused
|
||||
archived: Archived
|
||||
empty_state:
|
||||
heading: No goals yet
|
||||
body: Set a target, link the accounts you save into, and watch your progress add up. Goals can pull from multiple accounts.
|
||||
|
||||
@@ -126,6 +126,23 @@ class SavingsGoalTest < ActiveSupport::TestCase
|
||||
assert_equal :no_target_date, @goal.status
|
||||
end
|
||||
|
||||
test "display_status returns :archived for archived goal regardless of progress" do
|
||||
@goal.save!
|
||||
@goal.archive!
|
||||
assert_equal :archived, @goal.display_status
|
||||
end
|
||||
|
||||
test "display_status returns :paused for paused goal regardless of progress" do
|
||||
@goal.save!
|
||||
@goal.pause!
|
||||
assert_equal :paused, @goal.display_status
|
||||
end
|
||||
|
||||
test "display_status falls through to status for active goals" do
|
||||
@goal.target_amount = 1
|
||||
assert_equal :reached, @goal.display_status
|
||||
end
|
||||
|
||||
test "advisory_lock_key_for is stable per family" do
|
||||
k1 = SavingsGoal.advisory_lock_key_for(@family.id)
|
||||
k2 = SavingsGoal.advisory_lock_key_for(@family.id)
|
||||
|
||||
Reference in New Issue
Block a user