Fixes #3140 - Update time SLAs escalates tickets with agent response.
This commit is contained in:
parent
3f1057c190
commit
2c26e82354
25 changed files with 585 additions and 114 deletions
|
@ -418,11 +418,11 @@ class App.ControllerForm extends App.Controller
|
|||
|
||||
if !@constructor.fieldIsMandatory(field_by_name)
|
||||
field_by_name.attr('required', true)
|
||||
field_by_name.parents('.form-group').find('label span').html('*')
|
||||
field_by_name.closest('.form-group').find('label span').html('*')
|
||||
field_by_name.closest('.form-group').addClass('is-required')
|
||||
if !@constructor.fieldIsMandatory(field_by_data)
|
||||
field_by_data.attr('required', true)
|
||||
field_by_data.parents('.form-group').find('label span').html('*')
|
||||
field_by_data.closest('.form-group').find('label span').html('*')
|
||||
field_by_data.closest('.form-group').addClass('is-required')
|
||||
|
||||
optional: (name, el = @form) ->
|
||||
|
@ -434,11 +434,11 @@ class App.ControllerForm extends App.Controller
|
|||
|
||||
if @constructor.fieldIsMandatory(field_by_name)
|
||||
field_by_name.attr('required', false)
|
||||
field_by_name.parents('.form-group').find('label span').html('')
|
||||
field_by_name.closest('.form-group').find('label span').html('')
|
||||
field_by_name.closest('.form-group').removeClass('is-required')
|
||||
if @constructor.fieldIsMandatory(field_by_data)
|
||||
field_by_data.attr('required', false)
|
||||
field_by_data.parents('.form-group').find('label span').html('')
|
||||
field_by_data.closest('.form-group').find('label span').html('')
|
||||
field_by_data.closest('.form-group').removeClass('is-required')
|
||||
|
||||
readonly: (name, el = @form) ->
|
||||
|
|
|
@ -8,9 +8,11 @@ class App.UiElement.sla_times
|
|||
item = $( App.view('generic/sla_times')(
|
||||
attribute: attribute
|
||||
first_response_time: params.first_response_time
|
||||
response_time: params.response_time
|
||||
update_time: params.update_time
|
||||
solution_time: params.solution_time
|
||||
first_response_time_in_text: @toText(params.first_response_time)
|
||||
response_time_in_text: @toText(params.response_time)
|
||||
update_time_in_text: @toText(params.update_time)
|
||||
solution_time_in_text: @toText(params.solution_time)
|
||||
) )
|
||||
|
@ -26,12 +28,16 @@ class App.UiElement.sla_times
|
|||
row = element.closest('tr')
|
||||
if element.prop('checked')
|
||||
row.addClass('is-active')
|
||||
|
||||
if row.has('.js-updateTypeSelector').length > 0 && row.has('.js-updateTypeSelector:checked').length == 0
|
||||
row.find('.js-updateTypeSelector:first').prop('checked', true)
|
||||
else
|
||||
row.removeClass('is-active')
|
||||
|
||||
# reset data item
|
||||
row.find('.js-timeConvertFrom').val('')
|
||||
row.find('.js-timeConvertTo').val('')
|
||||
row.find('.js-updateTypeSelector').attr('checked', false)
|
||||
row.find('.help-inline').empty()
|
||||
row.removeClass('has-error')
|
||||
)
|
||||
|
@ -42,12 +48,16 @@ class App.UiElement.sla_times
|
|||
inText = element.val()
|
||||
|
||||
row = element.closest('tr')
|
||||
row.find('.js-activateRow').prop('checked', true)
|
||||
|
||||
row
|
||||
.find('.js-activateRow')
|
||||
.prop('checked', true)
|
||||
.trigger('change')
|
||||
|
||||
row.addClass('is-active')
|
||||
|
||||
element
|
||||
.closest('td')
|
||||
.find('.js-timeConvertTo')
|
||||
row
|
||||
.find("[name='#{element.data('name')}']")
|
||||
.val(@toMinutes(inText) || '')
|
||||
)
|
||||
|
||||
|
@ -56,9 +66,19 @@ class App.UiElement.sla_times
|
|||
$(e.currentTarget).closest('tr').find('.checkbox-replacement').click()
|
||||
)
|
||||
|
||||
# toggle update type on clicking around the element
|
||||
item.find('.js-forward-radio').bind('click', (e) ->
|
||||
elem = $(e.currentTarget).closest('p').find('.js-updateTypeSelector')
|
||||
|
||||
elem.prop('checked', true)
|
||||
elem.trigger('change')
|
||||
)
|
||||
|
||||
# focus time input on clicking surrounding cell
|
||||
item.find('.js-focus-input').bind('click', (e) ->
|
||||
$(e.currentTarget).find('.form-control').focus()
|
||||
$(e.currentTarget)
|
||||
.find('.form-control:visible')
|
||||
.focus()
|
||||
)
|
||||
|
||||
# show placeholder instead of 00:00
|
||||
|
@ -67,15 +87,36 @@ class App.UiElement.sla_times
|
|||
$(e.currentTarget).val('')
|
||||
)
|
||||
|
||||
# switch update/response times when type is selected accordingly
|
||||
item.find('.js-updateTypeSelector').bind('change', (e) ->
|
||||
element = $(e.target)
|
||||
row = element.closest('tr')
|
||||
row.find('.js-activateRow').prop('checked', true)
|
||||
row.addClass('is-active')
|
||||
|
||||
row
|
||||
.find('.js-timeConvertFrom')
|
||||
.addClass('hidden')
|
||||
.val('')
|
||||
|
||||
row
|
||||
.find('.js-timeConvertTo')
|
||||
.val('')
|
||||
|
||||
row
|
||||
.find("[data-name='#{element.val()}_time']")
|
||||
.removeClass('hidden')
|
||||
)
|
||||
|
||||
# set initial active/inactive rows
|
||||
item.find('.js-timeConvertFrom').each(->
|
||||
row = $(@).closest('tr')
|
||||
checkbox = row.find('.js-activateRow')
|
||||
if $(@).val()
|
||||
checkbox.prop('checked', true)
|
||||
row.addClass('is-active')
|
||||
else
|
||||
checkbox.prop('checked', false)
|
||||
|
||||
return if !$(@).val()
|
||||
|
||||
checkbox.prop('checked', true)
|
||||
row.addClass('is-active')
|
||||
)
|
||||
|
||||
item
|
||||
|
|
|
@ -26,12 +26,6 @@ class Sla extends App.ControllerSubContent
|
|||
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)
|
||||
sla.calendar = App.Calendar.find(sla.calendar_id)
|
||||
|
||||
|
@ -99,17 +93,4 @@ class Sla extends App.ControllerSubContent
|
|||
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: Sla, permission: ['admin.sla'] }, 'NavBarAdmin')
|
||||
|
|
|
@ -36,7 +36,7 @@ App.ViewHelpers =
|
|||
App.Utils.decimal(data, positions)
|
||||
|
||||
# define time_duration / mm:ss / hh:mm:ss format helper
|
||||
time_duration: (time) ->
|
||||
time_duration: (time, show_seconds = true) ->
|
||||
return '' if !time
|
||||
return '' if isNaN(parseInt(time))
|
||||
|
||||
|
@ -48,7 +48,7 @@ App.ViewHelpers =
|
|||
# Output like "1:01" or "4:03:59" or "123:03:59"
|
||||
mins = "0#{mins}" if mins < 10
|
||||
secs = "0#{secs}" if secs < 10
|
||||
if hrs > 0
|
||||
if hrs > 0 && show_seconds
|
||||
return "#{hrs}:#{mins}:#{secs}"
|
||||
"#{mins}:#{secs}"
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class App.Sla extends App.Model
|
||||
@configure 'Sla', 'name', 'first_response_time', 'update_time', 'solution_time', 'condition', 'calendar_id'
|
||||
@configure 'Sla', 'name', 'first_response_time', 'response_time', 'update_time', 'solution_time', 'condition', 'calendar_id'
|
||||
@extend Spine.Model.Ajax
|
||||
@url: @apiPath + '/slas'
|
||||
@configure_attributes = [
|
||||
|
@ -12,6 +12,7 @@ class App.Sla extends App.Model
|
|||
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
|
||||
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
|
||||
{ name: 'first_response_time',skipRendering: true },
|
||||
{ name: 'response_time', skipRendering: true },
|
||||
{ name: 'update_time', skipRendering: true },
|
||||
{ name: 'solution_time', skipRendering: true },
|
||||
]
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<th><%- @T('Time') %> <span class="text-muted"><%- @T('in hours') %></span>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="form-group">
|
||||
<tr>
|
||||
<td class="u-positionOrigin">
|
||||
<label class="checkbox-replacement checkbox-replacement--fullscreen dont-grey-out">
|
||||
<input type="checkbox" class="js-activateRow" id="first_response_time" name="first_response_time_enabled">
|
||||
|
@ -16,25 +16,51 @@
|
|||
<td class="u-clickable js-forward-click">
|
||||
<div><%- @T('First Response Time') %></div>
|
||||
<p class="subtle"><%- @T('Timeframe for the first response.') %></p>
|
||||
<td class="u-clickable js-focus-input">
|
||||
<td class="u-clickable js-focus-input form-group">
|
||||
<input type="hidden" name="first_response_time" value="<%= @first_response_time %>" class="js-timeConvertTo">
|
||||
<input type="text" value="<%= @first_response_time_in_text %>" class="form-control form-control--small timeframe js-timeConvertFrom" placeholder="hh:mm" name="first_response_time_in_text" data-name="first_response_time">
|
||||
|
||||
<tr class="form-group">
|
||||
<tr>
|
||||
<td class="u-positionOrigin">
|
||||
<label class="checkbox-replacement checkbox-replacement--fullscreen dont-grey-out">
|
||||
<input type="checkbox" class="js-activateRow" id="update_time" name="update_time_enabled">
|
||||
<%- @Icon('checkbox', 'icon-unchecked') %>
|
||||
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
||||
</label>
|
||||
<td class="u-clickable js-forward-click">
|
||||
<div><%- @T('Update Time') %></div>
|
||||
<p class="subtle"><%- @T('Timeframe for every following response.') %></p>
|
||||
<td>
|
||||
<input type="hidden" name="update_time" value="<%= @update_time %>" class="js-timeConvertTo">
|
||||
<input type="text" value="<%= @update_time_in_text %>" class="form-control form-control--small timeframe js-timeConvertFrom" placeholder="hh:mm" name="update_time_in_text" data-name="update_time">
|
||||
<div class="u-clickable js-forward-click">
|
||||
<div><%- @T('Update Time') %></div>
|
||||
<p class="subtle"><%- @T('Timeframe between agent updates or for an agent to respond.') %></p>
|
||||
</div>
|
||||
|
||||
<tr class="form-group">
|
||||
<p class="sla_radio_container js-forward-radio">
|
||||
<label class="inline-label radio-replacement">
|
||||
<input class="js-updateTypeSelector" type="radio" name="update_type" value="update" <% if @update_time: %>checked<% end %>>
|
||||
<%- @Icon('radio', 'icon-unchecked') %>
|
||||
<%- @Icon('radio-checked', 'icon-checked') %>
|
||||
<span class="label-text"><%- @T('between agent updates') %></span>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p class="sla_radio_container js-forward-radio u-clickable">
|
||||
<label class="inline-label radio-replacement">
|
||||
<input class="js-updateTypeSelector" type="radio" name="update_type" value="response" <% if @response_time: %>checked<% end %>>
|
||||
<%- @Icon('radio', 'icon-unchecked') %>
|
||||
<%- @Icon('radio-checked', 'icon-checked') %>
|
||||
<span class="label-text"><%- @T('for an agent to respond') %></span>
|
||||
</label>
|
||||
</p>
|
||||
<td class="form-group u-clickable js-focus-input u-clickable">
|
||||
<span class="form-group">
|
||||
<input type="hidden" name="update_time" value="<%= @update_time %>" class="js-timeConvertTo">
|
||||
<input type="text" value="<%= @update_time_in_text %>" class="form-control form-control--small timeframe js-timeConvertFrom <% if @response_time: %>hidden<% end %>" placeholder="hh:mm" name="update_time_in_text" data-name="update_time">
|
||||
</span>
|
||||
<span class="form-group">
|
||||
<input type="hidden" name="response_time" value="<%= @response_time %>" class="js-timeConvertTo">
|
||||
<input type="text" value="<%= @response_time_in_text %>" class="form-control form-control--small timeframe js-timeConvertFrom <% if !@response_time: %>hidden<% end %>" placeholder="hh:mm" name="response_time_in_text" data-name="response_time">
|
||||
</span>
|
||||
|
||||
<tr>
|
||||
<td class="u-positionOrigin">
|
||||
<label class="checkbox-replacement checkbox-replacement--fullscreen dont-grey-out">
|
||||
<input type="checkbox" id="solution_time" class="js-activateRow" name="solution_time_enabled">
|
||||
|
@ -44,7 +70,7 @@
|
|||
<td class="u-clickable js-forward-click">
|
||||
<div><%- @T('Solution Time') %></div>
|
||||
<p class="subtle"><%- @T('Timeframe for solving the problem.') %></p>
|
||||
<td>
|
||||
<td class="form-group u-clickable js-focus-input">
|
||||
<input type="hidden" name="solution_time" value="<%= @solution_time %>" class="js-timeConvertTo">
|
||||
<input type="text" value="<%= @solution_time_in_text %>" class="form-control form-control--small timeframe js-timeConvertFrom" placeholder="hh:mm" name="solution_time_in_text" data-name="solution_time">
|
||||
|
||||
|
|
|
@ -34,15 +34,19 @@
|
|||
<div class="action-block action-block--flex">
|
||||
<div class="label"><%- @T('Escalation Times') %></div>
|
||||
<% if sla.first_response_time: %>
|
||||
<%- sla.first_response_time_in_text %> <%- @T('hours') %> - <%- @T('First Response Time') %>
|
||||
<%- @time_duration(sla.first_response_time, false) %> <%- @T('hours') %> - <%- @T('First Response Time') %>
|
||||
<% end %>
|
||||
<% if sla.response_time: %>
|
||||
<br>
|
||||
<%- @time_duration(sla.response_time, false) %> <%- @T('hours') %> - <%- @T('Response Time') %>
|
||||
<% end %>
|
||||
<% if sla.update_time: %>
|
||||
<br>
|
||||
<%- sla.update_time_in_text %> <%- @T('hours') %> - <%- @T('Update Time') %>
|
||||
<%- @time_duration(sla.update_time, false) %> <%- @T('hours') %> - <%- @T('Update Time') %>
|
||||
<% end %>
|
||||
<% if sla.solution_time: %>
|
||||
<br>
|
||||
<%- sla.solution_time_in_text %> <%- @T('hours') %> - <%- @T('Solution Time') %>
|
||||
<%- @time_duration(sla.solution_time, false) %> <%- @T('hours') %> - <%- @T('Solution Time') %>
|
||||
<% end %>
|
||||
<br>
|
||||
</div>
|
||||
|
@ -57,4 +61,4 @@
|
|||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -13157,3 +13157,10 @@ span.is-disabled {
|
|||
.text-modules-box {
|
||||
max-height: 40vh;
|
||||
}
|
||||
|
||||
.sla_times {
|
||||
.sla_radio_container {
|
||||
padding-top: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,13 @@ class CoreWorkflow::Custom::AdminSla < CoreWorkflow::Custom::Backend
|
|||
end
|
||||
|
||||
def update_time_enabled
|
||||
return 'set_mandatory' if params['update_time_enabled'].present?
|
||||
return 'set_mandatory' if params['update_time_enabled'].present? && params['update_type'] == 'update'
|
||||
|
||||
'set_optional'
|
||||
end
|
||||
|
||||
def response_time_enabled
|
||||
return 'set_mandatory' if params['update_time_enabled'].present? && params['update_type'] == 'response'
|
||||
|
||||
'set_optional'
|
||||
end
|
||||
|
@ -32,6 +38,7 @@ class CoreWorkflow::Custom::AdminSla < CoreWorkflow::Custom::Backend
|
|||
# make fields mandatory if checkbox is checked
|
||||
result(first_response_time_enabled, 'first_response_time_in_text')
|
||||
result(update_time_enabled, 'update_time_in_text')
|
||||
result(response_time_enabled, 'response_time_in_text')
|
||||
result(solution_time_enabled, 'solution_time_in_text')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,8 @@ class Sla < ApplicationModel
|
|||
|
||||
validates :name, presence: true
|
||||
|
||||
validate :cannot_have_response_and_update
|
||||
|
||||
store :condition
|
||||
store :data
|
||||
|
||||
|
@ -32,4 +34,12 @@ class Sla < ApplicationModel
|
|||
end
|
||||
fallback
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cannot_have_response_and_update
|
||||
return if response_time.blank? || update_time.blank?
|
||||
|
||||
errors.add :base, 'cannot have both response time and update time'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -446,6 +446,7 @@ class CreateTicket < ActiveRecord::Migration[4.2]
|
|||
t.references :calendar, null: false
|
||||
t.column :name, :string, limit: 150, null: true
|
||||
t.column :first_response_time, :integer, null: true
|
||||
t.column :response_time, :integer, null: true
|
||||
t.column :update_time, :integer, null: true
|
||||
t.column :solution_time, :integer, null: true
|
||||
t.column :condition, :text, limit: 500.kilobytes + 1, null: true
|
||||
|
|
13
db/migrate/20210614063039_sla_add_response_time.rb
Normal file
13
db/migrate/20210614063039_sla_add_response_time.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class SlaAddResponseTime < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
return if !Setting.exists?(name: 'system_init_done')
|
||||
|
||||
change_table :slas do |t|
|
||||
t.integer :response_time
|
||||
end
|
||||
|
||||
Sla.reset_column_information
|
||||
end
|
||||
end
|
|
@ -97,7 +97,7 @@ class Escalation
|
|||
end
|
||||
|
||||
def update_escalations
|
||||
ticket.assign_attributes [escalation_first_response, escalation_update, escalation_close]
|
||||
ticket.assign_attributes [escalation_first_response, escalation_response, escalation_update, escalation_close]
|
||||
.compact
|
||||
.each_with_object({}) { |elem, memo| memo.merge!(elem) }
|
||||
|
||||
|
@ -105,7 +105,7 @@ class Escalation
|
|||
end
|
||||
|
||||
def update_statistics
|
||||
ticket.assign_attributes [statistics_first_response, statistics_update, statistics_close]
|
||||
ticket.assign_attributes [statistics_first_response, statistics_response, statistics_update, statistics_close]
|
||||
.compact
|
||||
.each_with_object({}) { |elem, memo| memo.merge!(elem) }
|
||||
end
|
||||
|
@ -130,11 +130,41 @@ class Escalation
|
|||
}
|
||||
end
|
||||
|
||||
def escalation_update
|
||||
def escalation_update_reset
|
||||
return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
|
||||
return if sla.response_time.present? || sla.update_time.present?
|
||||
|
||||
{ update_escalation_at: nil }
|
||||
end
|
||||
|
||||
def escalation_response_timestamp
|
||||
return if escalation_disabled? || ticket.agent_responded?
|
||||
|
||||
ticket.last_contact_customer_at
|
||||
end
|
||||
|
||||
def escalation_response
|
||||
return if sla.response_time.nil?
|
||||
return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
|
||||
|
||||
nullify = escalation_disabled? || ticket.agent_responded?
|
||||
timestamp = nullify ? nil : ticket.last_contact_customer_at
|
||||
timestamp = escalation_response_timestamp
|
||||
|
||||
{
|
||||
update_escalation_at: timestamp ? calculate_time(timestamp, sla.response_time) : nil
|
||||
}
|
||||
end
|
||||
|
||||
def escalation_update_timestamp
|
||||
return if escalation_disabled?
|
||||
|
||||
ticket.last_contact_agent_at || ticket.created_at
|
||||
end
|
||||
|
||||
def escalation_update
|
||||
return if sla.update_time.nil?
|
||||
return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
|
||||
|
||||
timestamp = escalation_update_timestamp
|
||||
|
||||
{
|
||||
update_escalation_at: timestamp ? calculate_time(timestamp, sla.update_time) : nil
|
||||
|
@ -186,11 +216,39 @@ class Escalation
|
|||
}
|
||||
end
|
||||
|
||||
def skip_statistics_response?
|
||||
return true if !forced? && !preferences.last_update_at_changed?(ticket)
|
||||
return true if !sla.response_time
|
||||
|
||||
!ticket.agent_responded?
|
||||
end
|
||||
|
||||
# ATTENTION: Recalculation after SLA change won't happen
|
||||
# SLA change will cause wrong statistics in some edge cases.
|
||||
# Since this changes `update_in_min` calculation to retain longest timespan.
|
||||
# But it does not keep track of previous update times.
|
||||
def statistics_response_applicable?(minutes)
|
||||
ticket.update_in_min.blank? || minutes > ticket.update_in_min # keep longest timespan
|
||||
end
|
||||
|
||||
def statistics_response
|
||||
return if skip_statistics_response?
|
||||
|
||||
minutes = calculate_minutes(ticket.last_contact_customer_at, ticket.last_contact_agent_at)
|
||||
|
||||
return if !forced? && !statistics_response_applicable?(minutes)
|
||||
|
||||
{
|
||||
update_in_min: minutes,
|
||||
update_diff_in_min: minutes ? (sla.response_time - minutes) : nil
|
||||
}
|
||||
end
|
||||
|
||||
def skip_statistics_update?
|
||||
return true if !forced? && !preferences.last_update_at_changed?(ticket)
|
||||
return true if !sla.update_time
|
||||
|
||||
!ticket.agent_responded?
|
||||
ticket.last_contact_agent_at.blank?
|
||||
end
|
||||
|
||||
# ATTENTION: Recalculation after SLA change won't happen
|
||||
|
@ -201,10 +259,28 @@ class Escalation
|
|||
ticket.update_in_min.blank? || minutes > ticket.update_in_min # keep longest timespan
|
||||
end
|
||||
|
||||
def statistics_update_responses
|
||||
ticket
|
||||
.articles
|
||||
.reverse
|
||||
.lazy
|
||||
.select { |article| article.sender&.name == 'Agent' && article.type&.communication }
|
||||
.first(2)
|
||||
end
|
||||
|
||||
def statistics_update_minutes
|
||||
last_agent_responses = statistics_update_responses
|
||||
|
||||
from = last_agent_responses.second&.created_at || ticket.created_at
|
||||
to = last_agent_responses.first&.created_at
|
||||
|
||||
calculate_minutes(from, to)
|
||||
end
|
||||
|
||||
def statistics_update
|
||||
return if skip_statistics_update?
|
||||
|
||||
minutes = calculate_minutes(ticket.last_contact_customer_at, ticket.last_contact_agent_at)
|
||||
minutes = statistics_update_minutes
|
||||
|
||||
return if !forced? && !statistics_update_applicable?(minutes)
|
||||
|
||||
|
|
|
@ -170,12 +170,15 @@ test('form checks', function() {
|
|||
first_response_time: '150',
|
||||
first_response_time_enabled: 'on',
|
||||
first_response_time_in_text: '02:30',
|
||||
response_time: '',
|
||||
response_time_in_text: '',
|
||||
solution_time: '',
|
||||
solution_time_enabled: undefined,
|
||||
solution_time_in_text: '',
|
||||
update_time: '45',
|
||||
update_time_enabled: 'on',
|
||||
update_time_in_text: '00:45',
|
||||
update_type: 'update',
|
||||
working_hours: {
|
||||
mon: {
|
||||
active: true,
|
||||
|
@ -318,15 +321,90 @@ test('form checks', function() {
|
|||
first_response_time: '30',
|
||||
first_response_time_enabled: 'on',
|
||||
first_response_time_in_text: '00:30',
|
||||
response_time: '',
|
||||
response_time_in_text: '',
|
||||
solution_time: '',
|
||||
solution_time_enabled: undefined,
|
||||
solution_time_in_text: '',
|
||||
update_time: '',
|
||||
update_time_enabled: undefined,
|
||||
update_time_in_text: '',
|
||||
update_type: undefined,
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check')
|
||||
|
||||
// change sla times
|
||||
el.find('#update_time').attr('checked', false)
|
||||
el.find('[value=response]').click()
|
||||
el.find('[name="response_time_in_text"]').val('4:30').trigger('blur')
|
||||
|
||||
var params = App.ControllerForm.params(el)
|
||||
var test_params = {
|
||||
priority1_id: '1',
|
||||
priority2_id: ['1', '2'],
|
||||
priority3_id: '2',
|
||||
priority4_id: '2',
|
||||
priority5_id: '1',
|
||||
working_hours: {
|
||||
mon: {
|
||||
active: true,
|
||||
timeframes: [
|
||||
['09:00','17:00']
|
||||
]
|
||||
},
|
||||
tue: {
|
||||
active: true,
|
||||
timeframes: [
|
||||
['00:00','22:00']
|
||||
]
|
||||
},
|
||||
wed: {
|
||||
active: true,
|
||||
timeframes: [
|
||||
['09:00','17:00']
|
||||
]
|
||||
},
|
||||
thu: {
|
||||
active: true,
|
||||
timeframes: [
|
||||
['09:00','12:00'],
|
||||
['13:00','17:00']
|
||||
]
|
||||
},
|
||||
fri: {
|
||||
active: true,
|
||||
timeframes: [
|
||||
['09:00','17:00']
|
||||
]
|
||||
},
|
||||
sat: {
|
||||
active: false,
|
||||
timeframes: [
|
||||
['10:00','14:00']
|
||||
]
|
||||
},
|
||||
sun: {
|
||||
active: false,
|
||||
timeframes: [
|
||||
['10:00','14:00']
|
||||
]
|
||||
},
|
||||
},
|
||||
first_response_time: '30',
|
||||
first_response_time_enabled: 'on',
|
||||
first_response_time_in_text: '00:30',
|
||||
response_time: '270',
|
||||
response_time_in_text: '04:30',
|
||||
solution_time: '',
|
||||
solution_time_enabled: undefined,
|
||||
solution_time_in_text: '',
|
||||
update_time: '',
|
||||
update_time_enabled: 'on',
|
||||
update_time_in_text: '',
|
||||
update_type: 'response'
|
||||
}
|
||||
deepEqual(params, test_params, 'form param check post response')
|
||||
|
||||
/* empty params or defaults */
|
||||
$('#forms').append('<hr><h1>form condition check</h1><form id="form2"></form>')
|
||||
var el = $('#form2')
|
||||
|
|
|
@ -57,6 +57,30 @@ test("form SLA times highlights and shows settings accordingly", function(assert
|
|||
equal(firstRow.find('input[data-name=first_response_time]').val(), '')
|
||||
ok(secondRow.hasClass('is-active'))
|
||||
equal(secondRow.find('input[data-name=update_time]').val(), '04:00')
|
||||
equal(secondRow.find('input[name=update_type]:checked').val(), 'update')
|
||||
|
||||
$('#forms').append('<hr><h1>SLA with response time set</h1><form id="form3a"></form>')
|
||||
|
||||
var el = $('#form3a')
|
||||
|
||||
var item = new App.Sla()
|
||||
item.id = '123'
|
||||
item.response_time = 180
|
||||
|
||||
new App.ControllerForm({
|
||||
el: el,
|
||||
model: item.constructor,
|
||||
params: item
|
||||
});
|
||||
|
||||
var firstRow = el.find('.sla_times tbody > tr:first')
|
||||
var secondRow = el.find('.sla_times tbody > tr:nth-child(2)')
|
||||
|
||||
notOk(firstRow.hasClass('is-active'))
|
||||
equal(firstRow.find('input[data-name=first_response_time]').val(), '')
|
||||
ok(secondRow.hasClass('is-active'))
|
||||
equal(secondRow.find('input[data-name=response_time]').val(), '03:00')
|
||||
equal(secondRow.find('input[name=update_type]:checked').val(), 'response')
|
||||
})
|
||||
|
||||
test("form SLA times clears field instead of 00:00", function(assert) {
|
||||
|
|
|
@ -3,13 +3,18 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ::Escalation do
|
||||
let(:instance) { described_class.new ticket, force: force }
|
||||
let(:instance) { described_class.new ticket, force: force }
|
||||
let(:instance_with_history) { described_class.new ticket_with_history, force: force }
|
||||
let(:instance_with_open) { described_class.new open_ticket_with_history, force: force }
|
||||
|
||||
let(:ticket) { create(:ticket) }
|
||||
let(:force) { false }
|
||||
let(:sla) { nil }
|
||||
let(:sla_247) { create(:sla, :condition_blank, first_response_time: 60, update_time: 60, solution_time: 75, calendar: create(:calendar, :'24/7')) }
|
||||
let(:calendar) { nil }
|
||||
let(:calendar) { create(:calendar, :'24/7') }
|
||||
|
||||
let(:sla_247) { create(:sla, :condition_blank, solution_time: 75, calendar: calendar) }
|
||||
let(:sla_247_response) { create(:sla, :condition_blank, first_response_time: 30, response_time: 45, solution_time: 75, calendar: calendar) }
|
||||
let(:sla_247_update) { create(:sla, :condition_blank, first_response_time: 30, update_time: 60, solution_time: 75, calendar: calendar) }
|
||||
|
||||
let(:ticket_with_history) do
|
||||
freeze_time
|
||||
ticket = create(:ticket)
|
||||
|
@ -252,7 +257,7 @@ RSpec.describe ::Escalation do
|
|||
|
||||
# https://github.com/zammad/zammad/issues/3140
|
||||
it 'customer contact sets #update_escalation_at' do
|
||||
sla_247
|
||||
sla_247_response
|
||||
ticket
|
||||
create(:ticket_article, :inbound_email, ticket: ticket)
|
||||
|
||||
|
@ -261,7 +266,7 @@ RSpec.describe ::Escalation do
|
|||
|
||||
context 'with ticket with sla and customer enquiry' do
|
||||
before do
|
||||
sla_247
|
||||
sla_247_response
|
||||
ticket
|
||||
|
||||
travel 10.minutes
|
||||
|
@ -289,34 +294,120 @@ RSpec.describe ::Escalation do
|
|||
let(:force) { true } # initial calculation
|
||||
|
||||
it 'returns attribute' do
|
||||
sla_247
|
||||
sla_247_response
|
||||
allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
|
||||
result = instance_with_history.send(:escalation_first_response)
|
||||
expect(result).to include first_response_escalation_at: 60.minutes.ago
|
||||
expect(result).to include first_response_escalation_at: 90.minutes.ago
|
||||
end
|
||||
|
||||
it 'returns nil when no sla#first_response_time' do
|
||||
sla_247.update! first_response_time: nil
|
||||
sla_247_response.update! first_response_time: nil
|
||||
allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
|
||||
result = instance_with_history.send(:escalation_first_response)
|
||||
expect(result).to include(first_response_escalation_at: nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#escalation_update' do
|
||||
it 'returns attribute' do
|
||||
describe '#escalation_update_reset' do
|
||||
it 'resets to nil when no sla#response_time and sla#update_time' do
|
||||
sla_247
|
||||
ticket_with_history.last_contact_customer_at = 2.hours.ago
|
||||
allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
|
||||
result = instance_with_history.send(:escalation_update)
|
||||
expect(result).to include update_escalation_at: 60.minutes.ago
|
||||
result = instance_with_history.send(:escalation_update_reset)
|
||||
expect(result).to include(update_escalation_at: nil)
|
||||
end
|
||||
|
||||
it 'returns nil when no sla#response_time' do
|
||||
sla_247_update
|
||||
allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
|
||||
result = instance_with_history.send(:escalation_update_reset)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
|
||||
it 'returns nil when no sla#update_time' do
|
||||
sla_247.update! update_time: nil
|
||||
sla_247_response
|
||||
allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
|
||||
result = instance_with_history.send(:escalation_update)
|
||||
expect(result).to include(update_escalation_at: nil)
|
||||
result = instance_with_history.send(:escalation_update_reset)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#escalation_response' do
|
||||
it 'returns attribute' do
|
||||
sla_247_response
|
||||
ticket_with_history.last_contact_customer_at = 2.hours.ago
|
||||
allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
|
||||
result = instance_with_history.send(:escalation_response)
|
||||
expect(result).to include update_escalation_at: 75.minutes.ago
|
||||
end
|
||||
|
||||
it 'returns nil when no sla#response_time' do
|
||||
sla_247
|
||||
allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
|
||||
result = instance_with_history.send(:escalation_response)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
|
||||
it 'response time is calculated when waiting for the first response with update-only SLA' do
|
||||
sla_247_response.update! first_response_time: nil
|
||||
ticket_with_history.last_contact_customer_at = 2.hours.ago
|
||||
allow(instance_with_history).to receive(:escalation_disabled?).and_return(false)
|
||||
result = instance_with_history.send(:escalation_response)
|
||||
expect(result).to include update_escalation_at: 75.minutes.ago
|
||||
end
|
||||
end
|
||||
|
||||
describe '#escalation_update' do
|
||||
context 'when has open ticket with history' do
|
||||
before do
|
||||
sla_247_update
|
||||
open_ticket_with_history
|
||||
allow(instance_with_open).to receive(:escalation_disabled?).and_return(false)
|
||||
end
|
||||
|
||||
it 'update time is calculated before first agent response' do
|
||||
result = instance_with_open.send(:escalation_update)
|
||||
expect(result).to include update_escalation_at: 50.minutes.from_now
|
||||
end
|
||||
|
||||
it 'update time is calculated after agent response' do
|
||||
create(:ticket_article, :outbound_email, ticket: open_ticket_with_history)
|
||||
result = instance_with_open.send(:escalation_update)
|
||||
expect(result).to include update_escalation_at: 60.minutes.from_now
|
||||
end
|
||||
|
||||
context 'when agent responds' do
|
||||
before do
|
||||
create(:ticket_article, :outbound_email, ticket: open_ticket_with_history)
|
||||
travel 30.minutes
|
||||
end
|
||||
|
||||
it 'update time is calculated after 2nd customer enquiry' do
|
||||
create(:ticket_article, :inbound_email, ticket: open_ticket_with_history)
|
||||
result = instance_with_open.send(:escalation_update)
|
||||
expect(result).to include update_escalation_at: 30.minutes.from_now
|
||||
end
|
||||
|
||||
it 'update time is calculated after 2nd agent response interrupted by customer' do
|
||||
create(:ticket_article, :inbound_email, ticket: open_ticket_with_history)
|
||||
travel 30.minutes
|
||||
create(:ticket_article, :outbound_email, ticket: open_ticket_with_history)
|
||||
result = instance_with_open.send(:escalation_update)
|
||||
expect(result).to include update_escalation_at: 60.minutes.from_now
|
||||
end
|
||||
|
||||
it 'update time is calculated after 2nd agent response in a row' do
|
||||
create(:ticket_article, :outbound_email, ticket: open_ticket_with_history)
|
||||
result = instance_with_open.send(:escalation_update)
|
||||
expect(result).to include update_escalation_at: 60.minutes.from_now
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns nil when no sla#update_time' do
|
||||
sla_247
|
||||
allow(instance_with_open).to receive(:escalation_disabled?).and_return(false)
|
||||
result = instance_with_open.send(:escalation_update)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -387,16 +478,16 @@ RSpec.describe ::Escalation do
|
|||
|
||||
describe '#statistics_first_response' do
|
||||
it 'calculates statistics' do
|
||||
sla_247
|
||||
sla_247_response
|
||||
ticket_with_history.first_response_at = 45.minutes.ago
|
||||
instance_with_history.force!
|
||||
|
||||
result = instance_with_history.send(:statistics_first_response)
|
||||
expect(result).to include(first_response_in_min: 75, first_response_diff_in_min: -15)
|
||||
expect(result).to include(first_response_in_min: 75, first_response_diff_in_min: -45)
|
||||
end
|
||||
|
||||
it 'does not touch statistics when sla time is nil' do
|
||||
sla_247.update! first_response_time: nil
|
||||
sla_247_response.update! first_response_time: nil
|
||||
ticket_with_history.first_response_at = 45.minutes.ago
|
||||
instance_with_history.force!
|
||||
|
||||
|
@ -405,9 +496,9 @@ RSpec.describe ::Escalation do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#statistics_update' do
|
||||
describe '#statistics_response' do
|
||||
before do
|
||||
sla_247
|
||||
sla_247_response
|
||||
freeze_time
|
||||
end
|
||||
|
||||
|
@ -415,22 +506,22 @@ RSpec.describe ::Escalation do
|
|||
ticket_with_history.last_contact_customer_at = 61.minutes.ago
|
||||
ticket_with_history.last_contact_agent_at = 60.minutes.ago
|
||||
|
||||
result = instance_with_history.send(:statistics_update)
|
||||
expect(result).to include(update_in_min: 1, update_diff_in_min: 59)
|
||||
result = instance_with_history.send(:statistics_response)
|
||||
expect(result).to include(update_in_min: 1, update_diff_in_min: 44)
|
||||
end
|
||||
|
||||
it 'does not calculate statistics when customer respose is last' do
|
||||
ticket_with_history.last_contact_customer_at = 59.minutes.ago
|
||||
ticket_with_history.last_contact_agent_at = 60.minutes.ago
|
||||
|
||||
result = instance_with_history.send(:statistics_update)
|
||||
result = instance_with_history.send(:statistics_response)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
|
||||
it 'does not calculate statistics when only customer enquiry present' do
|
||||
create(:ticket_article, :inbound_email, ticket: ticket)
|
||||
|
||||
result = instance.send(:statistics_update)
|
||||
result = instance.send(:statistics_response)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
|
||||
|
@ -440,7 +531,7 @@ RSpec.describe ::Escalation do
|
|||
create(:ticket_article, :outbound_email, ticket: ticket)
|
||||
|
||||
instance.force!
|
||||
expect(instance.send(:statistics_update)).to include(update_in_min: 10, update_diff_in_min: 50)
|
||||
expect(instance.send(:statistics_response)).to include(update_in_min: 10, update_diff_in_min: 35)
|
||||
end
|
||||
|
||||
context 'with multiple exchanges and later one being quicker' do
|
||||
|
@ -455,7 +546,95 @@ RSpec.describe ::Escalation do
|
|||
end
|
||||
|
||||
it 'keeps statistics of longest exchange' do
|
||||
expect(ticket.reload).to have_attributes(update_in_min: 10, update_diff_in_min: 50)
|
||||
expect(ticket.reload).to have_attributes(update_in_min: 10, update_diff_in_min: 35)
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not touch statistics when sla time is nil' do
|
||||
sla_247.update! update_time: nil
|
||||
ticket_with_history.last_contact_customer_at = 60.minutes.ago
|
||||
instance_with_history.force!
|
||||
|
||||
result = instance_with_history.send(:statistics_update)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
|
||||
it 'does not touch statistics when last update is nil' do
|
||||
ticket_with_history.assign_attributes last_contact_agent_at: nil, last_contact_customer_at: nil
|
||||
instance_with_history.force!
|
||||
|
||||
result = instance_with_history.send(:statistics_update)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#statistics_update' do
|
||||
before do
|
||||
sla_247_update
|
||||
freeze_time
|
||||
end
|
||||
|
||||
it 'does not calculate statistics when only customer enquiry present' do
|
||||
create(:ticket_article, :inbound_email, ticket: ticket)
|
||||
|
||||
result = instance.send(:statistics_update)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
|
||||
context 'when agent responds after 20 minutes' do
|
||||
before do
|
||||
ticket
|
||||
travel 20.minutes
|
||||
create(:ticket_article, :outbound_email, ticket: ticket)
|
||||
end
|
||||
|
||||
it 'does not touch statistics when customer response is most recent' do
|
||||
travel 30.minutes
|
||||
create(:ticket_article, :inbound_email, ticket: ticket)
|
||||
|
||||
result = instance.send(:statistics_update)
|
||||
expect(result).to include(update_diff_in_min: 40, update_in_min: 20)
|
||||
end
|
||||
|
||||
it 'calculates statistics when only agent update present' do
|
||||
result = instance.send(:statistics_update)
|
||||
expect(result).to include(update_diff_in_min: 40, update_in_min: 20)
|
||||
end
|
||||
|
||||
it 'calculates statistics when multiple agent updates present' do
|
||||
travel 30.minutes
|
||||
create(:ticket_article, :outbound_email, ticket: ticket)
|
||||
|
||||
result = instance.send(:statistics_update)
|
||||
expect(result).to include(update_diff_in_min: 30, update_in_min: 30)
|
||||
end
|
||||
|
||||
context 'when customer responds' do
|
||||
before do
|
||||
travel 10.minutes
|
||||
create(:ticket_article, :inbound_email, ticket: ticket)
|
||||
end
|
||||
|
||||
it 'calculates statistics when multiple agent updates intercepted by customer' do
|
||||
travel 35.minutes
|
||||
create(:ticket_article, :outbound_email, ticket: ticket)
|
||||
|
||||
result = instance.send(:statistics_update)
|
||||
expect(result).to include(update_diff_in_min: 15, update_in_min: 45)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple exchanges and later one being quicker' do
|
||||
before do
|
||||
travel 10.minutes
|
||||
create(:ticket_article, :outbound_email, ticket: ticket)
|
||||
travel 5.minutes
|
||||
create(:ticket_article, :outbound_email, ticket: ticket)
|
||||
end
|
||||
|
||||
it 'keeps statistics of longest exchange' do
|
||||
expect(ticket.reload).to have_attributes(update_in_min: 5, update_diff_in_min: 55)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -512,7 +691,7 @@ RSpec.describe ::Escalation do
|
|||
it 'switching state pushes escalation date' do
|
||||
sla_247
|
||||
open_ticket_with_history.reload
|
||||
expect(open_ticket_with_history.update_escalation_at).to eq open_ticket_with_history.created_at + 70.minutes
|
||||
expect(open_ticket_with_history.escalation_at).to eq open_ticket_with_history.created_at + 85.minutes
|
||||
end
|
||||
|
||||
def without_update_escalation_information_callback(&block)
|
||||
|
|
|
@ -303,7 +303,7 @@ RSpec.describe Calendar, type: :model do
|
|||
})
|
||||
end
|
||||
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, update_time: 120, solution_time: nil) }
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, response_time: 120, solution_time: nil) }
|
||||
|
||||
before do
|
||||
queue_adapter.perform_enqueued_jobs = true
|
||||
|
@ -400,7 +400,7 @@ RSpec.describe Calendar, type: :model do
|
|||
calendar: calendar,
|
||||
condition: {},
|
||||
first_response_time: 120,
|
||||
update_time: 180,
|
||||
response_time: 180,
|
||||
solution_time: 240)
|
||||
end
|
||||
|
||||
|
|
|
@ -377,7 +377,7 @@ RSpec.describe CoreWorkflow, type: :model do
|
|||
base_payload.merge(
|
||||
'screen' => 'edit',
|
||||
'class_name' => 'Sla',
|
||||
'params' => { 'update_time_enabled' => 'true' }
|
||||
'params' => { 'update_time_enabled' => 'true', 'update_type' => 'update' }
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ RSpec.shared_examples 'HasEscalationCalculationImpact', :performs_jobs do
|
|||
|
||||
context 'when affected Ticket existed' do
|
||||
|
||||
subject(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
|
||||
subject(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
|
||||
|
||||
let(:calendar) { create(:calendar, :business_hours_9_17) }
|
||||
let!(:ticket) { create(:ticket) }
|
||||
|
@ -78,7 +78,7 @@ RSpec.shared_examples 'HasEscalationCalculationImpact', :performs_jobs do
|
|||
},
|
||||
},
|
||||
first_response_time: 10,
|
||||
update_time: 20,
|
||||
response_time: 20,
|
||||
solution_time: 300)
|
||||
end
|
||||
|
||||
|
@ -92,7 +92,7 @@ RSpec.shared_examples 'HasEscalationCalculationImpact', :performs_jobs do
|
|||
},
|
||||
},
|
||||
first_response_time: 120,
|
||||
update_time: 180,
|
||||
response_time: 180,
|
||||
solution_time: 240)
|
||||
end
|
||||
|
||||
|
@ -130,7 +130,7 @@ RSpec.shared_examples 'HasEscalationCalculationImpact', :performs_jobs do
|
|||
},
|
||||
calendar: calendar,
|
||||
first_response_time: 60,
|
||||
update_time: 120,
|
||||
response_time: 120,
|
||||
solution_time: 180)
|
||||
end
|
||||
|
||||
|
@ -172,7 +172,7 @@ RSpec.shared_examples 'HasEscalationCalculationImpact', :performs_jobs do
|
|||
},
|
||||
calendar: calendar,
|
||||
first_response_time: 60,
|
||||
update_time: 120,
|
||||
response_time: 120,
|
||||
solution_time: 180)
|
||||
end
|
||||
|
||||
|
@ -214,7 +214,7 @@ RSpec.shared_examples 'HasEscalationCalculationImpact', :performs_jobs do
|
|||
},
|
||||
calendar: calendar,
|
||||
first_response_time: 60,
|
||||
update_time: 120,
|
||||
response_time: 120,
|
||||
solution_time: 180)
|
||||
end
|
||||
|
||||
|
|
|
@ -35,6 +35,28 @@ RSpec.describe Sla, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#cannot_have_response_and_update' do
|
||||
it 'allows neither #response_time nor #update_time' do
|
||||
instance = build(:sla, response_time: nil, update_time: nil)
|
||||
expect(instance).to be_valid
|
||||
end
|
||||
|
||||
it 'allows #response_time' do
|
||||
instance = build(:sla, response_time: 180, update_time: nil)
|
||||
expect(instance).to be_valid
|
||||
end
|
||||
|
||||
it 'allows #update_time' do
|
||||
instance = build(:sla, response_time: nil, update_time: 180)
|
||||
expect(instance).to be_valid
|
||||
end
|
||||
|
||||
it 'denies both #response_time and #update_time' do
|
||||
instance = build(:sla, response_time: 180, update_time: 180)
|
||||
expect(instance).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe '.for_ticket' do
|
||||
it 'returns matching SLA for the ticket' do
|
||||
sla
|
||||
|
|
|
@ -39,7 +39,7 @@ RSpec.shared_examples 'Ticket::Article::HasTicketContactAttributesImpact' do
|
|||
},
|
||||
})
|
||||
end
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 120, solution_time: 180) }
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 120, solution_time: 180) }
|
||||
|
||||
before do
|
||||
sla
|
||||
|
@ -174,7 +174,7 @@ RSpec.shared_examples 'Ticket::Article::HasTicketContactAttributesImpact' do
|
|||
})
|
||||
end
|
||||
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, update_time: 120, solution_time: nil) }
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, response_time: 120, solution_time: nil) }
|
||||
|
||||
before do
|
||||
sla
|
||||
|
@ -252,7 +252,7 @@ RSpec.shared_examples 'Ticket::Article::HasTicketContactAttributesImpact' do
|
|||
})
|
||||
end
|
||||
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, update_time: 120, solution_time: nil) }
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, response_time: 120, solution_time: nil) }
|
||||
|
||||
before do
|
||||
Setting.set('ticket_last_contact_behaviour', 'based_on_customer_reaction')
|
||||
|
|
|
@ -41,7 +41,7 @@ RSpec.shared_examples 'Ticket::Escalation' do
|
|||
},
|
||||
})
|
||||
end
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 120, solution_time: 180) }
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 120, solution_time: 180) }
|
||||
let(:article) { create(:'ticket/article', :inbound_email, ticket: ticket, created_at: '2013-03-21 09:30:00 UTC', updated_at: '2013-03-21 09:30:00 UTC') }
|
||||
|
||||
before do
|
||||
|
@ -434,7 +434,7 @@ RSpec.shared_examples 'Ticket::Escalation' do
|
|||
},
|
||||
},
|
||||
first_response_time: 60,
|
||||
update_time: 180,
|
||||
response_time: 180,
|
||||
solution_time: 240)
|
||||
end
|
||||
|
||||
|
@ -512,7 +512,7 @@ RSpec.shared_examples 'Ticket::Escalation' do
|
|||
})
|
||||
end
|
||||
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 120, update_time: 180, solution_time: 250) }
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 120, response_time: 180, solution_time: 250) }
|
||||
|
||||
context 'when Ticket is reopened' do
|
||||
|
||||
|
@ -663,7 +663,7 @@ RSpec.shared_examples 'Ticket::Escalation' do
|
|||
})
|
||||
end
|
||||
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, update_time: 180, solution_time: 250) }
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, response_time: 180, solution_time: 250) }
|
||||
|
||||
before do
|
||||
sla
|
||||
|
@ -774,7 +774,7 @@ RSpec.shared_examples 'Ticket::Escalation' do
|
|||
})
|
||||
end
|
||||
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, update_time: 180, solution_time: 240) }
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, response_time: 180, solution_time: 240) }
|
||||
|
||||
before do
|
||||
sla
|
||||
|
@ -851,7 +851,7 @@ RSpec.shared_examples 'Ticket::Escalation' do
|
|||
})
|
||||
end
|
||||
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, update_time: 180, solution_time: 240) }
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, response_time: 180, solution_time: 240) }
|
||||
|
||||
before do
|
||||
sla
|
||||
|
@ -950,7 +950,7 @@ RSpec.shared_examples 'Ticket::Escalation' do
|
|||
})
|
||||
end
|
||||
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, update_time: 180, solution_time: 240) }
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, response_time: 180, solution_time: 240) }
|
||||
|
||||
before do
|
||||
sla
|
||||
|
@ -1068,7 +1068,7 @@ RSpec.shared_examples 'Ticket::Escalation' do
|
|||
})
|
||||
end
|
||||
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, update_time: 1200, solution_time: nil) }
|
||||
let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, response_time: 1200, solution_time: nil) }
|
||||
|
||||
before do
|
||||
sla
|
||||
|
|
|
@ -1167,7 +1167,7 @@ RSpec.describe Ticket, type: :model do
|
|||
describe '#escalation_at' do
|
||||
before { travel_to(Time.current) } # freeze time
|
||||
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
|
||||
let(:calendar) { create(:calendar, :'24/7') }
|
||||
|
||||
context 'with no SLAs in the system' do
|
||||
|
@ -1378,7 +1378,7 @@ RSpec.describe Ticket, type: :model do
|
|||
describe '#first_response_escalation_at' do
|
||||
before { travel_to(Time.current) } # freeze time
|
||||
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
|
||||
let(:calendar) { create(:calendar, :'24/7') }
|
||||
|
||||
context 'with no SLAs in the system' do
|
||||
|
@ -1410,7 +1410,7 @@ RSpec.describe Ticket, type: :model do
|
|||
describe '#update_escalation_at' do
|
||||
before { travel_to(Time.current) } # freeze time
|
||||
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
|
||||
let(:calendar) { create(:calendar, :'24/7') }
|
||||
|
||||
context 'with no SLAs in the system' do
|
||||
|
@ -1450,7 +1450,7 @@ RSpec.describe Ticket, type: :model do
|
|||
describe '#close_escalation_at' do
|
||||
before { travel_to(Time.current) } # freeze time
|
||||
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
|
||||
let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
|
||||
let(:calendar) { create(:calendar, :'24/7') }
|
||||
|
||||
context 'with no SLAs in the system' do
|
||||
|
|
|
@ -4,8 +4,8 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe 'Ticket Escalation', type: :request do
|
||||
let(:sla_first_response) { 1.hour }
|
||||
let(:sla_update) { 3.hours }
|
||||
let(:sla_close) { 4.hours }
|
||||
let(:sla_response) { 3.hours }
|
||||
let(:sla_close) { 4.hours }
|
||||
|
||||
let!(:mail_group) { create(:group, email_address: create(:email_address)) }
|
||||
|
||||
|
@ -14,7 +14,7 @@ RSpec.describe 'Ticket Escalation', type: :request do
|
|||
create(:sla,
|
||||
calendar: calendar,
|
||||
first_response_time: sla_first_response / 1.minute,
|
||||
update_time: sla_update / 1.minute,
|
||||
response_time: sla_response / 1.minute,
|
||||
solution_time: sla_close / 1.minute)
|
||||
end
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ RSpec.describe 'Manage > Sla', type: :system do
|
|||
|
||||
# enable all checkboxes
|
||||
page.find('input#update_time', visible: false).find(:xpath, './/..').click
|
||||
page.first('.js-updateTypeSelector', visible: false).click
|
||||
page.find('input#solution_time', visible: false).find(:xpath, './/..').click
|
||||
|
||||
# check if required
|
||||
|
|
Loading…
Reference in a new issue