From 7faa4c310d5bb54116871b78c18f2e7507d0d162 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 10 Jul 2019 00:07:32 +0200 Subject: [PATCH] Implemented CTI ticket create screen popup on answering call. --- LICENSE-3RD-PARTY.txt | 6 +- .../app/controllers/_integration/cti.coffee | 83 ++++ .../controllers/_integration/placetel.coffee | 43 +- .../_integration/sipgate_io.coffee | 38 ++ .../controllers/agent_ticket_create.coffee | 2 +- .../javascripts/app/controllers/cti.coffee | 85 +++- .../app/controllers/widget/remote_task.coffee | 15 + .../app/lib/mixins/view_helpers.coffee | 14 + .../app/views/cti/caller_log.jst.eco | 89 +++-- .../app/views/cti/caller_log_avatar.jst.eco | 2 + .../app/views/integration/cti.jst.eco | 27 ++ .../app/views/integration/placetel.jst.eco | 29 +- .../app/views/integration/sipgate.jst.eco | 48 ++- .../app/views/navigation/menu.jst.eco | 2 +- .../views/navigation/menu_cti_ringing.jst.eco | 60 +++ app/assets/stylesheets/zammad.scss | 90 ++++- app/controllers/cti_controller.rb | 2 +- app/controllers/integration/cti_controller.rb | 92 ++--- .../integration/placetel_controller.rb | 202 ++-------- .../integration/sipgate_controller.rb | 111 ++---- app/models/cti/caller_id.rb | 26 ++ app/models/cti/driver/base.rb | 223 +++++++++++ app/models/cti/driver/cti.rb | 7 + app/models/cti/driver/placetel.rb | 139 +++++++ app/models/cti/driver/sipgate_io.rb | 107 +++++ app/models/cti/log.rb | 116 ++++-- config/routes/integration_sipgate.rb | 4 +- spec/models/cti/caller_id_spec.rb | 33 ++ spec/models/cti/log_spec.rb | 224 ++++++++++- spec/requests/integration/cti_spec.rb | 372 ++++++++++++++---- test/browser/integration_cti_test.rb | 12 +- 31 files changed, 1840 insertions(+), 463 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/widget/remote_task.coffee create mode 100644 app/assets/javascripts/app/views/cti/caller_log_avatar.jst.eco create mode 100644 app/assets/javascripts/app/views/navigation/menu_cti_ringing.jst.eco create mode 100644 app/models/cti/driver/base.rb create mode 100644 app/models/cti/driver/cti.rb create mode 100644 app/models/cti/driver/placetel.rb create mode 100644 app/models/cti/driver/sipgate_io.rb diff --git a/LICENSE-3RD-PARTY.txt b/LICENSE-3RD-PARTY.txt index 27261b6ee..aba2598d6 100644 --- a/LICENSE-3RD-PARTY.txt +++ b/LICENSE-3RD-PARTY.txt @@ -57,7 +57,7 @@ License: MIT license highlight.pack.js Source: https://highlightjs.org Copyright: 2006 Ivan Sagalaev (https://github.com/isagalaev) -License: BSD License +License: BSD License (BSD-3-Clause) ----------------------------------------------------------------------------- jquery.flot.js Source: https://github.com/dnschnur/flot @@ -74,7 +74,7 @@ jquery.fineuploader-3.0.js Source: http://github.com/Valums-File-Uploader/file-uploader Copyright: 2010 Andrew Valums 2012 Ray Nicholus -License: MIT license, GNU GPL 2 or later, GNU LGPL 2 or later +License: GNU GPL 2 or later, GNU LGPL 2 or later ----------------------------------------------------------------------------- jquery.noty.js Source: https://github.com/needim/noty/ @@ -171,7 +171,7 @@ License: MIT license Font Awesome icon font Source: http://fontawesome.io/ Copyright: Font Awesome by Dave Gandy - http://fontawesome.io -License: SIL OFL 1.1 +License: MIT License ----------------------------------------------------------------------------- Simple line icons font Source: https://github.com/thesabbir/simple-line-icons diff --git a/app/assets/javascripts/app/controllers/_integration/cti.coffee b/app/assets/javascripts/app/controllers/_integration/cti.coffee index 88fb87b43..4068b9480 100644 --- a/app/assets/javascripts/app/controllers/_integration/cti.coffee +++ b/app/assets/javascripts/app/controllers/_integration/cti.coffee @@ -26,8 +26,10 @@ class Form extends App.Controller 'submit form': 'update' 'click .js-inboundBlockCallerId .js-add': 'addInboundBlockCallerId' 'click .js-outboundRouting .js-add': 'addOutboundRouting' + 'click .js-notifyMap .js-addMap': 'addNotifyMap' 'click .js-inboundBlockCallerId .js-remove': 'removeInboundBlockCallerId' 'click .js-outboundRouting .js-remove': 'removeOutboundRouting' + 'click .js-notifyMap .js-removeMap': 'removeNotifyMap' constructor: -> super @@ -43,6 +45,8 @@ class Form extends App.Controller config.inbound = {} if !config.inbound.block_caller_ids config.inbound.block_caller_ids = [] + if !config.notify_map + config.notify_map = [] config setConfig: (value) -> @@ -56,6 +60,32 @@ class Form extends App.Controller cti_token: App.Setting.get('cti_token') ) + # placeholder + configure_attributes = [ + { name: 'user_ids', display: '', tag: 'column_select', multiple: true, null: true, relation: 'User', sortBy: 'firstname' }, + ] + new App.ControllerForm( + el: @$('.js-userSelectorBlank') + model: + configure_attributes: configure_attributes, + params: + user_ids: [] + autofocus: false + ) + + for row in @config.notify_map + configure_attributes = [ + { name: 'user_ids', display: '', tag: 'column_select', multiple: true, null: true, relation: 'User', sortBy: 'firstname' }, + ] + new App.ControllerForm( + el: @$("[name=queue][value='#{row.queue}']").closest('tr').find('.js-userSelector') + model: + configure_attributes: configure_attributes, + params: + user_ids: row.user_ids + autofocus: false + ) + updateCurrentConfig: => config = @config cleanupInput = @cleanupInput @@ -88,6 +118,17 @@ class Form extends App.Controller } ) + # notify map + config.notify_map = [] + @$('.js-notifyMap .js-row').each(-> + queue = $(@).find('input[name="queue"]').val() + user_ids = $(@).find('select[name="user_ids"]').val() + config.notify_map.push { + queue: cleanupInput(queue) + user_ids: user_ids + } + ) + @config = config update: (e) => @@ -127,6 +168,41 @@ class Form extends App.Controller } @render() + addNotifyMap: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + queue = @cleanupInput(element.find('input[name="queue"]').val()) + user_ids = element.find('select[name="user_ids"]').val() + if _.isEmpty(queue) + @notify( + type: 'error' + msg: App.i18n.translateContent('A queue is required!') + timeout: 6000 + ) + return + if _.isEmpty(user_ids) + @notify( + type: 'error' + msg: App.i18n.translateContent('A user is required!') + timeout: 6000 + ) + return + + for row in @config.notify_map + if row.queue is queue + @notify( + type: 'error' + msg: App.i18n.translateContent('Queue already exists!') + timeout: 6000 + ) + return + @config.notify_map.push { + queue: queue + user_ids: user_ids + } + @render() + removeInboundBlockCallerId: (e) => e.preventDefault() @updateCurrentConfig() @@ -141,6 +217,13 @@ class Form extends App.Controller element.remove() @updateCurrentConfig() + removeNotifyMap: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + element.remove() + @updateCurrentConfig() + class State @current: -> App.Setting.get('cti_integration') diff --git a/app/assets/javascripts/app/controllers/_integration/placetel.coffee b/app/assets/javascripts/app/controllers/_integration/placetel.coffee index 21052ef7e..f82d0c045 100644 --- a/app/assets/javascripts/app/controllers/_integration/placetel.coffee +++ b/app/assets/javascripts/app/controllers/_integration/placetel.coffee @@ -25,9 +25,11 @@ class Form extends App.Controller events: 'submit form': 'update' 'click .js-inboundBlockCallerId .js-add': 'addInboundBlockCallerId' - 'click .js-outboundRouting .js-add': 'addOutboundRouting' 'click .js-inboundBlockCallerId .js-remove': 'removeInboundBlockCallerId' + 'click .js-outboundRouting .js-add': 'addOutboundRouting' 'click .js-outboundRouting .js-remove': 'removeOutboundRouting' + 'click .js-userDeviceMap .js-add': 'addUserDeviceMap' + 'click .js-userDeviceMap .js-remove': 'removeUserDeviceMap' constructor: -> super @@ -43,6 +45,8 @@ class Form extends App.Controller config.inbound = {} if !config.inbound.block_caller_ids config.inbound.block_caller_ids = [] + if !config.user_device_map + config.user_device_map = [] config setConfig: (value) -> @@ -60,7 +64,7 @@ class Form extends App.Controller config = @config cleanupInput = @cleanupInput - config.api_token = @$('input[name=api_token]').val() + config.api_token = cleanupInput(@$('input[name=api_token]').val()) # default caller_id default_caller_id = @$('input[name=default_caller_id]').val() @@ -90,6 +94,17 @@ class Form extends App.Controller } ) + # user device map + config.user_device_map = [] + @$('.js-userDeviceMap .js-row').each(-> + device_id = $(@).find('input[name="device_id"]').val() + user_id = $(@).find('input[name="user_id"]').val() + config.user_device_map.push { + device_id: device_id + user_id: user_id + } + ) + @config = config update: (e) => @@ -114,6 +129,13 @@ class Form extends App.Controller } @render() + removeInboundBlockCallerId: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + element.remove() + @updateCurrentConfig() + addOutboundRouting: (e) => e.preventDefault() @updateCurrentConfig() @@ -129,14 +151,27 @@ class Form extends App.Controller } @render() - removeInboundBlockCallerId: (e) => + removeOutboundRouting: (e) => e.preventDefault() @updateCurrentConfig() element = $(e.currentTarget).closest('tr') element.remove() @updateCurrentConfig() - removeOutboundRouting: (e) => + addUserDeviceMap: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + user_id = @cleanupInput(element.find('input[name="user_id"]').val()) + device_id = @cleanupInput(element.find('input[name="device_id"]').val()) + return if _.isEmpty(user_id) || _.isEmpty(device_id) + @config.user_device_map.push { + user_id: user_id + device_id: device_id + } + @render() + + removeUserDeviceMap: (e) => e.preventDefault() @updateCurrentConfig() element = $(e.currentTarget).closest('tr') diff --git a/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee b/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee index 060e0c2d2..9f502dbaa 100644 --- a/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee +++ b/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee @@ -28,6 +28,8 @@ class Form extends App.Controller 'click .js-outboundRouting .js-add': 'addOutboundRouting' 'click .js-inboundBlockCallerId .js-remove': 'removeInboundBlockCallerId' 'click .js-outboundRouting .js-remove': 'removeOutboundRouting' + 'click .js-userRemoteMap .js-add': 'addUserRemoteMap' + 'click .js-userRemoteMap .js-remove': 'removeUserRemoteMap' constructor: -> super @@ -43,6 +45,8 @@ class Form extends App.Controller config.inbound = {} if !config.inbound.block_caller_ids config.inbound.block_caller_ids = [] + if !config.user_remote_map + config.user_remote_map = [] config setConfig: (value) -> @@ -59,6 +63,9 @@ class Form extends App.Controller config = @config cleanupInput = @cleanupInput + config.api_user = cleanupInput(@$('input[name=api_user]').val()) + config.api_password = cleanupInput(@$('input[name=api_password]').val()) + # default caller_id default_caller_id = @$('input[name=default_caller_id]').val() config.outbound.default_caller_id = cleanupInput(default_caller_id) @@ -87,6 +94,17 @@ class Form extends App.Controller } ) + # user device map + config.user_remote_map = [] + @$('.js-userRemoteMap .js-row').each(-> + remote_user_id = $(@).find('input[name="remote_user_id"]').val() + user_id = $(@).find('input[name="user_id"]').val() + config.user_remote_map.push { + remote_user_id: remote_user_id + user_id: user_id + } + ) + @config = config update: (e) => @@ -140,6 +158,26 @@ class Form extends App.Controller element.remove() @updateCurrentConfig() + addUserRemoteMap: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + user_id = @cleanupInput(element.find('input[name="user_id"]').val()) + remote_user_id = @cleanupInput(element.find('input[name="remote_user_id"]').val()) + return if _.isEmpty(user_id) || _.isEmpty(remote_user_id) + @config.user_remote_map.push { + user_id: user_id + remote_user_id: remote_user_id + } + @render() + + removeUserRemoteMap: (e) => + e.preventDefault() + @updateCurrentConfig() + element = $(e.currentTarget).closest('tr') + element.remove() + @updateCurrentConfig() + class State @current: -> App.Setting.get('sipgate_integration') diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee index e7decf400..11e4716f6 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee @@ -199,7 +199,7 @@ class App.TicketCreate extends App.Controller if _.isEmpty(params.ticket_id) && _.isEmpty(params.article_id) if !_.isEmpty(params.customer_id) - @renderQueue(options: { customer_id: params.customer_id }) + @renderQueue(options: params) return @renderQueue() return diff --git a/app/assets/javascripts/app/controllers/cti.coffee b/app/assets/javascripts/app/controllers/cti.coffee index 8bc773f72..9244701b9 100644 --- a/app/assets/javascripts/app/controllers/cti.coffee +++ b/app/assets/javascripts/app/controllers/cti.coffee @@ -6,7 +6,7 @@ class App.CTI extends App.Controller '.js-callerLog': 'callerLog' events: 'click .js-check': 'done' - 'click .js-userNew': 'userNew' + 'click .js-newUser': 'newUser' list: [] backends: [] meta: @@ -32,9 +32,30 @@ class App.CTI extends App.Controller return if data.state isnt 'newCall' return if data.direction isnt 'in' return if @switch() isnt true - @notify(data) + if !document.hasFocus() + @notify(data) 'cti_event' ) + @bind('menu:render', (data) => + return if @switch() isnt true + localHtml = '' + for item in @ringingCalls() + localHtml += App.view('navigation/menu_cti_ringing')( + item: item + ) + $('.js-phoneMenuItem').after(localHtml) + $('.call-widget').find('.js-newUser').bind('click', (e) => + @newUser(e) + ) + $('.call-widget').find('.js-newTicket').bind('click', (e) => + user = undefined + user_id = $(e.currentTarget).data('user-id') + if user_id + user = App.User.find(user_id) + console.log('user_id', user_id, user) + @newTicket(user) + ) + ) @bind('auth', (data) => @meta.counter = 0 ) @@ -57,6 +78,13 @@ class App.CTI extends App.Controller @initSpoolSent = true ) + ringingCalls: => + ringing = [] + for row in @list + if row.state is 'newCall' && row.done is false + ringing.push row + ringing + # fetch data, render view load: -> @ajax( @@ -148,8 +176,18 @@ class App.CTI extends App.Controller item.disabled = false @removePopovers() - @callerLog.html(App.view('cti/caller_log')(list: @list)) - @renderPopovers() + + list = $(App.view('cti/caller_log')(list: @list)) + list.find('.js-avatar').each( -> + $element = $(@) + new WidgetAvatar( + el: $element + object_id: $element.attr('data-id') + level: $element.attr('data-level') + size: 40 + ) + ) + @callerLog.html(list) @updateNavMenu() @@ -163,9 +201,15 @@ class App.CTI extends App.Controller data: JSON.stringify(done: done) ) - userNew: (e) -> + newTicket: (user) => + if user + @navigate("ticket/create/customer/#{user.id}") + return + @navigate('ticket/create') + + newUser: (e) -> e.preventDefault() - phone = $(e.currentTarget).text() + phone = $(e.currentTarget).data('phone') new App.ControllerGenericNew( pageData: title: 'Users' @@ -176,7 +220,7 @@ class App.CTI extends App.Controller genericObject: 'User' item: phone: phone - container: @el.closest('.content') + #container: @el.closest('.content') callback: @ticketNew ) @@ -221,6 +265,33 @@ class App.CTI extends App.Controller currentPosition: => @$('.main').scrollTop() +class WidgetAvatar extends App.ObserverController + @extend App.PopoverProvidable + @registerPopovers 'User' + + model: 'User' + observe: + login: true + firstname: true + lastname: true + organization_id: true + email: true + image: true + vip: true + out_of_office: true, + out_of_office_start_at: true, + out_of_office_end_at: true, + out_of_office_replacement_id: true, + active: true + + globalRerender: false + + render: (user) => + classes = ['user-popover', 'u-textTruncate'] + classes.push('is-inactive') if !user.active + @html(App.view('cti/caller_log_avatar')(user: user, classes: classes, level: @level)) + @renderPopovers() + class CTIRouter extends App.ControllerPermanent requiredPermission: 'cti.agent' constructor: (params) -> diff --git a/app/assets/javascripts/app/controllers/widget/remote_task.coffee b/app/assets/javascripts/app/controllers/widget/remote_task.coffee new file mode 100644 index 000000000..62368517e --- /dev/null +++ b/app/assets/javascripts/app/controllers/widget/remote_task.coffee @@ -0,0 +1,15 @@ +class Widget extends App.Controller + serverRestarted: false + constructor: -> + super + + App.Event.bind( + 'remote_task' + (data) => + console.log('remote_task', data) + App.TaskManager.execute(data) + @navigate(data.url) + 'remote_task' + ) + +App.Config.set('remote_task', Widget, 'Widgets') diff --git a/app/assets/javascripts/app/lib/mixins/view_helpers.coffee b/app/assets/javascripts/app/lib/mixins/view_helpers.coffee index be4666f28..ea09574f9 100644 --- a/app/assets/javascripts/app/lib/mixins/view_helpers.coffee +++ b/app/assets/javascripts/app/lib/mixins/view_helpers.coffee @@ -200,6 +200,20 @@ App.ViewHelpers = return true if contentType.match(/image\/(png|jpg|jpeg|gif)/i) false + unique_avatar: (seed, text, size = 40) -> + baseSize = 40 + width = 300 * size/baseSize + height = 226 * size/baseSize + + rng = new Math.seedrandom(seed) + x = rng() * (width - size) + y = rng() * (height - size) + + return App.view('avatar_unique') + x: x + y: y + initials: text + # icon with modifier based on visibility state # params: className, iconset, addStateClass iconWithModifier: (item, params) -> diff --git a/app/assets/javascripts/app/views/cti/caller_log.jst.eco b/app/assets/javascripts/app/views/cti/caller_log.jst.eco index 8f70744e7..4a7cec7cc 100644 --- a/app/assets/javascripts/app/views/cti/caller_log.jst.eco +++ b/app/assets/javascripts/app/views/cti/caller_log.jst.eco @@ -13,9 +13,9 @@ <% for item in @list: %> - class="is-grayed-out"<% end %> data-id="<%- item.id %>"> - -