mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-21 00:47:26 +00:00
Add AJAX search functionality for enterprise fees and related entities in reports
This commit is contained in:
@@ -4,6 +4,7 @@ module Admin
|
||||
class ReportsController < Spree::Admin::BaseController
|
||||
include ActiveStorage::SetCurrent
|
||||
include ReportsActions
|
||||
include Reports::AjaxSearch
|
||||
|
||||
helper ReportsHelper
|
||||
|
||||
|
||||
105
app/controllers/concerns/reports/ajax_search.rb
Normal file
105
app/controllers/concerns/reports/ajax_search.rb
Normal 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
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}})
|
||||
|
||||
@@ -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}})
|
||||
|
||||
105
app/webpacker/controllers/select2_ajax_controller.js
Normal file
105
app/webpacker/controllers/select2_ajax_controller.js
Normal 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]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user