Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 6c630204

Von Sven Schöling vor mehr als 9 Jahren hinzugefügt

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

TopQuickSearch: erste version

Unterschiede anzeigen:

SL/Controller/TopQuickSearch.pm
package SL::Controller::TopQuickSearch;
use strict;
use parent qw(SL::Controller::Base);
use SL::ClientJS;
use SL::JSON;
use SL::Locale::String qw(t8);
use Rose::Object::MakeMethods::Generic (
'scalar --get_set_init' => [ qw(module js) ],
);
my @available_modules = qw(
SL::Controller::TopQuickSearch::Assembly
SL::Controller::TopQuickSearch::Contact
SL::Controller::TopQuickSearch::GLTransaction
);
my %modules_by_name;
sub action_query_autocomplete {
my ($self) = @_;
my $hashes = $self->module->query_autocomplete;
$self->render(\ SL::JSON::to_json($hashes), { layout => 0, type => 'json', process => 0 });
}
sub action_select_autocomplete {
my ($self) = @_;
my $redirect_url = $self->module->select_autocomplete;
$self->js->redirect_to($redirect_url)->render;
}
sub action_do_search {
my ($self) = @_;
my $redirect_url = $self->module->do_search;
if ($redirect_url) {
$self->js->redirect_to($redirect_url)
}
$self->js->render;
}
sub available_modules {
my ($self) = @_;
$self->require_modules;
map { $_->new } @available_modules;
}
sub active_modules {
grep {
$::auth->assert($_->auth, 1)
} $_[0]->available_modules
}
sub init_module {
my ($self) = @_;
$self->require_modules;
die t8('Need module') unless $::form->{module};
$::lxdebug->dump(0, "modules", \%modules_by_name);
die t8('Unknown module #1', $::form->{module}) unless my $class = $modules_by_name{$::form->{module}};
$::lxdebug->dump(0, "auth:", $class->auth);
$::auth->assert($class->auth);
return $class->new;
}
sub init_js {
SL::ClientJS->new(controller => $_[0])
}
sub require_modules {
my ($self) = @_;
if (!$self->{__modules_required}) {
for my $class (@available_modules) {
eval "require $class" or die $@;
$modules_by_name{ $class->name } = $class;
}
$self->{__modules_required} = 1;
}
}
1;
__END__
=encoding utf-8
=head1 NAME
SL::Controller::TopQuickSearch - Framework for pluggable quicksearch fields in the layout top header.
=head1 SYNOPSIS
use SL::Controller::TopQuickSearch;
my $search = SL::Controller::TopQuickSearch->new;
# in layout
[%- FOREACH module = search.available_modules %]
<input type='text' id='top-search-[% module.name %]'>
[%- END %]
=head1 DESCRIPTION
This controller provides abstraction for different search plugins, and ensures
that all follow a common useability scheme.
Modules should be configurable, but currently are not. Diabling modules can be
done by removing them from available_modules.
=head1 BEHAVIOUR REQUIREMENTS
=over 4
=item *
A single text input field with the html5 placeholder containing a small
description of the target will be rendered from the plugin information.
=item *
On typing, the autocompletion must be enabled.
=item *
On C<Enter>, the search should redirect to an appropriate listing of matching
results.
If only one item matches the result, the plugin should instead redirect
directly to the matched item.
=item *
Search terms should accept the broadest possible matching, and if possible with
C<multi> parsing.
=item *
In case nothing is found, a visual indicator should be given, but no actual
redirect should occur.
=item *
Each search must check rights and must not present a backdoor into data that
the user should not see.
=back
=head1 INTERFACE
Plugins need to provide:
- name
- localized description for config
- localized description for textfield
- autocomplete callback
- redirect callback
the frontend will only generate urls of the forms:
action=TopQuickSearch/autocomplete&module=<module>&term=<term>
action=TopQuickSearch/search&module=<module>&term=<term>
=head1 TODO
- filter available searches with auth
- toggling with cofiguration doesn't work yet
=head1 BUGS
None yet :)
=head1 AUTHOR
Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
=cut
SL/Controller/TopQuickSearch/Assembly.pm
package SL::Controller::TopQuickSearch::Assembly;
use strict;
use parent qw(Rose::Object);
use SL::Locale::String qw(t8);
use SL::DB::Part;
use SL::Controller::Helper::GetModels;
use SL::Controller::Base;
use Rose::Object::MakeMethods::Generic (
'scalar --get_set_init' => [ qw(parts models part) ],
);
sub auth { 'part_service_assembly_edit' }
sub name { 'assembly' }
sub description_config { t8('Assemblies') }
sub description_field { t8('Assemblies') }
sub query_autocomplete {
my ($self) = @_;
my $objects = $self->models->get;
[
map {
value => $_->displayable_name,
label => $_->displayable_name,
id => $_->id,
}, @$objects
];
}
sub select_autocomplete {
redirect_to_part($::form->{id});
}
sub do_search {
my ($self) = @_;
my $objects = $self->models->get;
return !@$objects ? ()
: @$objects == 1 ? redirect_to_part($objects->[0]->id)
: redirect_to_search($::form->{term});
}
sub redirect_to_search {
SL::Controller::Base->new->url_for(
controller => 'ic.pl',
action => 'generate_report',
searchitems => 'assembly',
all => $_[0],
);
}
sub redirect_to_part {
SL::Controller::Base->new->url_for(
controller => 'ic.pl',
action => 'edit',
id => $_[0],
);
}
sub init_models {
my ($self) = @_;
SL::Controller::Helper::GetModels->new(
controller => $self,
model => 'Part',
source => {
filter => {
type => 'assembly',
'all:substr:multi::ilike' => $::form->{term},
},
},
sorted => {
_default => {
by => 'partnumber',
dir => 1,
},
partnumber => t8('Partnumber'),
},
paginated => {
per_page => 10,
},
)
}
1;
SL/Controller/TopQuickSearch/Base.pm
package SL::Controller::TopQuickSearch::Base;
use strict;
use parent qw(Rose::Object);
sub auth { ... }
sub name { ... }
sub description_config { ... }
sub description_field { ... }
sub query_autocomplete { ... }
sub select_autocomplete { ... }
sub do_search { ... }
1;
__END__
=encoding utf-8
=head1 NAME
SL::Controller::TopQuickSearch::Base - base interface class for quick search plugins
=head1 DESCRIPTION
see L<SL::Controller::TopQuickSearch>
=head1 INTERFACE
An implementation must provide these functions.
=over 4
=item C<auth>
Must return a string used for access checks. Empty string or undef will mean
unrestricted access.
=item C<name>
Internal name, must be plain ASCII.
=item C<description_config>
Localized name used in the configuration (NYI)
=item C<description_field>
Localized name used in the search field as hint. Should fit into an input of
length 20.
=item C<query_autocomplete>
Needs to take C<term> from C<$::form> and must return an arrayref of JSON
serializable matches fit for jquery autocomplete.
=item C<select_autocomplete>
Needs to take C<id> from C<$::form> and must return a redirect string to be
used with C<SL::Controller::Base::redirect_to> pointing to a representation of
the selected object.
=item C<do_search>
Needs to take C<term> from C<$::form> and must return a redirect string to be
used with C<SL::Controller::Base::redirect_to> pointing to a representation of
the search results. If the search will display only only one match, it should
instead return the same result as if that object was selected directly using
C<select_autocomplete>.
=back
=head1 BUGS
None yet :)
=head1 AUTHOR
Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
=cut
SL/Controller/TopQuickSearch/Contact.pm
package SL::Controller::TopQuickSearch::Contact;
use strict;
use parent qw(SL::Controller::TopQuickSearch::Base);
use SL::Controller::CustomerVendor;
use SL::DB::Vendor;
use SL::DBUtils qw(selectfirst_array_query);
use SL::Locale::String qw(t8);
sub auth { 'customer_vendor_edit' }
sub name { 'contact' }
sub description_config { t8('Contact') }
sub description_field { t8('Contacts') }
sub query_autocomplete {
my ($self) = @_;
my $result = SL::DB::Manager::Contact->get_all(
query => [
or => [
cp_name => { ilike => "%$::form->{term}%" },
cp_givenname => { ilike => "%$::form->{term}%" },
cp_email => { ilike => "%$::form->{term}%" },
],
cp_cv_id => [ \'SELECT id FROM customer UNION SELECT id FROM vendor' ],
],
limit => 10,
sort_by => 'cp_name',
);
return [
map {
value => $_->full_name,
label => $_->full_name,
id => $_->cp_id,
}, @$result
];
}
sub select_autocomplete {
my ($self) = @_;
my $contact = SL::DB::Manager::Contact->find_by(cp_id => $::form->{id});
SL::Controller::CustomerVendor->new->url_for(action => 'edit', id => $contact->cp_cv_id, db => db_for_contact($contact));
}
sub do_search {
my ($self) = @_;
my $results = $self->query_autocomplete;
if (@$results != 1) {
return SL::Controller::CustomerVendor->new->url_for(
controller => 'ct.pl',
action => 'list_contacts',
'filter.status' => 'active',
search_term => $::form->{term},
);
} else {
$::form->{id} = $results->[0]{id};
return $self->select_autocomplete;
}
}
sub db_for_contact {
my ($contact) = @_;
my ($customer, $vendor) = selectfirst_array_query($::form, $::form->get_standard_dbh, <<SQL, ($contact->cp_cv_id)x2);
SELECT (SELECT COUNT(id) FROM customer WHERE id = ?), (SELECT COUNT(id) FROM vendor WHERE id = ?);
SQL
die 'Contact is orphaned, cannot link to it' if !$customer && !$vendor;
$customer ? 'customer' : 'vendor';
}
# TODO: multi search
1;
SL/Controller/TopQuickSearch/GLTransaction.pm
package SL::Controller::TopQuickSearch::GLTransaction;
use strict;
use parent qw(Rose::Object);
use SL::DB::GLTransaction;
use SL::DB::Invoice;
use SL::DB::PurchaseInvoice;
use SL::DB::AccTransaction;
use SL::Locale::String qw(t8);
use List::Util qw(sum);
sub auth { 'general_ledger' }
sub name { 'gl_transction' }
sub description_config { t8('GL search') }
sub description_field { t8('GL search') }
sub query_autocomplete {
my ($self, %params) = @_;
my $limit = $::form->{limit} || 40; # max number of results per type (AR/AP/GL)
my $term = $::form->{term} || '';
my $descriptionquery = { ilike => '%' . $term . '%' };
my $referencequery = { ilike => '%' . $term . '%' };
my $apinvnumberquery = { ilike => '%' . $term . '%' };
my $namequery = { ilike => '%' . $term . '%' };
my $arinvnumberquery = { ilike => '%' . $term };
# ar match is more restrictive. Left fuzzy beginning so it also matches "Storno zu $INVNUMBER"
# and numbers like 000123 if you only enter 123.
# When used in quicksearch short numbers like 1 or 11 won't match because of the
# ajax autocomplete minlimit of 3 characters
my (@glfilter, @arfilter, @apfilter);
push( @glfilter, (or => [ description => $descriptionquery, reference => $referencequery ] ) );
push( @arfilter, (or => [ invnumber => $arinvnumberquery, name => $namequery ] ) );
push( @apfilter, (or => [ invnumber => $apinvnumberquery, name => $namequery ] ) );
my $gls = SL::DB::Manager::GLTransaction->get_all( query => [ @glfilter ], limit => $limit, sort_by => 'transdate DESC');
my $ars = SL::DB::Manager::Invoice->get_all( query => [ @arfilter ], limit => $limit, sort_by => 'transdate DESC', with_objects => [ 'customer' ]);
my $aps = SL::DB::Manager::PurchaseInvoice->get_all(query => [ @apfilter ], limit => $limit, sort_by => 'transdate DESC', with_objects => [ 'vendor' ]);
# use the sum of all credit amounts as the "amount" of the gl transaction
foreach my $gl ( @$gls ) {
$gl->{'amount'} = sum map { $_->amount if $_->amount > 0 } @{$gl->transactions};
};
my $gldata = [
map(
{
{
transdate => DateTime->from_object(object => $_->transdate)->ymd(),
label => $_->abbreviation. ": " . $_->description . " " . $_->reference . " " . $::form->format_amount(\%::myconfig, $_->{'amount'},2). " (" . $_->transdate->to_lxoffice . ")" ,
id => 'gl.pl?action=edit&id=' . $_->id,
}
}
@{$gls}
),
];
my $ardata = [
map(
{
{
transdate => DateTime->from_object(object => $_->transdate)->ymd(),
label => $_->abbreviation . ": " . $_->invnumber . " " . $_->customer->name . " " . $::form->format_amount(\%::myconfig, $_->amount,2) . " (" . $_->transdate->to_lxoffice . ")" ,
id => ($_->invoice ? "is" : "ar" ) . '.pl?action=edit&id=' . $_->id,
}
}
@{$ars}
),
];
my $apdata = [
map(
{
{
transdate => DateTime->from_object(object => $_->transdate)->ymd(),
label => $_->abbreviation . ": " . $_->invnumber . " " . $_->vendor->name . " " . $::form->format_amount(\%::myconfig, $_->amount,2) . " (" . $_->transdate->to_lxoffice . ")" ,
value => "",
id => ($_->invoice ? "ir" : "ap" ) . '.pl?action=edit&id=' . $_->id,
}
}
@{$aps}
),
];
my $data;
push(@{$data},@{$gldata});
push(@{$data},@{$ardata});
push(@{$data},@{$apdata});
@$data = reverse sort { $a->{'transdate'} cmp $b->{'transdate'} } @$data;
$data;
}
sub select_autocomplete {
$::form->{id}
}
sub do_search {
my ($self) = @_;
my $results = $self->query_autocomplete;
return @$results == 1
? $results->[0]{id}
: undef;
}
# TODO: result overview page
1;
SL/Layout/Top.pm
use strict;
use parent qw(SL::Layout::Base);
use SL::Controller::TopQuickSearch;
sub pre_content {
my ($self) = @_;
......
now => DateTime->now_local,
is_fastcgi => $::dispatcher ? scalar($::dispatcher->interface_type =~ /fastcgi/i) : 0,
is_links => scalar($ENV{HTTP_USER_AGENT} =~ /links/i),
quick_search => SL::Controller::TopQuickSearch->new,
);
}
......
}
sub javascripts {
('jquery-ui.js', 'quicksearch_input.js') x!! $::auth->assert('customer_vendor_edit|part_service_assembly_edit', 1),
('jquery-ui.js', 'glquicksearch.js') x!! $::auth->assert('general_ledger', 1)
'jquery-ui.js',
'kivi.QuickSearch.js',
}
1;
js/kivi.QuickSearch.js
namespace('kivi', function(k){
k.QuickSearch = function($real, options) {
if ($real.data("quick_search"))
return $real.data("quick_search");
var KEY = {
ENTER: 13,
};
var o = $.extend({
limit: 20,
delay: 50,
}, options);
function send_query(action, term, id, success) {
var data = { module: o.module };
if (term != undefined) data.term = term;
if (id != undefined) data.id = id;
$.ajax($.extend(o, {
url: 'controller.pl?action=TopQuickSearch/' + action,
dataType: "json",
data: data,
success: success
}));
}
function submit_search(term) {
send_query('do_search', term, undefined, kivi.eval_json_result);
}
$real.autocomplete({
source: function(req, rsp) {
send_query('query_autocomplete', req.term, undefined, function (data){ rsp(data) });
},
select: function(event, ui) {
send_query('select_autocomplete', undefined, ui.item.id, kivi.eval_json_result);
},
});
$real.keydown(function(event){
if (event.which == KEY.ENTER) {
if ($real.val() != '') {
submit_search($real.val());
}
}
});
$real.data('quick_search', {});
}
});
$(function(){
$('input[id^=top-quick-search]').each(function(_,e){
kivi.QuickSearch($(e), { module: $(e).attr('module') })
})
})
templates/webpages/menu/header.html
<span class="frame-header-element frame-header-left">
[<a href="controller.pl?action=LoginScreen/user_login" target="_blank" title="[% 'Open a further kivitendo window or tab' | $T8 %]">[% 'New window/tab' | $T8 %]</a>]
[<a href="JavaScript:top.print();" title="[% 'Hardcopy' | $T8 %]">[% 'Print' | $T8 %]</a>]
[%- IF AUTH.assert('part_service_assembly_edit', 1) %]
[<input name="frame_header_parts_search" id="frame_header_parts_search" placeholder="[% 'Search parts' | $T8 %]" size="14">]
[%- END %]
[%- IF AUTH.assert('customer_vendor_edit|customer_vendor_edit_all', 1) %]
[<input name="frame_header_contact_search" id="frame_header_contact_search" placeholder="[% 'Search contacts' | $T8 %]" size="14">]
[%- END %]
[%- IF AUTH.assert('general_ledger', 1) %]
[<input id="glquicksearch" name="glquicksearch" type="text" class="ui-widget" placeholder="[% 'GL search' | $T8 %]" maxlength="20">]
[%- FOREACH search = quick_search.active_modules %]
[<input id="top-quick-search-[% search.name %]" module="[% search.name %]" placeholder="[% search.description_field %]" maxlength="20">]
[%- END %]
</span>
[%- END %]
<span class="frame-header-element frame-header-right">

Auch abrufbar als: Unified diff