Files
sure/app/views/goals/index.html.erb
Guillem Arias 9b61e4a41b refactor: rename Savings Goals feature to Goals
User-facing rename + structural rename. Feature is now called just
"Goals" everywhere — page title, sidebar nav, modal headings, flash
messages, AI assistant tool. Code identifiers follow:

- Models: SavingsGoal → Goal, SavingsContribution → GoalContribution,
  SavingsGoalAccount → GoalAccount.
- Tables: savings_goals → goals, savings_contributions → goal_contributions,
  savings_goal_accounts → goal_accounts. FK columns savings_goal_id →
  goal_id. New migration db/migrate/20260511100003_rename_savings_to_goals.rb
  uses rename_table + rename_column; PG handles index renaming and FK
  redirection automatically.
- Controllers: SavingsGoalsController → GoalsController,
  SavingsContributionsController → GoalContributionsController.
- Routes: /savings_goals → /goals, nested /goals/:id/contributions
  (resource name shifts; old route name aliases dropped).
- ViewComponent namespace: Savings::* → Goals::*. Component class
  names drop their redundant "Goal" prefix where the namespace already
  carries it: Savings::GoalCardComponent → Goals::CardComponent,
  Savings::GoalAvatarComponent → Goals::AvatarComponent. Others keep
  their names (Goals::ProgressRingComponent, Goals::StatusPillComponent,
  Goals::AccountStackComponent, Goals::FundingAccountsBreakdownComponent).
- Stimulus controllers: savings_goal_* → goal_*, savings_goals_filter
  → goals_filter. Stimulus identifiers in data-controller / data-*
  attributes follow.
- Locale keys: savings_goals: → goals: (top level), savings_contributions:
  → goal_contributions: (top level). All t() callers updated.
- AI assistant tool: Assistant::Function::CreateSavingsGoal →
  Assistant::Function::CreateGoal, tool name "create_savings_goal" →
  "create_goal", description / response text updated.
- Sidebar nav label "Savings" → "Goals". Goals/show + index page title
  "Savings" → "Goals". Empty goals_section heading/subtitle dropped
  (duplicated the page title post-rename).

Original migrations create_savings_goals / create_savings_goal_accounts /
create_savings_contributions remain untouched so historical replay
still works; the rename migration runs on top.
2026-05-11 20:08:32 +02:00

187 lines
9.1 KiB
Plaintext

<div class="space-y-8 pb-6 lg:pb-12">
<header>
<h1 class="text-2xl font-semibold text-primary"><%= t(".title") %></h1>
<p class="text-sm text-secondary mt-1"><%= t(".subtitle") %></p>
</header>
<% if @counts["all"].zero? %>
<%= render "empty_state", linkable_account_count: @linkable_account_count %>
<% else %>
<%# KPI strip %>
<section class="grid grid-cols-1 md:grid-cols-3 gap-3">
<%# Velocity %>
<div class="bg-container rounded-xl shadow-border-xs px-5 py-5">
<p class="text-[11px] font-medium uppercase tracking-wide text-secondary"><%= t(".kpi.velocity_label") %></p>
<p class="text-3xl font-medium text-primary tabular-nums mt-2 privacy-sensitive">
<%= @kpi[:velocity_30d_sign] %><%= @kpi[:velocity_30d_money].format %>
</p>
<% if @kpi[:velocity_direction] == :flat %>
<p class="text-xs text-secondary mt-1 tabular-nums">
<% if @kpi[:velocity_prior_30d_money].zero? && @kpi[:velocity_30d_money].zero? %>
<%= t(".kpi.velocity_delta_zero_base") %>
<% else %>
<%= t(".kpi.velocity_delta_flat") %>
<% end %>
</p>
<% elsif @kpi[:velocity_delta_percent].nil? %>
<p class="text-xs text-success mt-1 tabular-nums"><%= t(".kpi.velocity_delta_zero_base") %></p>
<% else %>
<p class="text-xs <%= @kpi[:velocity_direction] == :up ? "text-success" : "text-destructive" %> mt-1 tabular-nums">
<%= t(".kpi.velocity_delta_#{@kpi[:velocity_direction]}", percent: @kpi[:velocity_delta_percent].abs) %>
</p>
<% end %>
</div>
<%# Needs this month %>
<div class="bg-container rounded-xl shadow-border-xs px-5 py-5">
<p class="text-[11px] font-medium uppercase tracking-wide text-secondary"><%= t(".kpi.needs_this_month_label") %></p>
<p class="text-3xl font-medium text-primary tabular-nums mt-2 privacy-sensitive"><%= @kpi[:needs_this_month_money].format %></p>
<p class="text-xs text-secondary mt-1">
<% if @kpi[:behind_count].zero? %>
<%= t(".kpi.needs_this_month_zero_sub") %>
<% else %>
<%= t(".kpi.needs_this_month_sub", count: @kpi[:behind_count]) %>
<% end %>
</p>
</div>
<%# On-track count %>
<div class="bg-container rounded-xl shadow-border-xs px-5 py-5">
<p class="text-[11px] font-medium uppercase tracking-wide text-secondary"><%= t(".kpi.on_track_label") %></p>
<p class="text-3xl font-medium text-primary tabular-nums mt-2">
<%= t(".kpi.on_track_value", on_track: @kpi[:on_track_count], total: @kpi[:active_total]) %>
</p>
<p class="text-xs text-secondary mt-1">
<%
parts = []
parts << t(".kpi.on_track_sub_parts.behind", count: @kpi[:behind_count]) if @kpi[:behind_count].positive?
parts << t(".kpi.on_track_sub_parts.no_date", count: @kpi[:no_date_count]) if @kpi[:no_date_count].positive?
parts << t(".kpi.on_track_sub_parts.paused", count: @kpi[:paused_count]) if @kpi[:paused_count].positive?
%>
<% if parts.any? %>
<%= parts.join(" · ") %>
<% else %>
<%= t(".kpi.on_track_sub_all_good") %>
<% end %>
</p>
</div>
</section>
<%# Goals section %>
<section data-controller="goals-filter"
data-goals-filter-empty-query-value="<%= t(".search.empty_with_query", query: "__QUERY__") %>"
data-goals-filter-empty-filter-value="<%= t(".search.empty_with_filter") %>"
data-goals-filter-empty-both-value="<%= t(".search.empty_with_both", query: "__QUERY__") %>"
data-goals-filter-empty-default-value="<%= t(".search.empty") %>">
<% if @linkable_account_count > 0 %>
<div class="flex items-center justify-end mb-3">
<%= render DS::Link.new(
text: t(".new_goal"),
variant: "primary",
href: new_goal_path,
icon: "plus",
frame: :modal
) %>
</div>
<% end %>
<% if @show_search %>
<div class="flex flex-wrap items-center gap-2.5 mb-4">
<div class="relative flex-1 min-w-[200px]">
<input type="search"
autocomplete="off"
data-goals-filter-target="input"
data-action="input->goals-filter#filter"
aria-label="<%= t(".search.aria_label") %>"
placeholder="<%= t(".search.placeholder") %>"
class="block w-full border border-secondary rounded-md py-2.5 pl-10 pr-3 bg-container focus:ring-gray-500 sm:text-sm">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<%= icon "search", class: "text-secondary" %>
</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 paused].each do |status| %>
<% active = status == "all" %>
<button type="button"
data-goals-filter-target="chip"
data-action="click->goals-filter#selectChip"
data-status="<%= status %>"
aria-pressed="<%= active %>"
class="px-2.5 py-1 text-xs font-medium rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-100 <%= active ? "bg-container shadow-border-xs text-primary" : "text-secondary" %>">
<%= t(".chips.#{status}") %>
</button>
<% end %>
</div>
</div>
<% end %>
<% if @active_goals.any? %>
<div class="flex items-center gap-1.5 mb-4 text-[11px] font-medium uppercase tracking-wide text-secondary">
<span><%= t(".ongoing_section.heading") %></span>
<span class="text-subdued">·</span>
<span class="tabular-nums" data-goals-filter-target="count"><%= @active_goals.size %></span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3.5" data-goals-filter-target="grid">
<% @active_goals.each do |goal| %>
<%= render Goals::CardComponent.new(goal: goal) %>
<% end %>
</div>
<div class="hidden bg-container rounded-xl shadow-border-xs py-10 text-center" data-goals-filter-target="empty">
<p class="text-sm text-secondary" data-goals-filter-target="emptyCopy"><%= t(".search.empty") %></p>
<div class="mt-3 flex items-center justify-center gap-2">
<button type="button"
class="hidden text-xs font-medium text-secondary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-100"
data-goals-filter-target="emptyClearSearch"
data-action="click->goals-filter#clearSearch">
<%= t(".search.clear_search") %>
</button>
<button type="button"
class="hidden text-xs font-medium text-secondary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-alpha-black-100"
data-goals-filter-target="emptyClearFilter"
data-action="click->goals-filter#clearFilter">
<%= t(".search.show_all") %>
</button>
</div>
</div>
<% else %>
<div class="bg-container rounded-xl shadow-border-xs py-12 text-center">
<p class="text-sm text-secondary"><%= t(".empty_filtered") %></p>
</div>
<% end %>
</section>
<% if @completed_goals.any? %>
<section>
<div class="flex items-center gap-1.5 mb-4 text-[11px] font-medium uppercase tracking-wide text-secondary">
<span><%= t(".completed_section.heading") %></span>
<span class="text-subdued">·</span>
<span class="tabular-nums"><%= @completed_goals.size %></span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3.5">
<% @completed_goals.each do |goal| %>
<%= render Goals::CardComponent.new(goal: goal) %>
<% end %>
</div>
</section>
<% end %>
<% if @archived_goals.any? %>
<section>
<details class="group">
<summary class="inline-flex items-center gap-1.5 mb-4 text-[11px] font-medium uppercase tracking-wide text-secondary cursor-pointer list-none">
<span class="text-subdued group-open:rotate-90 transition-transform"><%= icon("chevron-right", size: "sm") %></span>
<span><%= t(".archived_section.heading") %></span>
<span class="text-subdued">·</span>
<span class="tabular-nums"><%= @archived_goals.size %></span>
</summary>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3.5">
<% @archived_goals.each do |goal| %>
<%= render Goals::CardComponent.new(goal: goal) %>
<% end %>
</div>
</details>
</section>
<% end %>
<% end %>
</div>