diff --git a/app/controllers/api/v1/transactions_controller.rb b/app/controllers/api/v1/transactions_controller.rb index 08ca81cee..4af4ca5c2 100644 --- a/app/controllers/api/v1/transactions_controller.rb +++ b/app/controllers/api/v1/transactions_controller.rb @@ -105,6 +105,16 @@ class Api::V1::TransactionsController < Api::V1::BaseController end def update + if @entry.split_child? + render json: { error: "validation_failed", message: "Split child transactions cannot be edited directly. Use the split editor." }, status: :unprocessable_entity + return + end + + if @entry.split_parent? && split_financial_fields_changed? + render json: { error: "validation_failed", message: "Split parent amount, date, and type cannot be changed directly. Use the split editor." }, status: :unprocessable_entity + return + end + Entry.transaction do if @entry.update(entry_params_for_update) # Handle tags separately - only when explicitly provided in the request @@ -141,6 +151,11 @@ end end def destroy + if @entry.split_child? + render json: { error: "validation_failed", message: "Split child transactions cannot be deleted individually." }, status: :unprocessable_entity + return + end + @entry.destroy! @entry.sync_account_later @@ -313,6 +328,12 @@ end params[:transaction].key?(:tag_ids) end + def split_financial_fields_changed? + params.dig(:transaction, :amount).present? || + params.dig(:transaction, :date).present? || + params.dig(:transaction, :nature).present? + end + def calculate_signed_amount amount = transaction_params[:amount].to_f nature = transaction_params[:nature] diff --git a/app/controllers/settings/appearances_controller.rb b/app/controllers/settings/appearances_controller.rb new file mode 100644 index 000000000..ae2282443 --- /dev/null +++ b/app/controllers/settings/appearances_controller.rb @@ -0,0 +1,18 @@ +class Settings::AppearancesController < ApplicationController + layout "settings" + + def show + @user = Current.user + end + + def update + @user = Current.user + @user.transaction do + @user.lock! + updated_prefs = (@user.preferences || {}).deep_dup + updated_prefs["show_split_grouped"] = params.dig(:user, :show_split_grouped) == "1" + @user.update!(preferences: updated_prefs) + end + redirect_to settings_appearance_path + end +end diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index a1441439a..096c313e5 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -27,6 +27,30 @@ class TransactionsController < ApplicationController @pagy, @transactions = pagy(base_scope, limit: safe_per_page) + # Preload split parent data + entry_ids = @transactions.map { |t| t.entry.id } + + # Load split parent entries for grouped display (only when grouping is enabled) + @split_parents = if Current.user.show_split_grouped? + split_parent_ids = @transactions.filter_map { |t| t.entry.parent_entry_id }.uniq + if split_parent_ids.any? + Entry.where(id: split_parent_ids) + .includes(:account, entryable: [ :category, :merchant ]) + .index_by(&:id) + else + {} + end + else + {} + end + + # Preload which entries on this page are split parents (have children) to avoid N+1 + @split_parent_entry_ids = if entry_ids.any? + Entry.where(parent_entry_id: entry_ids).distinct.pluck(:parent_entry_id).to_set + else + Set.new + end + # Load projected recurring transactions for next 10 days @projected_recurring = Current.family.recurring_transactions .active diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 252cd114d..fd0c33f6e 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -83,6 +83,8 @@ class UsersController < ApplicationController redirect_to goals_onboarding_path when "trial" redirect_to trial_onboarding_path + when "appearance" + redirect_to settings_appearance_path, notice: notice when "ai_prompts" redirect_to settings_ai_prompts_path, notice: notice else diff --git a/app/helpers/entries_helper.rb b/app/helpers/entries_helper.rb index 1c3340ae0..87a3fc571 100644 --- a/app/helpers/entries_helper.rb +++ b/app/helpers/entries_helper.rb @@ -1,4 +1,28 @@ module EntriesHelper + SplitGroup = Data.define(:parent, :children) + + def group_split_entries(entries, split_parents) + return entries if split_parents.blank? + + result = [] + seen_parent_ids = Set.new + + entries.each do |entry| + if entry.split_child? && split_parents[entry.parent_entry_id] + parent_id = entry.parent_entry_id + next if seen_parent_ids.include?(parent_id) + + seen_parent_ids.add(parent_id) + children = entries.select { |e| e.parent_entry_id == parent_id } + result << SplitGroup.new(parent: split_parents[parent_id], children: children) + else + result << entry + end + end + + result + end + def entries_by_date(entries, totals: false) transfer_groups = entries.group_by do |entry| # Only check for transfer if it's a transaction diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 78364ab7b..79a3c248a 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -4,6 +4,7 @@ module SettingsHelper { name: "Accounts", path: :accounts_path }, { name: "Bank Sync", path: :settings_bank_sync_path }, { name: "Preferences", path: :settings_preferences_path }, + { name: "Appearance", path: :settings_appearance_path }, { name: "Profile Info", path: :settings_profile_path }, { name: "Security", path: :settings_security_path }, { name: "Payment", path: :settings_payment_path, condition: :not_self_hosted? }, diff --git a/app/javascript/controllers/split_transaction_controller.js b/app/javascript/controllers/split_transaction_controller.js index e338368af..bce070bf6 100644 --- a/app/javascript/controllers/split_transaction_controller.js +++ b/app/javascript/controllers/split_transaction_controller.js @@ -143,13 +143,13 @@ export default class extends Controller { if (balanced) { this.remainingTarget.classList.remove("text-destructive") this.remainingTarget.classList.add("text-success") - container.classList.remove("border-destructive", "bg-red-25") - container.classList.add("border-green-200", "bg-green-25") + container.classList.remove("border-destructive", "bg-red-tint-10") + container.classList.add("border-green-200", "bg-green-tint-10") } else { this.remainingTarget.classList.remove("text-success") this.remainingTarget.classList.add("text-destructive") - container.classList.remove("border-green-200", "bg-green-25") - container.classList.add("border-destructive", "bg-red-25") + container.classList.remove("border-green-200", "bg-green-tint-10") + container.classList.add("border-destructive", "bg-red-tint-10") } this.errorTarget.classList.toggle("hidden", balanced) diff --git a/app/models/entry.rb b/app/models/entry.rb index 2ebb7d3d1..6c90114d2 100644 --- a/app/models/entry.rb +++ b/app/models/entry.rb @@ -21,6 +21,7 @@ class Entry < ApplicationRecord validates :external_id, uniqueness: { scope: [ :account_id, :source ] }, if: -> { external_id.present? && source.present? } validate :cannot_unexclude_split_parent + validate :split_child_date_matches_parent before_destroy :prevent_individual_child_deletion, if: :split_child? @@ -423,10 +424,19 @@ class Entry < ApplicationRecord transaction do all.each do |entry| + changed = false + # Update standard attributes if bulk_attributes.present? - bulk_attributes[:entryable_attributes][:id] = entry.entryable_id if bulk_attributes[:entryable_attributes].present? - entry.update! bulk_attributes + attrs = bulk_attributes.dup + attrs.delete(:date) if entry.split_child? + + if attrs.present? + attrs[:entryable_attributes] = attrs[:entryable_attributes].dup if attrs[:entryable_attributes].present? + attrs[:entryable_attributes][:id] = entry.entryable_id if attrs[:entryable_attributes].present? + entry.update! attrs + changed = true + end end # Handle tags separately - only when explicitly requested @@ -434,10 +444,13 @@ class Entry < ApplicationRecord entry.transaction.tag_ids = tag_ids entry.transaction.save! entry.entryable.lock_attr!(:tag_ids) if entry.transaction.tags.any? + changed = true end - entry.lock_saved_attributes! - entry.mark_user_modified! + if changed + entry.lock_saved_attributes! + entry.mark_user_modified! + end end end @@ -453,6 +466,14 @@ class Entry < ApplicationRecord errors.add(:excluded, "cannot be toggled off for a split transaction") end + def split_child_date_matches_parent + return unless split_child? && date_changed? + return unless parent_entry.present? + return if date == parent_entry.date + + errors.add(:date, "must match the parent transaction date for split children") + end + def prevent_individual_child_deletion return if destroyed_by_association || unsplitting diff --git a/app/models/user.rb b/app/models/user.rb index 9c6a1882c..4cb17c6bd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -316,6 +316,10 @@ class User < ApplicationRecord preferences&.dig("transactions_collapsed_sections", section_key) == true end + def show_split_grouped? + preferences&.dig("show_split_grouped") != false + end + def update_transactions_preferences(prefs) transaction do lock! diff --git a/app/views/category/dropdowns/show.html.erb b/app/views/category/dropdowns/show.html.erb index 02ec05a7f..bdfc387eb 100644 --- a/app/views/category/dropdowns/show.html.erb +++ b/app/views/category/dropdowns/show.html.erb @@ -65,6 +65,16 @@ <% end %> <% end %> + <% if @transaction.splittable? %> + <%= link_to new_transaction_split_path(@transaction.entry), + class: "flex text-sm font-medium items-center gap-2 text-secondary w-full rounded-lg p-2 hover:bg-container-inset-hover", + data: { turbo_frame: "modal" } do %> + <%= icon("split") %> + +

<%= t("splits.show.button") %>

+ <% end %> + <% end %> +
<%= form_with url: transaction_path(@transaction.entry), diff --git a/app/views/entries/_split_group.html.erb b/app/views/entries/_split_group.html.erb new file mode 100644 index 000000000..cf182f8be --- /dev/null +++ b/app/views/entries/_split_group.html.erb @@ -0,0 +1,8 @@ +<%# locals: (split_group:) %> +
+ <%= render "transactions/split_parent_row", entry: split_group.parent %> + <% split_group.children.each do |child_entry| %> + <%= render partial: child_entry.entryable.to_partial_path, + locals: { entry: child_entry, in_split_group: true } %> + <% end %> +
diff --git a/app/views/settings/_settings_nav.html.erb b/app/views/settings/_settings_nav.html.erb index 90608e652..bc4f9dcd0 100644 --- a/app/views/settings/_settings_nav.html.erb +++ b/app/views/settings/_settings_nav.html.erb @@ -6,6 +6,7 @@ nav_sections = [ { label: t(".accounts_label"), path: accounts_path, icon: "layers" }, { label: t(".bank_sync_label"), path: settings_bank_sync_path, icon: "banknote" }, { label: t(".preferences_label"), path: settings_preferences_path, icon: "bolt" }, + { label: t(".appearance_label"), path: settings_appearance_path, icon: "palette" }, { label: t(".profile_label"), path: settings_profile_path, icon: "circle-user" }, { label: t(".security_label"), path: settings_security_path, icon: "shield-check" }, { label: t(".payment_label"), path: settings_payment_path, icon: "circle-dollar-sign", if: !self_hosted? && Current.family.can_manage_subscription? } diff --git a/app/views/settings/appearances/show.html.erb b/app/views/settings/appearances/show.html.erb new file mode 100644 index 000000000..c2e9e703e --- /dev/null +++ b/app/views/settings/appearances/show.html.erb @@ -0,0 +1,50 @@ +<%= content_for :page_title, t(".page_title") %> + +<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %> +
+ <%= form_with model: @user, class: "flex flex-col md:flex-row justify-between items-center gap-4", id: "theme_form", + data: { controller: "auto-submit-form", auto_submit_form_trigger_event_value: "change" } do |form| %> + <%= form.hidden_field :redirect_to, value: "appearance" %> + + <% theme_option_class = "text-center transition-all duration-200 p-3 rounded-lg hover:bg-surface-hover cursor-pointer [&:has(input:checked)]:bg-surface-hover [&:has(input:checked)]:border [&:has(input:checked)]:border-primary [&:has(input:checked)]:shadow-xs" %> + + <% [ + { value: "light", image: "light-mode-preview.png" }, + { value: "dark", image: "dark-mode-preview.png" }, + { value: "system", image: "system-mode-preview.png" } + ].each do |theme| %> + <%= form.label :"theme_#{theme[:value]}", class: "group" do %> +
+ <%= image_tag(theme[:image], alt: "#{theme[:value].titleize} Theme Preview", class: "max-h-44 mb-2") %> +
"> + <%= form.radio_button :theme, theme[:value], checked: @user.theme == theme[:value], class: "sr-only", + data: { auto_submit_form_target: "auto", autosubmit_trigger_event: "change", action: "theme#updateTheme" } %> + <%= t(".theme_#{theme[:value]}") %> +
+
+ <% end %> + <% end %> + <% end %> +
+<% end %> + +<%= settings_section title: t(".transactions_title"), subtitle: t(".transactions_subtitle") do %> +
+ <%= form_with url: settings_appearance_path, method: :patch, + class: "p-3", + data: { controller: "auto-submit-form" } do |f| %> +
+
+

<%= t(".split_grouped_title") %>

+

<%= t(".split_grouped_description") %>

+
+ <%= render DS::Toggle.new( + id: "user_show_split_grouped", + name: "user[show_split_grouped]", + checked: @user.show_split_grouped?, + data: { auto_submit_form_target: "auto" } + ) %> +
+ <% end %> +
+<% end %> diff --git a/app/views/settings/preferences/show.html.erb b/app/views/settings/preferences/show.html.erb index a9f44e029..eee21b87e 100644 --- a/app/views/settings/preferences/show.html.erb +++ b/app/views/settings/preferences/show.html.erb @@ -56,31 +56,3 @@ <% end %>
<% end %> - -<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %> -
- <%= form_with model: @user, class: "flex flex-col md:flex-row justify-between items-center gap-4", id: "theme_form", - data: { controller: "auto-submit-form", auto_submit_form_trigger_event_value: "change" } do |form| %> - <%= form.hidden_field :redirect_to, value: "preferences" %> - - <% theme_option_class = "text-center transition-all duration-200 p-3 rounded-lg hover:bg-surface-hover cursor-pointer [&:has(input:checked)]:bg-surface-hover [&:has(input:checked)]:border [&:has(input:checked)]:border-primary [&:has(input:checked)]:shadow-xs" %> - - <% [ - { value: "light", image: "light-mode-preview.png" }, - { value: "dark", image: "dark-mode-preview.png" }, - { value: "system", image: "system-mode-preview.png" } - ].each do |theme| %> - <%= form.label :"theme_#{theme[:value]}", class: "group" do %> -
- <%= image_tag(theme[:image], alt: "#{theme[:value].titleize} Theme Preview", class: "max-h-44 mb-2") %> -
"> - <%= form.radio_button :theme, theme[:value], checked: @user.theme == theme[:value], class: "sr-only", - data: { auto_submit_form_target: "auto", autosubmit_trigger_event: "change", action: "theme#updateTheme" } %> - <%= t(".theme_#{theme[:value]}") %> -
-
- <% end %> - <% end %> - <% end %> -
-<% end %> diff --git a/app/views/splits/edit.html.erb b/app/views/splits/edit.html.erb index a762df288..73afc37ac 100644 --- a/app/views/splits/edit.html.erb +++ b/app/views/splits/edit.html.erb @@ -86,7 +86,7 @@ <%# Remaining balance indicator %> -
+
<%= t("splits.new.remaining") %> diff --git a/app/views/splits/new.html.erb b/app/views/splits/new.html.erb index 8aae475b1..d4d101eaf 100644 --- a/app/views/splits/new.html.erb +++ b/app/views/splits/new.html.erb @@ -77,7 +77,7 @@ <%# Remaining balance indicator %> -
+
<%= t("splits.new.remaining") %> diff --git a/app/views/transactions/_list.html.erb b/app/views/transactions/_list.html.erb index 4c8d22713..8506bdf85 100644 --- a/app/views/transactions/_list.html.erb +++ b/app/views/transactions/_list.html.erb @@ -39,7 +39,19 @@
<%= entries_by_date(@transactions.map(&:entry), totals: true) do |entries| %> - <%= render entries %> + <% if Current.user.show_split_grouped? %> + <% group_split_entries(entries, @split_parents).each do |item| %> + <% if item.is_a?(EntriesHelper::SplitGroup) %> + <%= render "entries/split_group", split_group: item %> + <% else %> + <%= render item %> + <% end %> + <% end %> + <% else %> + <% entries.each do |entry| %> + <%= render entry %> + <% end %> + <% end %> <% end %>
diff --git a/app/views/transactions/_split_parent_row.html.erb b/app/views/transactions/_split_parent_row.html.erb new file mode 100644 index 000000000..70b44ebd2 --- /dev/null +++ b/app/views/transactions/_split_parent_row.html.erb @@ -0,0 +1,69 @@ +<%# locals: (entry:) %> +<% transaction = entry.entryable %> + +
+
+ <%# Empty space where checkbox would be, for alignment %> + + +
+
+ + +
+
+
+
+ <%= link_to entry.name, + entry_path(entry), + data: { turbo_frame: "drawer", turbo_prefetch: false }, + class: "hover:underline" %> +
+ +
+ + <%= icon "split", size: "sm", color: "current" %> + <%= t("transactions.split_parent_row.split_label") %> + +
+
+ +
+ <% if transaction.merchant&.present? %> + + <% end %> + +
+
+
+
+
+
+ + + +
+ <%= content_tag :p, format_money(-entry.amount_money) %> +
+
diff --git a/app/views/transactions/_transaction.html.erb b/app/views/transactions/_transaction.html.erb index 84ccb453b..db9f4c169 100644 --- a/app/views/transactions/_transaction.html.erb +++ b/app/views/transactions/_transaction.html.erb @@ -1,10 +1,10 @@ -<%# locals: (entry:, balance_trend: nil, view_ctx: "global") %> +<%# 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 %> -
"> +
<%= "pl-8 lg:pl-12" if in_split_group %>">
<%= check_box_tag dom_id(entry, "selection"), @@ -102,13 +102,13 @@ <% end %> <%# Split indicator %> - <% if entry.split_parent? %> + <% if @split_parent_entry_ids ? @split_parent_entry_ids.include?(entry.id) : entry.split_parent? %> "> <%= icon "split", size: "sm", color: "current" %> <%= t("transactions.transaction.split") %> <% end %> - <% if entry.split_child? %> + <% if entry.split_child? && !in_split_group %> "> <%= icon "corner-down-right", size: "sm", color: "current" %> diff --git a/app/views/transactions/show.html.erb b/app/views/transactions/show.html.erb index a42486d87..294e2c829 100644 --- a/app/views/transactions/show.html.erb +++ b/app/views/transactions/show.html.erb @@ -47,31 +47,33 @@ <%= render "entries/protection_indicator", entry: @entry, unlock_path: unlock_transaction_path(@entry.transaction) %> <% dialog.with_section(title: t(".overview"), open: true) do %>
+ <% split_locked = @entry.split_child? || @entry.split_parent? %> <%= styled_form_with model: @entry, url: transaction_path(@entry), class: "space-y-2", data: { controller: "auto-submit-form" } do |f| %> <%= f.text_field :name, label: t(".name_label"), + disabled: @entry.split_child?, "data-auto-submit-form-target": "auto" %> <%= f.date_field :date, label: t(".date_label"), max: Date.current, - disabled: @entry.linked?, + disabled: @entry.linked? || split_locked, "data-auto-submit-form-target": "auto" %> <% unless @entry.transaction.transfer? %>
<%= f.select :nature, [["Expense", "outflow"], ["Income", "inflow"]], { container_class: "w-1/3", label: t(".nature"), selected: @entry.amount.negative? ? "inflow" : "outflow" }, - { data: { "auto-submit-form-target": "auto" }, disabled: @entry.linked? } %> + { data: { "auto-submit-form-target": "auto" }, disabled: @entry.linked? || split_locked } %> <%= f.money_field :amount, label: t(".amount"), container_class: "w-2/3", auto_submit: true, min: 0, value: @entry.amount.abs, - disabled: @entry.linked?, - disable_currency: @entry.linked? %> + disabled: @entry.linked? || split_locked, + disable_currency: @entry.linked? || split_locked %>
<%= f.fields_for :entryable do |ef| %> <%= ef.collection_select :category_id, @@ -79,7 +81,7 @@ :id, :name, { label: t(".category_label"), class: "text-subdued", include_blank: t(".uncategorized"), - variant: :badge, searchable: true }, + variant: :badge, searchable: true, disabled: @entry.split_child? }, "data-auto-submit-form-target": "auto" %> <% end %> <% end %> @@ -105,14 +107,15 @@ :id, :name, { include_blank: t(".none"), label: t(".merchant_label"), - class: "text-subdued", variant: :logo, searchable: true }, + class: "text-subdued", variant: :logo, searchable: true, disabled: @entry.split_child? }, "data-auto-submit-form-target": "auto" %> <%= ef.select :tag_ids, Current.family.tags.alphabetically.pluck(:name, :id), { include_blank: t(".none"), multiple: true, - label: t(".tags_label") + label: t(".tags_label"), + disabled: @entry.split_child? }, { "data-controller": "multi-select", "data-auto-submit-form-target": "auto" } %> <% end %> @@ -121,6 +124,7 @@ label: t(".note_label"), placeholder: t(".note_placeholder"), rows: 5, + disabled: @entry.split_child?, "data-auto-submit-form-target": "auto" %> <% end %> <% end %> @@ -260,7 +264,7 @@ <% end %> <% dialog.with_section(title: t(".settings")) do %> - <% unless @entry.split_parent? %> + <% unless @entry.split_parent? || @entry.split_child? %>
<%= styled_form_with model: @entry, url: transaction_path(@entry), @@ -301,40 +305,44 @@ <% end %>
<% end %> -
- <%= styled_form_with model: @entry, - url: transaction_path(@entry), - class: "p-3", - data: { controller: "auto-submit-form" } do |f| %> - <%= f.fields_for :entryable do |ef| %> -
-
-

<%= t(".one_time_title", type: @entry.amount.negative? ? t("transactions.form.income") : t("transactions.form.expense")) %>

-

<%= t(".one_time_description") %>

+ <% unless @entry.split_child? %> +
+ <%= styled_form_with model: @entry, + url: transaction_path(@entry), + class: "p-3", + data: { controller: "auto-submit-form" } do |f| %> + <%= f.fields_for :entryable do |ef| %> +
+
+

<%= t(".one_time_title", type: @entry.amount.negative? ? t("transactions.form.income") : t("transactions.form.expense")) %>

+

<%= t(".one_time_description") %>

+
+ <%= ef.toggle :kind, { + checked: @entry.transaction.one_time?, + data: { auto_submit_form_target: "auto" } + }, "one_time", "standard" %>
- <%= ef.toggle :kind, { - checked: @entry.transaction.one_time?, - data: { auto_submit_form_target: "auto" } - }, "one_time", "standard" %> -
+ <% end %> <% end %> - <% end %> - <%# Split Transaction %> - <% if @entry.transaction.splittable? %> -
-
-

<%= t("splits.show.button_title") %>

-

<%= t("splits.show.button_description") %>

-
- <%= render DS::Link.new( - text: t("splits.show.button"), - icon: "split", - variant: "outline", - href: new_transaction_split_path(@entry), - frame: :modal - ) %> +
+ <% end %> + <%# Split Transaction %> + <% if @entry.transaction.splittable? %> +
+
+

<%= t("splits.show.button_title") %>

+

<%= t("splits.show.button_description") %>

- <% end %> + <%= render DS::Link.new( + text: t("splits.show.button"), + icon: "split", + variant: "outline", + href: new_transaction_split_path(@entry), + frame: :modal + ) %> +
+ <% end %> + <% unless @entry.split_child? %>

Transfer or Debt Payment?

@@ -396,23 +404,24 @@ frame: "_top" ) %>
- <%# Delete Transaction Form - hidden for split children %> - <% unless @entry.split_child? %> -
-
-

<%= t(".delete_title") %>

-

<%= t(".delete_subtitle") %>

-
- <%= render DS::Button.new( - text: t(".delete"), - variant: "outline-destructive", - href: entry_path(@entry), - method: :delete, - confirm: CustomConfirm.for_resource_deletion("transaction"), - frame: "_top" - ) %> + <% end %> + <%# Delete Transaction Form - hidden for split children %> + <% unless @entry.split_child? %> +
+
+

<%= t(".delete_title") %>

+

<%= t(".delete_subtitle") %>

- <% end %> + <%= render DS::Button.new( + text: t(".delete"), + variant: "outline-destructive", + href: entry_path(@entry), + method: :delete, + confirm: CustomConfirm.for_resource_deletion("transaction"), + frame: "_top" + ) %> +
+ <% end %>
<% end %> <% end %> diff --git a/config/locales/views/settings/en.yml b/config/locales/views/settings/en.yml index a4db0706a..d66cb7882 100644 --- a/config/locales/views/settings/en.yml +++ b/config/locales/views/settings/en.yml @@ -26,6 +26,18 @@ en: page_title: Payments subscription_subtitle: Update your credit card details subscription_title: Manage contributions + appearances: + show: + page_title: Appearance + theme_title: Theme + theme_subtitle: Choose a preferred theme for the app + theme_dark: Dark + theme_light: Light + theme_system: System + transactions_title: Transactions + transactions_subtitle: Customize how transactions are displayed + split_grouped_title: Group split transactions + split_grouped_description: Show split transactions grouped under their parent in the transaction list. When off, split children appear as individual rows. preferences: show: country: Country @@ -38,11 +50,6 @@ en: language: Language language_auto: Browser language page_title: Preferences - theme_dark: Dark - theme_light: Light - theme_subtitle: Choose a preferred theme for the app - theme_system: System - theme_title: Theme timezone: Timezone month_start_day: Budget month starts on month_start_day_hint: Set when your budget month starts (e.g., payday) @@ -142,6 +149,7 @@ en: transactions_section_title: Transactions whats_new_label: What's new api_keys_label: API Key + appearance_label: Appearance bank_sync_label: Bank Sync settings_nav_link_large: next: Next diff --git a/config/locales/views/transactions/en.yml b/config/locales/views/transactions/en.yml index bd91c1c16..836aabff3 100644 --- a/config/locales/views/transactions/en.yml +++ b/config/locales/views/transactions/en.yml @@ -81,6 +81,8 @@ en: potential_duplicate_description: This pending transaction may be the same as the posted transaction below. If so, merge them to avoid double-counting. merge_duplicate: Yes, merge them keep_both: No, keep both + split_parent_row: + split_label: "Split" transaction: pending: Pending pending_tooltip: Pending transaction — may change when posted diff --git a/config/routes.rb b/config/routes.rb index 017c15897..14eca4b64 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -167,6 +167,7 @@ Rails.application.routes.draw do namespace :settings do resource :profile, only: [ :show, :destroy ] resource :preferences, only: :show + resource :appearance, only: %i[show update] resource :hosting, only: %i[show update] do delete :clear_cache, on: :collection delete :disconnect_external_assistant, on: :collection