Part Picker
authorSven Schöling <s.schoeling@linet-services.de>
Fri, 17 May 2013 12:14:59 +0000 (14:14 +0200)
committerSven Schöling <s.schoeling@linet-services.de>
Thu, 11 Jul 2013 09:08:11 +0000 (11:08 +0200)
14 files changed:
SL/Controller/Part.pm
SL/DB/Manager/Part.pm
SL/Form.pm
SL/Presenter.pm
SL/Presenter/Part.pm [new file with mode: 0644]
SL/Template/Plugin/L.pm
css/kivitendo/main.css
css/lx-office-erp/main.css
js/autocomplete_part.js [new file with mode: 0644]
js/common.js
locale/de/all
templates/webpages/part/_part_picker_result.html [new file with mode: 0644]
templates/webpages/part/part_picker_search.html [new file with mode: 0644]
templates/webpages/part/test_page.html [new file with mode: 0644]

index 6f3a136..346a544 100644 (file)
@@ -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;
index e238346..159f599 100644 (file)
@@ -85,6 +85,16 @@ SQL
   return %qty_by_id;
 }
 
+sub _sort_spec {
+  (
+    default  => [ 'partnumber', 1 ],
+    columns  => {
+      SIMPLE => 'ALL',
+    },
+    nulls    => {},
+  );
+}
+
 1;
 __END__
 
index be73c85..874aace 100644 (file)
@@ -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";
index ade1412..e7ad009 100644 (file)
@@ -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 (file)
index 0000000..99a6f14
--- /dev/null
@@ -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;
index ce2fa2b..72cad2f 100644 (file)
@@ -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__
index 897e6e3..c14bfbd 100644 (file)
@@ -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;
+}
index 472e3c1..08e7a22 100644 (file)
@@ -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 (file)
index 0000000..f7fe0d5
--- /dev/null
@@ -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 <enter> 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 <tab> 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  = $('<span>').addClass('position-absolute');
+    var picker = $('<div>');
+    $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;
+    });
+  });
+})
index 71f1d31..b89a5b4 100644 (file)
@@ -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(); });
index 39d495e..4495cff 100755 (executable)
@@ -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&auml;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 (file)
index 0000000..0e65243
--- /dev/null
@@ -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 %]
+<div class='part_picker_part'>
+  <input type='hidden' class='part_picker_id' value='[% part.id %]'>
+  <input type='hidden' class='part_picker_partnumber' value='[% part.partnumber %]'>
+  <input type='hidden' class='part_picker_description' value='[% part.description %]'>
+  <span style='float:left'>[% part.partnumber | html %]</span>
+  <span style='float:right; font-weight:bold'>[% part.description | html %]</span>
+  <div style='clear:both;'></div>
+  [% 'Sellprice' | $T8 %]: [% part.sellprice_as_number | html %]
+</div>
+[%- END %]
+
+<div style='clear:both'></div>
+
+[% L.paginate_controls(target='#part_picker_result', selector='#part_picker_result') %]
+
+<script type='text/javascript'>
+  $('div.part_picker_part').each(function(){
+    $(this).click(function(){
+      var real_id = $('#part_picker_real_id').val();
+      var $dummy  = $('#' + real_id + '_name');
+      var $real   = $('#' + real_id);
+
+      $dummy.val($(this).children('input.part_picker_description').val());
+      $real.val($(this).children('input.part_picker_id').val());
+
+      $('#part_selection').jqmClose();
+
+      return true;
+    });
+  });
+</script>
diff --git a/templates/webpages/part/part_picker_search.html b/templates/webpages/part/part_picker_search.html
new file mode 100644 (file)
index 0000000..73e6271
--- /dev/null
@@ -0,0 +1,40 @@
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE T8 %]
+
+<h1>[% 'Part picker' | $T8 %]</h1>
+<div style='overflow:hidden'>
+
+[% 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) %]
+
+<div style='clear:both'></div>
+<div id='part_picker_result'></div>
+</div>
+
+<script type='text/javascript'>
+  var timer;
+  var update_results = function(){
+    var $type   = $('#[% FORM.real_id %]_type');
+    var $column = $('#[% FORM.real_id %]_column');
+    $.ajax({
+      url: 'controller.pl?action=Part/part_picker_result',
+      data: {
+        'filter.all:substr::ilike': function(){ var val = $('#part_picker_filter').val(); return val === undefined ? '' : val },
+        'filter.type': function(){ return $type.val() },
+        'column': function(){ return $column.val() },
+        'real_id': [% FORM.real_id.json %],
+      },
+      success: function(data){ $('#part_picker_result').html(data) }
+    });
+  };
+  $(function(){
+    $('#part_picker_filter').focus();
+    update_results();
+  });
+  $('#part_picker_filter').keypress(function (event){
+    window.clearTimeout(timer);
+    timer = window.setTimeout(update_results, 100);
+  });
+</script>
diff --git a/templates/webpages/part/test_page.html b/templates/webpages/part/test_page.html
new file mode 100644 (file)
index 0000000..3d28518
--- /dev/null
@@ -0,0 +1,11 @@
+[% USE L %]
+
+<h1>Waren Picker Testpage</h1>
+
+<br>
+Alle: <br>
+[% L.part_picker('part_id') %] <br>
+Nur Waren: <br>
+[% L.part_picker('part_id2', undef, type='part') %]<br>
+Nur Dienstleistungen: <br>
+[% L.part_picker('part_id3', undef, type='service') %]<br>