mirror of
https://github.com/we-promise/sure.git
synced 2026-04-11 16:24:51 +00:00
* Initial split transaction support
* Add support to unsplit and edit split
* Update show.html.erb
* FIX address reviews
* Improve UX
* Update show.html.erb
* Reviews
* Update edit.html.erb
* Add parent category to dialog
* Update en.yml
* Add UI indication to totals
* FIX ui update
* Add category select like rest of app
* Add split ui
* Add settings configuration for split transactions
- Adds a new settings section for appearance changes
- Also adds extra checks for delete and API calls
- Also adds checks for parent/child changes
* fixes
- split transactions dark mode fix
- add split transactions to context menu
* Update entry.rb
1. New validation split_child_date_matches_parent — prevents saving a split child with a date different from its parent. This is the root-cause fix that
protects all flows at once.
2. Bulk update guard — bulk_update! now strips :date from attributes when processing split children, preventing the validation from raising and silently
skipping the date change instead.
* N+1 fix for split_parent?
* Update entry.rb
Problem: In bulk_update!, when a split child has :date removed from attrs (line 432) and the remaining attrs is empty (e.g., the bulk update only
changed the date), entry.update! {} still ran as a no-op. But lock_saved_attributes! and mark_user_modified! at lines 443-444 executed unconditionally,
incorrectly marking untouched split children as user-modified and opting them out of future syncs.
Fix:
1. Added a changed flag to track whether any actual modification happened
2. Wrapped entry.update! in an if attrs.present? check so no-op updates are skipped
3. Gated lock_saved_attributes! and mark_user_modified! behind if changed, so they only run when the entry was actually modified (either via attribute
update or tag update)
* fixes
1. Indentation in show.html.erb Settings section — The split button block and delete block had extra indentation making them appear nested inside guard
blocks they weren't part of. Fixed to match actual nesting.
2. Skip @split_parents query when grouping is off — The controller now only loads split parent entries when show_split_grouped? is true, saving a query
with joins when the feature is disabled.
119 lines
5.5 KiB
Plaintext
119 lines
5.5 KiB
Plaintext
<%= render DS::Dialog.new(variant: "modal") do |dialog| %>
|
|
<% dialog.with_header do %>
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div>
|
|
<h2 class="text-lg font-semibold text-primary"><%= @entry.name %></h2>
|
|
<p class="text-sm text-secondary flex items-center gap-1.5">
|
|
<span><%= @entry.date.strftime("%b %d, %Y") %></span>
|
|
<% if (category = @entry.entryable.try(:category)) %>
|
|
<span class="text-secondary">·</span>
|
|
<span style="color: <%= category.color %>"><%= icon category.lucide_icon, size: "xs", color: "current" %></span>
|
|
<span><%= category.name %></span>
|
|
<% end %>
|
|
</p>
|
|
</div>
|
|
<div class="text-right shrink-0">
|
|
<p class="text-xs font-medium text-secondary uppercase tracking-wide"><%= t("splits.new.original_amount") %></p>
|
|
<p class="text-lg font-semibold text-primary"><%= format_money(-@entry.amount_money) %></p>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<% dialog.with_body do %>
|
|
<%= form_with(
|
|
url: transaction_split_path(@entry),
|
|
method: :patch,
|
|
scope: :split,
|
|
class: "space-y-3",
|
|
data: {
|
|
controller: "split-transaction",
|
|
split_transaction_total_value: (-@entry.amount).to_f,
|
|
split_transaction_currency_value: @entry.currency,
|
|
turbo_frame: :_top
|
|
}
|
|
) do %>
|
|
|
|
<%# Split rows pre-filled from existing children %>
|
|
<div data-split-transaction-target="rowsContainer" class="space-y-3">
|
|
<% @children.each_with_index do |child, index| %>
|
|
<div class="p-3 rounded-lg border border-secondary bg-container" data-split-transaction-target="row">
|
|
<div class="flex items-end gap-2">
|
|
<div class="flex-1 min-w-0">
|
|
<label class="text-xs font-medium text-secondary uppercase tracking-wide block mb-1"><%= t("splits.new.name_label") %></label>
|
|
<input type="text"
|
|
name="split[splits][<%= index %>][name]"
|
|
placeholder="<%= t("splits.new.name_placeholder") %>"
|
|
class="form-field__input border border-secondary rounded-md px-2.5 py-1.5 w-full text-sm text-primary bg-container"
|
|
required
|
|
autocomplete="off"
|
|
value="<%= child.name %>"
|
|
data-split-transaction-target="nameInput">
|
|
</div>
|
|
<div class="w-28 shrink-0">
|
|
<label class="text-xs font-medium text-secondary uppercase tracking-wide block mb-1"><%= t("splits.new.amount_label") %></label>
|
|
<input type="number"
|
|
name="split[splits][<%= index %>][amount]"
|
|
placeholder="0.00"
|
|
step="0.01"
|
|
class="form-field__input border border-secondary rounded-md px-2.5 py-1.5 w-full text-sm text-primary bg-container"
|
|
required
|
|
autocomplete="off"
|
|
value="<%= (-child.amount).to_f %>"
|
|
data-split-transaction-target="amountInput"
|
|
data-action="input->split-transaction#updateRemaining">
|
|
</div>
|
|
<%= render "splits/category_select",
|
|
name: "split[splits][#{index}][category_id]",
|
|
categories: @categories,
|
|
selected_id: child.entryable.try(:category_id) %>
|
|
<button type="button"
|
|
class="w-8 h-8 shrink-0 flex items-center justify-center rounded-md text-secondary hover:text-primary hover:bg-surface-hover transition-colors"
|
|
aria-label="<%= t("splits.new.remove_row") %>"
|
|
data-action="click->split-transaction#removeRow">
|
|
<%= icon "x", size: "sm" %>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
|
|
<%# Add split button %>
|
|
<button type="button"
|
|
class="flex items-center justify-center gap-1.5 w-full py-2 text-sm font-medium text-secondary hover:text-primary border border-dashed border-secondary hover:border-primary rounded-lg transition-colors"
|
|
data-action="click->split-transaction#addRow">
|
|
<%= icon "plus", size: "sm" %>
|
|
<%= t("splits.new.add_row") %>
|
|
</button>
|
|
|
|
<%# Remaining balance indicator %>
|
|
<div data-split-transaction-target="remainingContainer" class="p-3 rounded-lg border border-secondary bg-container text-sm">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-secondary font-medium"><%= t("splits.new.remaining") %></span>
|
|
<span data-split-transaction-target="remaining" class="font-semibold text-primary">
|
|
<%= (-@entry.amount).to_f %>
|
|
</span>
|
|
</div>
|
|
<p data-split-transaction-target="error" class="text-destructive text-xs mt-1.5 hidden">
|
|
<%= t("splits.new.amounts_must_match") %>
|
|
</p>
|
|
</div>
|
|
|
|
<%# Actions %>
|
|
<div class="flex justify-end gap-3 pt-4 border-t border-primary">
|
|
<%= render DS::Button.new(
|
|
text: t("splits.new.cancel"),
|
|
variant: "outline",
|
|
href: "#",
|
|
data: { action: "click->ds--dialog#close" }
|
|
) %>
|
|
<%= render DS::Button.new(
|
|
text: t("splits.edit.submit"),
|
|
variant: "primary",
|
|
type: "submit",
|
|
data: { split_transaction_target: "submitButton" }
|
|
) %>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|
|
<% end %>
|