- <%= 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' %>
diff --git a/assets/javascripts/autocomplete.js b/assets/javascripts/autocomplete.js
new file mode 100644
index 0000000..ff70373
--- /dev/null
+++ b/assets/javascripts/autocomplete.js
@@ -0,0 +1,102 @@
+(function () {
+
+ // Helper: escape HTML for safety
+ function escapeHtml(str) {
+ return $("
").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, "
$1");
+ }
+
+ 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 $("
")
+ .append($("").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);
+ });
+
+})();
\ No newline at end of file
diff --git a/assets/stylesheets/autocomplete.css b/assets/stylesheets/autocomplete.css
new file mode 100644
index 0000000..8530ce9
--- /dev/null
+++ b/assets/stylesheets/autocomplete.css
@@ -0,0 +1,5 @@
+/* Keep Redmine default look, just enhance metadata */
+.ui-autocomplete .autocomplete-meta {
+ color: #888;
+ font-size: 0.9em;
+}
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 4b3f757..b6a7441 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
\ No newline at end of file
diff --git a/lib/redmine_qbo/hooks/issues_hook_listener.rb b/lib/redmine_qbo/hooks/issues_hook_listener.rb
index 4e47847..7f5d7c6 100644
--- a/lib/redmine_qbo/hooks/issues_hook_listener.rb
+++ b/lib/redmine_qbo/hooks/issues_hook_listener.rb
@@ -23,13 +23,13 @@ module RedmineQbo
project = context[:project]
# 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'
+ # onchange event will update the hidden customer_id field
+ 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:
diff --git a/lib/redmine_qbo/hooks/view_hook_listener.rb b/lib/redmine_qbo/hooks/view_hook_listener.rb
index 4a5c944..da6862a 100644
--- a/lib/redmine_qbo/hooks/view_hook_listener.rb
+++ b/lib/redmine_qbo/hooks/view_hook_listener.rb
@@ -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