<%= render Goals::AvatarComponent.new(goal: @goal, size: "xl") %>
<%= @goal.name %>
<%= render Goals::StatusPillComponent.new(goal: @goal) %>
<%
primary_parts = []
if @goal.target_date
primary_parts << t(".header.target_by", amount: @goal.target_amount_money.format, date: I18n.l(@goal.target_date, format: :long))
unless @goal.completed? || @goal.status == :reached
days = (@goal.target_date - Date.current).to_i
if days > 0
primary_parts << t("goals.goal_card.days_left", count: days, date: I18n.l(@goal.target_date, format: :long)).split(" · ").first
end
end
else
primary_parts << t(".header.target", amount: @goal.target_amount_money.format)
end
%>
<%= primary_parts.join(" · ") %>
<% last_days = @goal.last_contribution_days_ago %>
<% unless last_days.nil? %>
<%= last_days.zero? ? t("goals.goal_card.footer_last_today") : t("goals.goal_card.footer_last_days", count: last_days) %>
<% end %>
<%= render DS::Link.new(
text: t(".edit"),
variant: "outline",
href: edit_goal_path(@goal),
icon: "pencil",
frame: :modal
) %>
<% unless @goal.completed? || @goal.status == :reached %>
<%= render DS::Link.new(
text: t(".add_contribution"),
variant: "primary",
href: new_goal_contribution_path(@goal),
icon: "plus",
frame: :modal
) %>
<% end %>
<%= render DS::Menu.new do |menu| %>
<% if @goal.may_pause? %>
<% menu.with_item(variant: "button", text: t(".pause"), icon: "pause", href: pause_goal_path(@goal), method: :patch) %>
<% end %>
<% if @goal.may_resume? %>
<% menu.with_item(variant: "button", text: t(".resume"), icon: "play", href: resume_goal_path(@goal), method: :patch) %>
<% end %>
<% if @goal.may_complete? %>
<% menu.with_item(
variant: "button",
text: t(".complete"),
icon: "circle-check-big",
href: complete_goal_path(@goal),
method: :patch,
confirm: CustomConfirm.new(
title: t(".confirm_complete_title"),
body: t(".confirm_complete_body"),
btn_text: t(".confirm_complete_cta")
)
) %>
<% end %>
<% if @goal.may_archive? %>
<% menu.with_item(
variant: "button",
text: t(".archive"),
icon: "archive",
href: archive_goal_path(@goal),
method: :patch,
confirm: CustomConfirm.new(
title: t(".confirm_archive_title"),
body: t(".confirm_archive_body"),
btn_text: t(".confirm_archive_cta")
)
) %>
<% end %>
<% if @goal.may_unarchive? %>
<% menu.with_item(variant: "button", text: t(".unarchive"), icon: "archive-restore", href: unarchive_goal_path(@goal), method: :patch) %>
<% end %>
<% if @goal.archived? %>
<% menu.with_item(
variant: "button",
text: t(".delete"),
icon: "trash-2",
href: goal_path(@goal),
method: :delete,
destructive: true,
confirm: CustomConfirm.for_resource_deletion(@goal.name, high_severity: true)
) %>
<% end %>
<% end %>
<% if @goal.paused? %>
<%# Paused banner %>
<%= render DS::Alert.new(variant: "info", title: t("goals.show.paused_banner.title")) do %>
<%= t("goals.show.paused_banner.body") %>
<%= render DS::Button.new(
text: t("goals.show.paused_banner.resume_cta"),
href: resume_goal_path(@goal),
variant: "primary",
size: "sm",
method: :patch
) %>
<% end %>
<% elsif @goal.archived? %>
<%# Archived banner %>
<%= render DS::Alert.new(variant: "info", title: t("goals.show.archived_banner.title")) do %>
<%= t("goals.show.archived_banner.body") %>
<% if @goal.may_unarchive? %>
<%= render DS::Button.new(
text: t("goals.show.archived_banner.restore_cta"),
href: unarchive_goal_path(@goal),
variant: "primary",
size: "sm",
method: :patch
) %>
<% end %>
<% end %>
<% elsif @goal.status == :behind && @goal.monthly_target_amount %>
<%# Catch-up callout %>
<% catch_up_money = Money.new(@goal.monthly_target_amount, @goal.currency) %>
<% catch_up_delta = @goal.monthly_target_amount.to_d - @stats[:avg_monthly].to_d %>
<% catch_up_delta_money = Money.new(catch_up_delta, @goal.currency) %>
<% catch_up_avg_money = Money.new(@stats[:avg_monthly], @goal.currency) %>
<%= render DS::Alert.new(variant: "warning", title: t("goals.show.catch_up.title", amount: catch_up_money.format)) do %>
<% if @goal.target_date %>
<%= t("goals.show.catch_up.body_with_date", avg: catch_up_avg_money.format, delta: catch_up_delta_money.format, date: I18n.l(@goal.target_date, format: :long)) %>
<% else %>
<%= t("goals.show.catch_up.body", avg: catch_up_avg_money.format, delta: catch_up_delta_money.format) %>
<% end %>
<%= render DS::Link.new(
text: t("goals.show.catch_up.cta", amount: catch_up_money.format),
variant: "primary",
size: "sm",
href: new_goal_contribution_path(@goal, amount: @goal.monthly_target_amount.to_f),
icon: "plus",
frame: :modal
) %>
<%= link_to t("goals.show.catch_up.adjust_target_cta"),
edit_goal_path(@goal),
data: { turbo_frame: :modal },
class: "text-xs text-secondary hover:text-primary underline-offset-2 hover:underline" %>
<% end %>
<% end %>
<%# Top row: ring card + projection chart card %>
<%= render Goals::ProgressRingComponent.new(goal: @goal, size: 180) %>
<%= @goal.current_balance_money.format %>
<%= t(".ring.of", target: @goal.target_amount_money.format) %>
<% unless @goal.completed? %>
· <%= t(".ring.to_go", amount: @goal.remaining_amount_money.format) %>
<% end %>
<% if @goal.archived? || @goal.paused? %>
<%# Paused / archived: pace + projection are misleading. Show a static recap card. %>
<%= icon(@goal.archived? ? "archive" : "pause", size: "2xl") %>
<%= t(@goal.archived? ? ".inactive.heading_archived" : ".inactive.heading_paused") %>
<%= t(".inactive.body", saved: @goal.current_balance_money.format, target: @goal.target_amount_money.format) %>
<% elsif @goal.completed? || @goal.status == :reached %>
<%# Reached celebration card %>
<%= icon("party-popper", size: "2xl", color: "success") %>
<%= t(".celebration.heading") %>
<%= t(".celebration.body", amount: @goal.target_amount_money.format) %>
<% if @goal.may_archive? %>
<%= render DS::Button.new(
text: t(".celebration.archive_cta"),
href: archive_goal_path(@goal),
variant: "outline",
size: "sm",
method: :patch
) %>
<% end %>
<% elsif @goal.goal_contributions.empty? %>
<%# No contributions yet: render an inline "add your first" CTA card
instead of a flat-at-$0 chart that looks broken. %>
<%= icon("piggy-bank", size: "2xl") %>
<%= t(".empty.heading") %>
<%= t(".empty.body") %>
<%= render DS::Link.new(
text: t(".empty.cta"),
variant: "primary",
size: "sm",
href: new_goal_contribution_path(@goal),
icon: "plus",
frame: :modal
) %>
<% else %>
<%= t(".projection.heading") %>
<%= @stats[:projection_summary].html_safe %>
<% projection_color = @goal.status == :on_track ? "var(--color-green-600)" : "var(--color-yellow-600)" %>
<%= t(".projection.legend_saved") %>
<% unless @goal.target_date.nil? %>
<%= t(".projection.legend_projection") %>
<% end %>
"
data-goal-projection-chart-aria-description-value="<%= strip_tags(@stats[:projection_summary]) %>">
<% if @goal.target_date.nil? %>
<%= icon("calendar-plus", size: "sm") %>
<%= t(".no_target_date.body") %>
<%= link_to t(".no_target_date.cta"),
edit_goal_path(@goal),
data: { turbo_frame: :modal },
class: "font-medium text-primary underline-offset-2 hover:underline" %>
<% end %>
<% end %>
<% unless @contributions.empty? %>
<%# Funding breakdown — balance-sheet-style widget (heading · total /
thin bar / dot legend / weight table). %>
<%= render Goals::FundingAccountsBreakdownComponent.new(goal: @goal, rows: @funding_breakdown) %>
<%# Contributions — chronological list, full width. %>
<%= t(".contributions_heading") %>
<%= @contributions.size %>
<%= render "contributions_list", contributions: @contributions %>
<% end %>
<% if @goal.notes.present? %>
<%= t(".notes") %>
<%= @goal.notes %>
<% end %>