Refactoring: Migrated static dashboard widgets to extendable backend structure.
This commit is contained in:
parent
9d8ce06948
commit
39bdbf22db
20 changed files with 580 additions and 197 deletions
|
@ -359,11 +359,22 @@ browser:build:
|
|||
- name: registry.znuny.com/docker/docker-imap-devel:latest
|
||||
alias: mail
|
||||
|
||||
## Browser core tests
|
||||
|
||||
.variables_browser_template: &variables_browser_definition
|
||||
RAILS_ENV: "production"
|
||||
APP_RESTART_CMD: "bundle exec rake zammad:ci:app:restart"
|
||||
|
||||
.test_browser_core_template: &test_browser_core_definition
|
||||
<<: *base_env
|
||||
stage: browser-core
|
||||
dependencies:
|
||||
- browser:build
|
||||
|
||||
## Capybara
|
||||
|
||||
.test_capybara_template: &test_capybara_definition
|
||||
<<: *base_env
|
||||
stage: browser-core
|
||||
<<: *test_browser_core_definition
|
||||
script:
|
||||
- bundle exec rake zammad:ci:test:prepare[with_elasticsearch]
|
||||
- bundle exec rspec --fail-fast -t type:system
|
||||
|
@ -396,17 +407,6 @@ test:browser:core:capybara_ff_mysql:
|
|||
<<: *variables_capybara_ff_definition
|
||||
<<: *services_browser_mysql_definition
|
||||
|
||||
## Browser core tests
|
||||
|
||||
.variables_browser_template: &variables_browser_definition
|
||||
RAILS_ENV: "production"
|
||||
APP_RESTART_CMD: "bundle exec rake zammad:ci:app:restart"
|
||||
|
||||
.test_browser_core_template: &test_browser_core_definition
|
||||
<<: *base_env
|
||||
stage: browser-core
|
||||
dependencies:
|
||||
- browser:build
|
||||
|
||||
### API clients
|
||||
|
||||
|
|
|
@ -5,103 +5,23 @@ class App.DashboardStats extends App.Controller
|
|||
@bind('dashboard_stats_rebuild', @load)
|
||||
|
||||
load: =>
|
||||
stats_store = App.StatsStore.first()
|
||||
if stats_store
|
||||
@render(stats_store.data)
|
||||
else
|
||||
@render()
|
||||
@setupStatsWidget('Stats', 'stats', @el)
|
||||
|
||||
render: (data = {}) ->
|
||||
if !data.StatsTicketWaitingTime
|
||||
data.StatsTicketWaitingTime =
|
||||
handling_time: 0
|
||||
average: 0
|
||||
state: 'supergood'
|
||||
average_per_agent: 0
|
||||
if !data.StatsTicketEscalation
|
||||
data.StatsTicketEscalation =
|
||||
state: 'supergood'
|
||||
own: 0
|
||||
total: 0
|
||||
if !data.StatsTicketChannelDistribution
|
||||
data.StatsTicketChannelDistribution =
|
||||
channels:
|
||||
1:
|
||||
inbound: 0
|
||||
outbound: 0
|
||||
inbound_in_percent: 0
|
||||
outbound_in_percent: 0
|
||||
2:
|
||||
inbound: 0
|
||||
outbound: 0
|
||||
inbound_in_percent: 0
|
||||
outbound_in_percent: 0
|
||||
3:
|
||||
inbound: 0
|
||||
outbound: 0
|
||||
inbound_in_percent: 0
|
||||
outbound_in_percent: 0
|
||||
if !data.StatsTicketLoadMeasure
|
||||
data.StatsTicketLoadMeasure =
|
||||
state: 'supergood'
|
||||
percent: 0
|
||||
own: 0
|
||||
total: 0
|
||||
average_per_agent: 0
|
||||
if !data.StatsTicketInProcess
|
||||
data.StatsTicketInProcess =
|
||||
state: 'supergood'
|
||||
percent: 0
|
||||
average_per_agent: 0
|
||||
if !data.StatsTicketReopen
|
||||
data.StatsTicketReopen =
|
||||
state: 'supergood'
|
||||
percent: 0
|
||||
average_per_agent: 0
|
||||
setupStatsWidget: (config, event, el) ->
|
||||
|
||||
@html App.view('dashboard/stats')(data)
|
||||
# load all statsWidgets ./stats/*
|
||||
App.Event.trigger(event + ':init')
|
||||
statsWidgets = App.Config.get(config)
|
||||
if statsWidgets
|
||||
widgets = $.map(statsWidgets, (v) -> v )
|
||||
widgets = _.sortBy(widgets, (item) -> return item.prio)
|
||||
|
||||
if data.StatsTicketWaitingTime
|
||||
@renderWidgetClockFace(data.StatsTicketWaitingTime.handling_time, data.StatsTicketWaitingTime.state, data.StatsTicketWaitingTime.percent)
|
||||
|
||||
renderWidgetClockFace: (time, state, percent) =>
|
||||
canvas = @el.find 'canvas'
|
||||
ctx = canvas.get(0).getContext '2d'
|
||||
radius = 26
|
||||
|
||||
@el.find('.time.stat-widget .stat-amount').text time
|
||||
|
||||
canvas.attr 'width', 2 * radius
|
||||
canvas.attr 'height', 2 * radius
|
||||
|
||||
handlingTimeColors = {}
|
||||
handlingTimeColors['supergood'] = '#38AE6A' # supergood
|
||||
handlingTimeColors['good'] = '#A9AC41' # good
|
||||
handlingTimeColors['ok'] = '#FAAB00' # ok
|
||||
handlingTimeColors['bad'] = '#F6820B' # bad
|
||||
handlingTimeColors['superbad'] = '#F35910' # superbad
|
||||
|
||||
for handlingState, timeColor of handlingTimeColors
|
||||
if state == handlingState
|
||||
backgroundColor = timeColor
|
||||
break
|
||||
|
||||
# 30% background
|
||||
if time isnt 0
|
||||
ctx.globalAlpha = 0.3
|
||||
ctx.fillStyle = backgroundColor
|
||||
ctx.beginPath()
|
||||
ctx.arc radius, radius, radius, 0, Math.PI * 2, true
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
# 100% pie piece
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo radius, radius
|
||||
arcsector = Math.PI * 2 * percent
|
||||
ctx.arc radius, radius, radius, -Math.PI/2, arcsector - Math.PI/2, false
|
||||
ctx.lineTo radius, radius
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
for widget in widgets
|
||||
if @permissionCheck(widget.permission)
|
||||
try
|
||||
new widget.controller(
|
||||
el: el
|
||||
)
|
||||
catch e
|
||||
@log 'error', "statsWidgets #{key}:", e
|
||||
App.Event.trigger(event + ':ready')
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
class Stats extends App.Controller
|
||||
constructor: ->
|
||||
super
|
||||
@load()
|
||||
|
||||
load: =>
|
||||
stats_store = App.StatsStore.first()
|
||||
if stats_store
|
||||
@render(stats_store.data)
|
||||
else
|
||||
@render()
|
||||
|
||||
render: (data = {}) ->
|
||||
if !data.StatsTicketChannelDistribution
|
||||
data.StatsTicketChannelDistribution =
|
||||
channels:
|
||||
1:
|
||||
inbound: 1
|
||||
outbound: 0
|
||||
inbound_in_percent: 0
|
||||
outbound_in_percent: 0
|
||||
2:
|
||||
inbound: 0
|
||||
outbound: 0
|
||||
inbound_in_percent: 0
|
||||
outbound_in_percent: 0
|
||||
3:
|
||||
inbound: 2
|
||||
outbound: 0
|
||||
inbound_in_percent: 0
|
||||
outbound_in_percent: 0
|
||||
|
||||
content = App.view('dashboard/stats/ticket_channel_distribution')(data)
|
||||
|
||||
if @$('.ticket_channel_distribution').length > 0
|
||||
@$('.ticket_channel_distribution').html(content)
|
||||
else
|
||||
@el.append(content)
|
||||
|
||||
App.Config.set('ticket_channel_distribution', {controller: Stats, permission: 'ticket.agent', prio: 300 }, 'Stats')
|
|
@ -0,0 +1,27 @@
|
|||
class Stats extends App.Controller
|
||||
constructor: ->
|
||||
super
|
||||
@load()
|
||||
|
||||
load: =>
|
||||
stats_store = App.StatsStore.first()
|
||||
if stats_store
|
||||
@render(stats_store.data)
|
||||
else
|
||||
@render()
|
||||
|
||||
render: (data = {}) ->
|
||||
if !data.StatsTicketEscalation
|
||||
data.StatsTicketEscalation =
|
||||
state: 'supergood'
|
||||
own: 0
|
||||
total: 0
|
||||
|
||||
content = App.view('dashboard/stats/ticket_escalation')(data)
|
||||
|
||||
if @$('.ticket_escalation').length > 0
|
||||
@$('.ticket_escalation').html(content)
|
||||
else
|
||||
@el.append(content)
|
||||
|
||||
App.Config.set('ticket_escalation', {controller: Stats, permission: 'ticket.agent', prio: 200 }, 'Stats')
|
|
@ -0,0 +1,28 @@
|
|||
class Stats extends App.Controller
|
||||
constructor: ->
|
||||
super
|
||||
@load()
|
||||
|
||||
load: =>
|
||||
stats_store = App.StatsStore.first()
|
||||
if stats_store
|
||||
@render(stats_store.data)
|
||||
else
|
||||
@render()
|
||||
|
||||
render: (data = {}) ->
|
||||
if !data.StatsTicketInProcess
|
||||
data.StatsTicketInProcess =
|
||||
state: 'supergood'
|
||||
percent: 0
|
||||
average_per_agent: 0
|
||||
|
||||
content = App.view('dashboard/stats/ticket_in_process')(data)
|
||||
|
||||
if @$('.ticket_in_process').length > 0
|
||||
@$('.ticket_in_process').html(content)
|
||||
else
|
||||
@el.append(content)
|
||||
|
||||
|
||||
App.Config.set('ticket_in_process', {controller: Stats, permission: 'ticket.agent', prio: 500 }, 'Stats')
|
|
@ -0,0 +1,29 @@
|
|||
class Stats extends App.Controller
|
||||
constructor: ->
|
||||
super
|
||||
@load()
|
||||
|
||||
load: =>
|
||||
stats_store = App.StatsStore.first()
|
||||
if stats_store
|
||||
@render(stats_store.data)
|
||||
else
|
||||
@render()
|
||||
|
||||
render: (data = {}) ->
|
||||
if !data.StatsTicketLoadMeasure
|
||||
data.StatsTicketLoadMeasure =
|
||||
state: 'supergood'
|
||||
percent: 0
|
||||
own: 0
|
||||
total: 0
|
||||
average_per_agent: 0
|
||||
|
||||
content = App.view('dashboard/stats/ticket_load_measure')(data)
|
||||
|
||||
if @$('.ticket_load_measure').length > 0
|
||||
@$('.ticket_load_measure').html(content)
|
||||
else
|
||||
@el.append(content)
|
||||
|
||||
App.Config.set('ticket_load_measure', {controller: Stats, permission: 'ticket.agent', prio: 400 }, 'Stats')
|
|
@ -0,0 +1,26 @@
|
|||
class Stats extends App.Controller
|
||||
constructor: ->
|
||||
super
|
||||
@load()
|
||||
|
||||
load: =>
|
||||
stats_store = App.StatsStore.first()
|
||||
if stats_store
|
||||
@render(stats_store.data)
|
||||
else
|
||||
@render()
|
||||
|
||||
render: (data = {}) ->
|
||||
if !data.StatsTicketReopen
|
||||
data.StatsTicketReopen =
|
||||
state: 'supergood'
|
||||
percent: 0
|
||||
average_per_agent: 0
|
||||
|
||||
content = App.view('dashboard/stats/ticket_reopen')(data)
|
||||
if @$('.ticket_reopen').length > 0
|
||||
@$('.ticket_reopen').html(content)
|
||||
else
|
||||
@el.append(content)
|
||||
|
||||
App.Config.set('ticket_reopen', {controller: Stats, permission: 'ticket.agent', prio: 600 }, 'Stats')
|
|
@ -0,0 +1,72 @@
|
|||
class Stats extends App.Controller
|
||||
constructor: ->
|
||||
super
|
||||
@load()
|
||||
|
||||
load: =>
|
||||
stats_store = App.StatsStore.first()
|
||||
if stats_store
|
||||
@render(stats_store.data)
|
||||
else
|
||||
@render()
|
||||
|
||||
render: (data = {}) ->
|
||||
if !data.StatsTicketWaitingTime
|
||||
data.StatsTicketWaitingTime =
|
||||
handling_time: 0
|
||||
average: 0
|
||||
state: 'supergood'
|
||||
average_per_agent: 0
|
||||
|
||||
content = App.view('dashboard/stats/ticket_waiting_time')(data)
|
||||
if @$('.ticket_waiting_time').length > 0
|
||||
@$('.ticket_waiting_time').html(content)
|
||||
else
|
||||
@el.append(content)
|
||||
|
||||
if data.StatsTicketWaitingTime
|
||||
@renderWidgetClockFace(data.StatsTicketWaitingTime.handling_time, data.StatsTicketWaitingTime.state, data.StatsTicketWaitingTime.percent)
|
||||
|
||||
renderWidgetClockFace: (time, state, percent) =>
|
||||
canvas = @el.find 'canvas'
|
||||
ctx = canvas.get(0).getContext '2d'
|
||||
radius = 26
|
||||
|
||||
@el.find('.time.stat-widget .stat-amount').text time
|
||||
|
||||
canvas.attr 'width', 2 * radius
|
||||
canvas.attr 'height', 2 * radius
|
||||
|
||||
handlingTimeColors = {}
|
||||
handlingTimeColors['supergood'] = '#38AE6A' # supergood
|
||||
handlingTimeColors['good'] = '#A9AC41' # good
|
||||
handlingTimeColors['ok'] = '#FAAB00' # ok
|
||||
handlingTimeColors['bad'] = '#F6820B' # bad
|
||||
handlingTimeColors['superbad'] = '#F35910' # superbad
|
||||
|
||||
for handlingState, timeColor of handlingTimeColors
|
||||
if state == handlingState
|
||||
backgroundColor = timeColor
|
||||
break
|
||||
|
||||
# 30% background
|
||||
if time isnt 0
|
||||
ctx.globalAlpha = 0.3
|
||||
ctx.fillStyle = backgroundColor
|
||||
ctx.beginPath()
|
||||
ctx.arc radius, radius, radius, 0, Math.PI * 2, true
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
# 100% pie piece
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo radius, radius
|
||||
arcsector = Math.PI * 2 * percent
|
||||
ctx.arc radius, radius, radius, -Math.PI/2, arcsector - Math.PI/2, false
|
||||
ctx.lineTo radius, radius
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
App.Config.set('ticket_waiting_time', {controller: Stats, permission: 'ticket.agent', prio: 100 }, 'Stats')
|
|
@ -1,77 +0,0 @@
|
|||
<div class="column">
|
||||
<div class="time stat-widget vertical">
|
||||
<div class="stat-title"><%- @T('∅ Waiting time today') %></div>
|
||||
<div class="stat-graphic">
|
||||
<div class="stat-stopwatch centered">
|
||||
<%- @Icon('stopwatch', 'stat-icon stopwatch-icon') %>
|
||||
<canvas class="stat-dial"></canvas>
|
||||
<div class="stat-amount"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-label"><%- @T('My handling time: %s minutes', @StatsTicketWaitingTime.handling_time) %></div>
|
||||
<div class="stat-detail"><%- @T('Average: %s minutes', @StatsTicketWaitingTime.average_per_agent) %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="mood stat-widget vertical">
|
||||
<div class="stat-title"><%- @T('Mood') %></div>
|
||||
<div class="stat-graphic">
|
||||
<%- @Icon("mood-#{@StatsTicketEscalation.state}", 'stat-icon mood-icon') %>
|
||||
</div>
|
||||
<div class="stat-label"><%- @T('%s of my tickets escalated.', @StatsTicketEscalation.own) %></div>
|
||||
<div class="stat-detail"><%- @T('Total: %s', @StatsTicketEscalation.total) %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="channel-distribution stat-widget vertical centered">
|
||||
<div class="stat-title"><%- @T('Channel Distribution') %></div>
|
||||
<div class="stat-graphic">
|
||||
<% for channel_name, channel of @StatsTicketChannelDistribution.channels: %>
|
||||
<div class="stats-row email-channel">
|
||||
<%- @Icon(channel.icon, 'stat-channel-icon') %>
|
||||
<div class="stat-bars">
|
||||
<div class="stat-bar primary" style="height: <%- channel.inbound_in_percent %>%" title="<%- @Ti('Inbound') %>: <%- channel.inbound_in_percent %>% (<%- channel.inbound %>)"></div>
|
||||
<div class="stat-bar secondary" style="height: <%- channel.outbound_in_percent %>%" title="<%- @Ti('Outbound') %>: <%- channel.outbound_in_percent %>% (<%- channel.outbound %>)"></div>
|
||||
</div>
|
||||
<div class="stat-label"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="status stat-widget vertical">
|
||||
<div class="stat-title"><%- @T('Assigned') %></div>
|
||||
<div class="stat-graphic">
|
||||
<div class="stat-tickets vertical reverse end">
|
||||
<% stack_counter = parseInt(@StatsTicketLoadMeasure.percent*0.16) %>
|
||||
<% for count in [1..stack_counter]: %>
|
||||
<%- @Icon('one-ticket', "one-ticket #{@StatsTicketLoadMeasure.state}-color") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%- @Icon('total-tickets', 'total-tickets') %>
|
||||
</div>
|
||||
<div class="stat-label"><%- @T('Tickets assigned to me: %s of %s', @StatsTicketLoadMeasure.own, @StatsTicketLoadMeasure.total) %></div>
|
||||
<div class="stat-detail"><%- @T('Average: %s', @StatsTicketLoadMeasure.average_per_agent) %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="in-process stat-widget vertical">
|
||||
<div class="stat-title"><%- @T('Your tickets in process') %></div>
|
||||
<div class="stat-graphic">
|
||||
<%- @Icon('in-process', "in-process-icon #{@StatsTicketInProcess.state}-color") %>
|
||||
</div>
|
||||
<div class="stat-label" title="<%- @T('%s of your tickets are currently in process.', @StatsTicketInProcess.in_process) %>"><%- @T('%s% are currently in process', @StatsTicketInProcess.percent) %></div>
|
||||
<div class="stat-detail"><%- @T('Average: %s%', @StatsTicketInProcess.average_per_agent) %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="reopening stat-widget vertical">
|
||||
<div class="stat-title"><%- @T('Reopening rate') %></div>
|
||||
<div class="stat-graphic">
|
||||
<%- @Icon('reopening', "reopening-icon #{@StatsTicketReopen.state}-color") %>
|
||||
</div>
|
||||
<div class="stat-label"><%- @T('%s% have been reopened', @StatsTicketReopen.percent) %></div>
|
||||
<div class="stat-detail"><%- @T('Average: %s%', @StatsTicketReopen.average_per_agent) %></div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,17 @@
|
|||
<div class="column ticket_channel_distribution">
|
||||
<div class="stat-widget vertical centered">
|
||||
<div class="stat-title"><%- @T('Channel Distribution') %></div>
|
||||
<div class="stat-graphic">
|
||||
<% for channel_name, channel of @StatsTicketChannelDistribution.channels: %>
|
||||
<div class="stats-row email-channel">
|
||||
<%- @Icon(channel.icon, 'stat-channel-icon') %>
|
||||
<div class="stat-bars">
|
||||
<div class="stat-bar primary" style="height: <%- channel.inbound_in_percent %>%" title="<%- @Ti('Inbound') %>: <%- channel.inbound_in_percent %>% (<%- channel.inbound %>)"></div>
|
||||
<div class="stat-bar secondary" style="height: <%- channel.outbound_in_percent %>%" title="<%- @Ti('Outbound') %>: <%- channel.outbound_in_percent %>% (<%- channel.outbound %>)"></div>
|
||||
</div>
|
||||
<div class="stat-label"></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
<div class="column ticket_escalation">
|
||||
<div class="stat-widget vertical">
|
||||
<div class="stat-title"><%- @T('Mood') %></div>
|
||||
<div class="stat-graphic">
|
||||
<%- @Icon("mood-#{@StatsTicketEscalation.state}", 'stat-icon mood-icon') %>
|
||||
</div>
|
||||
<div class="stat-label"><%- @T('%s of my tickets escalated.', @StatsTicketEscalation.own) %></div>
|
||||
<div class="stat-detail"><%- @T('Total: %s', @StatsTicketEscalation.total) %></div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
<div class="column ticket_in_process">
|
||||
<div class="stat-widget vertical">
|
||||
<div class="stat-title"><%- @T('Your tickets in process') %></div>
|
||||
<div class="stat-graphic">
|
||||
<%- @Icon('in-process', "in-process-icon #{@StatsTicketInProcess.state}-color") %>
|
||||
</div>
|
||||
<div class="stat-label" title="<%- @T('%s of your tickets are currently in process.', @StatsTicketInProcess.in_process) %>"><%- @T('%s% are currently in process', @StatsTicketInProcess.percent) %></div>
|
||||
<div class="stat-detail"><%- @T('Average: %s%', @StatsTicketInProcess.average_per_agent) %></div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,16 @@
|
|||
<div class="column ticket_load_measure">
|
||||
<div class="stat-widget vertical">
|
||||
<div class="stat-title"><%- @T('Assigned') %></div>
|
||||
<div class="stat-graphic">
|
||||
<div class="stat-tickets vertical reverse end">
|
||||
<% stack_counter = parseInt(@StatsTicketLoadMeasure.percent*0.16) %>
|
||||
<% for count in [1..stack_counter]: %>
|
||||
<%- @Icon('one-ticket', "one-ticket #{@StatsTicketLoadMeasure.state}-color") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%- @Icon('total-tickets', 'total-tickets') %>
|
||||
</div>
|
||||
<div class="stat-label"><%- @T('Tickets assigned to me: %s of %s', @StatsTicketLoadMeasure.own, @StatsTicketLoadMeasure.total) %></div>
|
||||
<div class="stat-detail"><%- @T('Average: %s', @StatsTicketLoadMeasure.average_per_agent) %></div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
<div class="column ticket_reopen">
|
||||
<div class="stat-widget vertical">
|
||||
<div class="stat-title"><%- @T('Reopening rate') %></div>
|
||||
<div class="stat-graphic">
|
||||
<%- @Icon('reopening', "reopening-icon #{@StatsTicketReopen.state}-color") %>
|
||||
</div>
|
||||
<div class="stat-label"><%- @T('%s% have been reopened', @StatsTicketReopen.percent) %></div>
|
||||
<div class="stat-detail"><%- @T('Average: %s%', @StatsTicketReopen.average_per_agent) %></div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
<div class="column ticket_waiting_time">
|
||||
<div class="time stat-widget vertical">
|
||||
<div class="stat-title"><%- @T('∅ Waiting time today') %></div>
|
||||
<div class="stat-graphic">
|
||||
<div class="stat-stopwatch centered">
|
||||
<%- @Icon('stopwatch', 'stat-icon stopwatch-icon') %>
|
||||
<canvas class="stat-dial"></canvas>
|
||||
<div class="stat-amount"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-label"><%- @T('My handling time: %s minutes', @StatsTicketWaitingTime.handling_time) %></div>
|
||||
<div class="stat-detail"><%- @T('Average: %s minutes', @StatsTicketWaitingTime.average_per_agent) %></div>
|
||||
</div>
|
||||
</div>
|
98
db/migrate/20181023163804_add_stats_backends.rb
Normal file
98
db/migrate/20181023163804_add_stats_backends.rb
Normal file
|
@ -0,0 +1,98 @@
|
|||
class AddStatsBackends < ActiveRecord::Migration[5.1]
|
||||
|
||||
def up
|
||||
|
||||
return if !Setting.find_by(name: 'system_init_done')
|
||||
|
||||
# add the dashboard stats backend for 'Stats::TicketWaitingTime'
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Stats Backend',
|
||||
name: 'Stats::TicketWaitingTime',
|
||||
area: 'Dashboard::Stats',
|
||||
description: 'Defines a dashboard stats backend that get scheduled automatically.',
|
||||
options: {},
|
||||
state: 'Stats::TicketWaitingTime',
|
||||
preferences: {
|
||||
permission: ['ticket.agent'],
|
||||
prio: 1,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
||||
# add the dashboard stats backend for 'Stats::TicketEscalation'
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Stats Backend',
|
||||
name: 'Stats::TicketEscalation',
|
||||
area: 'Dashboard::Stats',
|
||||
description: 'Defines a dashboard stats backend that get scheduled automatically.',
|
||||
options: {},
|
||||
state: 'Stats::TicketEscalation',
|
||||
preferences: {
|
||||
permission: ['ticket.agent'],
|
||||
prio: 2,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
||||
# add the dashboard stats backend for 'Stats::TicketChannelDistribution'
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Stats Backend',
|
||||
name: 'Stats::TicketChannelDistribution',
|
||||
area: 'Dashboard::Stats',
|
||||
description: 'Defines a dashboard stats backend that get scheduled automatically.',
|
||||
options: {},
|
||||
state: 'Stats::TicketChannelDistribution',
|
||||
preferences: {
|
||||
permission: ['ticket.agent'],
|
||||
prio: 3,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
||||
# add the dashboard stats backend for 'Stats::TicketLoadMeasure'
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Stats Backend',
|
||||
name: 'Stats::TicketLoadMeasure',
|
||||
area: 'Dashboard::Stats',
|
||||
description: 'Defines a dashboard stats backend that get scheduled automatically.',
|
||||
options: {},
|
||||
state: 'Stats::TicketLoadMeasure',
|
||||
preferences: {
|
||||
permission: ['ticket.agent'],
|
||||
prio: 4,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
||||
# add the dashboard stats backend for 'Stats::TicketInProcess'
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Stats Backend',
|
||||
name: 'Stats::TicketInProcess',
|
||||
area: 'Dashboard::Stats',
|
||||
description: 'Defines a dashboard stats backend that get scheduled automatically.',
|
||||
options: {},
|
||||
state: 'Stats::TicketInProcess',
|
||||
preferences: {
|
||||
permission: ['ticket.agent'],
|
||||
prio: 5,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
||||
# add the dashboard stats backend for 'Stats::TicketReopen'
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Stats Backend',
|
||||
name: 'Stats::TicketReopen',
|
||||
area: 'Dashboard::Stats',
|
||||
description: 'Defines a dashboard stats backend that get scheduled automatically.',
|
||||
options: {},
|
||||
state: 'Stats::TicketReopen',
|
||||
preferences: {
|
||||
permission: ['ticket.agent'],
|
||||
prio: 6,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
||||
end
|
||||
end
|
|
@ -4225,3 +4225,93 @@ Setting.create_if_not_exists(
|
|||
},
|
||||
frontend: false
|
||||
)
|
||||
|
||||
# add the dashboard stats backend for 'Stats::TicketWaitingTime'
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Stats Backend',
|
||||
name: 'Stats::TicketWaitingTime',
|
||||
area: 'Dashboard::Stats',
|
||||
description: 'Defines a dashboard stats backend that get scheduled automatically.',
|
||||
options: {},
|
||||
state: 'Stats::TicketWaitingTime',
|
||||
preferences: {
|
||||
permission: ['ticket.agent'],
|
||||
prio: 1,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
||||
# add the dashboard stats backend for 'Stats::TicketEscalation'
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Stats Backend',
|
||||
name: 'Stats::TicketEscalation',
|
||||
area: 'Dashboard::Stats',
|
||||
description: 'Defines a dashboard stats backend that get scheduled automatically.',
|
||||
options: {},
|
||||
state: 'Stats::TicketEscalation',
|
||||
preferences: {
|
||||
permission: ['ticket.agent'],
|
||||
prio: 2,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
||||
# add the dashboard stats backend for 'Stats::TicketChannelDistribution'
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Stats Backend',
|
||||
name: 'Stats::TicketChannelDistribution',
|
||||
area: 'Dashboard::Stats',
|
||||
description: 'Defines a dashboard stats backend that get scheduled automatically.',
|
||||
options: {},
|
||||
state: 'Stats::TicketChannelDistribution',
|
||||
preferences: {
|
||||
permission: ['ticket.agent'],
|
||||
prio: 3,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
||||
# add the dashboard stats backend for 'Stats::TicketLoadMeasure'
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Stats Backend',
|
||||
name: 'Stats::TicketLoadMeasure',
|
||||
area: 'Dashboard::Stats',
|
||||
description: 'Defines a dashboard stats backend that get scheduled automatically.',
|
||||
options: {},
|
||||
state: 'Stats::TicketLoadMeasure',
|
||||
preferences: {
|
||||
permission: ['ticket.agent'],
|
||||
prio: 4,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
||||
# add the dashboard stats backend for 'Stats::TicketInProcess'
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Stats Backend',
|
||||
name: 'Stats::TicketInProcess',
|
||||
area: 'Dashboard::Stats',
|
||||
description: 'Defines a dashboard stats backend that get scheduled automatically.',
|
||||
options: {},
|
||||
state: 'Stats::TicketInProcess',
|
||||
preferences: {
|
||||
permission: ['ticket.agent'],
|
||||
prio: 5,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
||||
# add the dashboard stats backend for 'Stats::TicketReopen'
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Stats Backend',
|
||||
name: 'Stats::TicketReopen',
|
||||
area: 'Dashboard::Stats',
|
||||
description: 'Defines a dashboard stats backend that get scheduled automatically.',
|
||||
options: {},
|
||||
state: 'Stats::TicketReopen',
|
||||
preferences: {
|
||||
permission: ['ticket.agent'],
|
||||
prio: 6,
|
||||
},
|
||||
frontend: false
|
||||
)
|
||||
|
|
28
lib/stats.rb
28
lib/stats.rb
|
@ -15,15 +15,6 @@ returns
|
|||
|
||||
def self.generate
|
||||
|
||||
backends = [
|
||||
Stats::TicketChannelDistribution,
|
||||
Stats::TicketInProcess,
|
||||
Stats::TicketLoadMeasure,
|
||||
Stats::TicketEscalation,
|
||||
Stats::TicketReopen,
|
||||
Stats::TicketWaitingTime,
|
||||
]
|
||||
|
||||
# generate stats per agent
|
||||
users = User.with_permissions('ticket.agent')
|
||||
agent_count = 0
|
||||
|
@ -34,7 +25,24 @@ returns
|
|||
|
||||
agent_count += 1
|
||||
data = {}
|
||||
backends.each do |backend|
|
||||
|
||||
backends = Setting.where(area: 'Dashboard::Stats')
|
||||
if backends.blank?
|
||||
raise "No settings with area 'Dashboard::Stats' defined"
|
||||
end
|
||||
|
||||
backends.each do |stats_item|
|
||||
# additional permission check
|
||||
next if stats_item.preferences[:permission] && !user.permissions?(stats_item.preferences[:permission])
|
||||
|
||||
backend = stats_item.state_current[:value]
|
||||
if !backend
|
||||
raise 'Dashboard::Stats backend ' + stats_item.name + ' is not defined'
|
||||
end
|
||||
|
||||
require_dependency backend.to_filename
|
||||
backend = backend.constantize
|
||||
|
||||
data[backend] = backend.generate(user)
|
||||
end
|
||||
user_result[user.id] = data
|
||||
|
|
29
spec/lib/stats_spec.rb
Normal file
29
spec/lib/stats_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Stats do
|
||||
|
||||
describe '#generate' do
|
||||
|
||||
before do
|
||||
# create a user for which the stats can be generated
|
||||
create(:agent_user)
|
||||
end
|
||||
|
||||
it 'generates stats' do
|
||||
expect { Stats.generate }.to_not raise_error
|
||||
end
|
||||
|
||||
context 'when backend registration is invalid' do
|
||||
|
||||
it 'fails for empty registration' do
|
||||
Setting.set('Stats::TicketWaitingTime', nil)
|
||||
expect { Stats.generate }.to raise_error(RuntimeError)
|
||||
end
|
||||
|
||||
it 'fails for unknown backend' do
|
||||
Setting.set('Stats::TicketWaitingTime', 'Stats::UNKNOWN')
|
||||
expect { Stats.generate }.to raise_error(LoadError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
16
spec/system/dashboard_spec.rb
Normal file
16
spec/system/dashboard_spec.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Dashboard', type: :system, authenticated: true do
|
||||
|
||||
it 'shows default widgets' do
|
||||
visit 'dashboard'
|
||||
|
||||
expect(page).to have_css('.stat-widgets')
|
||||
expect(page).to have_css('.ticket_waiting_time > div > div.stat-title', text: /∅ Waiting time today/i)
|
||||
expect(page).to have_css('.ticket_escalation > div > div.stat-title', text: /Mood/i)
|
||||
expect(page).to have_css('.ticket_channel_distribution > div > div.stat-title', text: /Channel Distribution/i)
|
||||
expect(page).to have_css('.ticket_load_measure > div > div.stat-title', text: /Assigned/i)
|
||||
expect(page).to have_css('.ticket_in_process > div > div.stat-title', text: /Your tickets in process/i)
|
||||
expect(page).to have_css('.ticket_reopen > div > div.stat-title', text: /Reopening rate/i)
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue