mirror of
https://github.com/we-promise/sure.git
synced 2026-05-09 21:54:58 +00:00
* feat(splits): add excluded attribute support for split children and improve rendering of split transactions * address coderabbitai suggestions to improve code quality * Fix split excluded coercion, DRY helpers, and clean up view partials Fix boolean coercion bug where string "false" from form params was truthy in Ruby, causing all split children to be marked excluded. Use ActiveModel::Type::Boolean for explicit casting in Entry#split!. Additional changes addressing code review feedback: - Extract duplicated in_split_group logic from TransactionsController and TransactionCategoriesController into TransactionsHelper - Remove redundant local_assigns.fetch calls in partials that already declare defaults via the Rails 7.1 locals: magic comment - Simplify ternary in _transaction.html.erb to pass grouped directly - Guard hidden_field_tag :grouped to only emit when value is "true" - Add model tests for excluded on split children (boolean and string) - Add controller test for excluded param through full HTTP stack - Add test confirming excluded children are dropped from balance queries * fix(splits): simplify excluded attribute boolean check * refactor(splits): extract truthy values constant for excluded check Extract the array of truthy values used for excluded attribute check into a private constant to improve code maintainability and avoid duplication of the magic array. * refactor: simplify split grouping link generation and add test coverage for excluded split parameters
187 lines
9.7 KiB
Plaintext
187 lines
9.7 KiB
Plaintext
<%# locals: (entry:, balance_trend: nil, view_ctx: "global", in_split_group: false) %>
|
|
|
|
<% transaction = entry.entryable %>
|
|
|
|
<%= turbo_frame_tag dom_id(entry) do %>
|
|
<%= turbo_frame_tag dom_id(transaction) do %>
|
|
<div class="group flex lg:grid lg:grid-cols-12 items-center text-primary text-sm font-medium p-3 lg:p-4 <%= "pl-8 lg:pl-12" if in_split_group %>">
|
|
|
|
<div class="pr-4 lg:pr-10 flex items-center gap-3 lg:gap-4 col-span-8 min-w-0">
|
|
<%= check_box_tag dom_id(entry, "selection"),
|
|
disabled: transaction.transfer.present?,
|
|
class: "checkbox checkbox--light hidden lg:block",
|
|
data: {
|
|
id: entry.id,
|
|
entry_type: entry.entryable_type,
|
|
"bulk-select-target": "row",
|
|
action: "bulk-select#toggleRowSelection",
|
|
checkbox_toggle_target: "selectionEntry"
|
|
} %>
|
|
|
|
<div class="flex md:hidden items-center gap-1 col-span-2 relative shrink-0">
|
|
<%= render "transactions/transaction_category", transaction: transaction, variant: "mobile", in_split_group: in_split_group %>
|
|
<% if transaction.merchant&.logo_url.present? %>
|
|
<%= image_tag Setting.transform_brand_fetch_url(transaction.merchant.logo_url),
|
|
class: "w-5 h-5 rounded-full absolute -bottom-1 -right-1 border border-secondary pointer-events-none",
|
|
loading: "lazy" %>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="max-w-full min-w-0 <%= "opacity-50 text-secondary" if entry.excluded %>">
|
|
<%= content_tag :div, class: ["flex items-center gap-3 lg:gap-4"] do %>
|
|
<div class="hidden lg:flex">
|
|
<% if transaction.transfer? %>
|
|
<%= render DS::FilledIcon.new(
|
|
icon: "arrow-right-left",
|
|
hex_color: Category::TRANSFER_COLOR,
|
|
size: "lg",
|
|
rounded: true
|
|
) %>
|
|
<% elsif transaction.merchant&.logo_url.present? %>
|
|
<%= image_tag Setting.transform_brand_fetch_url(transaction.merchant.logo_url),
|
|
class: "w-9 h-9 rounded-full border border-secondary",
|
|
loading: "lazy" %>
|
|
<% else %>
|
|
<div class="hidden lg:flex">
|
|
<%= render DS::FilledIcon.new(
|
|
variant: :text,
|
|
text: entry.name,
|
|
size: "lg",
|
|
rounded: true
|
|
) %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="truncate">
|
|
<div class="space-y-0.5">
|
|
<div class="flex items-center gap-1 min-w-0">
|
|
<div class="truncate flex-shrink">
|
|
<% if transaction.transfer? %>
|
|
<%= link_to(
|
|
entry.name,
|
|
transaction.transfer.present? ? transfer_path(transaction.transfer) : entry_path(entry),
|
|
data: {
|
|
turbo_frame: "drawer",
|
|
turbo_prefetch: false
|
|
},
|
|
class: "hover:underline"
|
|
) %>
|
|
<% else %>
|
|
<%= link_to(
|
|
entry.name,
|
|
in_split_group ? entry_path(entry, grouped: true) : entry_path(entry),
|
|
data: {
|
|
turbo_frame: "drawer",
|
|
turbo_prefetch: false
|
|
},
|
|
class: "hover:underline"
|
|
) %>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-1 flex-shrink-0">
|
|
<% if transaction.one_time? %>
|
|
<span class="text-orange-500" title="One-time <%= entry.amount.negative? ? "income" : "expense" %> (excluded from averages)">
|
|
<%= icon "asterisk", size: "sm", color: "current" %>
|
|
</span>
|
|
<% end %>
|
|
|
|
<%# Pending indicator %>
|
|
<% if transaction.pending? %>
|
|
<span class="inline-flex items-center gap-1 text-xs font-medium rounded-full px-1.5 py-0.5 border border-secondary text-secondary" title="<%= t("transactions.transaction.pending_tooltip") %>">
|
|
<%= icon "clock", size: "sm", color: "current" %>
|
|
<%= t("transactions.transaction.pending") %>
|
|
</span>
|
|
<% end %>
|
|
|
|
<%# Potential duplicate indicator - different styling for low vs medium confidence %>
|
|
<% if transaction.has_potential_duplicate? %>
|
|
<% if transaction.low_confidence_duplicate? %>
|
|
<span class="inline-flex items-center gap-1 text-xs font-medium rounded-full px-1.5 py-0.5 border border-secondary bg-surface-inset text-secondary" title="<%= t("transactions.transaction.review_recommended_tooltip") %>">
|
|
<%= icon "help-circle", size: "sm", color: "current" %>
|
|
<%= t("transactions.transaction.review_recommended") %>
|
|
</span>
|
|
<% else %>
|
|
<span class="inline-flex items-center gap-1 text-xs font-medium rounded-full px-1.5 py-0.5 border border-warning bg-warning/10 text-warning" title="<%= t("transactions.transaction.potential_duplicate_tooltip") %>">
|
|
<%= icon "alert-triangle", size: "sm", color: "current" %>
|
|
<%= t("transactions.transaction.possible_duplicate") %>
|
|
</span>
|
|
<% end %>
|
|
<% end %>
|
|
|
|
<%# Split indicator %>
|
|
<% if @split_parent_entry_ids ? @split_parent_entry_ids.include?(entry.id) : entry.split_parent? %>
|
|
<span class="inline-flex items-center gap-1 text-xs font-medium rounded-full px-1.5 py-0.5 border border-secondary bg-surface-inset text-secondary" title="<%= t("transactions.transaction.split_tooltip") %>">
|
|
<%= icon "split", size: "sm", color: "current" %>
|
|
<%= t("transactions.transaction.split") %>
|
|
</span>
|
|
<% end %>
|
|
<% if entry.split_child? && !in_split_group %>
|
|
<span class="text-secondary" title="<%= t("transactions.transaction.split_child_tooltip") %>">
|
|
<%= icon "corner-down-right", size: "sm", color: "current" %>
|
|
</span>
|
|
<% end %>
|
|
|
|
<% if transaction.transfer.present? %>
|
|
<%= render "transactions/transfer_match", transaction: transaction %>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-secondary text-xs font-normal">
|
|
<% if transaction.transfer? %>
|
|
<span class="text-secondary">
|
|
<%= transaction.loan_payment? ? t("transactions.show.loan_payment") : t("transactions.show.transfer") %> • <%= entry.account.name %>
|
|
</span>
|
|
<% else %>
|
|
<% if transaction.merchant&.present? %>
|
|
<span class="hidden lg:inline truncate"><%= transaction.merchant.name %> • </span>
|
|
<% end %>
|
|
<span class="text-secondary hidden lg:inline">
|
|
<%= link_to entry.account.name,
|
|
account_path(entry.account, tab: "transactions"),
|
|
data: { turbo_frame: "_top" },
|
|
class: "hover:underline" %>
|
|
</span>
|
|
<div class="flex min-w-0 items-center gap-1">
|
|
<%= render "categories/category_name_mobile", transaction: transaction %>
|
|
<% if transaction.merchant&.present? %>
|
|
<span class="lg:hidden min-w-0 truncate">• <%= transaction.merchant.name %></span>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="hidden md:flex min-w-0 items-center gap-1 col-span-2">
|
|
<% if entry.account.investment? && !transaction.transfer? %>
|
|
<%# For investment accounts, show activity label instead of category %>
|
|
<%= render "investment_activity/quick_edit_badge", entry: entry, entryable: transaction %>
|
|
<% else %>
|
|
<%= render "transactions/transaction_category", transaction: transaction, variant: "desktop", in_split_group: in_split_group %>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="shrink-0 col-span-4 lg:col-span-2 ml-auto flex items-center justify-end gap-2 <%= "opacity-50" if entry.excluded %>">
|
|
<%# Protection indicator - shows on hover when entry is protected from sync %>
|
|
<% if entry.protected_from_sync? && !entry.excluded? %>
|
|
<%= link_to entry_path(entry),
|
|
data: { turbo_frame: "drawer", turbo_prefetch: false },
|
|
class: "invisible group-hover:visible transition-opacity",
|
|
title: t("entries.protection.tooltip") do %>
|
|
<%= icon "lock", size: "sm", class: "text-secondary" %>
|
|
<% end %>
|
|
<% end %>
|
|
<%= content_tag :p,
|
|
transaction.transfer? && view_ctx == "global" ? "+/- #{format_money(entry.amount_money.abs)}" : format_money(-entry.amount_money),
|
|
class: ["privacy-sensitive", "text-green-600": entry.amount.negative?] %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|