diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index b00212dc5e2..bc2a9592968 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -57,13 +57,16 @@ def bulk_delete redirect_to transactions_url, notice: t(".success", count: destroyed.count) end + def bulk_edit + end + def bulk_update transactions = Current.family.transactions.where(id: bulk_update_params[:transaction_ids]) - updates = bulk_update_params.except(:transaction_ids) - if transactions.update_all(bulk_update_params.except(:transaction_ids).to_h) + if transactions.update_all(bulk_update_params.except(:transaction_ids).to_h.compact_blank!) redirect_to transactions_url, notice: t(".success", count: transactions.count) else - render :index, status: :unprocessable_entity, notice: t(".failure") + flash.now[:error] = t(".failure") + render :index, status: :unprocessable_entity end end @@ -90,7 +93,7 @@ def bulk_delete_params end def bulk_update_params - params.require(:bulk_update).permit(:category_id, :excluded, :currency, tag_ids: [], transaction_ids: []) + params.require(:bulk_update).permit(:date, :notes, :excluded, :category_id, :merchant_id, transaction_ids: []) end def search_params diff --git a/app/javascript/controllers/bulk_select_controller.js b/app/javascript/controllers/bulk_select_controller.js index 124ddf9cb2d..a1aff137a59 100644 --- a/app/javascript/controllers/bulk_select_controller.js +++ b/app/javascript/controllers/bulk_select_controller.js @@ -2,7 +2,7 @@ import {Controller} from "@hotwired/stimulus" // Connects to data-controller="bulk-select" export default class extends Controller { - static targets = ["row", "group", "selectionBar", "selectionBarText"] + static targets = ["row", "group", "selectionBar", "selectionBarText", "bulkEditDrawerTitle"] static values = { resource: String, selectedIds: {type: Array, default: []} @@ -18,9 +18,14 @@ export default class extends Controller { document.removeEventListener("turbo:load", this.#updateView) } - submitBulkDeletionRequest(e) { + bulkEditDrawerTitleTargetConnected(element) { + element.innerText = `Edit ${this.selectedIdsValue.length} ${this.#pluralizedResourceName()}` + } + + submitBulkRequest(e) { const form = e.target.closest("form"); - this.#addHiddenFormInputsForSelectedIds(form, "bulk_delete[transaction_ids][]", this.selectedIdsValue) + const scope = e.params.scope + this.#addHiddenFormInputsForSelectedIds(form, `${scope}[transaction_ids][]`, this.selectedIdsValue) form.requestSubmit() } @@ -96,11 +101,15 @@ export default class extends Controller { #updateSelectionBar() { const count = this.selectedIdsValue.length - this.selectionBarTextTarget.innerText = `${count} ${this.resourceValue}${count === 1 ? "" : "s"} selected` + this.selectionBarTextTarget.innerText = `${count} ${this.#pluralizedResourceName()} selected` this.selectionBarTarget.hidden = count === 0 this.selectionBarTarget.querySelector("input[type='checkbox']").checked = count > 0 } + #pluralizedResourceName() { + return `${this.resourceValue}${this.selectedIdsValue.length === 1 ? "" : "s"}` + } + #updateGroups() { this.groupTargets.forEach(group => { const rows = this.rowTargets.filter(row => group.contains(row)) diff --git a/app/views/transactions/_selection_bar.html.erb b/app/views/transactions/_selection_bar.html.erb index 4e42d7dc940..34cc0a1f30b 100644 --- a/app/views/transactions/_selection_bar.html.erb +++ b/app/views/transactions/_selection_bar.html.erb @@ -6,12 +6,17 @@
- <%= button_to "#", disabled: true, class: "cursor-not-allowed p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md", title: "Edit" do %> + <%= turbo_frame_tag "bulk_transaction_edit_drawer" %> + + <%= link_to bulk_edit_transactions_path, + class: "p-1.5 group hover:bg-gray-700 flex items-center justify-center rounded-md", + title: "Edit", + data: { turbo_frame: "bulk_transaction_edit_drawer" } do %> <%= lucide_icon "pencil-line", class: "w-5 group-hover:text-white" %> <% end %> <%= form_with url: bulk_delete_transactions_path, builder: ActionView::Helpers::FormBuilder, data: { turbo_confirm: true } do %> - <% end %> diff --git a/app/views/transactions/bulk_edit.html.erb b/app/views/transactions/bulk_edit.html.erb new file mode 100644 index 00000000000..107fcdcde39 --- /dev/null +++ b/app/views/transactions/bulk_edit.html.erb @@ -0,0 +1,80 @@ +<%= turbo_frame_tag "bulk_transaction_edit_drawer" do %> + + <%= form_with url: bulk_update_transactions_path, scope: "bulk_update", html: { class: "h-full" }, data: { turbo_frame: "_top" } do |form| %> +
+
+
+
+ <%= lucide_icon("x", class: "w-5 h-5 shrink-0") %> +
+
+ +
+
+
+

+ Edit transactions +

+
+ +
+
+ +

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

+ <%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %> +
+ +
+ <%= form.date_field :date, label: t(".date"), max: Date.current %> + <%= form.collection_select :category_id, Current.family.transaction_categories, :id, :name, { prompt: t(".select_category"), label: t(".category"), class: "text-gray-400" } %> + <%= form.collection_select :merchant_id, Current.family.transaction_merchants, :id, :name, { prompt: t(".select_merchant"), label: t(".merchant"), class: "text-gray-400" } %> +
+
+ +
+ +

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

+ <%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %> +
+ +
+ <%= form.text_area :notes, label: t(".note"), placeholder: t(".note_placeholder"), rows: 5 %> +
+
+ +
+ +

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

+ <%= lucide_icon "chevron-down", class: "group-open:transform group-open:rotate-180 text-gray-500 w-5" %> +
+ +
+
+

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

+

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

+
+ +
+ <%= form.check_box :excluded, class: "sr-only peer" %> + +
+
+
+
+
+
+
+
+ <%= link_to t(".cancel"), transactions_path, class: "text-sm font-medium text-gray-900 px-3 py-2" %> + + <%= tag.button t(".save"), + type: "button", + data: { "bulk-select-scope-param": "bulk_update", action: "bulk-select#submitBulkRequest" }, + class: "px-3 py-2 bg-gray-900 text-white text-sm font-medium rounded-lg" %> +
+
+ <% end %> +
+<% end %> diff --git a/config/locales/views/transactions/en.yml b/config/locales/views/transactions/en.yml index 08c99e3d9f2..7c0a046f7a2 100644 --- a/config/locales/views/transactions/en.yml +++ b/config/locales/views/transactions/en.yml @@ -3,6 +3,22 @@ en: transactions: bulk_delete: success: "%{count} transactions deleted" + bulk_edit: + additional: Additional + cancel: Cancel + category: Category + date: Date + exclude_subtitle: This excludes the transaction from any in-app features or + analytics. + exclude_title: Exclude transaction + merchant: Merchant + note: Notes + note_placeholder: Enter a note that will be applied to selected transactions + overview: Overview + save: Save + select_category: Select a category + select_merchant: Select a merchant + settings: Settings bulk_update: failure: Could not update transactions success: "%{count} transactions updated" diff --git a/config/routes.rb b/config/routes.rb index 974229efe7d..08c39db7de6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -44,6 +44,7 @@ resources :transactions do collection do post "bulk_delete" + get "bulk_edit" post "bulk_update" scope module: :transactions, as: :transaction do diff --git a/test/controllers/transactions_controller_test.rb b/test/controllers/transactions_controller_test.rb index 94d36affef6..6b36ab5213b 100644 --- a/test/controllers/transactions_controller_test.rb +++ b/test/controllers/transactions_controller_test.rb @@ -157,15 +157,21 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest transactions = @user.family.transactions.ordered.limit(20) transactions.each do |transaction| - transaction.update! excluded: false, currency: "USD", category_id: Transaction::Category.first.id + transaction.update! \ + excluded: false, + category_id: Transaction::Category.first.id, + merchant_id: Transaction::Merchant.first.id, + notes: "Starting note" end post bulk_update_transactions_url, params: { bulk_update: { + date: Date.current, transaction_ids: transactions.map(&:id), excluded: true, - currency: "CAD", - category_id: Transaction::Category.second.id + category_id: Transaction::Category.second.id, + merchant_id: Transaction::Merchant.second.id, + notes: "Updated note" } } @@ -173,9 +179,11 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest assert_equal "#{transactions.count} transactions updated", flash[:notice] transactions.reload.each do |transaction| + assert_equal Date.current, transaction.date assert transaction.excluded - assert_equal "CAD", transaction.currency assert_equal Transaction::Category.second, transaction.category + assert_equal Transaction::Merchant.second, transaction.merchant + assert_equal "Updated note", transaction.notes end end end