feat(savings_goals): status pill icons + paused variant, attention-first sort, paused chip, rename "No date" to "Open-ended"

P4: status pills now carry an icon alongside the colored tint
(circle-check / triangle-alert / star / infinity / pause), so color is
no longer the sole signal. Drop the redundant dot.

P4: default sort on the active goals list becomes attention-first —
behind → on_track → no_target_date → paused, alphabetical within bucket.
The user opens the page and lands on the goals that need them.

P5: add a Paused filter chip + render paused goal cards with opacity-75
so they read as inactive at a glance. Rename "No date" chip to
"Open-ended" — clearer to non-jargon readers.
This commit is contained in:
Guillem Arias
2026-05-11 14:17:54 +02:00
parent 8ba6cbcdc8
commit 44b3190cd8
6 changed files with 21 additions and 15 deletions

View File

@@ -1,9 +1,9 @@
<%= link_to savings_goal_path(goal),
class: "group block bg-container rounded-xl shadow-border-xs hover:bg-surface-hover transition-colors p-6",
class: "group block bg-container rounded-xl shadow-border-xs hover:bg-surface-hover transition-colors p-6 #{"opacity-75" if goal.paused?}",
data: {
savings_goals_filter_target: "card",
goal_name: goal.name,
goal_status: goal.status
goal_status: goal.paused? ? "paused" : goal.status
} do %>
<div class="flex items-start gap-3">
<%= render Savings::GoalAvatarComponent.new(goal: goal, size: "lg") %>

View File

@@ -1,4 +1,4 @@
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium whitespace-nowrap <%= classes %>">
<span class="w-1.5 h-1.5 rounded-full <%= dot_classes %>"></span>
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium whitespace-nowrap <%= classes %>" aria-label="<%= label %>">
<%= helpers.icon(icon_name, size: "xs") %>
<%= label %>
</span>

View File

@@ -1,32 +1,34 @@
class Savings::StatusPillComponent < ApplicationComponent
VARIANTS = {
on_track: { classes: "bg-green-500/10 text-success", dot: "bg-green-600" },
behind: { classes: "bg-yellow-500/10 text-warning", dot: "bg-yellow-600" },
reached: { classes: "bg-green-500/10 text-success", dot: "bg-green-600" },
no_target_date: { classes: "bg-surface-inset text-secondary", dot: "bg-gray-400" }
on_track: { classes: "bg-green-500/10 text-success", icon: "circle-check" },
behind: { classes: "bg-yellow-500/10 text-warning", icon: "triangle-alert" },
reached: { classes: "bg-green-500/10 text-success", icon: "star" },
no_target_date: { classes: "bg-surface-inset text-secondary", icon: "infinity" },
paused: { classes: "bg-surface-inset text-secondary", icon: "pause" }
}.freeze
def initialize(goal:)
@goal = goal
end
def status
def status_key
return :paused if @goal.paused?
@goal.status
end
def variant
VARIANTS.fetch(status, VARIANTS[:no_target_date])
VARIANTS.fetch(status_key, VARIANTS[:no_target_date])
end
def label
I18n.t("savings_goals.status.#{status}")
I18n.t("savings_goals.status.#{status_key}")
end
def classes
variant[:classes]
end
def dot_classes
variant[:dot]
def icon_name
variant[:icon]
end
end

View File

@@ -2,6 +2,7 @@ class SavingsGoalsController < ApplicationController
before_action :set_savings_goal, only: %i[show edit update destroy pause resume complete archive unarchive]
STATE_FILTERS = %w[all active paused completed archived].freeze
ACTIVE_STATUS_RANK = { behind: 0, on_track: 1, no_target_date: 2 }.freeze
def index
@counts = STATE_FILTERS.each_with_object({}) do |state, h|
@@ -10,6 +11,7 @@ class SavingsGoalsController < ApplicationController
all_goals = Current.family.savings_goals.with_current_balance.alphabetically.to_a
@active_goals = all_goals.reject { |g| %w[completed archived].include?(g.state) }
.sort_by { |g| [ g.paused? ? 3 : ACTIVE_STATUS_RANK.fetch(g.status, 4), g.name.downcase ] }
@completed_goals = all_goals.select { |g| g.state == "completed" }
@linkable_account_count = Current.family.accounts.where(accountable_type: "Depository").visible.count

View File

@@ -119,7 +119,7 @@
</div>
</div>
<div class="inline-flex items-center gap-1 p-1 bg-surface-inset rounded-xl">
<% %w[all on_track behind no_target_date].each do |status| %>
<% %w[all on_track behind no_target_date paused].each do |status| %>
<% active = status == "all" %>
<button type="button"
data-savings-goals-filter-target="chip"

View File

@@ -53,7 +53,8 @@ en:
all: All
on_track: On track
behind: Behind
no_target_date: No date
no_target_date: Open-ended
paused: Paused
account_card:
funds:
one: Funds 1 goal
@@ -151,6 +152,7 @@ en:
behind: Behind
reached: Reached
no_target_date: No date
paused: Paused
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.