Files
sure/app/components/goals/card_component.rb
Guillem Arias 82e3ba8ef7 fix(goals): keep archived cards out of the filter loop entirely
The earlier 'filter archived too' attempts kept toggling the archived
section based on chip state, which produced more confusion than value
(filter shows partial counts, archived hides on some chips, etc.).
Step back: archived stays in its own collapsed-by-default section,
always visible, never reacts to the chip / search filter. Render
the cards with filterable: false so they don't add a filter target
in the first place — no JS handling needed, and the active grid +
chips behave exactly like they did before this whole thread.
2026-05-18 22:16:38 +02:00

107 lines
3.2 KiB
Ruby

class Goals::CardComponent < ApplicationComponent
RING_SIZE = 64
RING_STROKE = 6
def initialize(goal:, filterable: true)
@goal = goal
@filterable = filterable
end
attr_reader :goal, :filterable
def progress_percent
goal.progress_percent
end
def ring_color
case goal.status
when :reached then "var(--color-green-600)"
when :behind then "var(--color-yellow-600)"
when :on_track then "var(--color-green-500)"
else "var(--color-gray-400)"
end
end
def linked_accounts
@linked_accounts ||= goal.linked_accounts.to_a
end
def linked_accounts_count_label
I18n.t("goals.goal_card.accounts", count: linked_accounts.size)
end
# Single screen-reader sentence for the card's title <a> aria-label.
# Without this, the whole-card link would inherit every nested text node
# as its accessible name (>15 strings on a typical card).
def aria_label
status_text = I18n.t("goals.status.#{goal.display_status}")
progress_text = I18n.t("goals.goal_card.aria_progress",
percent: progress_percent,
target: goal.target_amount_money.format(precision: 0))
[ goal.name, status_text, progress_text ].join(", ")
end
def secondary_line
if goal.completed?
I18n.t("goals.goal_card.completed")
elsif goal.target_date.nil?
I18n.t("goals.goal_card.no_target_date")
else
days = (goal.target_date - Date.current).to_i
if days >= 0
I18n.t("goals.goal_card.days_left_by", count: days, date: I18n.l(goal.target_date, format: :long))
else
I18n.t("goals.goal_card.past_due")
end
end
end
def ring_circumference
@ring_circumference ||= 2 * Math::PI * ring_radius
end
def ring_radius
@ring_radius ||= (RING_SIZE - RING_STROKE) / 2.0
end
def ring_offset
pct = [ [ progress_percent.to_i, 0 ].max, 100 ].min
ring_circumference * (1 - pct / 100.0)
end
def pace_line
return nil if goal.archived? || goal.paused? || goal.completed? || goal.status == :reached
avg = goal.pace_money.format(precision: 0)
target = goal.monthly_target_amount ? Money.new(goal.monthly_target_amount, goal.currency).format(precision: 0) : nil
if target
I18n.t("goals.goal_card.pace_with_target", avg: avg, target: target)
else
I18n.t("goals.goal_card.pace_no_target", avg: avg)
end
end
def footer_line
if goal.archived?
I18n.t("goals.goal_card.footer_archived")
elsif goal.paused?
I18n.t("goals.goal_card.footer_paused")
elsif goal.completed? || goal.status == :reached
I18n.t("goals.goal_card.footer_reached")
elsif goal.status == :behind && goal.monthly_target_amount
I18n.t("goals.goal_card.footer_catch_up", amount: goal.catch_up_delta_money.format(precision: 0))
elsif goal.status == :no_target_date
I18n.t("goals.goal_card.footer_no_deadline")
else
days = goal.last_matched_pledge_days_ago
if days.nil?
I18n.t("goals.goal_card.footer_no_pledges")
elsif days.zero?
I18n.t("goals.goal_card.footer_last_today")
else
I18n.t("goals.goal_card.footer_last_days", count: days)
end
end
end
end