From c21f1ce93f20cbc7b22836e1b2956707d5f71d45 Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Tue, 15 Sep 2015 15:11:00 +0200 Subject: [PATCH] Init sla management feature. --- .../_application_controller_form.js.coffee | 388 +----------------- .../_application_controller_generic.js.coffee | 2 + .../_ui_element/autocompletion_ajax.js.coffee | 18 + .../controllers/_ui_element/input.js.coffee | 3 + .../controllers/_ui_element/select.js.coffee | 2 +- .../_ui_element/sla_times.js.coffee | 46 ++- .../_ui_element/ticket_selector.js.coffee | 267 ++++++++++++ .../javascripts/app/controllers/sla.js.coffee | 126 +++++- .../app/controllers/users.js.coffee | 4 +- .../app/models/organization.js.coffee | 3 +- .../javascripts/app/models/sla.js.coffee | 10 +- .../javascripts/app/models/ticket.js.coffee | 16 +- .../javascripts/app/models/user.js.coffee | 1 + .../app/views/calendar/index.jst.eco | 22 +- app/controllers/slas_controller.rb | 23 +- test/browser/manage_test.rb | 10 +- test/browser_test_helper.rb | 10 +- 17 files changed, 516 insertions(+), 435 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/_ui_element/autocompletion_ajax.js.coffee create mode 100644 app/assets/javascripts/app/controllers/_ui_element/input.js.coffee create mode 100644 app/assets/javascripts/app/controllers/_ui_element/ticket_selector.js.coffee diff --git a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee index 3696718ac..a0e836fd1 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee @@ -227,284 +227,6 @@ class App.ControllerForm extends App.Controller if App.UiElement[attribute.tag] item = App.UiElement[attribute.tag].render(attribute, @params, @) - # working_hour - else if attribute.tag is 'time_before_last' - if !attribute.value - attribute.value = {} - item = $( App.view('generic/time_before_last')( attribute: attribute ) ) - item.find( "[name=\"#{attribute.name}::direction\"]").find("option[value=\"#{attribute.value.direction}\"]").attr( 'selected', 'selected' ) - item.find( "[name=\"#{attribute.name}::count\"]").find("option[value=\"#{attribute.value.count}\"]").attr( 'selected', 'selected' ) - item.find( "[name=\"#{attribute.name}::area\"]").find("option[value=\"#{attribute.value.area}\"]").attr( 'selected', 'selected' ) - - # ticket attribute set - else if attribute.tag is 'ticket_attribute_set' - - # list of possible attributes - item = $( - App.view('generic/ticket_attribute_manage')( - attribute: attribute - ) - ) - - addShownAttribute = ( key, value ) => - parts = key.split(/::/) - key = parts[0] - type = parts[1] - if key is 'tickets.title' - attribute_config = { - name: attribute.name + '::tickets.title' - display: 'Title' - tag: 'input' - type: 'text' - null: false - value: value - remove: true - } - else if key is 'tickets.group_id' - attribute_config = { - name: attribute.name + '::tickets.group_id' - display: 'Group' - tag: 'select' - multiple: false - null: false - nulloption: false - relation: 'Group' - value: value - remove: true - } - else if key is 'tickets.owner_id' || key is 'tickets.customer_id' - display = 'Owner' - name = 'owner_id' - if key is 'customer_id' - display = 'Customer' - name = 'customer_id' - attribute_config = { - name: attribute.name + '::tickets.' + name - display: display - tag: 'select' - multiple: false - null: false - nulloption: false - relation: 'User' - value: value || null - remove: true - filter: ( all, type ) -> - return all if type isnt 'collection' - all = _.filter( all, (item) -> - return if item.id is 1 - return item - ) - all.unshift( { - id: '' - name: '--' - } ) - all.unshift( { - id: 1 - name: '*** not set ***' - } ) - all.unshift( { - id: 'current_user.id' - name: '*** current user ***' - } ) - all - } - else if key is 'tickets.organization_id' - attribute_config = { - name: attribute.name + '::tickets.organization_id' - display: 'Organization' - tag: 'select' - multiple: false - null: false - nulloption: false - relation: 'Organization' - value: value || null - remove: true - filter: ( all, type ) -> - return all if type isnt 'collection' - all.unshift( { - id: '' - name: '--' - } ) - all.unshift( { - id: 'current_user.organization_id' - name: '*** organization of current user ***' - } ) - all - } - else if key is 'tickets.state_id' - attribute_config = { - name: attribute.name + '::tickets.state_id' - display: 'State' - tag: 'select' - multiple: false - null: false - nulloption: false - relation: 'TicketState' - value: value - translate: true - remove: true - } - else if key is 'tickets.priority_id' - attribute_config = { - name: attribute.name + '::tickets.priority_id' - display: 'Priority' - tag: 'select' - multiple: false - null: false - nulloption: false - relation: 'TicketPriority' - value: value - translate: true - remove: true - } - else - attribute_config = { - name: attribute.name + '::' + key - display: 'FIXME!' - tag: 'input' - type: 'text' - value: value - remove: true - } - item.find('select[name=ticket_attribute_list] option[value="' + key + '"]').hide().prop('disabled', true) - - itemSub = @formGenItem( attribute_config ) - itemSub.find('.glyphicon-minus').bind('click', (e) -> - e.preventDefault() - value = $(e.target).closest('.controls').find('[name]').attr('name') - if value - value = value.replace("#{attribute.name}::", '') - $(e.target).closest('.sub_attribute').find('select[name=ticket_attribute_list] option[value="' + value + '"]').show().prop('disabled', false) - $(@).parent().parent().parent().remove() - ) -# itemSub.append('') - item.find('.ticket_attribute_item').append( itemSub ) - - # list of existing attributes - attribute_config = { - name: 'ticket_attribute_list' - display: 'Add Attribute' - tag: 'select' - multiple: false - null: false -# nulloption: true - options: [ - { - value: '' - name: '-- Ticket --' - selected: false - disable: true - }, - { - value: 'tickets.title' - name: 'Title' - selected: false - disable: false - }, - { - value: 'tickets.group_id' - name: 'Group' - selected: false - disable: false - }, - { - value: 'tickets.state_id' - name: 'State' - selected: false - disable: false - }, - { - value: 'tickets.priority_id' - name: 'Priority' - selected: true - disable: false - }, - { - value: 'tickets.owner_id' - name: 'Owner' - selected: true - disable: false - }, -# # { -# value: 'tag' -# name: 'Tag' -# selected: true -# disable: false -# }, -# { -# value: '-a' -# name: '-- ' + App.i18n.translateInline('Article') + ' --' -# selected: false -# disable: true -# }, -# { -# value: 'ticket_articles.from' -# name: 'From' -# selected: true -# disable: false -# }, -# { -# value: 'ticket_articles.to' -# name: 'To' -# selected: true -# disable: false -# }, -# { -# value: 'ticket_articles.cc' -# name: 'Cc' -# selected: true -# disable: false -# }, -# { -# value: 'ticket_articles.subject' -# name: 'Subject' -# selected: true -# disable: false -# }, -# { -# value: 'ticket_articles.body' -# name: 'Text' -# selected: true -# disable: false -# }, - { - value: '-c' - name: '-- ' + App.i18n.translateInline('Customer') + ' --' - selected: false - disable: true - }, - { - value: 'customers.id' - name: 'Customer' - selected: true - disable: false - }, - { - value: 'organization.id' - name: 'Organization' - selected: true - disable: false - }, - ] - default: '' - translate: true - class: 'medium' - add: true - } - list = @formGenItem( attribute_config ) - list.find('.glyphicon-plus').bind('click', (e) -> - e.preventDefault() - value = $(e.target).closest('.controls').find('[name=ticket_attribute_list]').val() - addShownAttribute( value, '' ) - ) - item.find('.ticket_attribute_list').prepend( list ) - - # list of shown attributes - show = [] - if attribute.value - for key, value of attribute.value - addShownAttribute( key, value ) - # ticket attribute selection else if attribute.tag is 'ticket_attribute_selection' @@ -948,92 +670,8 @@ class App.ControllerForm extends App.Controller for key, value of attribute.value addShownAttribute( key, value ) - # timeplan - else if attribute.tag is 'timeplan' - item = $( App.view('generic/timeplan')( attribute: attribute ) ) - attribute_config = { - name: "#{attribute.name}::days" - tag: 'select' - multiple: true - null: false - options: [ - { - value: 'mon' - name: 'Monday' - selected: false - disable: false - }, - { - value: 'tue' - name: 'Tuesday' - selected: false - disable: false - }, - { - value: 'wed' - name: 'Wednesday' - selected: false - disable: false - }, - { - value: 'thu' - name: 'Thursday' - selected: false - disable: false - }, - { - value: 'fri' - name: 'Friday' - selected: false - disable: false - }, - { - value: 'sat' - name: 'Saturday' - selected: false - disable: false - }, - { - value: 'sun' - name: 'Sunday' - selected: false - disable: false - }, - ] - default: attribute.default?.days - } - item.find('.days').append( @formGenItem( attribute_config ) ) - - hours = {} - for hour in [0..23] - localHour = "0#{hour}" - hours[hour] = localHour.substr(localHour.length-2,2) - attribute_config = { - name: "#{attribute.name}::hours" - tag: 'select' - multiple: true - null: false - options: hours - default: attribute.default?.hours - } - item.find('.hours').append( @formGenItem( attribute_config ) ) - - minutes = {} - for minute in [0..5] - minutes["#{minute}0"] = "#{minute}0" - attribute_config = { - name: "#{attribute.name}::minutes" - tag: 'select' - multiple: true - null: false - options: minutes - default: attribute.default?.miuntes - } - item.find('.minutes').append( @formGenItem( attribute_config ) ) - - # input else - item = $( App.view('generic/input')( attribute: attribute ) ) + throw "Invalid UiElement.#{attribute.tag}" if @handlers item.bind('change', (e) => @@ -1193,9 +831,9 @@ class App.ControllerForm extends App.Controller continue # collect all params, push it to an array if already exists - if param[key.name] + if param[key.name] isnt undefined if typeof param[key.name] is 'string' - param[key.name] = [ param[key.name], key.value] + param[key.name] = [param[key.name], key.value] else param[key.name].push key.value else @@ -1293,17 +931,15 @@ class App.ControllerForm extends App.Controller inputSelectObject = {} for key of param parts = key.split '::' - if parts[0] && parts[1] && !parts[2] - if !inputSelectObject[ parts[0] ] + if parts[0] && parts[1] + if !(parts[0] of inputSelectObject) inputSelectObject[ parts[0] ] = {} - inputSelectObject[ parts[0] ][ parts[1] ] = param[ key ] - delete param[ key ] - if parts[0] && parts[1] && parts[2] - if !inputSelectObject[ parts[0] ] - inputSelectObject[ parts[0] ] = {} - if !inputSelectObject[ parts[0] ][ parts[1] ] - inputSelectObject[ parts[0] ][ parts[1] ] = {} - inputSelectObject[ parts[0] ][ parts[1] ][ parts[2] ] = param[ key ] + if !parts[2] + inputSelectObject[ parts[0] ][ parts[1] ] = param[ key ] + else + if !(parts[1] of inputSelectObject[ parts[0] ]) + inputSelectObject[ parts[0] ][ parts[1] ] = {} + inputSelectObject[ parts[0] ][ parts[1] ][ parts[2] ] = param[ key ] delete param[ key ] # set new object params @@ -1311,7 +947,7 @@ class App.ControllerForm extends App.Controller param[ key ] = inputSelectObject[ key ] #App.Log.notice 'ControllerForm', 'formParam', form, param - return param + param @formId: -> formId = new Date().getTime() + Math.floor( Math.random() * 99999 ) diff --git a/app/assets/javascripts/app/controllers/_application_controller_generic.js.coffee b/app/assets/javascripts/app/controllers/_application_controller_generic.js.coffee index 7cec42683..0123a8ecd 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_generic.js.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_generic.js.coffee @@ -196,6 +196,7 @@ class App.ControllerGenericIndex extends App.Controller pageData: @pageData genericObject: @genericObject container: @container + large: @large ) new: (e) -> @@ -204,6 +205,7 @@ class App.ControllerGenericIndex extends App.Controller pageData: @pageData genericObject: @genericObject container: @container + large: @large ) description: (e) => diff --git a/app/assets/javascripts/app/controllers/_ui_element/autocompletion_ajax.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/autocompletion_ajax.js.coffee new file mode 100644 index 000000000..c08ad6ab5 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_ui_element/autocompletion_ajax.js.coffee @@ -0,0 +1,18 @@ +class App.UiElement.autocompletion_ajax + @render: (attribute, params = {}) -> + if params[attribute.name] + object = App[attribute.relation].find(params[attribute.name]) + valueName = object.displayName() + + # selectable search + searchableAjaxSelectObject = new App.SearchableAjaxSelect( + attribute: + value: params[attribute.name] + valueName: valueName + name: attribute.name + id: params.organization_id + placeholder: App.i18n.translateInline('Search...') + limt: 10 + object: attribute.relation + ) + searchableAjaxSelectObject.element() diff --git a/app/assets/javascripts/app/controllers/_ui_element/input.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/input.js.coffee new file mode 100644 index 000000000..d34638671 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_ui_element/input.js.coffee @@ -0,0 +1,3 @@ +class App.UiElement.input + @render: (attribute) -> + $( App.view('generic/input')( attribute: attribute ) ) diff --git a/app/assets/javascripts/app/controllers/_ui_element/select.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/select.js.coffee index a290eb92a..66fc12e44 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/select.js.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/select.js.coffee @@ -1,5 +1,5 @@ class App.UiElement.select extends App.UiElement.ApplicationUiElement - @render: (attribute, params, form_controller) -> + @render: (attribute, params) -> # set multiple option if attribute.multiple diff --git a/app/assets/javascripts/app/controllers/_ui_element/sla_times.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/sla_times.js.coffee index 76b18f04d..4ebe5febf 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/sla_times.js.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/sla_times.js.coffee @@ -1,4 +1,46 @@ class App.UiElement.sla_times - @render: (attribute) -> + @render: (attribute, params = {}) -> - $( App.view('generic/sla_times')( attribute: attribute ) ) + item = $( App.view('generic/sla_times')( + attribute: attribute + first_response_time: params.first_response_time + update_time: params.update_time + close_time: params.close_time + first_response_time_in_text: @toText(params.first_response_time) + update_time_in_text: @toText(params.update_time) + close_time_in_text: @toText(params.close_time) + ) ) + + item.find('.js-timeConvertFrom').bind('keyup', (e) => + inText = $(e.target).val() + inMinutes = @toMinutes(inText) + if !inMinutes + $(e.target).addClass('has-error') + else + $(e.target).removeClass('has-error') + dest = $(e.target).closest('td').find('.js-timeConvertTo') + dest.val(inMinutes) + ) + + item + + @toMinutes: (hh) -> + hh = hh.split(':') + hour = parseInt(hh[0]) + minute = parseInt(hh[1]) + return if hour is NaN + return if minute is NaN + (hour * 60) + minute + + @toText: (m) -> + m = parseInt(m) + return if !m + minutes = m % 60 + hours = Math.floor(m / 60) + + if minutes < 10 + minutes = "0#{minutes}" + if hours < 10 + hours = "0#{hours}" + + "#{hours}:#{minutes}" diff --git a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.js.coffee b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.js.coffee new file mode 100644 index 000000000..378333ff9 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.js.coffee @@ -0,0 +1,267 @@ +class App.UiElement.ticket_selector extends App.UiElement.ApplicationUiElement + @render: (attribute, params = {}) -> + + # list of attributes + groups = + tickets: + name: 'Ticket' + model: 'Ticket' + users: + name: 'Customer' + model: 'User' + organizations: + name: 'Organization' + model: 'Organization' + + elements = + tickets: + title: + tag: 'input' + operator: ['contains', 'contains not'] + number: + tag: 'input' + operator: ['contains', 'contains not'] + group_id: + relation: 'Group' + tag: 'select' + multible: true + operator: ['is', 'is not'] + priority_id: + relation: 'Priority' + tag: 'select' + multible: true + operator: ['is', 'is not'] + state_id: + relation: 'State' + tag: 'select' + multible: true + operator: ['is', 'is not'] + owner_id: + tag: 'user_selection' + relation: 'User' + operator: ['is', 'is not'] + customer_id: + tag: 'user_selection' + relation: 'User' + operator: ['is', 'is not'] + organization_id: + tag: '' + relation: 'Organization' + operator: ['is', 'is not'] + tag: + tag: 'tag' + multible: true + operator: ['is', 'is not'] + created_at: + tag: 'timestamp' + operator: ['before', 'after'] + updated_at: + tag: 'timestamp' + operator: ['before', 'after'] + escalation_time: + tag: 'timestamp' + operator: ['before', 'after'] + users: + firstname: + tag: 'input' + operator: ['contains', 'contains not'] + lastname: + tag: 'input' + operator: ['contains', 'contains not'] + email: + tag: 'input' + operator: ['contains', 'contains not'] + login: + tag: 'input' + operator: ['contains', 'contains not'] + created_at: + tag: 'time_selector_enhanced' + operator: ['before', 'after'] + updated_at: + tag: 'time_selector_enhanced' + operator: ['before', 'after'] + organizations: + name: + tag: 'input' + operator: ['contains', 'contains not'] + shared: + tag: 'boolean' + operator: ['is', 'is not'] + created_at: + tag: 'time_selector_enhanced' + operator: ['before', 'after'] + updated_at: + tag: 'time_selector_enhanced' + operator: ['before', 'after'] + + # megre config + for groupKey, groupMeta of groups + for elementKey, elementGroup of elements + if elementKey is groupKey + configure_attributes = App[groupMeta.model].configure_attributes + for attributeName, attributeConfig of elementGroup + for attribute in configure_attributes + if attribute.name is attributeName + attributeConfig.config = attribute + + selector = @buildAttributeSelector(groups, elements) + + # return item + item = $( App.view('generic/ticket_selector')( attribute: attribute ) ) + item.find('.js-attributeSelector').prepend(selector) + + # add filter + item.find('.js-add').bind('click', (e) => + element = $(e.target).closest('.js-filterElement') + elementClone = element.clone(true) + element.after(elementClone) + elementClone.find('.js-attributeSelector select').trigger('change') + ) + + # remove filter + item.find('.js-remove').bind('click', (e) => + $(e.target).closest('.js-filterElement').remove() + @rebuildAttributeSelectors(item) + ) + + # change filter + item.find('.js-attributeSelector select').bind('change', (e) => + groupAndAttribute = $(e.target).find('option:selected').attr('value') + elementRow = $(e.target).closest('.js-filterElement') + + console.log('CHANGE', groupAndAttribute, $(e.target)) + + @rebuildAttributeSelectors(item, elementRow, groupAndAttribute) + @rebuildOperater(item, elementRow, groupAndAttribute, elements) + @buildValue(item, elementRow, groupAndAttribute, elements) + ) + + # build inital params + console.log('P', params) + if !_.isEmpty(params.condition) + selectorExists = false + for position of params.condition.attribute + + # get stored params + groupAndAttribute = params.condition.attribute[position] + if params.condition[groupAndAttribute] + selectorExists = true + operator = params.condition[groupAndAttribute].operator + value = params.condition[groupAndAttribute].value + + # get selector rows + elementFirst = item.find('.js-filterElement').first() + elementLast = item.find('.js-filterElement').last() + + # clone, rebuild and append + elementClone = elementFirst.clone(true) + @rebuildAttributeSelectors(item, elementClone, groupAndAttribute) + @rebuildOperater(item, elementClone, groupAndAttribute, elements, operator) + @buildValue(item, elementClone, groupAndAttribute, elements, value) + elementLast.after(elementClone) + + # remove first dummy row + if selectorExists + item.find('.js-filterElement').first().remove() + item + + @getElementConfig: (groupAndAttribute, elements) -> + for elementGroup, elementConfig of elements + for elementKey, elementItem of elementConfig + if "#{elementGroup}.#{elementKey}" is groupAndAttribute + return elementItem + false + + @buildValue: (elementFull, elementRow, groupAndAttribute, elements, value) -> + + # do nothing if item already exists + name = "condition::#{groupAndAttribute}::value" + return if elementRow.find("[name=\"#{name}\"]").get(0) + + # build new item + attributeConfig = @getElementConfig(groupAndAttribute, elements) + item = '' + if attributeConfig && attributeConfig.config && App.UiElement[attributeConfig.config.tag] + config = _.clone(attributeConfig.config) + config['name'] = name + config['value'] = value + if 'multiple' of config + config.multiple = true + config.nulloption = false + item = App.UiElement[attributeConfig.config.tag].render(config, {}) + elementRow.find('.js-value').html(item) + + @buildAttributeSelector: (groups, elements) -> + selection = $('') + for groupKey, groupMeta of groups + displayName = App.i18n.translateInline(groupMeta.name) + selection.closest('select').append("") + optgroup = selection.find("optgroup.js-#{groupKey}") + for elementKey, elementGroup of elements + if elementKey is groupKey + for attributeName, attributeConfig of elementGroup + if attributeConfig.config && attributeConfig.config.display + displayName = App.i18n.translateInline(attributeConfig.config.display) + else + displayName = App.i18n.translateInline(attributeName) + optgroup.append("") + selection + + @rebuildAttributeSelectors: (elementFull, elementRow, groupAndAttribute) -> + + # enable all + elementFull.find('.js-attributeSelector select option').removeAttr('disabled') + + # disable all used attributes + elementFull.find('.js-attributeSelector select').each(-> + keyLocal = $(@).val() + elementFull.find('.js-attributeSelector select option[value="' + keyLocal + '"]').attr('disabled', true) + elementFull.find('.js-hiddenAttribute').val(keyLocal) + ) + + # disable - if we only have one attribute + if elementFull.find('.js-attributeSelector select').length > 1 + elementFull.find('.js-remove').removeClass('is-disabled') + else + elementFull.find('.js-remove').addClass('is-disabled') + + # set attribute + if groupAndAttribute + elementRow.find('.js-attributeSelector select').val(groupAndAttribute) + elementRow.find('[name="condition::attribute"]').val("#{groupAndAttribute}") + + @buildOperator: (elementFull, elementRow, groupAndAttribute, elements, current_operator) -> + selection = $("") + attributeConfig = @getElementConfig(groupAndAttribute, elements) + for operator in attributeConfig.operator + operatorName = App.i18n.translateInline(operator) + selected = '' + if current_operator is operator + selected = 'selected="selected"' + selection.append("") + selection + + @rebuildOperater: (elementFull, elementRow, groupAndAttribute, elements, current_operator) -> + return if !groupAndAttribute + + # do nothing if item already exists + name = "condition::#{groupAndAttribute}::operator" + return if elementRow.find("[name=\"#{name}\"]").get(0) + + # render new operator + operator = @buildOperator(elementFull, elementRow, groupAndAttribute, elements, current_operator) + elementRow.find('.js-operator select').replaceWith(operator) + + @humanText: (condition) -> + return [] if _.isEmpty(condition) + rules = [] + for position of condition.attribute + + # get stored params + groupAndAttribute = condition.attribute[position] + if condition[groupAndAttribute] + selectorExists = true + operator = condition[groupAndAttribute].operator + value = condition[groupAndAttribute].value + rules.push "Where #{groupAndAttribute} #{operator} #{value}." + rules \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/sla.js.coffee b/app/assets/javascripts/app/controllers/sla.js.coffee index ca0ea6ba5..663c4f5fe 100644 --- a/app/assets/javascripts/app/controllers/sla.js.coffee +++ b/app/assets/javascripts/app/controllers/sla.js.coffee @@ -1,27 +1,119 @@ class Index extends App.ControllerContent + events: + 'click .js-new': 'new' + 'click .js-edit': 'edit' + 'click .js-delete': 'delete' + 'click .js-description': 'description' + constructor: -> super # check authentication return if !@authenticate() - new App.ControllerGenericIndex( - el: @el - id: @id - genericObject: 'Sla' - pageData: - title: 'SLA' - home: 'slas' - object: 'SLA' - objects: 'SLAs' - navupdate: '#slas' - notes: [ -# 'SLA are ...' - ] - buttons: [ - { name: 'New SLA', 'data-type': 'new', class: 'btn--success' } - ] - container: @el.closest('.content') + @load() + #@subscribeId = App.Calendar.subscribe(@render) + + load: => + @ajax( + id: 'sla_index' + type: 'GET' + url: @apiPath + '/slas' + processData: true + success: (data, status, xhr) => + + # load assets + App.Collection.loadAssets(data.assets) + + @render(data) ) + render: => + slas = App.Sla.search( + sortBy: 'name' + ) + for sla in slas + if sla.first_response_time + sla.first_response_time_in_text = @toText(sla.first_response_time) + if sla.update_time + sla.update_time_in_text = @toText(sla.update_time) + if sla.solution_time + sla.solution_time_in_text = @toText(sla.solution_time) + sla.rules = App.UiElement.ticket_selector.humanText(sla.condition) + + # show description button, only if content exists + showDescription = false + if App.Sla.description + if !_.isEmpty(slas) + showDescription = true + else + description = marked(App.Sla.description) + + @html App.view('sla/index')( + slas: slas + showDescription: showDescription + description: description + ) + + release: => + if @subscribeId + App.Calendar.unsubscribe(@subscribeId) + + new: (e) -> + e.preventDefault() + new App.ControllerGenericNew( + pageData: + title: 'SLAs' + object: 'Sla' + objects: 'SLAs' + genericObject: 'Sla' + container: @el.closest('.content') + callback: @load + large: true + ) + + edit: (e) -> + e.preventDefault() + id = $(e.target).closest('.action').data('id') + new App.ControllerGenericEdit( + id: id + pageData: + title: 'SLAs' + object: 'Sla' + objects: 'SLAs' + genericObject: 'Sla' + callback: @load + container: @el.closest('.content') + large: true + ) + + delete: (e) => + e.preventDefault() + id = $(e.target).closest('.action').data('id') + item = App.Sla.find(id) + new App.ControllerGenericDestroyConfirm( + item: item + container: @el.closest('.content') + callback: @load + ) + + description: (e) => + new App.ControllerGenericDescription( + description: App.Calendar.description + container: @el.closest('.content') + ) + + toText: (m) -> + m = parseInt(m) + return if !m + minutes = m % 60 + hours = Math.floor(m / 60) + + if minutes < 10 + minutes = "0#{minutes}" + if hours < 10 + hours = "0#{hours}" + + "#{hours}:#{minutes}" + App.Config.set( 'Sla', { prio: 2900, name: 'SLAs', parent: '#manage', target: '#manage/slas', controller: Index, role: ['Admin'] }, 'NavBarAdmin' ) \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/users.js.coffee b/app/assets/javascripts/app/controllers/users.js.coffee index e17ba7a40..62ab5be40 100644 --- a/app/assets/javascripts/app/controllers/users.js.coffee +++ b/app/assets/javascripts/app/controllers/users.js.coffee @@ -1,8 +1,8 @@ class Index extends App.Controller elements: - '.js-search' : 'searchInput' + '.js-search': 'searchInput' events: - 'click [data-type="new"]': 'new' + 'click [data-type=new]': 'new' constructor: -> super diff --git a/app/assets/javascripts/app/models/organization.js.coffee b/app/assets/javascripts/app/models/organization.js.coffee index 910a6cafb..4d3d2a839 100644 --- a/app/assets/javascripts/app/models/organization.js.coffee +++ b/app/assets/javascripts/app/models/organization.js.coffee @@ -6,8 +6,9 @@ class App.Organization extends App.Model { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false, info: true }, { name: 'shared', display: 'Shared organization', tag: 'boolean', note: 'Customers in the organization can view each other items.', type: 'boolean', default: true, null: false, info: false }, { name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true, info: true }, - { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1, info: false }, { name: 'active', display: 'Active', tag: 'active', default: true, info: false }, + { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1, info: false }, + { name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1, info: false }, ] @configure_overview = [ 'name', diff --git a/app/assets/javascripts/app/models/sla.js.coffee b/app/assets/javascripts/app/models/sla.js.coffee index e621675a7..d3d334b4a 100644 --- a/app/assets/javascripts/app/models/sla.js.coffee +++ b/app/assets/javascripts/app/models/sla.js.coffee @@ -3,12 +3,10 @@ class App.Sla extends App.Model @extend Spine.Model.Ajax @url: @apiPath + '/slas' @configure_attributes = [ - { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, - { name: 'first_response_time', display: 'First Response Time', tag: 'input', type: 'text', limit: 100, null: true, note: 'In minutes, only business times are counted.' }, - { name: 'update_time', display: 'Update Time', tag: 'input', type: 'text', limit: 100, null: true, note: 'In minutes, only business times are counted.' }, - { name: 'close_time', display: 'Solution Time', tag: 'input', type: 'text', limit: 100, null: true, note: 'In minutes, only business times are counted.' }, - { name: 'calendar_id', display: 'Calendar', tag: 'select', relation: 'Calendar', null: false }, - { name: 'condition', display: 'Conditions where SLA is used', tag: 'ticket_attribute_selection', null: true }, + { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, + { name: 'condition', display: 'Selector', tag: 'ticket_selector', null: false, note: 'Create rules that single out the tickets for the Service Level Agreement.' }, + { name: 'calendar_id', display: 'Calendar', tag: 'select', relation: 'Calendar', null: false }, + { name: 'sla_times', display: 'SLA Times', tag: 'sla_times', null: true }, { name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 }, { name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 }, { name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 }, diff --git a/app/assets/javascripts/app/models/ticket.js.coffee b/app/assets/javascripts/app/models/ticket.js.coffee index 7b6343b96..c816cf275 100644 --- a/app/assets/javascripts/app/models/ticket.js.coffee +++ b/app/assets/javascripts/app/models/ticket.js.coffee @@ -3,14 +3,14 @@ class App.Ticket extends App.Model @extend Spine.Model.Ajax @url: @apiPath + '/tickets' @configure_attributes = [ - { name: 'number', display: '#', tag: 'input', type: 'text', limit: 100, null: true, read_only: true, style: 'width: 60px' }, - { name: 'customer_id', display: 'Customer', tag: 'input', type: 'text', limit: 100, null: false, autocapitalize: false, relation: 'User' }, - { name: 'organization_id', display: 'Organization', relation: 'Organization', tagreadonly: 1 }, - { name: 'group_id', display: 'Group', tag: 'select', multiple: false, limit: 100, null: false, relation: 'Group', style: 'width: 10%', edit: true }, - { name: 'owner_id', display: 'Owner', tag: 'select', multiple: false, limit: 100, null: true, relation: 'User', style: 'width: 12%', edit: true }, - { name: 'title', display: 'Title', tag: 'input', type: 'text', limit: 100, null: false, parentClass: 'noTruncate' }, - { name: 'state_id', display: 'State', tag: 'select', multiple: false, null: false, relation: 'TicketState', default: 'new', style: 'width: 12%', edit: true, customer: true, }, - { name: 'priority_id', display: 'Priority', tag: 'select', multiple: false, null: false, relation: 'TicketPriority', default: '2 normal', style: 'width: 12%', edit: true, customer: true, }, + { name: 'number', display: '#', tag: 'input', type: 'text', limit: 100, null: true, read_only: true, style: 'width: 60px' }, + { name: 'customer_id', display: 'Customer', tag: 'input', type: 'text', limit: 100, null: false, autocapitalize: false, relation: 'User' }, + { name: 'organization_id', display: 'Organization', tag: 'select', relation: 'Organization', tagreadonly: 1 }, + { name: 'group_id', display: 'Group', tag: 'select', multiple: false, limit: 100, null: false, relation: 'Group', style: 'width: 10%', edit: true }, + { name: 'owner_id', display: 'Owner', tag: 'select', multiple: false, limit: 100, null: true, relation: 'User', style: 'width: 12%', edit: true }, + { name: 'title', display: 'Title', tag: 'input', type: 'text', limit: 100, null: false, parentClass: 'noTruncate' }, + { name: 'state_id', display: 'State', tag: 'select', multiple: false, null: false, relation: 'TicketState', default: 'new', style: 'width: 12%', edit: true, customer: true, }, + { name: 'priority_id', display: 'Priority', tag: 'select', multiple: false, null: false, relation: 'TicketPriority', default: '2 normal', style: 'width: 12%', edit: true, customer: true, }, { name: 'last_contact', display: 'Last contact', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' }, { name: 'last_contact_agent', display: 'Last contact (Agent)', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' }, { name: 'last_contact_customer', display: 'Last contact (Customer)', tag: 'datetime', null: true, style: 'width: 12%', parentClass: 'noTruncate' }, diff --git a/app/assets/javascripts/app/models/user.js.coffee b/app/assets/javascripts/app/models/user.js.coffee index cedbf80c4..14d0ea475 100644 --- a/app/assets/javascripts/app/models/user.js.coffee +++ b/app/assets/javascripts/app/models/user.js.coffee @@ -23,6 +23,7 @@ class App.User extends App.Model { name: 'role_ids', display: 'Roles', tag: 'checkbox', multiple: true, null: false, relation: 'Role' }, { name: 'group_ids', display: 'Groups', tag: 'checkbox', multiple: true, null: true, relation: 'Group', invite_agent: true }, { name: 'active', display: 'Active', tag: 'active', default: true }, + { name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 }, { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, ] @configure_overview = [ diff --git a/app/assets/javascripts/app/views/calendar/index.jst.eco b/app/assets/javascripts/app/views/calendar/index.jst.eco index 338994e79..a169ad80b 100644 --- a/app/assets/javascripts/app/views/calendar/index.jst.eco +++ b/app/assets/javascripts/app/views/calendar/index.jst.eco @@ -19,41 +19,41 @@
-

<%- @Icon('status', 'ok inline') %> <%= calendar.name %>

+

<%- @Icon('status', 'ok inline') %> <%= calendar.name %>

<%- @T('Timezone') %>: <%= calendar.timezone %>
<%- @T('Business Hours') %>:
- + - + - + - + - + - + - +
<%- @T('Monday') %><% if _.isEmpty(calendar.business_hours['mon']): %>-<% else: %><% for from, till of calendar.business_hours['mon']: %><%= from %>:<%= till %> <% end %><% end %><%- @T('Monday') %><% if _.isEmpty(calendar.business_hours['mon']): %>-<% else: %><% for from, till of calendar.business_hours['mon']: %><%= from %>-<%= till %> <% end %><% end %>
<%- @T('Tuesday') %><% if _.isEmpty(calendar.business_hours['tue']): %>-<% else: %><% for from, till of calendar.business_hours['tue']: %><%= from %>:<%= till %> <% end %><% end %><%- @T('Tuesday') %><% if _.isEmpty(calendar.business_hours['tue']): %>-<% else: %><% for from, till of calendar.business_hours['tue']: %><%= from %>-<%= till %> <% end %><% end %>
<%- @T('Wednesday') %><% if _.isEmpty(calendar.business_hours['wed']): %>-<% else: %><% for from, till of calendar.business_hours['wed']: %><%= from %>:<%= till %> <% end %><% end %><%- @T('Wednesday') %><% if _.isEmpty(calendar.business_hours['wed']): %>-<% else: %><% for from, till of calendar.business_hours['wed']: %><%= from %>-<%= till %> <% end %><% end %>
<%- @T('Thursday') %><% if _.isEmpty(calendar.business_hours['thu']): %>-<% else: %><% for from, till of calendar.business_hours['thu']: %><%= from %>:<%= till %> <% end %><% end %><%- @T('Thursday') %><% if _.isEmpty(calendar.business_hours['thu']): %>-<% else: %><% for from, till of calendar.business_hours['thu']: %><%= from %>-<%= till %> <% end %><% end %>
<%- @T('Friday') %><% if _.isEmpty(calendar.business_hours['fri']): %>-<% else: %><% for from, till of calendar.business_hours['fri']: %><%= from %>:<%= till %> <% end %><% end %><%- @T('Friday') %><% if _.isEmpty(calendar.business_hours['fri']): %>-<% else: %><% for from, till of calendar.business_hours['fri']: %><%= from %>-<%= till %> <% end %><% end %>
<%- @T('Saturday') %><% if _.isEmpty(calendar.business_hours['sat']): %>-<% else: %><% for from, till of calendar.business_hours['sat']: %><%= from %>:<%= till %> <% end %><% end %><%- @T('Saturday') %><% if _.isEmpty(calendar.business_hours['sat']): %>-<% else: %><% for from, till of calendar.business_hours['sat']: %><%= from %>-<%= till %> <% end %><% end %>
<%- @T('Sunday') %><% if _.isEmpty(calendar.business_hours['sun']): %>-<% else: %><% for from, till of calendar.business_hours['sun']: %><%= from %>:<%= till %> <% end %><% end %><%- @T('Sunday') %><% if _.isEmpty(calendar.business_hours['sun']): %>-<% else: %><% for from, till of calendar.business_hours['sun']: %><%= from %>-<%= till %> <% end %><% end %>
<%- @T('Public Holidays') %>: - <% for holiday, meta of calendar.public_holidays_preview: %> - class="is-inactive"<% end %>> - <% end %> + <% for holiday, meta of calendar.public_holidays_preview: %> + class="is-inactive"<% end %>> + <% end %>
<%- @Tdate(holiday) %><%= meta.summary %>
<%- @Tdate(holiday) %><%= meta.summary %>
diff --git a/app/controllers/slas_controller.rb b/app/controllers/slas_controller.rb index 17b1d809c..d6baddb7f 100644 --- a/app/controllers/slas_controller.rb +++ b/app/controllers/slas_controller.rb @@ -48,7 +48,28 @@ curl http://localhost/api/v1/slas.json -v -u #{login}:#{password} def index return if deny_if_not_role(Z_ROLENAME_ADMIN) - model_index_render(Sla, params) + + assets = {} + + # calendars + calendar_ids = [] + Calendar.all.each {|calendar| + calendar_ids.push calendar.id + assets = calendar.assets(assets) + } + + # slas + sla_ids = [] + Sla.all.each {|sla| + sla_ids.push sla.id + assets = sla.assets(assets) + } + + render json: { + calendar_ids: calendar_ids, + sla_ids: sla_ids, + assets: assets, + }, status: :ok end =begin diff --git a/test/browser/manage_test.rb b/test/browser/manage_test.rb index 5c53a4e43..d69e11e9a 100644 --- a/test/browser/manage_test.rb +++ b/test/browser/manage_test.rb @@ -45,7 +45,7 @@ class ManageTest < TestCase sla_create( data: { name: 'some sla' + random, - first_response_time: 61 + first_response_time_in_text: '1:01' } ) watch_for( @@ -54,7 +54,7 @@ class ManageTest < TestCase ) sleep 1 - click( css: '.table-overview tr:last-child td' ) + click( css: '.content:not(.hide) .action:last-child .js-edit' ) sleep 1 set( @@ -62,8 +62,8 @@ class ManageTest < TestCase value: 'some sla update ' + random, ) set( - css: '.modal input[name="first_response_time"]', - value: 121, + css: '.modal input[name="first_response_time_in_text"]', + value: '2:01', ) click( css: '.modal button.js-submit' ) @@ -73,7 +73,7 @@ class ManageTest < TestCase ) sleep 4 - click( css: '[data-type="destroy"]:last-child' ) + click( css: '.content:not(.hide) .action:last-child .js-delete' ) sleep 2 click( css: '.modal button.js-submit' ) diff --git a/test/browser_test_helper.rb b/test/browser_test_helper.rb index 02bff8d11..3d602116f 100644 --- a/test/browser_test_helper.rb +++ b/test/browser_test_helper.rb @@ -1576,8 +1576,8 @@ wait untill text in selector disabppears sla_create( :browser => browser2, :data => { - :name => 'some sla' + random, - :first_response_time => 61 + :name => 'some sla' + random, + :first_response_time_in_text => 61 }, ) @@ -1592,14 +1592,14 @@ wait untill text in selector disabppears instance.find_elements( { css: 'a[href="#manage"]' } )[0].click instance.find_elements( { css: 'a[href="#manage/slas"]' } )[0].click sleep 2 - instance.find_elements( { css: 'a[data-type="new"]' } )[0].click + instance.find_elements( { css: 'a.js-new' } )[0].click sleep 2 element = instance.find_elements( { css: '.modal input[name=name]' } )[0] element.clear element.send_keys( data[:name] ) - element = instance.find_elements( { css: '.modal input[name=first_response_time]' } )[0] + element = instance.find_elements( { css: '.modal input[name=first_response_time_in_text]' } )[0] element.clear - element.send_keys( data[:first_response_time] ) + element.send_keys( data[:first_response_time_in_text] ) instance.find_elements( { css: '.modal button.js-submit' } )[0].click (1..8).each { element = instance.find_elements( { css: 'body' } )[0]