mirror of
https://github.com/rickbarrette/redmine_qbo.git
synced 2026-04-02 08:21:57 -04:00
Compare commits
2 Commits
460bcd466f
...
b0e6236cee
| Author | SHA1 | Date | |
|---|---|---|---|
| b0e6236cee | |||
| b367687113 |
1
Gemfile
1
Gemfile
@@ -4,7 +4,6 @@ gem 'quickbooks-ruby'
|
|||||||
gem 'oauth2'
|
gem 'oauth2'
|
||||||
gem 'roxml'
|
gem 'roxml'
|
||||||
gem 'will_paginate'
|
gem 'will_paginate'
|
||||||
gem 'rails-jquery-autocomplete'
|
|
||||||
gem 'jquery-ui-rails'
|
gem 'jquery-ui-rails'
|
||||||
gem 'rexml'
|
gem 'rexml'
|
||||||
gem 'combine_pdf'
|
gem 'combine_pdf'
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ class CustomersController < ApplicationController
|
|||||||
before_action :view_customer, except: [:new, :view]
|
before_action :view_customer, except: [:new, :view]
|
||||||
skip_before_action :verify_authenticity_token, :check_if_login_required, only: [: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)
|
def address_to_s(address)
|
||||||
return if address.nil?
|
return if address.nil?
|
||||||
|
|
||||||
@@ -62,6 +60,19 @@ class CustomersController < ApplicationController
|
|||||||
params.require(:customer).permit(:name, :email, :primary_phone, :mobile_phone, :phone_number, :notes)
|
params.require(:customer).permit(:name, :email, :primary_phone, :mobile_phone, :phone_number, :notes)
|
||||||
end
|
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
|
def create
|
||||||
@customer = Customer.new(allowed_params)
|
@customer = Customer.new(allowed_params)
|
||||||
@customer.save
|
@customer.save
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: 15px;">
|
<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_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_invoices), invoices_sync_path, class: 'button icon icon-reload' %>
|
||||||
<%= link_to t(:label_sync_now_estimate), estimates_sync_path, class: 'button icon icon-reload' %>
|
<%= link_to t(:label_sync_now_estimate), estimates_sync_path, class: 'button icon icon-reload' %>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
!function(t){t.fn.railsAutocomplete=function(e){var a=function(){this.railsAutoCompleter||(this.railsAutoCompleter=new t.railsAutocomplete(this))};if(void 0!==t.fn.on){if(!e)return;return t(document).on("focus",e,a)}return this.live("focus",a)},t.railsAutocomplete=function(t){var e=t;this.init(e)},t.railsAutocomplete.options={showNoMatches:!0,noMatchesLabel:"no existing match"},t.railsAutocomplete.fn=t.railsAutocomplete.prototype={railsAutocomplete:"0.0.1"},t.railsAutocomplete.fn.extend=t.railsAutocomplete.extend=t.extend,t.railsAutocomplete.fn.extend({init:function(e){function a(t){return t.split(e.delimiter)}function i(t){return a(t).pop().replace(/^\s+/,"")}e.delimiter=t(e).attr("data-delimiter")||null,e.min_length=t(e).attr("data-min-length")||t(e).attr("min-length")||2,e.append_to=t(e).attr("data-append-to")||null,e.autoFocus=t(e).attr("data-auto-focus")||!1,t(e).autocomplete({appendTo:e.append_to,autoFocus:e.autoFocus,delay:t(e).attr("delay")||0,source:function(a,r){var n=this.element[0],o={term:i(a.term)};t(e).attr("data-autocomplete-fields")&&t.each(t.parseJSON(t(e).attr("data-autocomplete-fields")),function(e,a){o[e]=t(a).val()}),t.getJSON(t(e).attr("data-autocomplete"),o,function(){var a={};t.extend(a,t.railsAutocomplete.options),t.each(a,function(i,r){if(a.hasOwnProperty(i)){var n=t(e).attr("data-"+i);a[i]=n?n:r}}),0==arguments[0].length&&t.inArray(a.showNoMatches,[!0,"true"])>=0&&(arguments[0]=[],arguments[0][0]={id:"",label:a.noMatchesLabel}),t(arguments[0]).each(function(a,i){var r={};r[i.id]=i,t(e).data(r)}),r.apply(null,arguments),t(n).trigger("railsAutocomplete.source",arguments)})},change:function(e,a){if(t(this).is("[data-id-element]")&&""!==t(t(this).attr("data-id-element")).val()&&(t(t(this).attr("data-id-element")).val(a.item?a.item.id:"").trigger("change"),t(this).attr("data-update-elements"))){var i=t.parseJSON(t(this).attr("data-update-elements")),r=a.item?t(this).data(a.item.id.toString()):{};if(i&&""===t(i.id).val())return;for(var n in i){var o=t(i[n]);o.is(":checkbox")?null!=r[n]&&o.prop("checked",r[n]):o.val(a.item?r[n]:"").trigger("change")}}},search:function(){var t=i(this.value);return t.length<e.min_length?!1:void 0},focus:function(){return!1},select:function(i,r){if(r.item.value=r.item.value.toString(),-1!=r.item.value.toLowerCase().indexOf("no match")||-1!=r.item.value.toLowerCase().indexOf("too many results"))return t(this).trigger("railsAutocomplete.noMatch",r),!1;var n=a(this.value);if(n.pop(),n.push(r.item.value),null!=e.delimiter)n.push(""),this.value=n.join(e.delimiter);else if(this.value=n.join(""),t(this).attr("data-id-element")&&t(t(this).attr("data-id-element")).val(r.item.id).trigger("change"),t(this).attr("data-update-elements")){var o=r.item,l=-1!=r.item.value.indexOf("Create New")?!0:!1,u=t.parseJSON(t(this).attr("data-update-elements"));for(var s in u)"checkbox"===t(u[s]).attr("type")?o[s]===!0||1===o[s]?t(u[s]).attr("checked","checked"):t(u[s]).removeAttr("checked"):l&&o[s]&&-1==o[s].indexOf("Create New")||!l?t(u[s]).val(o[s]).trigger("change"):t(u[s]).val("").trigger("change")}var c=this.value;return t(this).bind("keyup.clearId",function(){t.trim(t(this).val())!=t.trim(c)&&(t(t(this).attr("data-id-element")).val("").trigger("change"),t(this).unbind("keyup.clearId"))}),t(e).trigger("railsAutocomplete.select",r),!1}}),t(e).trigger("railsAutocomplete.init")}}),t(document).ready(function(){t("input[data-autocomplete]").railsAutocomplete("input[data-autocomplete]")})}(jQuery);
|
|
||||||
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
|
#manual sync
|
||||||
get 'qbo/sync', to: 'qbo#sync'
|
get 'qbo/sync', to: 'qbo#sync'
|
||||||
get 'customers/sync', to: 'customers#sync'
|
|
||||||
get 'invoices/sync', to: 'invoices#sync'
|
get 'invoices/sync', to: 'invoices#sync'
|
||||||
get 'estimates/sync', to: 'estimates#sync'
|
get 'estimates/sync', to: 'estimates#sync'
|
||||||
get 'employees/sync', to: 'employees#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'
|
get 'filter_invoices_by_customer' => 'customers#filter_invoices_by_customer'
|
||||||
|
|
||||||
resources :customers do
|
resources :customers do
|
||||||
get :autocomplete_customer_name, on: :collection
|
collection do
|
||||||
|
get :autocomplete
|
||||||
|
get :sync
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -23,13 +23,13 @@ module RedmineQbo
|
|||||||
project = context[:project]
|
project = context[:project]
|
||||||
|
|
||||||
# Customer Name Text Box with database backed autocomplete
|
# Customer Name Text Box with database backed autocomplete
|
||||||
# onchange event will update the hidden customer_id field
|
# onchange event will update the hidden customer_id field
|
||||||
search_customer = f.autocomplete_field :customer,
|
search_customer = f.text_field :customer,
|
||||||
autocomplete_customer_name_customers_path,
|
placeholder: l(:field_customer),
|
||||||
selected: issue.customer,
|
class: "customer-name",
|
||||||
update_elements: {
|
autocomplete: "off",
|
||||||
id: '#issue_customer_id',
|
data: {
|
||||||
value: '#issue_customer'
|
autocomplete_url: "/customers/autocomplete"
|
||||||
}
|
}
|
||||||
|
|
||||||
# We need to handle 3 cases for the onchange event of the customer name field:
|
# 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 = {})
|
def view_layouts_base_html_head(context = {})
|
||||||
safe_join([
|
safe_join([
|
||||||
javascript_include_tag( 'application.js', plugin: :redmine_qbo),
|
javascript_include_tag( 'application.js', plugin: :redmine_qbo),
|
||||||
javascript_include_tag( 'autocomplete-rails.js', plugin: :redmine_qbo),
|
javascript_include_tag( 'autocomplete.js', plugin: :redmine_qbo),
|
||||||
javascript_include_tag( 'checkbox_controller.js', plugin: :redmine_qbo)
|
javascript_include_tag( 'checkbox_controller.js', plugin: :redmine_qbo),
|
||||||
|
stylesheet_link_tag( 'autocomplete', plugin: :redmine_qbo)
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user