From f16c552035ab973a9aed4a1dc29d0e16be7ff541 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Sven=20Sch=C3=B6ling?= Date: Tue, 9 Sep 2014 19:06:30 +0200 Subject: [PATCH] CustomerVendor: Picker nach Art von PartPicker MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit - reinit_widgets fähig - Tab und Enter atomar - unterstützt onChange und set_item:CustomerVendorPicker trigger - unterstützt fat_set_item --- SL/Controller/CustomerVendor.pm | 134 +++++++++--- SL/DB/Customer.pm | 2 + SL/DB/Manager/Customer.pm | 12 +- SL/DB/Manager/Vendor.pm | 6 +- SL/DB/Vendor.pm | 2 + SL/Presenter/CustomerVendor.pm | 23 ++- SL/Template/Plugin/L.pm | 11 +- css/kivitendo/main.css | 1 + css/lx-office-erp/main.css | 1 + js/autocomplete_customer.js | 191 ++++++++++++++++-- js/kivi.js | 5 + .../webpages/customer_vendor/test_page.html | 52 +++++ 12 files changed, 379 insertions(+), 61 deletions(-) create mode 100644 templates/webpages/customer_vendor/test_page.html diff --git a/SL/Controller/CustomerVendor.pm b/SL/Controller/CustomerVendor.pm index d472be710..fa9d0000c 100644 --- a/SL/Controller/CustomerVendor.pm +++ b/SL/Controller/CustomerVendor.pm @@ -7,6 +7,7 @@ use SL::JSON; use SL::DBUtils; use SL::Helper::Flash; use SL::Locale::String; +use SL::Controller::Helper::GetModels; use SL::DB::Customer; use SL::DB::Vendor; @@ -23,6 +24,10 @@ use SL::DB::FollowUpLink; use SL::DB::History; use SL::DB::Currency; +use Rose::Object::MakeMethods::Generic ( + 'scalar --get_set_init' => [ qw(customer_models vendor_models) ], +); + # safety __PACKAGE__->run_before( sub { @@ -50,6 +55,7 @@ __PACKAGE__->run_before( '_load_customer_vendor', only => [ 'edit', + 'show', 'update', 'ajaj_get_shipto', 'ajaj_get_contact', @@ -88,6 +94,22 @@ sub action_edit { ); } +sub action_show { + my ($self) = @_; + + if ($::request->type eq 'json') { + my $cv_hash; + if (!$self->{cv}) { + # TODO error + } else { + $cv_hash = $self->{cv}->as_tree; + $cv_hash->{cvars} = $self->{cv}->cvar_as_hashref; + } + + $self->render(\ SL::JSON::to_json($cv_hash), { layout => 0, type => 'json', process => 0 }); + } +} + sub _save { my ($self) = @_; @@ -539,40 +561,64 @@ sub action_ajaj_get_contact { $self->render(\SL::JSON::to_json($data), { type => 'json', process => 0 }); } -sub action_ajaj_customer_autocomplete { +sub action_ajaj_autocomplete { my ($self, %params) = @_; - my $limit = $::form->{limit} || 20; - my $type = $::form->{type} || {}; - my $query = { ilike => '%'. $::form->{term} .'%' }; - - my @filter; - push( - @filter, - $::form->{column} ? ($::form->{column} => $query) : (or => [ customernumber => $query, name => $query ]) - ); + my ($model, $manager, $number, $matches); + + # first see if this is customer or vendor picking + if ($::form->{type} eq 'customer') { + $model = $self->customer_models; + $manager = 'SL::DB::Manager::Customer'; + $number = 'customernumber'; + } elsif ($::form->{type} eq 'vendor') { + $model = $self->vendor_models; + $manager = 'SL::DB::Manager::Vendor'; + $number = 'vendornumber'; + } else { + die "unknown type $::form->{type}"; + } - push @filter, (or => [ obsolete => undef, obsolete => 0 ]) if !$::form->{obsolete}; + # if someone types something, and hits enter, assume he entered the full name. + # if something matches, treat that as sole match + # unfortunately get_models can't do more than one per package atm, so we d it + # the oldfashioned way. + if ($::form->{prefer_exact}) { + my $exact_matches; + if (1 == scalar @{ $exact_matches = $manager->get_all( + query => [ + obsolete => 0, + or => [ + name => { ilike => $::form->{filter}{'all:substr:multi::ilike'} }, + $number => { ilike => $::form->{filter}{'all:substr:multi::ilike'} }, + ] + ], + limit => 2, + ) }) { + $matches = $exact_matches; + } + } - my $customers = SL::DB::Manager::Customer->get_all(query => [ @filter ], limit => $limit); - my $value_col = $::form->{column} || 'name'; + $matches //= $model->get; + + my @hashes = map { + +{ + value => $_->name, + label => $_->displayable_name, + id => $_->id, + $number => $_->$number, + name => $_->name, + type => $::form->{type}, + cvars => { map { ($_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }) } @{ $_->cvars_by_config } }, + } + } @{ $matches }; - my $data = [ - map( - { - { - value => $_->can($value_col)->($_), - label => $_->displayable_name, - id => $_->id, - customernumber => $_->customernumber, - name => $_->name, - } - } - @{$customers} - ) - ]; + $self->render(\ SL::JSON::to_json(\@hashes), { layout => 0, type => 'json', process => 0 }); +} - $self->render(\SL::JSON::to_json($data), { layout => 0, type => 'json' }); +sub action_test_page { + $::request->{layout}->add_javascripts('autocomplete_customer.js'); + $_[0]->render('customer_vendor/test_page'); } sub is_vendor { @@ -892,4 +938,36 @@ sub home_address_for_google_maps { return $address; } +sub init_customer_models { + my ($self) = @_; + + SL::Controller::Helper::GetModels->new( + controller => $self, + model => 'Customer', + sorted => { + _default => { + by => 'name', + dir => 1, + }, + name => t8('name'), + }, + ); +} + +sub init_vendor_models { + my ($self) = @_; + + SL::Controller::Helper::GetModels->new( + controller => $self, + model => 'Vendor', + sorted => { + _default => { + by => 'name', + dir => 1, + }, + name => t8('name'), + }, + ); +} + 1; diff --git a/SL/DB/Customer.pm b/SL/DB/Customer.pm index 6f37c5221..e3e30ddff 100644 --- a/SL/DB/Customer.pm +++ b/SL/DB/Customer.pm @@ -2,6 +2,8 @@ package SL::DB::Customer; use strict; +use Rose::DB::Object::Helpers qw(as_tree); + use SL::DB::MetaSetup::Customer; use SL::DB::Manager::Customer; use SL::DB::Helper::TransNumberGenerator; diff --git a/SL/DB/Manager/Customer.pm b/SL/DB/Manager/Customer.pm index 0d62fe8f2..30c32a9d3 100644 --- a/SL/DB/Manager/Customer.pm +++ b/SL/DB/Manager/Customer.pm @@ -3,14 +3,22 @@ package SL::DB::Manager::Customer; use strict; use SL::DB::Helper::Manager; -use base qw(SL::DB::Helper::Manager); - use SL::DB::Helper::Sorted; +use SL::DB::Helper::Paginated; +use SL::DB::Helper::Filtered; +use base qw(SL::DB::Helper::Manager); sub object_class { 'SL::DB::Customer' } __PACKAGE__->make_manager_methods; +__PACKAGE__->add_filter_specs( + all => sub { + my ($key, $value, $prefix) = @_; + return or => [ map { $prefix . $_ => $value } qw(customernumber name) ] + } +); + sub _sort_spec { return ( default => [ 'name', 1 ], columns => { SIMPLE => 'ALL', diff --git a/SL/DB/Manager/Vendor.pm b/SL/DB/Manager/Vendor.pm index 838d12b97..af4b02993 100644 --- a/SL/DB/Manager/Vendor.pm +++ b/SL/DB/Manager/Vendor.pm @@ -3,10 +3,10 @@ package SL::DB::Manager::Vendor; use strict; use SL::DB::Helper::Manager; -use base qw(SL::DB::Helper::Manager); - -use SL::DB::Helper::Filtered; use SL::DB::Helper::Sorted; +use SL::DB::Helper::Paginated; +use SL::DB::Helper::Filtered; +use base qw(SL::DB::Helper::Manager); sub object_class { 'SL::DB::Vendor' } diff --git a/SL/DB/Vendor.pm b/SL/DB/Vendor.pm index e3b994c79..ebbe8125f 100644 --- a/SL/DB/Vendor.pm +++ b/SL/DB/Vendor.pm @@ -2,6 +2,8 @@ package SL::DB::Vendor; use strict; +use Rose::DB::Object::Helpers qw(as_tree); + use SL::DB::MetaSetup::Vendor; use SL::DB::Manager::Vendor; use SL::DB::Helper::TransNumberGenerator; diff --git a/SL/Presenter/CustomerVendor.pm b/SL/Presenter/CustomerVendor.pm index 209431fd3..2bf5ebc3c 100644 --- a/SL/Presenter/CustomerVendor.pm +++ b/SL/Presenter/CustomerVendor.pm @@ -5,7 +5,7 @@ use strict; use parent qw(Exporter); use Exporter qw(import); -our @EXPORT = qw(customer vendor); +our @EXPORT = qw(customer vendor customer_vendor_picker); use Carp; @@ -36,6 +36,27 @@ sub _customer_vendor { return $self->escaped_text($text); } +sub customer_vendor_picker { + my ($self, $name, $value, %params) = @_; + + $value = SL::DB::Manager::Customer->find_by(id => $value) if $value && !ref $value; + my $id = delete($params{id}) || $self->name_to_id($name); + my $fat_set_item = delete $params{fat_set_item}; + + my @classes = $params{class} ? ($params{class}) : (); + push @classes, 'customer_vendor_autocomplete'; + push @classes, 'customer-vendor-picker-fat-set-item' if $fat_set_item; + + my $ret = + $self->input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => "@classes", type => 'hidden', id => $id) . + join('', map { $params{$_} ? $self->input_tag("", delete $params{$_}, id => "${id}_${_}", type => 'hidden') : '' } qw(type)) . + $self->input_tag("", (ref $value && $value->can('name')) ? $value->name : '', id => "${id}_name", %params); + + $::request->presenter->need_reinit_widgets($id); + + $self->html_tag('span', $ret, class => 'customer_vendor_picker'); +} + 1; __END__ diff --git a/SL/Template/Plugin/L.pm b/SL/Template/Plugin/L.pm index 88cf4d21e..2e2ae9f03 100644 --- a/SL/Template/Plugin/L.pm +++ b/SL/Template/Plugin/L.pm @@ -67,6 +67,7 @@ sub input_tag { return _call_presenter('input_tag', @_); } sub truncate { return _call_presenter('truncate', @_); } sub simple_format { return _call_presenter('simple_format', @_); } sub part_picker { return _call_presenter('part_picker', @_); } +sub customer_vendor_picker { return _call_presenter('customer_vendor_picker', @_); } sub _set_id_attribute { my ($attributes, $name, $unique) = @_; @@ -241,16 +242,6 @@ sub date_tag { ); } -sub customer_picker { - my ($self, $name, $value, %params) = _hashify(3, @_); - my $name_e = _H($name); - - $::request->{layout}->add_javascripts('autocomplete_customer.js'); - - $self->hidden_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => 'customer_autocomplete') . - $self->input_tag('', (ref $value && $value->can('name')) ? $value->name : '', id => $self->name_to_id("$name_e\_name"), %params); -} - # simple version with select_tag sub vendor_selector { my ($self, $name, $value, %params) = _hashify(3, @_); diff --git a/css/kivitendo/main.css b/css/kivitendo/main.css index 9b87c4417..c11c5fe5a 100644 --- a/css/kivitendo/main.css +++ b/css/kivitendo/main.css @@ -404,6 +404,7 @@ label { padding-right: 16px; } +.customer-vendor-picker-undefined, .partpicker-undefined { color: red; font-style: italic; diff --git a/css/lx-office-erp/main.css b/css/lx-office-erp/main.css index e8814bb85..09ba80409 100644 --- a/css/lx-office-erp/main.css +++ b/css/lx-office-erp/main.css @@ -455,6 +455,7 @@ label { .part_picker { padding-right: 16px; } +.customer-vendor-picker-undefined, .partpicker-undefined { color: red; font-style: italic; diff --git a/js/autocomplete_customer.js b/js/autocomplete_customer.js index 9359ee536..e3c1c7d42 100644 --- a/js/autocomplete_customer.js +++ b/js/autocomplete_customer.js @@ -1,25 +1,182 @@ -$(function(){ - $('input.customer_autocomplete').each(function(i,real){ - var dummy = $('#' + real.id + '_name'); - $(dummy).autocomplete({ - source: function(req, rsp) { +namespace('kivi', function(k){ + k.CustomerVendorPicker = function($real, options) { + // short circuit in case someone double inits us + if ($real.data("customer_vendor_picker")) + return $real.data("customer_vendor_picker"); + + var KEY = { + ESCAPE: 27, + ENTER: 13, + TAB: 9, + LEFT: 37, + RIGHT: 39, + PAGE_UP: 33, + PAGE_DOWN: 34, + }; + var CLASSES = { + PICKED: 'customer-vendor-picker-picked', + UNDEFINED: 'customer-vendor-picker-undefined', + FAT_SET_ITEM: 'customer-vendor-picker-fat-set-item', + } + var o = $.extend({ + limit: 20, + delay: 50, + fat_set_item: $real.hasClass(CLASSES.FAT_SET_ITEM), + }, options); + var STATES = { + PICKED: CLASSES.PICKED, + UNDEFINED: CLASSES.UNDEFINED + } + var real_id = $real.attr('id'); + var $dummy = $('#' + real_id + '_name'); + var $type = $('#' + real_id + '_type'); + var $unit = $('#' + real_id + '_unit'); + var state = STATES.PICKED; + var last_real = $real.val(); + var last_dummy = $dummy.val(); + var timer; + + function ajax_data(term) { + var data = { + 'filter.all:substr:multi::ilike': term, + 'filter.obsolete': 0, + current: $real.val(), + type: $type.val(), + }; + + return data; + } + + function set_item (item) { + if (item.id) { + $real.val(item.id); + // autocomplete ui has name, ajax items have description + $dummy.val(item.name ? item.name : item.description); + } else { + $real.val(''); + $dummy.val(''); + } + state = STATES.PICKED; + last_real = $real.val(); + last_dummy = $dummy.val(); + last_unverified_dummy = $dummy.val(); + $real.trigger('change'); + + if (o.fat_set_item && item.id) { $.ajax({ - url: 'controller.pl?action=CustomerVendor/ajaj_customer_autocomplete', - dataType: "json", - data: { - term: req.term, - current: function() { real.val }, - obsolete: 0, + url: 'controller.pl?action=CustomerVendor/show.json', + data: { id: item.id, db: item.type }, + success: function(rsp) { + $real.trigger('set_item:CustomerVendorPicker', rsp); }, - success: function (data){ rsp(data) } }); + } else { + $real.trigger('set_item:CustomerVendorPicker', item); + } + annotate_state(); + } + + function make_defined_state () { + if (state == STATES.PICKED) { + annotate_state(); + return true + } else if (state == STATES.UNDEFINED && $dummy.val() == '') + set_item({}) + else { + last_unverified_dummy = $dummy.val(); + set_item({ id: last_real, name: last_dummy }) + } + annotate_state(); + } + + function annotate_state () { + if (state == STATES.PICKED) + $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED); + else if (state == STATES.UNDEFINED && $dummy.val() == '') + $dummy.removeClass(STATES.UNDEFINED).addClass(STATES.PICKED); + else { + last_unverified_dummy = $dummy.val(); + $dummy.addClass(STATES.UNDEFINED).removeClass(STATES.PICKED); + } + } + + $dummy.autocomplete({ + source: function(req, rsp) { + $.ajax($.extend(o, { + url: 'controller.pl?action=CustomerVendor/ajaj_autocomplete', + dataType: "json", + data: ajax_data(req.term), + success: function (data){ rsp(data) } + })); }, - limit: 20, - delay: 50, select: function(event, ui) { - $(real).val(ui.item.id); - $(dummy).val(ui.item.name); + set_item(ui.item); }, }); - }); + /* In case users are impatient and want to skip ahead: + * Capture key events and check if it's a unique hit. + * If it is, go ahead and assume it was selected. If it wasn't don't do + * anything so that autocompletion kicks in. For don't prevent + * propagation. It would be nice to catch it, but javascript is too stupid + * to fire a tab event later on, so we'd have to reimplement the "find + * next active element in tabindex order and focus it". + */ + /* note: + * event.which does not contain tab events in keypressed in firefox but will report 0 + * chrome does not fire keypressed at all on tab or escape + */ + $dummy.keydown(function(event){ + if (event.which == KEY.ENTER || event.which == KEY.TAB) { + // if string is empty assume they want to delete + if ($dummy.val() == '') { + set_item({}); + return true; + } else if (state == STATES.PICKED) { + return true; + } + if (event.which == KEY.TAB) event.preventDefault(); + $.ajax({ + url: 'controller.pl?action=CustomerVendor/ajaj_autocomplete', + dataType: "json", + data: $.extend( ajax_data($dummy.val()), { prefer_exact: 1 } ), + success: function (data) { + if (data.length == 1) { + set_item(data[0]); + if (event.which == KEY.ENTER) + $('#update_button').click(); + } else { + } + annotate_state(); + } + }); + if (event.which == KEY.ENTER) + return false; + } else { + state = STATES.UNDEFINED; + } + }); + + $dummy.blur(function(){ + window.clearTimeout(timer); + timer = window.setTimeout(annotate_state, 100); + }); + + // now add a picker div after the original input + var pp = { + real: function() { return $real }, + dummy: function() { return $dummy }, + type: function() { return $type }, + set_item: set_item, + reset: make_defined_state, + is_defined_state: function() { return state == STATES.PICKED }, + } + $real.data('customer_vendor_picker', pp); + return pp; + } +}); + +$(function(){ + $('input.customer_vendor_autocomplete').each(function(i,real){ + kivi.CustomerVendorPicker($(real)); + }) }); diff --git a/js/kivi.js b/js/kivi.js index 17c0ab056..5ae742c38 100644 --- a/js/kivi.js +++ b/js/kivi.js @@ -127,6 +127,11 @@ namespace("kivi", function(ns) { kivi.PartPicker($(elt)); }); + if (ns.CustomerVendorPicker) + ns.run_once_for('input.customer_vendor_autocomplete', 'customer_vendor_picker', function(elt) { + kivi.CustomerVendorPicker($(elt)); + }); + var func = kivi.get_function_by_name('local_reinit_widgets'); if (func) func(); diff --git a/templates/webpages/customer_vendor/test_page.html b/templates/webpages/customer_vendor/test_page.html new file mode 100644 index 000000000..41652b8a0 --- /dev/null +++ b/templates/webpages/customer_vendor/test_page.html @@ -0,0 +1,52 @@ +[% USE L %] + +

Customer Vendor Autocomplete Testpage

+ +
+Customer: with preselected id 822
+[% L.customer_vendor_picker('customer_id', 822, type='customer') %]
+ +

+Vendor:
+[% L.customer_vendor_picker('vendor_id', '', type='vendor') %]
+ +

+customer with fat change
+[% L.customer_vendor_picker('customer_id2', '', type='customer', fat_set_item=1) %]
+
id from change
+
greeting from fat change
+ +

+fat vendor with change
+[% L.customer_vendor_picker('vendor_id2', '', type='vendor', fat_set_item=1) %]
+
id from change
+
greeting from fat change
+ +

+this one will be reinit_widget after 4s:
+ + + + + + +

+this shouold have three '-' before and after touching:
+---[% L.customer_vendor_picker('vendor5_id', '', type='vendor') %]--- + + + + -- 2.20.1