From 2504ebe1552b488aa48999535963cc0e6bc3d4ae Mon Sep 17 00:00:00 2001 From: =?utf8?q?Sven=20Sch=C3=B6ling?= Date: Fri, 17 May 2013 14:14:59 +0200 Subject: [PATCH] Part Picker --- SL/Controller/Part.pm | 45 +++++++++ SL/DB/Manager/Part.pm | 10 ++ SL/Form.pm | 2 +- SL/Presenter.pm | 1 + SL/Presenter/Part.pm | 21 +++++ SL/Template/Plugin/L.pm | 12 +++ css/kivitendo/main.css | 37 ++++++++ css/lx-office-erp/main.css | 36 +++++++ js/autocomplete_part.js | 94 +++++++++++++++++++ js/common.js | 8 ++ locale/de/all | 1 + .../webpages/part/_part_picker_result.html | 43 +++++++++ .../webpages/part/part_picker_search.html | 40 ++++++++ templates/webpages/part/test_page.html | 11 +++ 14 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 SL/Presenter/Part.pm create mode 100644 js/autocomplete_part.js create mode 100644 templates/webpages/part/_part_picker_result.html create mode 100644 templates/webpages/part/part_picker_search.html create mode 100644 templates/webpages/part/test_page.html diff --git a/SL/Controller/Part.pm b/SL/Controller/Part.pm index 6f3a136c9..346a5446d 100644 --- a/SL/Controller/Part.pm +++ b/SL/Controller/Part.pm @@ -3,11 +3,39 @@ package SL::Controller::Part; use strict; use parent qw(SL::Controller::Base); +use Clone qw(clone); use SL::DB::Part; +use SL::Controller::Helper::GetModels; +use SL::Controller::Helper::Filtered; +use SL::Controller::Helper::Sorted; +use SL::Controller::Helper::Paginated; +use SL::Controller::Helper::Filtered; +use SL::Locale::String qw(t8); + +use Rose::Object::MakeMethods::Generic ( + 'scalar --get_set_init' => [ qw(parts) ], +); # safety __PACKAGE__->run_before(sub { $::auth->assert('part_service_assembly_edit') }); +__PACKAGE__->make_filtered( + ONLY => [ qw(part_picker_search part_picker_result) ], + LAUNDER_TO => 'filter', +); +__PACKAGE__->make_paginated( + ONLY => [ qw(part_picker_search part_picker_result) ], +); + +__PACKAGE__->make_sorted( + ONLY => [ qw(part_picker_search part_picker_result) ], + + DEFAULT_BY => 'partnumber', + DEFAULT_DIR => 1, + + partnumber => t8('Partnumber'), +); + sub action_ajax_autocomplete { my ($self, %params) = @_; @@ -26,5 +54,22 @@ sub action_ajax_autocomplete { $self->render('part/ajax_autocomplete', { layout => 0, type => 'json' }); } +sub action_test_page { + $::request->{layout}->add_javascripts('autocomplete_part.js'); + + $_[0]->render('part/test_page'); +} + +sub action_part_picker_search { + $_[0]->render('part/part_picker_search', { layout => 0 }, parts => $_[0]->parts); +} + +sub action_part_picker_result { + $_[0]->render('part/_part_picker_result', { layout => 0 }); +} + +sub init_parts { + $_[0]->get_models; +} 1; diff --git a/SL/DB/Manager/Part.pm b/SL/DB/Manager/Part.pm index e23834625..159f59998 100644 --- a/SL/DB/Manager/Part.pm +++ b/SL/DB/Manager/Part.pm @@ -85,6 +85,16 @@ SQL return %qty_by_id; } +sub _sort_spec { + ( + default => [ 'partnumber', 1 ], + columns => { + SIMPLE => 'ALL', + }, + nulls => {}, + ); +} + 1; __END__ diff --git a/SL/Form.pm b/SL/Form.pm index be73c8554..874aacee6 100644 --- a/SL/Form.pm +++ b/SL/Form.pm @@ -468,7 +468,7 @@ sub header { $layout->use_javascript("$_.js") for (qw( jquery jquery-ui jquery.cookie jqModal jquery.checkall jquery.download - common part_selection switchmenuframe + common part_selection switchmenuframe autocomplete_part ), "jquery/ui/i18n/jquery.ui.datepicker-$::myconfig{countrycode}"); $self->{favicon} ||= "favicon.ico"; diff --git a/SL/Presenter.pm b/SL/Presenter.pm index ade14123b..e7ad00976 100644 --- a/SL/Presenter.pm +++ b/SL/Presenter.pm @@ -12,6 +12,7 @@ use SL::Presenter::DeliveryOrder; use SL::Presenter::EscapedText; use SL::Presenter::Invoice; use SL::Presenter::Order; +use SL::Presenter::Part; use SL::Presenter::Project; use SL::Presenter::Record; use SL::Presenter::SepaExport; diff --git a/SL/Presenter/Part.pm b/SL/Presenter/Part.pm new file mode 100644 index 000000000..99a6f14e9 --- /dev/null +++ b/SL/Presenter/Part.pm @@ -0,0 +1,21 @@ +package SL::Presenter::Part; + +use strict; + +use Exporter qw(import); +our @EXPORT = qw(part_picker); + +sub part_picker { + my ($self, $name, $value, %params) = @_; + my $name_e = $self->escape($name); + + my $ret = + $self->input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => 'part_autocomplete', type => 'hidden') . + $self->input_tag("", delete $params{type}, id => $self->name_to_id("$name_e\_type"), type => 'hidden') . + $self->input_tag("", (ref $value && $value->can('description')) ? $value->description : '', id => $self->name_to_id("$name_e\_name"), %params) . + $self->input_tag("", delete $params{column}, id => $self->name_to_id("$name_e\_column"), type => 'hidden'); + + $self->html_tag('span', $ret, class => 'part_picker'); +} + +1; diff --git a/SL/Template/Plugin/L.pm b/SL/Template/Plugin/L.pm index ce2fa2b29..72cad2fa4 100644 --- a/SL/Template/Plugin/L.pm +++ b/SL/Template/Plugin/L.pm @@ -491,6 +491,18 @@ sub paginate_controls { return SL::Presenter->get->render('common/paginate', %template_params); } +sub part_picker { + my ($self, $name, $value, %params) = _hashify(3, @_); + my $name_e = _H($name); + + my $ret = $self->hidden_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => 'part_autocomplete') . + $self->hidden_tag("", delete $params{type}, id => $self->name_to_id("$name_e\_type")) . + $self->input_tag("", (ref $value && $value->can('description')) ? $value->description : '', id => $self->name_to_id("$name_e\_name"), %params) . + $self->hidden_tag("", delete $params{column}, id => $self->name_to_id("$name_e\_column")); + + $self->html_tag('span', $ret, class => 'part_picker'); +} + 1; __END__ diff --git a/css/kivitendo/main.css b/css/kivitendo/main.css index 897e6e3fb..c14bfbd05 100644 --- a/css/kivitendo/main.css +++ b/css/kivitendo/main.css @@ -380,3 +380,40 @@ label { .small-text { font-size: 0.75em; } + +.float-left { + float: left; +} + +.block-context { + overflow: hidden; +} + +.position-relative { + position: relative; +} + +.position-absolute { + position: absolute; +} + +div.part_picker_part { + float:left; width: 350px; + padding: 5px; + margin: 5px; + overflow:hidden; + border: 1px; + border-color: darkgray; + border-style: solid; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + background-color: whitesmoke; + cursor: pointer; +} + +div.part_picker_part:hover { + background-color: #CCCCCC; + color: #FE5F14; + border-color: gray; +} diff --git a/css/lx-office-erp/main.css b/css/lx-office-erp/main.css index 472e3c178..08e7a22ce 100644 --- a/css/lx-office-erp/main.css +++ b/css/lx-office-erp/main.css @@ -432,3 +432,39 @@ label { .small-text { font-size: 0.75em; } + +.float-left { + float: left; +} + +.block-context { + overflow: hidden; +} + +.position-relative { + position: relative; +} + +.position-absolute { + position: absolute; +} + +div.part_picker_part { + float:left; width: 350px; + padding: 5px; + margin: 5px; + overflow:hidden; + border: 1px; + border-color: darkgray; + border-style: solid; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + background-color: whitesmoke; + cursor: pointer; +} + +div.part_picker_part:hover { + background-color: lightgray; + border-color: gray; +} diff --git a/js/autocomplete_part.js b/js/autocomplete_part.js new file mode 100644 index 000000000..f7fe0d520 --- /dev/null +++ b/js/autocomplete_part.js @@ -0,0 +1,94 @@ +$(function(){ + $('input.part_autocomplete').each(function(i,real){ + var $dummy = $('#' + real.id + '_name'); + var $type = $('#' + real.id + '_type'); + var $column = $('#' + real.id + '_column'); + $dummy.autocomplete({ + source: function(req, rsp) { + $.ajax({ + url: 'controller.pl?action=Part/ajax_autocomplete', + dataType: "json", + data: { + term: req.term, + type: function() { return $type.val() }, + column: function() { return $column.val()===undefined ? '' : $column.val() }, + current: function() { return real.value }, + obsolete: 0, + }, + success: function (data){ rsp(data) } + }); + }, + limit: 20, + delay: 50, + select: function(event, ui) { + $(real).val(ui.item.id); + $dummy.val(ui.item.name); + }, + }); + /* 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". + */ + $dummy.keypress(function(event){ + if (event.keyCode == 13 || event.keyCode == 9) { // enter or tab or tab + // if string is empty asume they want to delete + if ($dummy.val() == '') { + $(real).val(''); + return true; + } + $.ajax({ + url: 'controller.pl?action=Part/ajax_autocomplete', + dataType: "json", + data: { + term: $dummy.val(), + type: function() { return $type.val() }, + column: function() { return $column.val()===undefined ? '' : $column.val() }, + current: function() { return real.value }, + obsolete: 0, + }, + success: function (data){ + // only one + if (data.length == 1) { + $(real).val(data[0].id); + $dummy.val(data[0].description); + if (event.keyCode == 13) + $('#update_button').click(); + } + } + }); + if (event.keyCode == 13) + return false; + }; + }); + + $dummy.blur(function(){ + if ($dummy.val() == '') + $(real).val(''); + }) + + // now add a picker div after the original input + var pcont = $('').addClass('position-absolute'); + var picker = $('
'); + $dummy.after(pcont); + pcont.append(picker); + picker.addClass('icon16 CRM--Schnellsuche').click(function(){ + open_jqm_window({ + url: 'controller.pl', + data: { + action: 'Part/part_picker_search', + real_id: function() { return $(real).attr('id') }, + 'filter.all:substr::ilike': function(){ return $dummy.val() }, + 'filter.type': function(){ return $type.val() }, + 'column': function(){ return $column.val() }, + 'real_id': function() { return real.id }, + }, + id: 'part_selection', + }); + return true; + }); + }); +}) diff --git a/js/common.js b/js/common.js index 71f1d3104..b89a5b41e 100644 --- a/js/common.js +++ b/js/common.js @@ -185,6 +185,14 @@ function open_jqm_window(params) { return true; } +function close_jqm_window(params) { + params = params || { }; + var url = params.url; + var id = params.id ? params.id : 'jqm_popup_dialog'; + + $('#' + id).jqmClose()(); +} + $(document).ready(function () { // initialize all jQuery UI tab elements: $(".tabwidget").each(function(idx, element) { $(element).tabs(); }); diff --git a/locale/de/all b/locale/de/all index 39d495ed8..4495cffd7 100755 --- a/locale/de/all +++ b/locale/de/all @@ -1422,6 +1422,7 @@ $self->{texts} = { 'Part Notes' => 'Bemerkungen', 'Part Number' => 'Artikelnummer', 'Part Number missing!' => 'Artikelnummer fehlt!', + 'Part picker' => 'Artikelauswahl', 'Partnumber' => 'Artikelnummer', 'Partnumber must not be set to empty!' => 'Die Artikelnummer darf nicht auf leer geändert werden.', 'Partnumber not unique!' => 'Artikelnummer bereits vorhanden!', diff --git a/templates/webpages/part/_part_picker_result.html b/templates/webpages/part/_part_picker_result.html new file mode 100644 index 000000000..0e6524380 --- /dev/null +++ b/templates/webpages/part/_part_picker_result.html @@ -0,0 +1,43 @@ +[%- USE T8 %] +[%- USE HTML %] +[%- USE L %] +[%- USE LxERP %] + +[%# L.dump(SELF.parts) %] + +[% FOREACH part = SELF.parts %] + [% PROCESS part_block %] +[% END %] + +[%- BLOCK part_block %] +
+ + + + [% part.partnumber | html %] + [% part.description | html %] +
+ [% 'Sellprice' | $T8 %]: [% part.sellprice_as_number | html %] +
+[%- END %] + +
+ +[% L.paginate_controls(target='#part_picker_result', selector='#part_picker_result') %] + + diff --git a/templates/webpages/part/part_picker_search.html b/templates/webpages/part/part_picker_search.html new file mode 100644 index 000000000..73e627170 --- /dev/null +++ b/templates/webpages/part/part_picker_search.html @@ -0,0 +1,40 @@ +[%- USE HTML %] +[%- USE L %] +[%- USE LxERP %] +[%- USE T8 %] + +

[% 'Part picker' | $T8 %]

+
+ +[% L.input_tag('part_picker_filter', SELF.filter.all_substr__ilike, class='part_picker_filter') %] +[% L.hidden_tag('part_picker_real_id', FORM.real_id) %] + +
+
+
+ + diff --git a/templates/webpages/part/test_page.html b/templates/webpages/part/test_page.html new file mode 100644 index 000000000..3d285185e --- /dev/null +++ b/templates/webpages/part/test_page.html @@ -0,0 +1,11 @@ +[% USE L %] + +

Waren Picker Testpage

+ +
+Alle:
+[% L.part_picker('part_id') %]
+Nur Waren:
+[% L.part_picker('part_id2', undef, type='part') %]
+Nur Dienstleistungen:
+[% L.part_picker('part_id3', undef, type='service') %]
-- 2.20.1