mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2026-04-02 08:21:57 -04:00
Implmented custom autocomplete for customer field
This commit is contained in:
1
Gemfile
1
Gemfile
@@ -4,7 +4,6 @@ gem 'quickbooks-ruby'
|
||||
gem 'oauth2'
|
||||
gem 'roxml'
|
||||
gem 'will_paginate'
|
||||
gem 'rails-jquery-autocomplete'
|
||||
gem 'jquery-ui-rails'
|
||||
gem 'rexml'
|
||||
gem 'combine_pdf'
|
||||
|
||||
@@ -30,8 +30,6 @@ class CustomersController < ApplicationController
|
||||
before_action :view_customer, except: [:new, :view]
|
||||
skip_before_action :verify_authenticity_token, :check_if_login_required, only: [:view]
|
||||
|
||||
autocomplete :customer, :name, full: true, extra_data: [:id]
|
||||
|
||||
def address_to_s(address)
|
||||
return if address.nil?
|
||||
|
||||
@@ -62,6 +60,19 @@ class CustomersController < ApplicationController
|
||||
params.require(:customer).permit(:name, :email, :primary_phone, :mobile_phone, :phone_number, :notes)
|
||||
end
|
||||
|
||||
# Used for autocomplete form
|
||||
def autocomplete
|
||||
term = ActiveRecord::Base.sanitize_sql_like(params[:q].to_s)
|
||||
|
||||
items = Customer.where("name LIKE :t OR phone_number LIKE :t OR mobile_phone_number LIKE :t", t: "%#{term}%")
|
||||
.order(:name)
|
||||
.limit(20)
|
||||
|
||||
render json: items.map { |i|
|
||||
{ id: i.id, name: i.name, phone_number: i.phone_number, mobile_phone_number: i.mobile_phone_number }
|
||||
}
|
||||
end
|
||||
|
||||
def create
|
||||
@customer = Customer.new(allowed_params)
|
||||
@customer.save
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<%= link_to t(:label_sync_now_customers), customers_sync_path, class: 'button icon icon-reload' %>
|
||||
<%= link_to t(:label_sync_now_customers), sync_customers_path, class: 'button icon icon-reload' %>
|
||||
<%= link_to t(:label_sync_now_employees), employees_sync_path, class: 'button icon icon-reload' %>
|
||||
<%= link_to t(:label_sync_now_invoices), invoices_sync_path, class: 'button icon icon-reload' %>
|
||||
<%= link_to t(:label_sync_now_estimate), estimates_sync_path, class: 'button icon icon-reload' %>
|
||||
|
||||
102
assets/javascripts/autocomplete.js
Normal file
102
assets/javascripts/autocomplete.js
Normal file
@@ -0,0 +1,102 @@
|
||||
(function () {
|
||||
|
||||
// Helper: escape HTML for safety
|
||||
function escapeHtml(str) {
|
||||
return $("<div>").text(str).html();
|
||||
}
|
||||
|
||||
// Helper: highlight all occurrences of term (case-insensitive)
|
||||
function highlightTerm(text, term) {
|
||||
if (!term) return text;
|
||||
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regex = new RegExp("(" + escapedTerm + ")", "ig");
|
||||
return text.replace(regex, "<strong>$1</strong>");
|
||||
}
|
||||
|
||||
window.initCustomerAutocomplete = function(context) {
|
||||
let scope = context || document;
|
||||
|
||||
$(scope).find(".customer-name").each(function() {
|
||||
if ($(this).data("autocomplete-initialized")) return;
|
||||
$(this).data("autocomplete-initialized", true);
|
||||
|
||||
let $input = $(this);
|
||||
|
||||
let ac = $input.autocomplete({
|
||||
appendTo: "body", // crucial for Redmine positioning
|
||||
minLength: 2,
|
||||
|
||||
source: function(request, response) {
|
||||
$.getJSON("/customers/autocomplete", { q: request.term })
|
||||
.done(function(data) {
|
||||
response(data.map(function(item) {
|
||||
// combine secondary info
|
||||
let secondary = [];
|
||||
if (item.phone_number) secondary.push(item.phone_number);
|
||||
if (item.mobile_phone_number) secondary.push(item.mobile_phone_number);
|
||||
|
||||
let meta = secondary.length ? " (" + secondary.join(" • ") + ")" : "";
|
||||
|
||||
// escape HTML to avoid XSS
|
||||
let safeText = escapeHtml(item.name + meta);
|
||||
|
||||
return {
|
||||
label: item.name + meta, // plain fallback
|
||||
value: item.name, // goes into input
|
||||
id: item.id,
|
||||
html: highlightTerm(safeText, request.term)
|
||||
};
|
||||
}));
|
||||
})
|
||||
.fail(function() {
|
||||
response([]);
|
||||
});
|
||||
},
|
||||
|
||||
select: function(event, ui) {
|
||||
$input.val(ui.item.value); // visible text
|
||||
$("#issue_customer_id").val(ui.item.id); // hidden ID
|
||||
|
||||
// trigger Redmine form update safely
|
||||
setTimeout(function() {
|
||||
$("#issue_customer_id").trigger("change");
|
||||
}, 0);
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
change: function(event, ui) {
|
||||
// clear hidden field if no valid selection
|
||||
if (!ui.item && !$input.val()) {
|
||||
$("#issue_customer_id").val("");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Render item HTML for highlight
|
||||
ac.autocomplete("instance")._renderItem = function(ul, item) {
|
||||
return $("<li>")
|
||||
.append($("<div>").html(item.html))
|
||||
.appendTo(ul);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Re-init after Redmine AJAX form updates
|
||||
$(document).on("ajaxComplete", function() {
|
||||
if (window.initCustomerAutocomplete) {
|
||||
window.initCustomerAutocomplete(document);
|
||||
}
|
||||
});
|
||||
|
||||
// Init on page load
|
||||
$(document).ready(function() {
|
||||
window.initCustomerAutocomplete(document);
|
||||
});
|
||||
|
||||
// Also init on Turbo/Redmine load events
|
||||
document.addEventListener("turbo:load", function() {
|
||||
window.initCustomerAutocomplete(document);
|
||||
});
|
||||
|
||||
})();
|
||||
5
assets/stylesheets/autocomplete.css
Normal file
5
assets/stylesheets/autocomplete.css
Normal file
@@ -0,0 +1,5 @@
|
||||
/* Keep Redmine default look, just enhance metadata */
|
||||
.ui-autocomplete .autocomplete-meta {
|
||||
color: #888;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -14,7 +14,6 @@ get 'qbo/oauth_callback', to: 'qbo#oauth_callback'
|
||||
|
||||
#manual sync
|
||||
get 'qbo/sync', to: 'qbo#sync'
|
||||
get 'customers/sync', to: 'customers#sync'
|
||||
get 'invoices/sync', to: 'invoices#sync'
|
||||
get 'estimates/sync', to: 'estimates#sync'
|
||||
get 'employees/sync', to: 'employees#sync'
|
||||
@@ -39,5 +38,8 @@ get 'filter_estimates_by_customer' => 'customers#filter_estimates_by_customer'
|
||||
get 'filter_invoices_by_customer' => 'customers#filter_invoices_by_customer'
|
||||
|
||||
resources :customers do
|
||||
get :autocomplete_customer_name, on: :collection
|
||||
collection do
|
||||
get :autocomplete
|
||||
get :sync
|
||||
end
|
||||
end
|
||||
@@ -24,12 +24,12 @@ module RedmineQbo
|
||||
|
||||
# Customer Name Text Box with database backed autocomplete
|
||||
# onchange event will update the hidden customer_id field
|
||||
search_customer = f.autocomplete_field :customer,
|
||||
autocomplete_customer_name_customers_path,
|
||||
selected: issue.customer,
|
||||
update_elements: {
|
||||
id: '#issue_customer_id',
|
||||
value: '#issue_customer'
|
||||
search_customer = f.text_field :customer,
|
||||
placeholder: l(:field_customer),
|
||||
class: "customer-name",
|
||||
autocomplete: "off",
|
||||
data: {
|
||||
autocomplete_url: "/customers/autocomplete"
|
||||
}
|
||||
|
||||
# We need to handle 3 cases for the onchange event of the customer name field:
|
||||
|
||||
@@ -17,8 +17,9 @@ module RedmineQbo
|
||||
def view_layouts_base_html_head(context = {})
|
||||
safe_join([
|
||||
javascript_include_tag( 'application.js', plugin: :redmine_qbo),
|
||||
javascript_include_tag( 'autocomplete-rails.js', plugin: :redmine_qbo),
|
||||
javascript_include_tag( 'checkbox_controller.js', plugin: :redmine_qbo)
|
||||
javascript_include_tag( 'autocomplete.js', plugin: :redmine_qbo),
|
||||
javascript_include_tag( 'checkbox_controller.js', plugin: :redmine_qbo),
|
||||
stylesheet_link_tag( 'autocomplete', plugin: :redmine_qbo)
|
||||
])
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user