Files
sure/app/views/savings_goals/show.html.erb

157 lines
8.0 KiB
Plaintext

<div class="space-y-4 pb-6 lg:pb-12">
<div class="text-xs">
<%= link_to savings_goals_path, class: "inline-flex items-center gap-1 text-secondary hover:text-primary" do %>
<%= icon("arrow-left", size: "sm") %>
<%= t(".back_to_all") %>
<% end %>
</div>
<header class="flex items-start gap-4">
<%= render Savings::GoalAvatarComponent.new(goal: @savings_goal, size: "xl") %>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 mb-1">
<h1 class="text-2xl font-semibold text-primary truncate"><%= @savings_goal.name %></h1>
<%= render Savings::StatusPillComponent.new(goal: @savings_goal) %>
</div>
<p class="text-sm text-secondary">
<% if @savings_goal.target_date %>
<%= t(".header.target_by", amount: @savings_goal.target_amount_money.format, date: I18n.l(@savings_goal.target_date, format: :long)) %>
<% days = (@savings_goal.target_date - Date.current).to_i %>
<% if days > 0 %>
· <%= t("savings_goals.goal_card.days_left", count: days, date: I18n.l(@savings_goal.target_date, format: :long)).split(" · ").first %>
<% end %>
<% else %>
<%= t(".header.target", amount: @savings_goal.target_amount_money.format) %>
<% end %>
</p>
</div>
<div class="flex items-center gap-2">
<%= render DS::Link.new(
text: t(".edit"),
variant: "outline",
href: edit_savings_goal_path(@savings_goal),
icon: "pencil",
frame: :modal
) %>
<%= render DS::Link.new(
text: t(".add_contribution"),
variant: "primary",
href: new_savings_goal_contribution_path(@savings_goal),
icon: "plus",
frame: :modal
) %>
<%= render DS::Menu.new do |menu| %>
<% if @savings_goal.may_pause? %>
<% menu.with_item(variant: "button", text: t(".pause"), icon: "pause", href: pause_savings_goal_path(@savings_goal), method: :patch) %>
<% end %>
<% if @savings_goal.may_resume? %>
<% menu.with_item(variant: "button", text: t(".resume"), icon: "play", href: resume_savings_goal_path(@savings_goal), method: :patch) %>
<% end %>
<% if @savings_goal.may_complete? %>
<% menu.with_item(variant: "button", text: t(".complete"), icon: "circle-check-big", href: complete_savings_goal_path(@savings_goal), method: :patch) %>
<% end %>
<% if @savings_goal.may_archive? %>
<% menu.with_item(variant: "button", text: t(".archive"), icon: "archive", href: archive_savings_goal_path(@savings_goal), method: :patch) %>
<% end %>
<% if @savings_goal.may_unarchive? %>
<% menu.with_item(variant: "button", text: t(".unarchive"), icon: "archive-restore", href: unarchive_savings_goal_path(@savings_goal), method: :patch) %>
<% end %>
<% if @savings_goal.archived? %>
<% menu.with_item(
variant: "button",
text: t(".delete"),
icon: "trash-2",
href: savings_goal_path(@savings_goal),
method: :delete,
destructive: true,
confirm: CustomConfirm.for_resource_deletion(@savings_goal.name, high_severity: true)
) %>
<% end %>
<% end %>
</div>
</header>
<%# Top row: ring card + projection chart card %>
<section class="grid grid-cols-1 lg:grid-cols-[320px_minmax(0,1fr)] gap-3">
<div class="bg-container rounded-xl shadow-border-xs p-5 flex flex-col items-center justify-center text-center">
<%= render Savings::ProgressRingComponent.new(goal: @savings_goal, size: 180) %>
<p class="text-xl font-medium text-primary tabular-nums privacy-sensitive mt-4"><%= @savings_goal.current_balance_money.format %></p>
<p class="text-xs text-subdued tabular-nums mt-0.5">
<%= t(".ring.of", target: @savings_goal.target_amount_money.format) %>
<% unless @savings_goal.completed? %>
· <%= t(".ring.to_go", amount: @savings_goal.remaining_amount_money.format) %>
<% end %>
</p>
</div>
<div class="bg-container rounded-xl shadow-border-xs p-5 flex flex-col">
<div class="flex items-start justify-between mb-2 gap-3">
<div class="min-w-0">
<h3 class="text-sm font-medium text-primary"><%= t(".projection.heading") %></h3>
<p class="text-xs text-secondary mt-0.5"><%= @stats[:projection_summary].html_safe %></p>
</div>
<div class="flex items-center gap-3 text-[11px] text-secondary shrink-0">
<span class="inline-flex items-center gap-1.5">
<svg width="18" height="6"><line x1="0" y1="3" x2="18" y2="3" stroke="var(--text-primary)" stroke-width="2" /></svg>
<%= t(".projection.legend_saved") %>
</span>
<span class="inline-flex items-center gap-1.5">
<svg width="18" height="6"><line x1="0" y1="3" x2="18" y2="3" stroke="var(--color-yellow-600)" stroke-width="2" stroke-dasharray="3 3" /></svg>
<%= t(".projection.legend_projection") %>
</span>
</div>
</div>
<div class="flex-1 min-h-[200px]"
data-controller="savings-goal-projection-chart"
data-savings-goal-projection-chart-data-value="<%= @savings_goal.projection_payload.to_json %>"></div>
</div>
</section>
<%# Stat row %>
<section class="grid grid-cols-2 md:grid-cols-4 gap-3">
<div class="bg-container rounded-xl shadow-border-xs px-5 py-4">
<p class="text-[11px] text-secondary mb-1"><%= t(".stats.avg_monthly") %></p>
<p class="text-lg font-medium text-primary tabular-nums"><%= Money.new(@stats[:avg_monthly], @savings_goal.currency).format %></p>
<p class="text-[11px] text-subdued mt-1"><%= @stats[:avg_monthly_sub] %></p>
</div>
<div class="bg-container rounded-xl shadow-border-xs px-5 py-4">
<p class="text-[11px] text-secondary mb-1"><%= t(".stats.total_contributions") %></p>
<p class="text-lg font-medium text-primary tabular-nums"><%= @stats[:contributions_count] %></p>
<p class="text-[11px] text-subdued mt-1"><%= t(".stats.across_all_accounts") %></p>
</div>
<div class="bg-container rounded-xl shadow-border-xs px-5 py-4">
<p class="text-[11px] text-secondary mb-1"><%= t(".stats.linked_balance") %></p>
<p class="text-lg font-medium text-primary tabular-nums"><%= Money.new(@stats[:linked_balance], @savings_goal.currency).format %></p>
<p class="text-[11px] text-subdued mt-1"><%= @stats[:linked_balance_sub] %></p>
</div>
<div class="bg-container rounded-xl shadow-border-xs px-5 py-4">
<p class="text-[11px] text-secondary mb-1"><%= t(".stats.started") %></p>
<p class="text-lg font-medium text-primary"><%= I18n.l(@savings_goal.created_at.to_date, format: :long) %></p>
<p class="text-[11px] text-subdued mt-1"><%= @stats[:started_sub] %></p>
</div>
</section>
<%# Bottom row: contributions + funding accounts %>
<section class="grid grid-cols-1 lg:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)] gap-3">
<div class="bg-container rounded-xl shadow-border-xs overflow-hidden">
<div class="flex items-center px-5 py-3.5 border-b border-subdued">
<h3 class="text-sm font-medium text-primary"><%= t(".contributions_heading") %></h3>
<span class="ml-2 text-xs text-subdued tabular-nums"><%= @contributions.size %></span>
</div>
<%= render "contributions_list", contributions: @contributions %>
</div>
<div class="bg-container rounded-xl shadow-border-xs p-5">
<h3 class="text-sm font-medium text-primary mb-3"><%= t(".funding_accounts_heading") %></h3>
<%= render Savings::FundingAccountsBreakdownComponent.new(goal: @savings_goal, rows: @funding_breakdown) %>
</div>
</section>
<% if @savings_goal.notes.present? %>
<section class="bg-container rounded-xl shadow-border-xs p-5">
<h3 class="text-sm font-medium text-primary mb-2"><%= t(".notes") %></h3>
<p class="text-sm text-secondary whitespace-pre-line"><%= @savings_goal.notes %></p>
</section>
<% end %>
</div>