From a3c08ceb7c45dcb10877493ff45392cd737b9742 Mon Sep 17 00:00:00 2001 From: Ahmed Ejaz Date: Tue, 30 Dec 2025 01:25:25 +0500 Subject: [PATCH] Add AJAX search functionality for enterprise fees and related entities in reports --- app/controllers/admin/reports_controller.rb | 1 + .../concerns/reports/ajax_search.rb | 105 ++++++++++++++++++ app/helpers/reports_helper.rb | 21 ---- app/models/spree/ability.rb | 4 +- ...se_fees_with_tax_report_by_order.html.haml | 12 +- ...fees_with_tax_report_by_producer.html.haml | 17 +-- .../controllers/select2_ajax_controller.js | 105 ++++++++++++++++++ config/routes/admin.rb | 6 + lib/reporting/frontend_data.rb | 6 +- 9 files changed, 239 insertions(+), 38 deletions(-) create mode 100644 app/controllers/concerns/reports/ajax_search.rb create mode 100644 app/webpacker/controllers/select2_ajax_controller.js diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index bea2a2adc0..d8efede625 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -4,6 +4,7 @@ module Admin class ReportsController < Spree::Admin::BaseController include ActiveStorage::SetCurrent include ReportsActions + include Reports::AjaxSearch helper ReportsHelper diff --git a/app/controllers/concerns/reports/ajax_search.rb b/app/controllers/concerns/reports/ajax_search.rb new file mode 100644 index 0000000000..dc397c84e4 --- /dev/null +++ b/app/controllers/concerns/reports/ajax_search.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +module Reports + module AjaxSearch + extend ActiveSupport::Concern + + def search_enterprise_fees + report = report_class.new(spree_current_user, params, render: false) + fee_ids = enterprise_fee_ids(report.search.result) + query = EnterpriseFee.where(id: fee_ids) + + render json: build_search_response(query) + end + + def search_enterprise_fee_owners + report = report_class.new(spree_current_user, params, render: false) + owner_ids = enterprise_fee_owner_ids(report.search.result) + query = Enterprise.where(id: owner_ids) + + render json: build_search_response(query) + end + + def search_distributors + query = frontend_data.distributors + + render json: build_search_response(query) + end + + def search_order_cycles + query = frontend_data.order_cycles + + render json: build_search_response(query) + end + + def search_order_customers + query = frontend_data.order_customers + + render json: build_search_response(query) + end + + def search_suppliers + query = frontend_data.orders_suppliers + + render json: build_search_response(query) + end + + private + + def build_search_response(query) + page = (params[:page] || 1).to_i + per_page = 30 + + filtered_query = apply_search_filter(query) + total_count = filtered_query.size + items = paginated_items(filtered_query, page, per_page) + results = format_results(items) + + { results: results, pagination: { more: (page * per_page) < total_count } } + end + + def apply_search_filter(query) + search_term = params[:q] + return query if search_term.blank? + + # Handle different model types + if query.model == OrderCycle + query.where("order_cycles.name ILIKE ?", "%#{search_term}%") + elsif query.model == Customer + query.where("customers.email ILIKE ?", "%#{search_term}%") + else + query.where("name ILIKE ?", "%#{search_term}%") + end + end + + def paginated_items(query, page, per_page) + if query.model == Customer + query.order(:email).offset((page - 1) * per_page).limit(per_page).pluck(:email, :id) + elsif query.model == OrderCycle + query.order('order_cycles.orders_close_at DESC') + .offset((page - 1) * per_page) + .limit(per_page).pluck( + :name, :id + ) + else + query.order(:name).offset((page - 1) * per_page).limit(per_page).pluck(:name, :id) + end + end + + def format_results(items) + items.map { |name, id| { id: id, text: name } } + end + + def frontend_data + @frontend_data ||= Reporting::FrontendData.new(spree_current_user) + end + + def enterprise_fee_owner_ids(orders) + EnterpriseFee.where(id: enterprise_fee_ids(orders)).select(:enterprise_id) + end + + def enterprise_fee_ids(orders) + Spree::Adjustment.enterprise_fee.where(order_id: orders.select(:id)).select(:originator_id) + end + end +end diff --git a/app/helpers/reports_helper.rb b/app/helpers/reports_helper.rb index 1caab98aa9..0fa95863e0 100644 --- a/app/helpers/reports_helper.rb +++ b/app/helpers/reports_helper.rb @@ -34,29 +34,8 @@ module ReportsHelper end end - def fee_name_options(orders) - EnterpriseFee.where(id: enterprise_fee_ids(orders)) - .pluck(:name, :id) - end - - def fee_owner_options(orders) - Enterprise.where(id: enterprise_fee_owner_ids(orders)) - .pluck(:name, :id) - end - delegate :currency_symbol, to: :'Spree::Money' - def enterprise_fee_owner_ids(orders) - EnterpriseFee.where(id: enterprise_fee_ids(orders)) - .pluck(:enterprise_id) - end - - def enterprise_fee_ids(orders) - Spree::Adjustment.enterprise_fee - .where(order_id: orders.map(&:id)) - .pluck(:originator_id) - end - def datepicker_time(datetime) datetime = Time.zone.parse(datetime) if datetime.is_a? String datetime.strftime('%Y-%m-%d %H:%M') diff --git a/app/models/spree/ability.rb b/app/models/spree/ability.rb index 63833e309f..654b76a781 100644 --- a/app/models/spree/ability.rb +++ b/app/models/spree/ability.rb @@ -260,7 +260,9 @@ module Spree can [:admin, :index, :import], ::Admin::DfcProductImportsController # Reports page - can [:admin, :index, :show, :create], ::Admin::ReportsController + can [:admin, :index, :show, :create, :search_enterprise_fees, :search_enterprise_fee_owners, + :search_distributors, :search_suppliers, :search_order_cycles, :search_order_customers], + ::Admin::ReportsController can [:admin, :show, :create, :customers, :orders_and_distributors, :group_buys, :payments, :orders_and_fulfillment, :products_and_inventory, :order_cycle_management, :packing, :enterprise_fee_summary, :bulk_coop, :suppliers], :report diff --git a/app/views/admin/reports/filters/_enterprise_fees_with_tax_report_by_order.html.haml b/app/views/admin/reports/filters/_enterprise_fees_with_tax_report_by_order.html.haml index 4a3af1ef5d..6939f28eb7 100644 --- a/app/views/admin/reports/filters/_enterprise_fees_with_tax_report_by_order.html.haml +++ b/app/views/admin/reports/filters/_enterprise_fees_with_tax_report_by_order.html.haml @@ -1,23 +1,25 @@ +- search_url_query = {report_type: :enterprise_fee_summary, report_subtype: :enterprise_fees_with_tax_report_by_order} .row .alpha.two.columns= label_tag nil, t(:report_hubs) - .omega.fourteen.columns= f.collection_select(:distributor_id_in, @data.distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) + .omega.fourteen.columns + = f.select(:distributor_id_in, [], {selected: params.dig(:q, :distributor_id_in)}, {class: "fullwidth", multiple: true, data: {controller: "select2-ajax", select2_ajax_url_value: admin_search_distributors_reports_url}}) .row .alpha.two.columns= label_tag nil, t(:report_customers_cycle) .omega.fourteen.columns - = f.select(:order_cycle_id_in, report_order_cycle_options(@data.order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true}) + = f.select(:order_cycle_id_in, [], {selected: params.dig(:q, :order_cycle_id_in)}, {class: "fullwidth", multiple: true, data: {controller: "select2-ajax", select2_ajax_url_value: admin_search_order_cycles_reports_url}}) .row .alpha.two.columns= label_tag nil, t(:fee_name) .omega.fourteen.columns - = f.select(:enterprise_fee_id_in, fee_name_options(@report.search.result), {selected: params.dig(:q, :enterprise_fee_id_in)}, {class: "select2 fullwidth", multiple: true}) + = f.select(:enterprise_fee_id_in, [], {selected: params.dig(:q, :enterprise_fee_id_in)}, {class: "fullwidth", multiple: true, data: {controller: "select2-ajax", select2_ajax_url_value: admin_search_enterprise_fees_reports_url(search_url_query)}}) .row .alpha.two.columns= label_tag nil, t(:fee_owner) .omega.fourteen.columns - = f.select(:enterprise_fee_owner_id_in, fee_owner_options(@report.search.result), {selected: params.dig(:q, :enterprise_fee_owner_id_in)}, {class: "select2 fullwidth", multiple: true}) + = f.select(:enterprise_fee_owner_id_in, [], {selected: params.dig(:q, :enterprise_fee_owner_id_in)}, {class: "fullwidth", multiple: true, data: {controller: "select2-ajax", select2_ajax_url_value: admin_search_enterprise_fee_owners_reports_url(search_url_query)}}) .row .alpha.two.columns= label_tag nil, t(:report_customers) .omega.fourteen.columns - = f.select(:customer_id_in, customer_email_options(@data.order_customers), {selected: params.dig(:q, :customer_id_in)}, {class: "select2 fullwidth", multiple: true}) + = f.select(:customer_id_in, [], {selected: params.dig(:q, :customer_id_in)}, {class: "fullwidth", multiple: true, data: {controller: "select2-ajax", select2_ajax_url_value: admin_search_order_customers_reports_url}}) diff --git a/app/views/admin/reports/filters/_enterprise_fees_with_tax_report_by_producer.html.haml b/app/views/admin/reports/filters/_enterprise_fees_with_tax_report_by_producer.html.haml index 1c19788df6..ef0b54245c 100644 --- a/app/views/admin/reports/filters/_enterprise_fees_with_tax_report_by_producer.html.haml +++ b/app/views/admin/reports/filters/_enterprise_fees_with_tax_report_by_producer.html.haml @@ -1,27 +1,28 @@ +- search_url_query = {report_type: :enterprise_fee_summary, report_subtype: :enterprise_fees_with_tax_report_by_producer} .row .alpha.two.columns= label_tag nil, t(:report_hubs) - .omega.fourteen.columns= f.collection_select(:distributor_id_in, @data.distributors, :id, :name, {}, {class: "select2 fullwidth", multiple: true}) - + .omega.fourteen.columns + = f.select(:distributor_id_in, [], {selected: params.dig(:q, :distributor_id_in)}, {class: "fullwidth", multiple: true, data: {controller: "select2-ajax", select2_ajax_url_value: admin_search_distributors_reports_url}}) .row .alpha.two.columns= label_tag nil, t(:report_producers) - .omega.fourteen.columns= select_tag(:supplier_id_in, options_from_collection_for_select(@data.orders_suppliers, :id, :name, params[:supplier_id_in]), {class: "select2 fullwidth", multiple: true}) + .omega.fourteen.columns + = select_tag(:supplier_id_in, [], {class: "fullwidth", multiple: true, data: {controller: "select2-ajax", select2_ajax_url_value: admin_search_suppliers_reports_url}}) .row .alpha.two.columns= label_tag nil, t(:report_customers_cycle) .omega.fourteen.columns - = f.select(:order_cycle_id_in, report_order_cycle_options(@data.order_cycles), {selected: params.dig(:q, :order_cycle_id_in)}, {class: "select2 fullwidth", multiple: true}) - + = f.select(:order_cycle_id_in, [], {selected: params.dig(:q, :order_cycle_id_in)}, {class: "fullwidth", multiple: true, data: {controller: "select2-ajax", select2_ajax_url_value: admin_search_order_cycles_reports_url}}) .row .alpha.two.columns= label_tag nil, t(:fee_name) .omega.fourteen.columns - = f.select(:enterprise_fee_id_in, fee_name_options(@report.search.result), {selected: params.dig(:q, :enterprise_fee_id_in)}, {class: "select2 fullwidth", multiple: true}) + = f.select(:enterprise_fee_id_in, [], {selected: params.dig(:q, :enterprise_fee_id_in)}, {class: "fullwidth", multiple: true, data: {controller: "select2-ajax", select2_ajax_url_value: admin_search_enterprise_fees_reports_url(search_url_query)}}) .row .alpha.two.columns= label_tag nil, t(:fee_owner) .omega.fourteen.columns - = f.select(:enterprise_fee_owner_id_in, fee_owner_options(@report.search.result), {selected: params.dig(:q, :enterprise_fee_owner_id_in)}, {class: "select2 fullwidth", multiple: true}) + = f.select(:enterprise_fee_owner_id_in, [], {selected: params.dig(:q, :enterprise_fee_owner_id_in)}, {class: "fullwidth", multiple: true, data: {controller: "select2-ajax", select2_ajax_url_value: admin_search_enterprise_fee_owners_reports_url(search_url_query)}}) .row .alpha.two.columns= label_tag nil, t(:report_customers) .omega.fourteen.columns - = f.select(:customer_id_in, customer_email_options(@data.order_customers), {selected: params.dig(:q, :customer_id_in)}, {class: "select2 fullwidth", multiple: true}) + = f.select(:customer_id_in, [], {selected: params.dig(:q, :customer_id_in)}, {class: "fullwidth", multiple: true, data: {controller: "select2-ajax", select2_ajax_url_value: admin_search_order_customers_reports_url}}) diff --git a/app/webpacker/controllers/select2_ajax_controller.js b/app/webpacker/controllers/select2_ajax_controller.js new file mode 100644 index 0000000000..24735406e0 --- /dev/null +++ b/app/webpacker/controllers/select2_ajax_controller.js @@ -0,0 +1,105 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + static values = { + url: String, + }; + + connect() { + if (typeof $ === "undefined" || typeof $.fn.select2 === "undefined") { + console.error("Select2 AJAX Controller: jQuery or Select2 not loaded"); + return; + } + + const ajaxUrl = this.urlValue; + if (!ajaxUrl) return; + + const selectName = this.element.name; + const selectId = this.element.id; + const isMultiple = this.element.multiple; + + const container = document.createElement("div"); + container.dataset.select2HiddenContainer = "true"; + + this.element.replaceWith(container); + + // select2 methods are accessible via jQuery + // Plus, ajax calls with multi-select require a hidden input in select2 + const $select2Input = $(''); + $select2Input.attr("id", selectId); + container.appendChild($select2Input[0]); + + // IN-MEMORY cache to avoid repeated ajax calls for same query/page + const ajaxCache = {}; + + const select2Options = { + ajax: { + url: ajaxUrl, + dataType: "json", + quietMillis: 300, + data: function (term, page) { + return { + q: term || "", + page: page || 1, + }; + }, + transport: function (params) { + const term = params.data.q || ""; + const page = params.data.page || 1; + const cacheKey = `${term}::${page}`; + + if (ajaxCache[cacheKey]) { + params.success(ajaxCache[cacheKey]); + return; + } + + const request = $.ajax(params); + + request.then((data) => { + ajaxCache[cacheKey] = data; + params.success(data); + }); + + return request; + }, + results: function (data, _page) { + return { + results: data.results || [], + more: (data.pagination && data.pagination.more) || false, + }; + }, + }, + allowClear: true, + minimumInputLength: 0, + multiple: isMultiple, + width: "100%", + formatResult: (item) => item.text, + formatSelection: (item) => item.text, + }; + + // Initialize select2 with ajax options on hidden input + $select2Input.select2(select2Options); + + // Rails-style array submission requires multiple hidden inputs with same name + const syncHiddenInputs = (values) => { + // remove old inputs + container.querySelectorAll(`input[name="${selectName}"]`).forEach((e) => e.remove()); + + values.forEach((value) => { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = selectName; + input.value = value; + container.appendChild(input); + }); + }; + + // On change → rebuild hidden inputs to submit filter values + $select2Input.on("change", () => { + const valuesString = $select2Input.val() || ""; + const values = valuesString.split(",") || []; + + syncHiddenInputs(Array.isArray(values) ? values : [values]); + }); + } +} diff --git a/config/routes/admin.rb b/config/routes/admin.rb index b80d340bd4..68c60b29f9 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -137,6 +137,12 @@ Openfoodnetwork::Application.routes.draw do end get '/reports', to: 'reports#index', as: :reports + get '/reports/search_enterprise_fees', to: 'reports#search_enterprise_fees', as: :search_enterprise_fees_reports + get '/reports/search_enterprise_fee_owners', to: 'reports#search_enterprise_fee_owners', as: :search_enterprise_fee_owners_reports + get '/reports/search_distributors', to: 'reports#search_distributors', as: :search_distributors_reports + get '/reports/search_suppliers', to: 'reports#search_suppliers', as: :search_suppliers_reports + get '/reports/search_order_cycles', to: 'reports#search_order_cycles', as: :search_order_cycles_reports + get '/reports/search_order_customers', to: 'reports#search_order_customers', as: :search_order_customers_reports match '/reports/:report_type(/:report_subtype)', to: 'reports#show', via: :get, as: :report match '/reports/:report_type(/:report_subtype)', to: 'reports#create', via: :post end diff --git a/lib/reporting/frontend_data.rb b/lib/reporting/frontend_data.rb index 5a1152d4a2..682e071fb1 100644 --- a/lib/reporting/frontend_data.rb +++ b/lib/reporting/frontend_data.rb @@ -46,7 +46,7 @@ module Reporting end def order_customers - Customer.where(id: visible_order_customer_ids).select("customers.id, customers.email") + Customer.where(id: visible_order_customer_ids_query).select("customers.id, customers.email") end private @@ -57,8 +57,8 @@ module Reporting @permissions ||= OpenFoodNetwork::Permissions.new(current_user) end - def visible_order_customer_ids - Permissions::Order.new(current_user).visible_orders.pluck(:customer_id) + def visible_order_customer_ids_query + Permissions::Order.new(current_user).visible_orders.select(:customer_id) end end end