mirror of
https://github.com/we-promise/sure.git
synced 2026-04-19 03:54:08 +00:00
Refactor report and dashboard table layouts to semantic HTML (#1222)
* Refactor report and dashboard tables from div grids to semantic HTML Convert div-based grid layouts to proper <table>/<thead>/<tbody>/<tr>/<th>/<td> elements in report views and the dashboard investment summary: - reports/_breakdown_table + _category_row (income/expense breakdown) - reports/_trends_insights (monthly trends) - reports/_net_worth (asset/liability summaries) - reports/_investment_performance (top holdings) - pages/dashboard/_investment_summary (top holdings) Replaces shared/ruler dividers with border-b border-divider on <tr> elements. Updates test selectors from div[data-category] to tr[data-category] and from [role="columnheader"] to thead/th. Closes #1121 * Address PR review feedback - Restore w-max sm:w-full wrapper on report tables to preserve horizontal scroll behavior on narrow screens - Add sr-only accessible header for net worth amount columns - Use border-divider instead of border-primary in dashboard investment summary * Fix rounded corners on semantic table body containers Move rounded-lg, shadow-border-xs, and bg-container from tbody (where border-radius and box-shadow don't apply) to a wrapper div with overflow-hidden. Add bg-container-inset on thead to preserve the two-tone card design.
This commit is contained in:
@@ -28,40 +28,44 @@
|
||||
<% holdings = investment_statement.top_holdings(limit: 5) %>
|
||||
<% if holdings.any? %>
|
||||
<div class="bg-container-inset rounded-xl p-1 mx-4 overflow-x-auto">
|
||||
<div class="w-max sm:w-full">
|
||||
<div class="px-4 py-2 grid grid-cols-4 items-center uppercase text-xs font-medium text-secondary">
|
||||
<div class="flex-1"><%= t(".holding") %></div>
|
||||
<div class="text-right"><%= t(".weight") %></div>
|
||||
<div class="text-right"><%= t(".value") %></div>
|
||||
<div class="text-right"><%= t(".return") %></div>
|
||||
</div>
|
||||
|
||||
<div class="shadow-border-xs rounded-lg bg-container font-medium text-sm">
|
||||
<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(".holding") %></th>
|
||||
<th class="px-4 py-2 text-right font-medium"><%= t(".weight") %></th>
|
||||
<th class="px-4 py-2 text-right font-medium"><%= t(".value") %></th>
|
||||
<th class="px-4 py-2 text-right font-medium"><%= t(".return") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="font-medium text-sm">
|
||||
<% holdings.each_with_index do |holding, idx| %>
|
||||
<div class="p-4 grid grid-cols-4 items-center <%= idx < holdings.size - 1 ? "border-b border-primary" : "" %>">
|
||||
<div class="flex-1 flex items-center gap-3">
|
||||
<% if holding.security.logo_url.present? %>
|
||||
<img src="<%= holding.security.logo_url %>" alt="<%= holding.ticker %>" class="w-8 h-8 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.to_s.first(2).presence || "—" %>
|
||||
<tr class="<%= idx < holdings.size - 1 ? "border-b border-divider" : "" %>">
|
||||
<td class="p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<% if holding.security.logo_url.present? %>
|
||||
<img src="<%= holding.security.logo_url %>" alt="<%= holding.ticker %>" class="w-8 h-8 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.to_s.first(2).presence || "—" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="max-w-32">
|
||||
<p class="font-medium truncate"><%= holding.ticker %></p>
|
||||
<p class="text-xs text-secondary"><%= truncate(holding.name, length: 20) %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="max-w-32">
|
||||
<p class="font-medium truncate"><%= holding.ticker %></p>
|
||||
<p class="text-xs text-secondary"><%= truncate(holding.name, length: 20) %></p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<div class="text-right text-secondary privacy-sensitive">
|
||||
<td class="p-4 text-right text-secondary privacy-sensitive">
|
||||
<%= number_to_percentage(holding.weight || 0, precision: 1) %>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<div class="text-right privacy-sensitive">
|
||||
<td class="p-4 text-right privacy-sensitive">
|
||||
<%= format_money(holding.amount_money) %>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<div class="text-right">
|
||||
<td class="p-4 text-right">
|
||||
<% if holding.trend %>
|
||||
<span class="privacy-sensitive" style="color: <%= holding.trend.color %>">
|
||||
<%= holding.trend.percent_formatted %>
|
||||
@@ -69,10 +73,11 @@
|
||||
<% else %>
|
||||
<span class="text-secondary">-</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -15,30 +15,30 @@
|
||||
</div>
|
||||
|
||||
<div class="bg-container-inset rounded-xl p-1 overflow-x-auto">
|
||||
<div class="w-max sm:w-full">
|
||||
<div class="grid grid-cols-4 items-center uppercase text-xs font-medium text-secondary px-4 py-2">
|
||||
<div class="col-span-2 font-medium text-secondary"><%= t("reports.transactions_breakdown.table.category") %></div>
|
||||
<div class="col-span-1 text-right font-medium text-secondary">
|
||||
<%= link_to reports_path(amount_sort_params), class: "inline-flex items-center gap-1 hover:text-primary" do %>
|
||||
<%= t("reports.transactions_breakdown.table.amount") %>
|
||||
<% if current_sort_by == "amount" %>
|
||||
<%= icon(current_sort_direction == "desc" ? "chevron-down" : "chevron-up", class: "w-3 h-3") %>
|
||||
<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" colspan="2"><%= t("reports.transactions_breakdown.table.category") %></th>
|
||||
<th class="px-4 py-2 text-right font-medium">
|
||||
<%= link_to reports_path(amount_sort_params), class: "inline-flex items-center gap-1 hover:text-primary" do %>
|
||||
<%= t("reports.transactions_breakdown.table.amount") %>
|
||||
<% if current_sort_by == "amount" %>
|
||||
<%= icon(current_sort_direction == "desc" ? "chevron-down" : "chevron-up", class: "w-3 h-3") %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="col-span-1 text-right font-medium text-secondary"><%= t("reports.transactions_breakdown.table.percentage") %></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-container rounded-lg shadow-border-xs">
|
||||
</th>
|
||||
<th class="px-4 py-2 text-right font-medium"><%= t("reports.transactions_breakdown.table.percentage") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% groups.each_with_index do |group, idx| %>
|
||||
<%= render "reports/category_row",
|
||||
item: group,
|
||||
total: total,
|
||||
color_class: color_class,
|
||||
level: :category %>
|
||||
<% if idx < groups.size - 1 %>
|
||||
<%= render "shared/ruler", classes: "mx-3 lg:mx-4" %>
|
||||
<% end %>
|
||||
level: :category,
|
||||
show_border: idx < groups.size - 1 || group[:subcategories].present? %>
|
||||
<%# Render subcategories if present %>
|
||||
<% if group[:subcategories].present? && group[:subcategories].any? %>
|
||||
<% group[:subcategories].each_with_index do |subcategory, sub_idx| %>
|
||||
@@ -46,14 +46,13 @@
|
||||
item: subcategory,
|
||||
total: total,
|
||||
color_class: color_class,
|
||||
level: :subcategory %>
|
||||
<% if sub_idx < group[:subcategories].size - 1 %>
|
||||
<%= render "shared/ruler", classes: "mx-3 lg:mx-4" %>
|
||||
<% end %>
|
||||
level: :subcategory,
|
||||
show_border: sub_idx < group[:subcategories].size - 1 %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,50 +1,53 @@
|
||||
<%
|
||||
percentage = total.zero? ? 0 : (item[:total].to_f / total * 100).round(1)
|
||||
is_sub = level == :subcategory
|
||||
show_border = local_assigns.fetch(:show_border, false)
|
||||
%>
|
||||
|
||||
<div data-category="category-<%= item[:category_id] %>" class="grid grid-cols-4 items-center text-secondary text-sm py-3 px-4 lg:px-6 <%= is_sub ? "pl-7 lg:pl-9" : "" %>">
|
||||
<div class="col-span-2 flex items-center gap-2">
|
||||
<% if is_sub %>
|
||||
<div class="text-subdued">
|
||||
<%= icon "corner-down-right" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if item[:category_icon] %>
|
||||
<div class="h-7 w-7 flex-shrink-0 rounded-full flex justify-center items-center"
|
||||
style="
|
||||
background-color: color-mix(in oklab, <%= item[:category_color] %> 10%, transparent);
|
||||
border-color: color-mix(in oklab, <%= item[:category_color] %> 10%, transparent);
|
||||
color: <%= item[:category_color] %>;
|
||||
">
|
||||
<%= icon(item[:category_icon], color: "current", size: "sm") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= render DS::FilledIcon.new(
|
||||
variant: :text,
|
||||
hex_color: item[:category_color],
|
||||
text: item[:category_name],
|
||||
size: "md",
|
||||
rounded: true
|
||||
) %>
|
||||
<% end %>
|
||||
<span class="font-medium text-primary">
|
||||
<%= item[:category_name] %>
|
||||
</span>
|
||||
<span class="text-xs text-subdued whitespace-nowrap">
|
||||
(<%= t("reports.transactions_breakdown.table.entries", count: item[:count]) %>)
|
||||
</span>
|
||||
</div>
|
||||
<tr data-category="category-<%= item[:category_id] %>" class="text-secondary text-sm <%= show_border ? "border-b border-divider" : "" %>">
|
||||
<td colspan="2" class="py-3 px-4 lg:px-6 <%= is_sub ? "pl-7 lg:pl-9" : "" %>">
|
||||
<div class="flex items-center gap-2">
|
||||
<% if is_sub %>
|
||||
<div class="text-subdued">
|
||||
<%= icon "corner-down-right" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if item[:category_icon] %>
|
||||
<div class="h-7 w-7 flex-shrink-0 rounded-full flex justify-center items-center"
|
||||
style="
|
||||
background-color: color-mix(in oklab, <%= item[:category_color] %> 10%, transparent);
|
||||
border-color: color-mix(in oklab, <%= item[:category_color] %> 10%, transparent);
|
||||
color: <%= item[:category_color] %>;
|
||||
">
|
||||
<%= icon(item[:category_icon], color: "current", size: "sm") %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= render DS::FilledIcon.new(
|
||||
variant: :text,
|
||||
hex_color: item[:category_color],
|
||||
text: item[:category_name],
|
||||
size: "md",
|
||||
rounded: true
|
||||
) %>
|
||||
<% end %>
|
||||
<span class="font-medium text-primary">
|
||||
<%= item[:category_name] %>
|
||||
</span>
|
||||
<span class="text-xs text-subdued whitespace-nowrap">
|
||||
(<%= t("reports.transactions_breakdown.table.entries", count: item[:count]) %>)
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<div class="col-span-1 text-right">
|
||||
<td class="py-3 px-4 lg:px-6 text-right">
|
||||
<span class="text-sm <%= color_class %> privacy-sensitive">
|
||||
<%= Money.new(item[:total], Current.family.currency).format %>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<div class="col-span-1 text-right">
|
||||
<td class="py-3 px-4 lg:px-6 text-right">
|
||||
<span class="text-sm text-secondary">
|
||||
<%= percentage %>%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -60,33 +60,39 @@
|
||||
<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 role="columnheader" class="grid grid-cols-4 items-center uppercase text-xs font-medium text-secondary px-4 py-2">
|
||||
<div class="font-medium text-secondary"><%= t("reports.investment_performance.holding") %></div>
|
||||
<div class="text-right font-medium text-secondary"><%= t("reports.investment_performance.weight") %></div>
|
||||
<div class="text-right font-medium text-secondary"><%= t("reports.investment_performance.value") %></div>
|
||||
<div class="text-right font-medium text-secondary"><%= t("reports.investment_performance.return") %></div>
|
||||
</div>
|
||||
<div class="bg-container rounded-lg shadow-border-xs">
|
||||
<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| %>
|
||||
<div class="grid grid-cols-4 items-center text-secondary text-sm py-3 px-4 lg:px-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<% if holding.security.brandfetch_icon_url.present? %>
|
||||
<img src="<%= holding.security.brandfetch_icon_url %>" alt="<%= holding.ticker %>" class="w-6 h-6 rounded-full">
|
||||
<% elsif holding.security.logo_url.present? %>
|
||||
<img src="<%= Setting.transform_brand_fetch_url(holding.security.logo_url) %>" 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] %>
|
||||
<tr class="<%= idx < investment_metrics[:top_holdings].size - 1 ? "border-b border-divider" : "" %>">
|
||||
<td class="py-3 px-4 lg:px-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<% if holding.security.brandfetch_icon_url.present? %>
|
||||
<img src="<%= holding.security.brandfetch_icon_url %>" alt="<%= holding.ticker %>" class="w-6 h-6 rounded-full">
|
||||
<% elsif holding.security.logo_url.present? %>
|
||||
<img src="<%= Setting.transform_brand_fetch_url(holding.security.logo_url) %>" 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>
|
||||
<% 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>
|
||||
<div class="text-right text-secondary"><%= number_to_percentage(holding.weight || 0, precision: 1) %></div>
|
||||
<div class="text-right font-medium text-primary privacy-sensitive"><%= format_money(holding.amount_money) %></div>
|
||||
<div class="text-right">
|
||||
</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 %>
|
||||
@@ -94,13 +100,12 @@
|
||||
<% else %>
|
||||
<span class="text-secondary"><%= t("reports.investment_performance.no_data") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% if idx < investment_metrics[:top_holdings].size - 1 %>
|
||||
<%= render "shared/ruler", classes: "mx-3 lg:mx-4" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,43 +40,55 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<%# Assets Summary %>
|
||||
<div class="bg-container-inset rounded-xl p-1 overflow-x-auto w-full flex flex-col">
|
||||
<div class="grid grid-cols-2 items-center uppercase text-xs font-medium text-secondary px-4 py-2">
|
||||
<div class="col-span-1 font-medium text-secondary"><%= t("reports.net_worth.total_assets") %></div>
|
||||
</div>
|
||||
<div class="bg-container rounded-lg shadow-border-xs flex-1 flex flex-col">
|
||||
<% net_worth_metrics[:asset_groups].each_with_index do |group, idx| %>
|
||||
<div class="grid grid-cols-2 items-center text-secondary text-sm py-3 px-4 lg:px-6">
|
||||
<div class="col-span-1 font-medium text-secondary"><%= group[:name] %></div>
|
||||
<div class="col-span-1 font-medium text-secondary justify-self-end text-success privacy-sensitive"><%= group[:total].format %></div>
|
||||
</div>
|
||||
<% if idx < net_worth_metrics[:asset_groups].size - 1 %>
|
||||
<%= render "shared/ruler", classes: "mx-3 lg:mx-4" %>
|
||||
<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.net_worth.total_assets") %></th>
|
||||
<th class="px-4 py-2 text-right font-medium"><span class="sr-only"><%= t("reports.transactions_breakdown.table.amount") %></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% net_worth_metrics[:asset_groups].each_with_index do |group, idx| %>
|
||||
<tr class="<%= idx < net_worth_metrics[:asset_groups].size - 1 ? "border-b border-divider" : "" %>">
|
||||
<td class="py-3 px-4 lg:px-6 text-sm font-medium text-secondary"><%= group[:name] %></td>
|
||||
<td class="py-3 px-4 lg:px-6 text-sm font-medium text-right text-success privacy-sensitive"><%= group[:total].format %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if net_worth_metrics[:asset_groups].empty? %>
|
||||
<p class="text-sm text-subdued py-3 px-4 lg:px-6"><%= t("reports.net_worth.no_assets") %></p>
|
||||
<% end %>
|
||||
<% if net_worth_metrics[:asset_groups].empty? %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-sm text-subdued py-3 px-4 lg:px-6"><%= t("reports.net_worth.no_assets") %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# Liabilities Summary %>
|
||||
<div class="bg-container-inset rounded-xl p-1 overflow-x-auto w-full flex flex-col">
|
||||
<div class="grid grid-cols-2 items-center uppercase text-xs font-medium text-secondary px-4 py-2">
|
||||
<div class="col-span-1 font-medium text-secondary"><%= t("reports.net_worth.total_liabilities") %></div>
|
||||
</div>
|
||||
<div class="bg-container rounded-lg shadow-border-xs flex-1 flex flex-col">
|
||||
<% net_worth_metrics[:liability_groups].each_with_index do |group, idx| %>
|
||||
<div class="grid grid-cols-2 items-center text-secondary text-sm py-3 px-4 lg:px-6">
|
||||
<div class="col-span-1 font-medium text-secondary"><%= group[:name] %></div>
|
||||
<div class="col-span-1 font-medium text-secondary justify-self-end text-destructive privacy-sensitive"><%= group[:total].format %></div>
|
||||
</div>
|
||||
<% if idx < net_worth_metrics[:liability_groups].size - 1 %>
|
||||
<%= render "shared/ruler", classes: "mx-3 lg:mx-4" %>
|
||||
<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.net_worth.total_liabilities") %></th>
|
||||
<th class="px-4 py-2 text-right font-medium"><span class="sr-only"><%= t("reports.transactions_breakdown.table.amount") %></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% net_worth_metrics[:liability_groups].each_with_index do |group, idx| %>
|
||||
<tr class="<%= idx < net_worth_metrics[:liability_groups].size - 1 ? "border-b border-divider" : "" %>">
|
||||
<td class="py-3 px-4 lg:px-6 text-sm font-medium text-secondary"><%= group[:name] %></td>
|
||||
<td class="py-3 px-4 lg:px-6 text-sm font-medium text-right text-destructive privacy-sensitive"><%= group[:total].format %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if net_worth_metrics[:liability_groups].empty? %>
|
||||
<p class="text-sm text-subdued py-3 px-4 lg:px-6"><%= t("reports.net_worth.no_liabilities") %></p>
|
||||
<% end %>
|
||||
<% if net_worth_metrics[:liability_groups].empty? %>
|
||||
<tr>
|
||||
<td colspan="2" class="text-sm text-subdued py-3 px-4 lg:px-6"><%= t("reports.net_worth.no_liabilities") %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,41 +8,46 @@
|
||||
<% if trends_data.any? %>
|
||||
<div class="bg-container-inset rounded-xl p-1 overflow-x-auto">
|
||||
<div class="w-max sm:w-full">
|
||||
<div role="columnheader" class="grid grid-cols-5 items-center uppercase text-xs font-medium text-secondary px-4 py-2">
|
||||
<div class="font-medium text-secondary"><%= t("reports.trends.month") %></div>
|
||||
<div class="text-right font-medium text-secondary"><%= t("reports.trends.income") %></div>
|
||||
<div class="text-right font-medium text-secondary"><%= t("reports.trends.expenses") %></div>
|
||||
<div class="text-right font-medium text-secondary"><%= t("reports.trends.net") %></div>
|
||||
<div class="text-right font-medium text-secondary"><%= t("reports.trends.savings_rate") %></div>
|
||||
</div>
|
||||
<div class="bg-container rounded-lg shadow-border-xs">
|
||||
<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.trends.month") %></th>
|
||||
<th class="px-4 py-2 text-right font-medium"><%= t("reports.trends.income") %></th>
|
||||
<th class="px-4 py-2 text-right font-medium"><%= t("reports.trends.expenses") %></th>
|
||||
<th class="px-4 py-2 text-right font-medium"><%= t("reports.trends.net") %></th>
|
||||
<th class="px-4 py-2 text-right font-medium"><%= t("reports.trends.savings_rate") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-secondary text-sm">
|
||||
<% trends_data.each_with_index do |trend, idx| %>
|
||||
<div class="grid grid-cols-5 items-center text-secondary text-sm py-3 px-4 lg:px-6">
|
||||
<div class="flex items-center gap-2 <%= trend[:is_current_month] ? "font-medium" : "" %>">
|
||||
<%= trend[:month] %>
|
||||
<% if trend[:is_current_month] %>
|
||||
<span class="text-xs text-subdued">(<%= t("reports.trends.current") %>)</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="text-right privacy-sensitive">
|
||||
<tr class="<%= idx < trends_data.size - 1 ? "border-b border-divider" : "" %>">
|
||||
<td class="py-3 px-4 lg:px-6">
|
||||
<span class="flex items-center gap-2 <%= trend[:is_current_month] ? "font-medium" : "" %>">
|
||||
<%= trend[:month] %>
|
||||
<% if trend[:is_current_month] %>
|
||||
<span class="text-xs text-subdued">(<%= t("reports.trends.current") %>)</span>
|
||||
<% end %>
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 lg:px-6 text-right privacy-sensitive">
|
||||
<%= Money.new(trend[:income], Current.family.currency).format %>
|
||||
</div>
|
||||
<div class="text-right privacy-sensitive">
|
||||
</td>
|
||||
<td class="py-3 px-4 lg:px-6 text-right privacy-sensitive">
|
||||
<%= Money.new(trend[:expenses], Current.family.currency).format %>
|
||||
</div>
|
||||
<div class="text-right privacy-sensitive <%= trend[:net] >= 0 ? "text-success" : "text-destructive" %>">
|
||||
</td>
|
||||
<td class="py-3 px-4 lg:px-6 text-right privacy-sensitive <%= trend[:net] >= 0 ? "text-success" : "text-destructive" %>">
|
||||
<%= Money.new(trend[:net], Current.family.currency).format %>
|
||||
</div>
|
||||
<div class="text-right <%= trend[:net] >= 0 ? "text-success" : "text-destructive" %>">
|
||||
</td>
|
||||
<td class="py-3 px-4 lg:px-6 text-right <%= trend[:net] >= 0 ? "text-success" : "text-destructive" %>">
|
||||
<% savings_rate = trend[:income] > 0 ? ((trend[:net].to_f / trend[:income].to_f) * 100).round(1) : 0 %>
|
||||
<%= savings_rate %>%
|
||||
</div>
|
||||
</div>
|
||||
<% if idx < trends_data.size - 1 %>
|
||||
<%= render "shared/ruler", classes: "mx-3 lg:mx-4" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -82,8 +82,8 @@ class ReportsControllerTest < ActionDispatch::IntegrationTest
|
||||
get reports_path(period_type: :monthly)
|
||||
assert_response :ok
|
||||
assert_select "h2", text: I18n.t("reports.trends.title")
|
||||
assert_select '[role="columnheader"]' do
|
||||
assert_select "div", text: I18n.t("reports.trends.month")
|
||||
assert_select "thead" do
|
||||
assert_select "th", text: I18n.t("reports.trends.month")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -239,10 +239,10 @@ class ReportsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :ok
|
||||
|
||||
# Parent category
|
||||
assert_select "div[data-category='category-#{parent_category.id}']", text: /^Entertainment/
|
||||
assert_select "tr[data-category='category-#{parent_category.id}']", text: /^Entertainment/
|
||||
|
||||
# Subcategories
|
||||
assert_select "div[data-category='category-#{subcategory_movies.id}']", text: /^Movies/
|
||||
assert_select "div[data-category='category-#{subcategory_games.id}']", text: /^Games/
|
||||
assert_select "tr[data-category='category-#{subcategory_movies.id}']", text: /^Movies/
|
||||
assert_select "tr[data-category='category-#{subcategory_games.id}']", text: /^Games/
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user