From 53d80f2a01439ee73cf679c53326ec8fb32ab1ed Mon Sep 17 00:00:00 2001 From: Moritz Bunkus Date: Tue, 24 Jun 2014 13:15:43 +0200 Subject: [PATCH] =?utf8?q?Generische=20Unterst=C3=BCtzung=20f=C3=BCr=20CTI?= =?utf8?q?:=20Click-to-dial?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- SL/CTI.pm | 52 ++++++++++++++ SL/Controller/CTI.pm | 66 ++++++++++++++++++ bin/mozilla/ct.pl | 13 ++++ config/kivitendo.conf.default | 21 ++++++ css/kivitendo/main.css | 12 ++++ css/lx-office-erp/main.css | 12 ++++ .../16x16/Program--Internal Phone List.png | 1 + image/icons/16x16/phone.png | Bin 0 -> 515 bytes js/kivi.CustomerVendor.js | 40 +++++++++++ locale/de/all | 9 +++ menus/erp.ini | 4 ++ t/cti/call_link.t | 22 ++++++ t/cti/sanitize_number.t | 17 +++++ templates/webpages/admin/edit_user.html | 14 ++++ templates/webpages/cti/calling.html | 4 ++ .../cti/list_internal_extensions.html | 25 +++++++ templates/webpages/customer_vendor/form.html | 2 + .../customer_vendor/tabs/contacts.html | 2 +- .../webpages/customer_vendor/tabs/shipto.html | 2 +- 19 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 SL/CTI.pm create mode 100644 SL/Controller/CTI.pm create mode 120000 image/icons/16x16/Program--Internal Phone List.png create mode 100644 image/icons/16x16/phone.png create mode 100644 t/cti/call_link.t create mode 100644 t/cti/sanitize_number.t create mode 100644 templates/webpages/cti/calling.html create mode 100644 templates/webpages/cti/list_internal_extensions.html diff --git a/SL/CTI.pm b/SL/CTI.pm new file mode 100644 index 000000000..8605ff163 --- /dev/null +++ b/SL/CTI.pm @@ -0,0 +1,52 @@ +package SL::CTI; + +use strict; + +use String::ShellQuote; + +use SL::MoreCommon qw(uri_encode); + +sub call { + my ($class, %params) = @_; + + my $config = $::lx_office_conf{cti} || {}; + my $command = $config->{dial_command} || die $::locale->text('Dial command missing in kivitendo configuration\'s [cti] section'); + my $external_prefix = $params{internal} ? '' : ($config->{external_prefix} // ''); + + my %command_args = ( + phone_extension => $::myconfig{phone_extension} || die($::locale->text('Phone extension missing in user configuration')), + phone_password => $::myconfig{phone_password} || die($::locale->text('Phone password missing in user configuration')), + number => $external_prefix . $class->sanitize_number(%params), + ); + + foreach my $key (keys %command_args) { + my $value = shell_quote($command_args{$key}); + $command =~ s{<\% ${key} \%>}{$value}gx; + } + + return `$command`; +} + +sub call_link { + my ($class, %params) = @_; + + return "controller.pl?action=CTI/call&number=" . uri_encode($class->sanitize_number(number => $params{number})) . ($params{internal} ? '&internal=1' : ''); +} + +sub sanitize_number { + my ($class, %params) = @_; + + my $config = $::lx_office_conf{cti} || {}; + my $idp = $config->{international_dialing_prefix} // '00'; + + my $number = $params{number} // ''; + $number =~ s/[^0-9+\.-]//g; # delete unsupported characters + my $countrycode = $number =~ s/^(?: $idp | \+ ) ( \d{2} )//x ? $1 : ''; # TODO: countrycodes can have more or less than 2 digits + $number =~ s/^0//x if $countrycode; # kill non standard optional zero after global identifier + + return '' unless $number; + + return ($countrycode ? $idp . $countrycode : '') . $number; +} + +1; diff --git a/SL/Controller/CTI.pm b/SL/Controller/CTI.pm new file mode 100644 index 000000000..24e3a8449 --- /dev/null +++ b/SL/Controller/CTI.pm @@ -0,0 +1,66 @@ +package SL::Controller::CTI; + +use strict; + +use SL::CTI; +use SL::DB::AuthUserConfig; +use SL::Helper::Flash; +use SL::Locale::String; + +use parent qw(SL::Controller::Base); + +use Rose::Object::MakeMethods::Generic ( + 'scalar --get_set_init' => [ qw(internal_extensions) ], +); + +sub action_call { + my ($self) = @_; + + eval { + my $res = SL::CTI->call(number => $::form->{number}, internal => $::form->{internal}); + flash('info', t8('Calling #1 now', $::form->{number})); + 1; + } or do { + flash('error', $@); + }; + + $self->render('cti/calling'); +} + +sub action_list_internal_extensions { + my ($self) = @_; + + $self->render('cti/list_internal_extensions', title => t8('Internal Phone List')); +} + +# +# filters +# + +sub init_internal_extensions { + my ($self) = @_; + + my $user_configs = SL::DB::Manager::AuthUserConfig->get_all( + where => [ + cfg_key => 'phone_extension', + '!cfg_value' => undef, + '!cfg_value' => '', + ], + with_objects => [ qw(user) ], + ); + + my %users; + foreach my $config (@{ $user_configs }) { + $users{$config->user_id} ||= { + name => $config->user->get_config_value('name') || $config->user->login, + phone_extension => $config->cfg_value, + call_link => SL::CTI->call_link(number => $config->cfg_value, internal => 1), + }; + } + + return [ + sort { lc($a->{name}) cmp lc($b->{name}) } values %users + ]; +} + +1; diff --git a/bin/mozilla/ct.pl b/bin/mozilla/ct.pl index 0793a4459..b5b83c19f 100644 --- a/bin/mozilla/ct.pl +++ b/bin/mozilla/ct.pl @@ -48,6 +48,7 @@ use POSIX qw(strftime); use SL::CT; +use SL::CTI; use SL::CVar; use SL::Request qw(flatten); use SL::DB::Business; @@ -268,6 +269,11 @@ sub list_names { my $column = $ref->{formtype} eq 'invoice' ? 'invnumber' : $ref->{formtype} eq 'order' ? 'ordnumber' : 'quonumber'; $row->{$column}->{data} = $ref->{$column}; + if (my $number = SL::CTI->sanitize_number(number => $ref->{phone})) { + $row->{phone}->{link} = SL::CTI->call_link(number => $number); + $row->{phone}->{link_class} = 'cti_call_action'; + } + $report->add_data($row); } @@ -392,6 +398,13 @@ sub list_contacts { $row->{$_}->{link} = 'mailto:' . E($ref->{$_}) if $ref->{$_}; } + for (qw(cp_phone1 cp_phone2 cp_mobile1)) { + next unless my $number = SL::CTI->sanitize_number(number => $ref->{$_}); + + $row->{$_}->{link} = SL::CTI->call_link(number => $number); + $row->{$_}->{link_class} = 'cti_call_action'; + } + $report->add_data($row); } diff --git a/config/kivitendo.conf.default b/config/kivitendo.conf.default index e6849a554..25a8d4c3b 100644 --- a/config/kivitendo.conf.default +++ b/config/kivitendo.conf.default @@ -314,3 +314,24 @@ auto_reload_resources = 0 # If set to 1 each exception will include a full stack backtrace. backtrace_on_die = 0 + +[cti] +# If you want phone numbers to be clickable then this must be set to a +# command that does the actually dialing. Within this command three +# variables are replaced before it is executed: +# +# 1. <%phone_extension%> and <%phone_password%> are taken from the user +# configuration (changeable in the admin interface). +# 2. <%number%> is the number to dial. It has already been sanitized +# and formatted correctly regarding e.g. the international dialing +# prefix. +# +# The following is an example that works with the OpenUC telephony +# server: +# dial_command = curl --insecure -X PUT https://<%phone_extension%>:<%phone_password%>@IP.AD.DR.ESS:8443/sipxconfig/rest/my/call/<%number%> +dial_command = +# If you need to dial something before the actual number then set +# external_prefix to it. +external_prefix = 0 +# The prefix for international calls (numbers starting with +). +international_dialing_prefix = 00 diff --git a/css/kivitendo/main.css b/css/kivitendo/main.css index 3b8599383..6fa432dc0 100644 --- a/css/kivitendo/main.css +++ b/css/kivitendo/main.css @@ -443,3 +443,15 @@ div.ppp_line span.ppp_block_sellprice { span.toggle_selected { font-weight: bold; } + +/* CTI */ +a.cti_call_action { + display: inline-block; + padding-left: 18px; + height: 16px; + position: relative; + top: 2px; + vertical-align: center; + background-image: url(../../image/icons/16x16/phone.png); + background-repeat: no-repeat; +} diff --git a/css/lx-office-erp/main.css b/css/lx-office-erp/main.css index f42c8724f..ab71226dc 100644 --- a/css/lx-office-erp/main.css +++ b/css/lx-office-erp/main.css @@ -494,3 +494,15 @@ div.ppp_line span.ppp_block_sellprice { span.toggle_selected { font-weight: bold; } + +/* CTI */ +a.cti_call_action { + display: inline-block; + padding-left: 18px; + height: 16px; + position: relative; + top: 2px; + vertical-align: center; + background-image: url(../../image/icons/16x16/phone.png); + background-repeat: no-repeat; +} diff --git a/image/icons/16x16/Program--Internal Phone List.png b/image/icons/16x16/Program--Internal Phone List.png new file mode 120000 index 000000000..282275d6d --- /dev/null +++ b/image/icons/16x16/Program--Internal Phone List.png @@ -0,0 +1 @@ +phone.png \ No newline at end of file diff --git a/image/icons/16x16/phone.png b/image/icons/16x16/phone.png new file mode 100644 index 0000000000000000000000000000000000000000..f189191d7c42809f8736f0e7c4350dd77e0d159f GIT binary patch literal 515 zcmV+e0{s1nP)h($8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H10f0$FK~y-6m61V9R8bg)pZAQ0;SdA`LN06*NdwuYMFj0zw2&~# z@~W67{f02~2lxXbGn=M`K|<|*u+?DLDmg$1UzDkEf^#JZn% zd?{vesj$Fc(6Zn(`DvG?3ke7bbKc_Kp{Mae0>(DH27Nuy7vI&-FHCPwoGUrjyQ1y% z^$Q`kMh_&&c-)tOf)={2A8K7nU1-nhX8+fkZez!iEb?!jY)n} zolReL!x?%4oXSnSR!!1X0L?ue8)|5H1?dE41Xu*sqrzl}t1bDp*&0>@J@fufIq>J?e%5LKd61fW02@p6%<^ zBwalWLwku+J6Dbm{{Hnq)=kUfz&GHtGjG=B)Azli{Q(;6lx{7IsWkuq002ovPDHLk FV1g1T+%o_G literal 0 HcmV?d00001 diff --git a/js/kivi.CustomerVendor.js b/js/kivi.CustomerVendor.js index 59c07354e..bfef730d4 100644 --- a/js/kivi.CustomerVendor.js +++ b/js/kivi.CustomerVendor.js @@ -160,4 +160,44 @@ namespace('kivi.CustomerVendor', function(ns) { var url = "common.pl?INPUT_ENCODING=UTF-8&action=show_history&longdescription=&input_name="+ encodeURIComponent(id); window.open(url, "_new_generic", parm); }; + + this.update_dial_action = function($input) { + var $action = $('#' + $input.prop('id') + '-dial-action'); + + if (!$action) + return true; + + var number = $input.val().replace(/\s+/g, ''); + if (number == '') + $action.hide(); + else + $action.prop('href', 'controller.pl?action=CTI/call&number=' + encodeURIComponent(number)).show(); + + return true; + }; + + this.init_dial_action = function(input) { + if ($('#_cti_enabled').val() != 1) + return false; + + var $input = $(input); + var action_id = $input.prop('id') + '-dial-action'; + + if (!$('#' + action_id).size()) { + var $action = $(''); + $input.wrap('').after($action); + + $input.change(function() { kivi.CustomerVendor.update_dial_action($input); }); + } + + kivi.CustomerVendor.update_dial_action($input); + + return true; + }; }); + +function local_reinit_widgets() { + $('#cv_phone,#shipto_shiptophone,#contact_cp_phone1,#contact_cp_phone2,#contact_cp_mobile1,#contact_cp_mobile2').each(function(idx, elt) { + kivi.CustomerVendor.init_dial_action($(elt)); + }); +} diff --git a/locale/de/all b/locale/de/all index 191f9f611..4d6c3c4c4 100755 --- a/locale/de/all +++ b/locale/de/all @@ -408,7 +408,9 @@ $self->{texts} = { 'CSV import: parts and services' => 'CSV-Import: Waren und Dienstleistungen', 'CSV import: projects' => 'CSV-Import: Projekte', 'CSV import: shipping addresses' => 'CSV-Import: Lieferadressen', + 'CTI settings' => 'CTI-Einstellungen', 'Calculate' => 'Berechnen', + 'Calling #1 now' => 'Wähle jetzt #1', 'Can not create that quantity with current stock' => 'Diese Anzahl kann mit dem gegenwärtigen Lagerbestand nicht hergestellt werden.', 'Cancel' => 'Abbrechen', 'Cancel Accounts Payables Transaction' => 'Kreditorenbuchung stornieren', @@ -803,6 +805,7 @@ $self->{texts} = { 'Destination warehouse and bin' => 'Ziellager und -lagerplatz', 'Detail view' => 'Detailanzeige', 'Details (one letter abbreviation)' => 'D', + 'Dial command missing in kivitendo configuration\'s [cti] section' => 'Wählbefehl fehlt im Abschnitt [cti] der kivitendo-Konfiguration', 'Difference' => 'Differenz', 'Dimensions' => 'Abmessungen', 'Directory' => 'Verzeichnis', @@ -1265,6 +1268,7 @@ $self->{texts} = { 'Interest' => 'Zinsen', 'Interest Rate' => 'Zinssatz', 'Internal Notes' => 'Interne Bemerkungen', + 'Internal Phone List' => 'Interne Telefonliste', 'Internal comment' => 'Interne Bemerkungen', 'Internet' => 'Internet', 'Introduction of clients' => 'Einführung von Mandanten', @@ -1535,6 +1539,7 @@ $self->{texts} = { 'No file has been uploaded yet.' => 'Es wurde noch keine Datei hochgeladen.', 'No function blocks have been created yet.' => 'Es wurden noch keine Funktionsblöcke angelegt.', 'No groups have been created yet.' => 'Es wurden noch keine Gruppen angelegt.', + 'No internal phone extensions have been configured yet.' => 'Es wurden noch keine internen Durchwahlen konfiguriert.', 'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => 'Es wurde kein oder ein unbekanntes Authentifizierungsmodul in "config/kivitendo.conf" angegeben.', 'No part was found matching the search parameters.' => 'Es wurde kein Artikel gefunden, auf den die Suchparameter zutreffen.', 'No payment term has been created yet.' => 'Es wurden noch keine Zahlungsbedingungen angelegt.', @@ -1733,6 +1738,10 @@ $self->{texts} = { 'Person' => 'Person', 'Personal settings' => 'Persönliche Einstellungen', 'Phone' => 'Telefon', + 'Phone extension' => 'Durchwahl', + 'Phone extension missing in user configuration' => 'Durchwahl fehlt in der Benutzerkonfiguration', + 'Phone password' => 'Telefonpasswort', + 'Phone password missing in user configuration' => 'Telefonpasswort fehlt in der Benutzerkonfiguration', 'Phone1' => 'Telefon 1 ', 'Phone2' => 'Telefon 2', 'Pick List' => 'Sammelliste', diff --git a/menus/erp.ini b/menus/erp.ini index 637ac6431..3297621f9 100644 --- a/menus/erp.ini +++ b/menus/erp.ini @@ -787,6 +787,10 @@ action=Employee/list module=am.pl action=config +[Program--Internal Phone List] +module=controller.pl +action=CTI/list_internal_extensions + [Program--Version] module=login.pl action=company_logo diff --git a/t/cti/call_link.t b/t/cti/call_link.t new file mode 100644 index 000000000..ce762399b --- /dev/null +++ b/t/cti/call_link.t @@ -0,0 +1,22 @@ +use Test::More tests => 9; + +use strict; +use lib 't'; +use utf8; + +use_ok 'SL::CTI'; + +{ + no warnings 'once'; + $::lx_office_conf{cti}->{international_dialing_prefix} = '00'; +} + +is SL::CTI->call_link(number => '0371 5347 620'), 'controller.pl?action=CTI/call&number=03715347620'; +is SL::CTI->call_link(number => '0049(0)421-22232 22'), 'controller.pl?action=CTI/call&number=0049421-2223222'; +is SL::CTI->call_link(number => '+49(0)421-22232 22'), 'controller.pl?action=CTI/call&number=0049421-2223222'; +is SL::CTI->call_link(number => 'Tel: +49 40 809064 0'), 'controller.pl?action=CTI/call&number=0049408090640'; + +is SL::CTI->call_link(number => '0371 5347 620', internal => 1), 'controller.pl?action=CTI/call&number=03715347620&internal=1'; +is SL::CTI->call_link(number => '0049(0)421-22232 22', internal => 1), 'controller.pl?action=CTI/call&number=0049421-2223222&internal=1'; +is SL::CTI->call_link(number => '+49(0)421-22232 22', internal => 1), 'controller.pl?action=CTI/call&number=0049421-2223222&internal=1'; +is SL::CTI->call_link(number => 'Tel: +49 40 809064 0', internal => 1), 'controller.pl?action=CTI/call&number=0049408090640&internal=1'; diff --git a/t/cti/sanitize_number.t b/t/cti/sanitize_number.t new file mode 100644 index 000000000..639e7167b --- /dev/null +++ b/t/cti/sanitize_number.t @@ -0,0 +1,17 @@ +use Test::More tests => 5; + +use strict; +use lib 't'; +use utf8; + +use_ok 'SL::CTI'; + +{ + no warnings 'once'; + $::lx_office_conf{cti}->{international_dialing_prefix} = '00'; +} + +is SL::CTI->sanitize_number(number => '0371 5347 620'), '03715347620'; +is SL::CTI->sanitize_number(number => '0049(0)421-22232 22'), '0049421-2223222'; +is SL::CTI->sanitize_number(number => '+49(0)421-22232 22'), '0049421-2223222'; +is SL::CTI->sanitize_number(number => 'Tel: +49 40 809064 0'), '0049408090640'; diff --git a/templates/webpages/admin/edit_user.html b/templates/webpages/admin/edit_user.html index 94db96b12..3a8f12959 100644 --- a/templates/webpages/admin/edit_user.html +++ b/templates/webpages/admin/edit_user.html @@ -99,6 +99,20 @@ +

[%- LxERP.t8("CTI settings") %]

+ + + + + + + + + + + +
[% LxERP.t8("Phone extension") %][% L.input_tag("user.config_values.phone_extension", props.phone_extension) %]
[% LxERP.t8("Phone password") %][% L.input_tag("user.config_values.phone_password", props.phone_password) %]
+

[%- LxERP.t8("Access to clients") %]

[% IF SELF.all_clients.size %] diff --git a/templates/webpages/cti/calling.html b/templates/webpages/cti/calling.html new file mode 100644 index 000000000..60bbcd729 --- /dev/null +++ b/templates/webpages/cti/calling.html @@ -0,0 +1,4 @@ + +[% PROCESS 'common/flash.html' %] + + diff --git a/templates/webpages/cti/list_internal_extensions.html b/templates/webpages/cti/list_internal_extensions.html new file mode 100644 index 000000000..1a5dfafd4 --- /dev/null +++ b/templates/webpages/cti/list_internal_extensions.html @@ -0,0 +1,25 @@ +[%- USE HTML %][%- USE LxERP -%] + + +

[% HTML.escape(title) %]

+ +[% IF !SELF.internal_extensions.size %] +

[% LxERP.t8("No internal phone extensions have been configured yet.") %]

+ +[% ELSE %] + + + + + + + [%- FOREACH extension = SELF.internal_extensions %] + + + + + [%- END %] +
[% LxERP.t8("Name") %][% LxERP.t8("Phone extension") %]
[% HTML.escape(extension.name) %][% HTML.escape(extension.phone_extension) %]
+[% END %] + + diff --git a/templates/webpages/customer_vendor/form.html b/templates/webpages/customer_vendor/form.html index ad2da05ab..e56b3e584 100644 --- a/templates/webpages/customer_vendor/form.html +++ b/templates/webpages/customer_vendor/form.html @@ -2,6 +2,8 @@ [%- USE LxERP %] [%- USE L %] +[% L.hidden_tag('_cti_enabled', !!LXCONFIG.cti.dial_command) %] + [% cv_cvars = SELF.cv.cvars_by_config %]
diff --git a/templates/webpages/customer_vendor/tabs/contacts.html b/templates/webpages/customer_vendor/tabs/contacts.html index aca56cd00..8d03d8b60 100644 --- a/templates/webpages/customer_vendor/tabs/contacts.html +++ b/templates/webpages/customer_vendor/tabs/contacts.html @@ -18,7 +18,7 @@ empty_title = LxERP.t8('New contact'), value_key = 'cp_id', title_key = 'full_name', - onchange = "kivi.CustomerVendor.selectContact({onFormSet: function(){contactsMapWidget.testInputs();}});", + onchange = "kivi.CustomerVendor.selectContact({onFormSet: function(){ contactsMapWidget.testInputs(); local_reinit_widgets(); }});", ) %] diff --git a/templates/webpages/customer_vendor/tabs/shipto.html b/templates/webpages/customer_vendor/tabs/shipto.html index 8f4997772..ded32f3d1 100644 --- a/templates/webpages/customer_vendor/tabs/shipto.html +++ b/templates/webpages/customer_vendor/tabs/shipto.html @@ -16,7 +16,7 @@ title_key = 'displayable_id', with_empty = 1, empty_title = LxERP.t8('New shipto'), - onchange = "kivi.CustomerVendor.selectShipto({onFormSet: function(){shiptoMapWidget.testInputs();}});", + onchange = "kivi.CustomerVendor.selectShipto({onFormSet: function(){ shiptoMapWidget.testInputs(); local_reinit_widgets(); }});", ) %] -- 2.20.1