mirror of
https://github.com/we-promise/sure.git
synced 2026-05-28 06:54:56 +00:00
* refactor(transactions): migrate 5 transaction badges to DS::Pill (#1751 PR B) Migrates the hand-rolled "Pending" / "Review recommended" / "Potential duplicate" / "Split" badges across the transaction views to the extended DS::Pill primitive from #1902. **Visual contract for badge mode** In #1902 the badge mode (`marker: false`) used `rounded-md` (chip shape) because the marker mode does. But every existing pill / status badge in the codebase uses `rounded-full` — see `settings/providers/_status_pill.html.erb`, `settings/providers/_maturity_badge.html.erb`, and the inline transaction badges this PR is migrating. To keep the visual contract consistent, this PR shifts `DS::Pill`'s badge mode to `rounded-full` (marker mode stays `rounded-md`, unchanged from #1829). The shape distinction now reads: markers are tags, badges are pills. **Callsites migrated** (5): - `app/views/transactions/_transaction.html.erb` — Pending, Review-recommended, Possible-duplicate, Split badges - `app/views/transactions/_header.html.erb` — Pending badge - `app/views/transactions/_split_parent_row.html.erb` — Split badge **Tone mapping** | Badge | Tone | Notes | |---|---|---| | Pending | `:neutral` | unchanged copy/icon, gains subtle DS-controlled bg | | Review recommended | `:neutral` | matches existing `bg-surface-inset` look | | Possible duplicate | `:warning` | DS semantic alias for the existing `text-warning` | | Split | `:neutral` | matches existing `bg-surface-inset` look | **Deferred to follow-up PRs** - `app/views/transactions/_transfer_match.html.erb` — uses two responsive-visibility variants (`hidden lg:inline-flex` for long copy, `inline-flex lg:hidden` for short). DS::Pill currently has no `class:` arg for caller-controlled wrapper classes; deferring until that lands. - `app/views/transactions/searches/filters/_badge.html.erb` — has a close button alongside the label (`button_to clear_filter_*`) and uses `rounded-3xl p-1.5` instead of a true pill. Closer to a removable filter chip — better fit for a separate `DS::FilterChip` primitive than for `DS::Pill`. Refs #1751. * refactor(misc): migrate misc badges to DS::Pill (#1751 PR D) Replaces five misc badge callsites with `DS::Pill` (badge mode: `marker: false`, `show_dot: false`) so the long-tail badges share the same shape, padding, and dark-mode tokens as the rest of the design system. No raw palette classes remain in the migrated files. Migrated: - app/views/shared/_badge.html.erb — converted to a thin shim that renders `DS::Pill`; preserves the block-content API and the `pulse: true` option (wraps the pill in `animate-pulse`). Maps `success`/`error`/`warning`/default → `:success`/`:error`/`:warning`/`:neutral`. - app/views/accounts/_tax_treatment_badge.html.erb — maps tax treatments to DS tones: `:tax_exempt → :green`, `:tax_deferred → :indigo` (was raw blue-500/10), `:tax_advantaged → :violet` (was raw purple-500/10), default → `:neutral`. - app/views/reports/_investment_performance.html.erb (line ~121, inline twin of the tax-treatment badge) — uses the same mapping via a new `tax_treatment_pill_tone` helper. - app/helpers/reports_helper.rb — replaces `tax_treatment_badge_classes` with `tax_treatment_pill_tone` (the old helper had no other callers). - app/views/import/qif_category_selections/show.html.erb (~line 86) — inline split badge → `tone: :warning`. - app/views/investment_activity/_badge.html.erb — fixed activity enum mapped to DS tones: Buy/Reinvestment → :indigo, Sell → :red, Dividend/Interest → :green, Contribution → :violet, Withdrawal → :amber, others → :gray. Skipped (true mismatches, not extendable without changing DS::Pill): - app/views/shared/_color_badge.html.erb — takes an arbitrary user-supplied color via `color-mix(in oklab, #{color} ...)`. DS::Pill only supports the fixed tone enum, so this would lose information. - app/views/categories/_badge.html.erb — same reason; renders `category.color` (arbitrary hex per record). - app/views/investment_activity/_quick_edit_badge.html.erb — interactive button with a Stimulus controller, click action, hover state, and dropdown anchor. DS::Pill renders a `<span>`; converting would destroy the interactive surface. Stack: based on `feat/ds-pill-transactions-1751-b` (PR #1917), which ships the `marker: false` → `rounded-full` badge shape this PR depends on. Refs #1751.
224 lines
12 KiB
Plaintext
224 lines
12 KiB
Plaintext
<%# locals: (investment_metrics:) %>
|
|
|
|
<% if investment_metrics[:has_investments] %>
|
|
<div class="space-y-6">
|
|
<%# Investment Summary Cards %>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<%# Portfolio Value Card %>
|
|
<div class="bg-container-inset rounded-lg p-4">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<%= icon("briefcase", size: "sm") %>
|
|
<span class="text-sm text-secondary"><%= t("reports.investment_performance.portfolio_value") %></span>
|
|
</div>
|
|
<p class="text-xl font-semibold text-primary privacy-sensitive">
|
|
<%= format_money(investment_metrics[:portfolio_value]) %>
|
|
</p>
|
|
</div>
|
|
|
|
<%# Total Return Card %>
|
|
<div class="bg-container-inset rounded-lg p-4">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<%= icon("trending-up", size: "sm") %>
|
|
<span class="text-sm text-secondary"><%= t("reports.investment_performance.total_return") %></span>
|
|
</div>
|
|
<% if investment_metrics[:unrealized_trend] %>
|
|
<p class="text-xl font-semibold privacy-sensitive" style="color: <%= investment_metrics[:unrealized_trend].color %>">
|
|
<%= format_money(Money.new(investment_metrics[:unrealized_trend].value, Current.family.currency)) %>
|
|
(<%= investment_metrics[:unrealized_trend].percent_formatted %>)
|
|
</p>
|
|
<% else %>
|
|
<p class="text-xl font-semibold text-secondary"><%= t("reports.investment_performance.no_data") %></p>
|
|
<% end %>
|
|
</div>
|
|
|
|
<%# Period Contributions Card %>
|
|
<div class="bg-container-inset rounded-lg p-4">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<%= icon("arrow-down-to-line", size: "sm") %>
|
|
<span class="text-sm text-secondary"><%= t("reports.investment_performance.contributions") %></span>
|
|
</div>
|
|
<p class="text-xl font-semibold text-primary privacy-sensitive">
|
|
<%= format_money(investment_metrics[:period_contributions]) %>
|
|
</p>
|
|
</div>
|
|
|
|
<%# Period Withdrawals Card %>
|
|
<div class="bg-container-inset rounded-lg p-4">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<%= icon("arrow-up-from-line", size: "sm") %>
|
|
<span class="text-sm text-secondary"><%= t("reports.investment_performance.withdrawals") %></span>
|
|
</div>
|
|
<p class="text-xl font-semibold text-primary privacy-sensitive">
|
|
<%= format_money(investment_metrics[:period_withdrawals]) %>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<%# Top Holdings Table %>
|
|
<% if investment_metrics[:top_holdings].any? %>
|
|
<div class="space-y-3">
|
|
<h4 class="text-lg font-medium"><%= t("reports.investment_performance.top_holdings") %></h4>
|
|
<div class="bg-container-inset rounded-xl p-1 overflow-x-auto">
|
|
<div class="w-max sm:w-full">
|
|
<div class="rounded-lg shadow-border-xs bg-container overflow-hidden">
|
|
<table class="w-full">
|
|
<thead class="bg-container-inset">
|
|
<tr class="uppercase text-xs font-medium text-secondary">
|
|
<th class="px-4 py-2 text-left font-medium"><%= t("reports.investment_performance.holding") %></th>
|
|
<th class="px-4 py-2 text-right font-medium"><%= t("reports.investment_performance.weight") %></th>
|
|
<th class="px-4 py-2 text-right font-medium"><%= t("reports.investment_performance.value") %></th>
|
|
<th class="px-4 py-2 text-right font-medium"><%= t("reports.investment_performance.return") %></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-secondary text-sm">
|
|
<% investment_metrics[:top_holdings].each_with_index do |holding, idx| %>
|
|
<tr class="<%= idx < investment_metrics[:top_holdings].size - 1 ? "table-divider" : "" %>">
|
|
<td class="py-3 px-4 lg:px-6">
|
|
<div class="flex items-center gap-3">
|
|
<% if (logo = holding.security.display_logo_url).present? %>
|
|
<img src="<%= Setting.transform_brand_fetch_url(logo) %>" alt="<%= holding.ticker %>" class="w-6 h-6 rounded-full">
|
|
<% else %>
|
|
<div class="w-8 h-8 rounded-full bg-container-inset flex items-center justify-center text-xs font-medium text-secondary">
|
|
<%= holding.ticker[0..1] %>
|
|
</div>
|
|
<% end %>
|
|
<div>
|
|
<p class="font-medium text-primary"><%= holding.ticker %></p>
|
|
<p class="text-xs text-secondary"><%= truncate(holding.name, length: 25) %></p>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 lg:px-6 text-right text-secondary"><%= number_to_percentage(holding.weight || 0, precision: 1) %></td>
|
|
<td class="py-3 px-4 lg:px-6 text-right font-medium text-primary privacy-sensitive"><%= format_money(holding.amount_money) %></td>
|
|
<td class="py-3 px-4 lg:px-6 text-right">
|
|
<% if holding.trend %>
|
|
<span style="color: <%= holding.trend.color %>">
|
|
<%= holding.trend.percent_formatted %>
|
|
</span>
|
|
<% else %>
|
|
<span class="text-secondary"><%= t("reports.investment_performance.no_data") %></span>
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%# Gains by Tax Treatment %>
|
|
<% if investment_metrics[:gains_by_tax_treatment].present? %>
|
|
<div class="space-y-3">
|
|
<h4 class="text-lg font-medium"><%= t("reports.investment_performance.gains_by_tax_treatment") %></h4>
|
|
|
|
<div class="flex flex-wrap gap-4">
|
|
<% investment_metrics[:gains_by_tax_treatment].each do |treatment, data| %>
|
|
<div class="bg-container-inset rounded-lg p-4 space-y-3 flex-1 min-w-64">
|
|
<div class="flex items-center justify-between">
|
|
<%= render DS::Pill.new(
|
|
label: t("accounts.tax_treatments.#{treatment}"),
|
|
tone: tax_treatment_pill_tone(treatment),
|
|
marker: false,
|
|
show_dot: false,
|
|
title: t("accounts.tax_treatment_descriptions.#{treatment}")
|
|
) %>
|
|
<span class="text-sm font-semibold text-primary privacy-sensitive">
|
|
<%= format_money(data[:total_gain]) %>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-secondary"><%= t("reports.investment_performance.unrealized_gains") %></span>
|
|
<span class="text-primary privacy-sensitive"><%= format_money(data[:unrealized_gain]) %></span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-secondary"><%= t("reports.investment_performance.realized_gains") %></span>
|
|
<span class="text-primary privacy-sensitive"><%= format_money(data[:realized_gain]) %></span>
|
|
</div>
|
|
</div>
|
|
|
|
<% if treatment == :taxable && data[:realized_gain].amount > 0 %>
|
|
<p class="text-xs text-warning flex items-center gap-1">
|
|
<%= icon("alert-triangle", size: "sm") %>
|
|
<%= t("reports.investment_performance.taxable_realized_note") %>
|
|
</p>
|
|
<% end %>
|
|
|
|
<details class="group">
|
|
<summary class="cursor-pointer text-xs text-secondary hover:text-primary flex items-center gap-1">
|
|
<%= icon "chevron-right", size: "sm", class: "group-open:rotate-90 transition-transform" %>
|
|
<%= t("reports.investment_performance.view_details") %>
|
|
(<%= t("reports.investment_performance.holdings_count", count: data[:holdings].count) %>, <%= t("reports.investment_performance.sells_count", count: data[:sell_trades].count) %>)
|
|
</summary>
|
|
|
|
<div class="mt-2 space-y-2 text-xs">
|
|
<% if data[:holdings].any? %>
|
|
<div class="space-y-1">
|
|
<p class="text-secondary font-medium"><%= t("reports.investment_performance.holdings") %></p>
|
|
<% data[:holdings].first(5).each do |holding| %>
|
|
<div class="flex justify-between pl-2">
|
|
<span class="text-primary"><%= holding.ticker %></span>
|
|
<span class="<%= holding.trend ? "" : "text-secondary" %>" style="<%= holding.trend ? "color: #{holding.trend.color}" : "" %>">
|
|
<%= holding.trend ? format_money(Money.new(holding.trend.value, Current.family.currency)) : t("reports.investment_performance.no_data") %>
|
|
</span>
|
|
</div>
|
|
<% end %>
|
|
<% if data[:holdings].count > 5 %>
|
|
<p class="text-secondary pl-2"><%= t("reports.investment_performance.and_more", count: data[:holdings].count - 5) %></p>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
|
|
<% if data[:sell_trades].any? %>
|
|
<div class="space-y-1">
|
|
<p class="text-secondary font-medium"><%= t("reports.investment_performance.sell_trades") %></p>
|
|
<% data[:sell_trades].first(5).each do |trade| %>
|
|
<% gain = trade.realized_gain_loss %>
|
|
<div class="flex justify-between pl-2">
|
|
<span class="text-primary"><%= trade.security.ticker %></span>
|
|
<span class="<%= gain ? "" : "text-secondary" %>" style="<%= gain ? "color: #{gain.color}" : "" %>">
|
|
<%= gain ? format_money(Money.new(gain.value, Current.family.currency)) : t("reports.investment_performance.no_data") %>
|
|
</span>
|
|
</div>
|
|
<% end %>
|
|
<% if data[:sell_trades].count > 5 %>
|
|
<p class="text-secondary pl-2"><%= t("reports.investment_performance.and_more", count: data[:sell_trades].count - 5) %></p>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%# Investment Accounts Summary %>
|
|
<% if investment_metrics[:accounts].any? %>
|
|
<div class="space-y-3">
|
|
<h4 class="text-lg font-medium"><%= t("reports.investment_performance.accounts") %></h4>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
<% investment_metrics[:accounts].each do |account| %>
|
|
<%= link_to account_path(account), class: "bg-container-inset rounded-lg p-4 flex items-center justify-between hover:bg-container-hover transition-colors" do %>
|
|
<div class="flex items-center gap-3">
|
|
<%= render "accounts/logo", account: account, size: "sm" %>
|
|
<div>
|
|
<p class="font-medium text-primary text-sm"><%= account.name %></p>
|
|
<p class="text-xs text-secondary"><%= account.short_subtype_label %></p>
|
|
</div>
|
|
</div>
|
|
<p class="font-medium text-primary privacy-sensitive"><%= format_money(account.balance_money) %></p>
|
|
<% end %>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|