From 9c1756973ebb125b91734aac8dda7b4f7d2001ba Mon Sep 17 00:00:00 2001 From: naraveni Date: Thu, 3 Apr 2025 16:13:28 -0400 Subject: [PATCH] Filter By Item For Donations Page --- app/controllers/donations_controller.rb | 11 ++++++++- app/models/donation.rb | 1 + app/services/donation_totals_service.rb | 32 +++++++++++++++++++++++++ app/views/donations/index.html.erb | 9 ++++++- spec/system/donation_system_spec.rb | 16 +++++++++++++ 5 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 app/services/donation_totals_service.rb diff --git a/app/controllers/donations_controller.rb b/app/controllers/donations_controller.rb index b82f220d13..4b3f5dd455 100644 --- a/app/controllers/donations_controller.rb +++ b/app/controllers/donations_controller.rb @@ -32,6 +32,7 @@ def index # Using the @donations allows drilling down instead of always starting with the total dataset @donations_quantity = @donations.collect(&:total_quantity).sum @paginated_donations_quantity = @paginated_donations.collect(&:total_quantity).sum + @items = current_organization.items.alphabetized.select(:id, :name) @total_value_all_donations = total_value(@donations) @paginated_in_kind_value = total_value(@paginated_donations) @total_money_raised = total_money_raised(@donations) @@ -39,6 +40,8 @@ def index @selected_storage_location = filter_params[:at_storage_location] @sources = @donations.collect(&:source).uniq.sort @selected_source = filter_params[:by_source] + @selected_item = filter_params[:by_item_id] + @donation_totals = DonationTotalsService.call(current_organization.donations.class_filter(scope_filters)) @donation_sites = @donations.collect(&:donation_site).compact.uniq.sort_by { |site| site.name.downcase } @selected_donation_site = filter_params[:from_donation_site] @selected_product_drive = filter_params[:by_product_drive] @@ -155,7 +158,7 @@ def donation_item_params def filter_params return {} unless params.key?(:filters) - params.require(:filters).permit(:at_storage_location, :by_source, :from_donation_site, :by_product_drive, :by_product_drive_participant, :from_manufacturer) + params.require(:filters).permit(:at_storage_location, :by_source, :from_donation_site, :by_product_drive, :by_product_drive_participant, :from_manufacturer, :by_item_id) end # Omits donation_site_id or product_drive_participant_id if those aren't selected as source @@ -167,6 +170,12 @@ def strip_unnecessary_params params end + def scope_filters + filter_params + .except(:date_range) + .merge(during: helpers.selected_range) + end + # If line_items have submitted with empty rows, clear those out first. def compact_line_items return params unless params[:donation].key?(:line_items_attributes) diff --git a/app/models/donation.rb b/app/models/donation.rb index fe1f1c9a4a..b06fb9ffaf 100644 --- a/app/models/donation.rb +++ b/app/models/donation.rb @@ -51,6 +51,7 @@ class Donation < ApplicationRecord scope :from_manufacturer, ->(manufacturer_id) { where(manufacturer_id: manufacturer_id) } + scope :by_item_id, ->(item_id) { includes(:items).where(items: { id: item_id }) } before_create :combine_duplicates diff --git a/app/services/donation_totals_service.rb b/app/services/donation_totals_service.rb new file mode 100644 index 0000000000..b37c363636 --- /dev/null +++ b/app/services/donation_totals_service.rb @@ -0,0 +1,32 @@ +class DonationTotalsService + DonationTotal = Data.define(:quantity, :value) + + class << self + # @param donations [Donation::ActiveRecord_Relation] + # @return [Hash] + def call(donations) + calculate_totals(donations) + end + + private + + # Returns a hash with quantity/value totals for each donation. + # + # @return [Hash] + def calculate_totals(donations) + donations + .left_joins(line_items: [:item]) + .group("donations.id, line_items.id, items.id") + .pluck( + Arel.sql( + "donations.id, + COALESCE(SUM(line_items.quantity) OVER (PARTITION BY donations.id), 0) AS quantity, + COALESCE(SUM(COALESCE(items.value_in_cents, 0) * line_items.quantity) OVER (PARTITION BY donations.id), 0) AS value" + ) + ) + .to_h do |(id, quantity, value)| + [id, DonationTotal.new(quantity:, value:)] + end + end + end +end diff --git a/app/views/donations/index.html.erb b/app/views/donations/index.html.erb index f8c34de309..e7819e5452 100644 --- a/app/views/donations/index.html.erb +++ b/app/views/donations/index.html.erb @@ -71,6 +71,11 @@ <%= filter_select(label: "Filter by Donation Site", scope: :from_donation_site, collection: @donation_sites, key: :id, value: :name, selected: @selected_donation_site) %> <% end %> + <% if @items.present? %> +
+ <%= filter_select(label: "Filter by Item", scope: :by_item_id, collection: @items, selected: @selected_item) %> +
+ <% end %>
<%= label_tag "Date Range" %> <%= render partial: "shared/date_range_picker", locals: {css_class: "form-control"} %> @@ -107,7 +112,9 @@ Date Details Storage Location - Quantity of Items + + <% filtered_item_name = @selected_item ? @items.find { |i| i.id == filter_params[:by_item_id].to_i }&.name : "Items" %> + Quantity of <%= filtered_item_name %> Money Raised In Kind Value Comments diff --git a/spec/system/donation_system_spec.rb b/spec/system/donation_system_spec.rb index e94e8789a7..c9ed4b3d90 100644 --- a/spec/system/donation_system_spec.rb +++ b/spec/system/donation_system_spec.rb @@ -89,6 +89,22 @@ expect(page).to have_css("table tbody tr", count: 1) end + it "Filters by Item id" do + category_1 = create(:item_category, name: "Category 1", organization: organization) + category_2 = create(:item_category, name: "Category 2", organization: organization) + item_1 = create(:item, item_category: category_1, name: "Item 1", value_in_cents: 100) + item_2 = create(:item, item_category: category_2, name: "Item 2", value_in_cents: 200) + create(:donation, :with_items, item_quantity: 25, item: item_1) + create(:donation, :with_items, item_quantity: 30, item: item_2) + visit subject + expect(page).to have_css("table tbody tr", count: 2) + select item_1.name, from: "filters[by_item_id]" + click_button "Filter" + expect(page).to have_css("table tbody tr", count: 1) + expect(page).to have_css("table thead tr th", text: "Quantity of #{item_1.name}") + expect(page).to have_css("table td.in-kind", text: "25") + end + it "Filter by product drive participant sticks around" do x = create(:product_drive, name: 'x') a = create(:product_drive_participant, business_name: "A")