Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 6c630204

Von Sven Schöling vor fast 9 Jahren hinzugefügt

  • ID 6c63020409f486043d63c3a324db96a4a162ff67
  • Vorgänger 15b67fd5
  • Nachfolger a97ea1ce

TopQuickSearch: erste version

Unterschiede anzeigen:

SL/Controller/TopQuickSearch.pm
1
package SL::Controller::TopQuickSearch;
2

  
3
use strict;
4
use parent qw(SL::Controller::Base);
5

  
6
use SL::ClientJS;
7
use SL::JSON;
8
use SL::Locale::String qw(t8);
9

  
10
use Rose::Object::MakeMethods::Generic (
11
 'scalar --get_set_init' => [ qw(module js) ],
12
);
13

  
14
my @available_modules = qw(
15
  SL::Controller::TopQuickSearch::Assembly
16
  SL::Controller::TopQuickSearch::Contact
17
  SL::Controller::TopQuickSearch::GLTransaction
18
);
19
my %modules_by_name;
20

  
21
sub action_query_autocomplete {
22
  my ($self) = @_;
23

  
24
  my $hashes = $self->module->query_autocomplete;
25

  
26
  $self->render(\ SL::JSON::to_json($hashes), { layout => 0, type => 'json', process => 0 });
27
}
28

  
29
sub action_select_autocomplete {
30
  my ($self) = @_;
31

  
32
  my $redirect_url = $self->module->select_autocomplete;
33

  
34
  $self->js->redirect_to($redirect_url)->render;
35
}
36

  
37
sub action_do_search {
38
  my ($self) = @_;
39

  
40
  my $redirect_url = $self->module->do_search;
41

  
42
  if ($redirect_url) {
43
    $self->js->redirect_to($redirect_url)
44
  }
45

  
46
  $self->js->render;
47
}
48

  
49
sub available_modules {
50
  my ($self) = @_;
51

  
52
  $self->require_modules;
53

  
54
  map { $_->new } @available_modules;
55
}
56

  
57
sub active_modules {
58
  grep {
59
    $::auth->assert($_->auth, 1)
60
  } $_[0]->available_modules
61
}
62

  
63
sub init_module {
64
  my ($self) = @_;
65

  
66
  $self->require_modules;
67

  
68
  die t8('Need module') unless $::form->{module};
69

  
70
  $::lxdebug->dump(0,  "modules", \%modules_by_name);
71

  
72
  die t8('Unknown module #1', $::form->{module}) unless my $class = $modules_by_name{$::form->{module}};
73

  
74
  $::lxdebug->dump(0,  "auth:", $class->auth);
75

  
76
  $::auth->assert($class->auth);
77

  
78
  return $class->new;
79
}
80

  
81
sub init_js {
82
  SL::ClientJS->new(controller => $_[0])
83
}
84

  
85
sub require_modules {
86
  my ($self) = @_;
87

  
88
  if (!$self->{__modules_required}) {
89
    for my $class (@available_modules) {
90
      eval "require $class" or die $@;
91
      $modules_by_name{ $class->name } = $class;
92
    }
93
    $self->{__modules_required} = 1;
94
  }
95
}
96

  
97
1;
98

  
99
__END__
100

  
101
=encoding utf-8
102

  
103
=head1 NAME
104

  
105
SL::Controller::TopQuickSearch - Framework for pluggable quicksearch fields in the layout top header.
106

  
107
=head1 SYNOPSIS
108

  
109
use SL::Controller::TopQuickSearch;
110
my $search = SL::Controller::TopQuickSearch->new;
111

  
112
# in layout
113
[%- FOREACH module = search.available_modules %]
114
<input type='text' id='top-search-[% module.name %]'>
115
[%- END %]
116

  
117
=head1 DESCRIPTION
118

  
119
This controller provides abstraction for different search plugins, and ensures
120
that all follow a common useability scheme.
121

  
122
Modules should be configurable, but currently are not. Diabling modules can be
123
done by removing them from available_modules.
124

  
125
=head1 BEHAVIOUR REQUIREMENTS
126

  
127
=over 4
128

  
129
=item *
130

  
131
A single text input field with the html5 placeholder containing a small
132
description of the target will be rendered from the plugin information.
133

  
134
=item *
135

  
136
On typing, the autocompletion must be enabled.
137

  
138
=item *
139

  
140
On C<Enter>, the search should redirect to an appropriate listing of matching
141
results.
142

  
143
If only one item matches the result, the plugin should instead redirect
144
directly to the matched item.
145

  
146
=item *
147

  
148
Search terms should accept the broadest possible matching, and if possible with
149
C<multi> parsing.
150

  
151
=item *
152

  
153
In case nothing is found, a visual indicator should be given, but no actual
154
redirect should occur.
155

  
156
=item *
157

  
158
Each search must check rights and must not present a backdoor into data that
159
the user should not see.
160

  
161
=back
162

  
163
=head1 INTERFACE
164

  
165
Plugins need to provide:
166

  
167
 - name
168
 - localized description for config
169
 - localized description for textfield
170
 - autocomplete callback
171
 - redirect callback
172

  
173
the frontend will only generate urls of the forms:
174
  action=TopQuickSearch/autocomplete&module=<module>&term=<term>
175
  action=TopQuickSearch/search&module=<module>&term=<term>
176

  
177
=head1 TODO
178

  
179
 - filter available searches with auth
180
 - toggling with cofiguration doesn't work yet
181

  
182
=head1 BUGS
183

  
184
None yet :)
185

  
186
=head1 AUTHOR
187

  
188
Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
189

  
190
=cut
SL/Controller/TopQuickSearch/Assembly.pm
1
package SL::Controller::TopQuickSearch::Assembly;
2

  
3
use strict;
4
use parent qw(Rose::Object);
5

  
6
use SL::Locale::String qw(t8);
7
use SL::DB::Part;
8
use SL::Controller::Helper::GetModels;
9
use SL::Controller::Base;
10

  
11
use Rose::Object::MakeMethods::Generic (
12
  'scalar --get_set_init' => [ qw(parts models part) ],
13
);
14

  
15
sub auth { 'part_service_assembly_edit' }
16

  
17
sub name { 'assembly' }
18

  
19
sub description_config { t8('Assemblies') }
20

  
21
sub description_field { t8('Assemblies') }
22

  
23
sub query_autocomplete {
24
  my ($self) = @_;
25

  
26
  my $objects = $self->models->get;
27

  
28
  [
29
    map {
30
     value       => $_->displayable_name,
31
     label       => $_->displayable_name,
32
     id          => $_->id,
33
    }, @$objects
34
  ];
35
}
36

  
37
sub select_autocomplete {
38
  redirect_to_part($::form->{id});
39
}
40

  
41
sub do_search {
42
  my ($self) = @_;
43

  
44
  my $objects = $self->models->get;
45

  
46
  return !@$objects     ? ()
47
       : @$objects == 1 ? redirect_to_part($objects->[0]->id)
48
       :                  redirect_to_search($::form->{term});
49
}
50

  
51
sub redirect_to_search {
52
  SL::Controller::Base->new->url_for(
53
    controller  => 'ic.pl',
54
    action      => 'generate_report',
55
    searchitems => 'assembly',
56
    all         => $_[0],
57
  );
58
}
59

  
60
sub redirect_to_part {
61
  SL::Controller::Base->new->url_for(
62
    controller => 'ic.pl',
63
    action     => 'edit',
64
    id         => $_[0],
65
  );
66
}
67

  
68
sub init_models {
69
  my ($self) = @_;
70

  
71
  SL::Controller::Helper::GetModels->new(
72
    controller => $self,
73
    model      => 'Part',
74
    source     => {
75
      filter => {
76
        type                      => 'assembly',
77
        'all:substr:multi::ilike' => $::form->{term},
78
      },
79
    },
80
    sorted     => {
81
      _default   => {
82
        by  => 'partnumber',
83
        dir => 1,
84
      },
85
      partnumber => t8('Partnumber'),
86
    },
87
    paginated  => {
88
      per_page => 10,
89
    },
90
  )
91
}
92

  
93
1;
SL/Controller/TopQuickSearch/Base.pm
1
package SL::Controller::TopQuickSearch::Base;
2

  
3
use strict;
4
use parent qw(Rose::Object);
5

  
6
sub auth { ... }
7

  
8
sub name { ... }
9

  
10
sub description_config { ... }
11

  
12
sub description_field { ... }
13

  
14
sub query_autocomplete { ... }
15

  
16
sub select_autocomplete { ... }
17

  
18
sub do_search { ... }
19

  
20
1;
21

  
22
__END__
23

  
24
=encoding utf-8
25

  
26
=head1 NAME
27

  
28
SL::Controller::TopQuickSearch::Base - base interface class for quick search plugins
29

  
30
=head1 DESCRIPTION
31

  
32
see L<SL::Controller::TopQuickSearch>
33

  
34
=head1 INTERFACE
35

  
36
An implementation must provide these functions.
37

  
38
=over 4
39

  
40
=item C<auth>
41

  
42
Must return a string used for access checks. Empty string or undef will mean
43
unrestricted access.
44

  
45
=item C<name>
46

  
47
Internal name, must be plain ASCII.
48

  
49
=item C<description_config>
50

  
51
Localized name used in the configuration (NYI)
52

  
53
=item C<description_field>
54

  
55
Localized name used in the search field as hint. Should fit into an input of
56
length 20.
57

  
58
=item C<query_autocomplete>
59

  
60
Needs to take C<term> from C<$::form> and must return an arrayref of JSON
61
serializable matches fit for jquery autocomplete.
62

  
63
=item C<select_autocomplete>
64

  
65
Needs to take C<id> from C<$::form> and must return a redirect string to be
66
used with C<SL::Controller::Base::redirect_to> pointing to a representation of
67
the selected object.
68

  
69
=item C<do_search>
70

  
71
Needs to take C<term> from C<$::form> and must return a redirect string to be
72
used with C<SL::Controller::Base::redirect_to> pointing to a representation of
73
the search results. If the search will display only only one match, it should
74
instead return the same result as if that object was selected directly using
75
C<select_autocomplete>.
76

  
77
=back
78

  
79
=head1 BUGS
80

  
81
None yet :)
82

  
83
=head1 AUTHOR
84

  
85
Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
86

  
87
=cut
SL/Controller/TopQuickSearch/Contact.pm
1
package SL::Controller::TopQuickSearch::Contact;
2

  
3
use strict;
4
use parent qw(SL::Controller::TopQuickSearch::Base);
5

  
6
use SL::Controller::CustomerVendor;
7
use SL::DB::Vendor;
8
use SL::DBUtils qw(selectfirst_array_query);
9
use SL::Locale::String qw(t8);
10

  
11
sub auth { 'customer_vendor_edit' }
12

  
13
sub name { 'contact' }
14

  
15
sub description_config { t8('Contact') }
16

  
17
sub description_field { t8('Contacts') }
18

  
19
sub query_autocomplete {
20
  my ($self) = @_;
21

  
22
  my $result = SL::DB::Manager::Contact->get_all(
23
    query => [
24
      or => [
25
        cp_name      => { ilike => "%$::form->{term}%" },
26
        cp_givenname => { ilike => "%$::form->{term}%" },
27
        cp_email     => { ilike => "%$::form->{term}%" },
28
      ],
29
      cp_cv_id => [ \'SELECT id FROM customer UNION SELECT id FROM vendor' ],
30
    ],
31
    limit => 10,
32
    sort_by => 'cp_name',
33
  );
34

  
35
  return [
36
    map {
37
     value       => $_->full_name,
38
     label       => $_->full_name,
39
     id          => $_->cp_id,
40
    }, @$result
41
  ];
42
}
43

  
44
sub select_autocomplete {
45
  my ($self) = @_;
46

  
47
  my $contact = SL::DB::Manager::Contact->find_by(cp_id => $::form->{id});
48

  
49
  SL::Controller::CustomerVendor->new->url_for(action => 'edit', id => $contact->cp_cv_id, db => db_for_contact($contact));
50
}
51

  
52
sub do_search {
53
  my ($self) = @_;
54

  
55
  my $results = $self->query_autocomplete;
56

  
57
  if (@$results != 1) {
58
    return SL::Controller::CustomerVendor->new->url_for(
59
      controller      => 'ct.pl',
60
      action          => 'list_contacts',
61
      'filter.status' => 'active',
62
      search_term     => $::form->{term},
63
    );
64
  } else {
65
    $::form->{id} = $results->[0]{id};
66
    return $self->select_autocomplete;
67
  }
68
}
69

  
70

  
71
sub db_for_contact {
72
  my ($contact) = @_;
73

  
74
  my ($customer, $vendor) = selectfirst_array_query($::form, $::form->get_standard_dbh, <<SQL, ($contact->cp_cv_id)x2);
75
    SELECT (SELECT COUNT(id) FROM customer WHERE id = ?), (SELECT COUNT(id) FROM vendor WHERE id = ?);
76
SQL
77

  
78
  die 'Contact is orphaned, cannot link to it'         if !$customer && !$vendor;
79

  
80
  $customer ? 'customer' : 'vendor';
81
}
82

  
83
# TODO: multi search
84

  
85
1;
SL/Controller/TopQuickSearch/GLTransaction.pm
1
package SL::Controller::TopQuickSearch::GLTransaction;
2

  
3
use strict;
4
use parent qw(Rose::Object);
5

  
6
use SL::DB::GLTransaction;
7
use SL::DB::Invoice;
8
use SL::DB::PurchaseInvoice;
9
use SL::DB::AccTransaction;
10
use SL::Locale::String qw(t8);
11
use List::Util qw(sum);
12

  
13
sub auth { 'general_ledger' }
14

  
15
sub name { 'gl_transction' }
16

  
17
sub description_config { t8('GL search') }
18

  
19
sub description_field { t8('GL search') }
20

  
21
sub query_autocomplete {
22
  my ($self, %params) = @_;
23

  
24
  my $limit = $::form->{limit} || 40; # max number of results per type (AR/AP/GL)
25
  my $term  = $::form->{term}  || '';
26

  
27
  my $descriptionquery = { ilike => '%' . $term . '%' };
28
  my $referencequery   = { ilike => '%' . $term . '%' };
29
  my $apinvnumberquery = { ilike => '%' . $term . '%' };
30
  my $namequery        = { ilike => '%' . $term . '%' };
31
  my $arinvnumberquery = { ilike => '%' . $term       };
32
  # ar match is more restrictive. Left fuzzy beginning so it also matches "Storno zu $INVNUMBER"
33
  # and numbers like 000123 if you only enter 123.
34
  # When used in quicksearch short numbers like 1 or 11 won't match because of the
35
  # ajax autocomplete minlimit of 3 characters
36

  
37
  my (@glfilter, @arfilter, @apfilter);
38

  
39
  push( @glfilter, (or => [ description => $descriptionquery, reference => $referencequery ] ) );
40
  push( @arfilter, (or => [ invnumber   => $arinvnumberquery, name      => $namequery ] ) );
41
  push( @apfilter, (or => [ invnumber   => $apinvnumberquery, name      => $namequery ] ) );
42

  
43
  my $gls = SL::DB::Manager::GLTransaction->get_all(  query => [ @glfilter ], limit => $limit, sort_by => 'transdate DESC');
44
  my $ars = SL::DB::Manager::Invoice->get_all(        query => [ @arfilter ], limit => $limit, sort_by => 'transdate DESC', with_objects => [ 'customer' ]);
45
  my $aps = SL::DB::Manager::PurchaseInvoice->get_all(query => [ @apfilter ], limit => $limit, sort_by => 'transdate DESC', with_objects => [ 'vendor' ]);
46

  
47
  # use the sum of all credit amounts as the "amount" of the gl transaction
48
  foreach my $gl ( @$gls ) {
49
    $gl->{'amount'} = sum map { $_->amount if $_->amount > 0 } @{$gl->transactions};
50
  };
51

  
52
  my $gldata = [
53
    map(
54
      {
55
        {
56
           transdate => DateTime->from_object(object => $_->transdate)->ymd(),
57
           label     => $_->abbreviation. ": " . $_->description . " " . $_->reference . " " . $::form->format_amount(\%::myconfig, $_->{'amount'},2). " (" . $_->transdate->to_lxoffice . ")" ,
58
           id        => 'gl.pl?action=edit&id=' . $_->id,
59
        }
60
      }
61
      @{$gls}
62
    ),
63
  ];
64

  
65
  my $ardata = [
66
    map(
67
      {
68
        {
69
           transdate => DateTime->from_object(object => $_->transdate)->ymd(),
70
           label     => $_->abbreviation . ": " . $_->invnumber . "   " . $_->customer->name . " " . $::form->format_amount(\%::myconfig, $_->amount,2)  . " (" . $_->transdate->to_lxoffice . ")" ,
71
           id        => ($_->invoice ? "is" : "ar" ) . '.pl?action=edit&id=' . $_->id,
72
        }
73
      }
74
      @{$ars}
75
    ),
76
  ];
77

  
78
  my $apdata = [
79
    map(
80
      {
81
        {
82
           transdate => DateTime->from_object(object => $_->transdate)->ymd(),
83
           label     => $_->abbreviation . ": " . $_->invnumber . " " . $_->vendor->name . " " . $::form->format_amount(\%::myconfig, $_->amount,2)  . " (" . $_->transdate->to_lxoffice . ")" ,
84
           value     => "",
85
           id        => ($_->invoice ? "ir" : "ap" ) . '.pl?action=edit&id=' . $_->id,
86
        }
87
      }
88
      @{$aps}
89
    ),
90
  ];
91

  
92
  my $data;
93
  push(@{$data},@{$gldata});
94
  push(@{$data},@{$ardata});
95
  push(@{$data},@{$apdata});
96

  
97
  @$data = reverse sort { $a->{'transdate'} cmp $b->{'transdate'} } @$data;
98

  
99
  $data;
100
}
101

  
102
sub select_autocomplete {
103
  $::form->{id}
104
}
105

  
106
sub do_search {
107
  my ($self) = @_;
108

  
109
  my $results = $self->query_autocomplete;
110

  
111
  return @$results == 1
112
    ? $results->[0]{id}
113
    : undef;
114
}
115

  
116
# TODO: result overview page
117

  
118
1;
SL/Layout/Top.pm
3 3
use strict;
4 4
use parent qw(SL::Layout::Base);
5 5

  
6
use SL::Controller::TopQuickSearch;
7

  
6 8
sub pre_content {
7 9
  my ($self) = @_;
8 10

  
......
10 12
    now        => DateTime->now_local,
11 13
    is_fastcgi => $::dispatcher ? scalar($::dispatcher->interface_type =~ /fastcgi/i) : 0,
12 14
    is_links   => scalar($ENV{HTTP_USER_AGENT}         =~ /links/i),
15
    quick_search => SL::Controller::TopQuickSearch->new,
13 16
  );
14 17
}
15 18

  
......
18 21
}
19 22

  
20 23
sub javascripts {
21
  ('jquery-ui.js', 'quicksearch_input.js') x!! $::auth->assert('customer_vendor_edit|part_service_assembly_edit', 1),
22
  ('jquery-ui.js', 'glquicksearch.js')     x!! $::auth->assert('general_ledger', 1)
24
  'jquery-ui.js',
25
  'kivi.QuickSearch.js',
23 26
}
24 27

  
25 28
1;
js/kivi.QuickSearch.js
1
namespace('kivi', function(k){
2
  k.QuickSearch = function($real, options) {
3
    if ($real.data("quick_search"))
4
      return $real.data("quick_search");
5

  
6
    var KEY = {
7
      ENTER:     13,
8
    };
9
    var o = $.extend({
10
      limit: 20,
11
      delay: 50,
12
    }, options);
13

  
14
    function send_query(action, term, id, success) {
15
      var data = { module: o.module };
16
      if (term != undefined) data.term = term;
17
      if (id   != undefined) data.id   = id;
18
      $.ajax($.extend(o, {
19
        url:      'controller.pl?action=TopQuickSearch/' + action,
20
        dataType: "json",
21
        data:     data,
22
        success:  success
23
      }));
24
    }
25

  
26
    function submit_search(term) {
27
      send_query('do_search', term, undefined, kivi.eval_json_result);
28
    }
29

  
30
    $real.autocomplete({
31
      source: function(req, rsp) {
32
        send_query('query_autocomplete', req.term, undefined, function (data){ rsp(data) });
33
      },
34
      select: function(event, ui) {
35
        send_query('select_autocomplete', undefined, ui.item.id, kivi.eval_json_result);
36
      },
37
    });
38
    $real.keydown(function(event){
39
      if (event.which == KEY.ENTER) {
40
        if ($real.val() != '') {
41
          submit_search($real.val());
42
        }
43
      }
44
    });
45

  
46
    $real.data('quick_search', {});
47
  }
48
});
49

  
50
$(function(){
51
  $('input[id^=top-quick-search]').each(function(_,e){
52
    kivi.QuickSearch($(e), { module: $(e).attr('module') })
53
  })
54
})
templates/webpages/menu/header.html
5 5
 <span class="frame-header-element frame-header-left">
6 6
    [<a href="controller.pl?action=LoginScreen/user_login" target="_blank" title="[% 'Open a further kivitendo window or tab' | $T8 %]">[% 'New window/tab' | $T8 %]</a>]
7 7
    [<a href="JavaScript:top.print();" title="[% 'Hardcopy' | $T8 %]">[% 'Print' | $T8 %]</a>]
8
[%- IF AUTH.assert('part_service_assembly_edit', 1) %]
9
    [<input name="frame_header_parts_search" id="frame_header_parts_search" placeholder="[% 'Search parts' | $T8 %]" size="14">]
10
[%- END %]
11
[%- IF AUTH.assert('customer_vendor_edit|customer_vendor_edit_all', 1) %]
12
    [<input name="frame_header_contact_search" id="frame_header_contact_search" placeholder="[% 'Search contacts' | $T8 %]" size="14">]
13
[%- END %]
14
[%- IF AUTH.assert('general_ledger', 1) %]
15
    [<input id="glquicksearch" name="glquicksearch" type="text" class="ui-widget" placeholder="[% 'GL search' | $T8 %]" maxlength="20">]
8

  
9
[%- FOREACH search = quick_search.active_modules %]
10
    [<input id="top-quick-search-[% search.name %]" module="[% search.name %]" placeholder="[% search.description_field %]" maxlength="20">]
16 11
[%- END %]
12

  
17 13
 </span>
18 14
[%- END %]
19 15
 <span class="frame-header-element frame-header-right">

Auch abrufbar als: Unified diff