Add AJAX search functionality for enterprise fees and related entities in reports

This commit is contained in:
Ahmed Ejaz
2025-12-30 01:25:25 +05:00
parent ca14d557c1
commit a3c08ceb7c
9 changed files with 239 additions and 38 deletions

View File

@@ -4,6 +4,7 @@ module Admin
class ReportsController < Spree::Admin::BaseController
include ActiveStorage::SetCurrent
include ReportsActions
include Reports::AjaxSearch
helper ReportsHelper

View File

@@ -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

View File

@@ -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')

View File

@@ -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

View File

@@ -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}})

View File

@@ -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}})

View File

@@ -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 = $('<input type="hidden" />');
$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]);
});
}
}

View File

@@ -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

View File

@@ -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