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
|
- name: registry.znuny.com/docker/docker-imap-devel:latest
|
||||||
alias: mail
|
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
|
## Capybara
|
||||||
|
|
||||||
.test_capybara_template: &test_capybara_definition
|
.test_capybara_template: &test_capybara_definition
|
||||||
<<: *base_env
|
<<: *test_browser_core_definition
|
||||||
stage: browser-core
|
|
||||||
script:
|
script:
|
||||||
- bundle exec rake zammad:ci:test:prepare[with_elasticsearch]
|
- bundle exec rake zammad:ci:test:prepare[with_elasticsearch]
|
||||||
- bundle exec rspec --fail-fast -t type:system
|
- bundle exec rspec --fail-fast -t type:system
|
||||||
|
@ -396,17 +407,6 @@ test:browser:core:capybara_ff_mysql:
|
||||||
<<: *variables_capybara_ff_definition
|
<<: *variables_capybara_ff_definition
|
||||||
<<: *services_browser_mysql_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
|
### API clients
|
||||||
|
|
||||||
|
|
|
@ -5,103 +5,23 @@ class App.DashboardStats extends App.Controller
|
||||||
@bind('dashboard_stats_rebuild', @load)
|
@bind('dashboard_stats_rebuild', @load)
|
||||||
|
|
||||||
load: =>
|
load: =>
|
||||||
stats_store = App.StatsStore.first()
|
@setupStatsWidget('Stats', 'stats', @el)
|
||||||
if stats_store
|
|
||||||
@render(stats_store.data)
|
|
||||||
else
|
|
||||||
@render()
|
|
||||||
|
|
||||||
render: (data = {}) ->
|
setupStatsWidget: (config, event, el) ->
|
||||||
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
|
|
||||||
|
|
||||||
@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
|
for widget in widgets
|
||||||
@renderWidgetClockFace(data.StatsTicketWaitingTime.handling_time, data.StatsTicketWaitingTime.state, data.StatsTicketWaitingTime.percent)
|
if @permissionCheck(widget.permission)
|
||||||
|
try
|
||||||
renderWidgetClockFace: (time, state, percent) =>
|
new widget.controller(
|
||||||
canvas = @el.find 'canvas'
|
el: el
|
||||||
ctx = canvas.get(0).getContext '2d'
|
)
|
||||||
radius = 26
|
catch e
|
||||||
|
@log 'error', "statsWidgets #{key}:", e
|
||||||
@el.find('.time.stat-widget .stat-amount').text time
|
App.Event.trigger(event + ':ready')
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
|
@ -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
|
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
|
def self.generate
|
||||||
|
|
||||||
backends = [
|
|
||||||
Stats::TicketChannelDistribution,
|
|
||||||
Stats::TicketInProcess,
|
|
||||||
Stats::TicketLoadMeasure,
|
|
||||||
Stats::TicketEscalation,
|
|
||||||
Stats::TicketReopen,
|
|
||||||
Stats::TicketWaitingTime,
|
|
||||||
]
|
|
||||||
|
|
||||||
# generate stats per agent
|
# generate stats per agent
|
||||||
users = User.with_permissions('ticket.agent')
|
users = User.with_permissions('ticket.agent')
|
||||||
agent_count = 0
|
agent_count = 0
|
||||||
|
@ -34,7 +25,24 @@ returns
|
||||||
|
|
||||||
agent_count += 1
|
agent_count += 1
|
||||||
data = {}
|
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)
|
data[backend] = backend.generate(user)
|
||||||
end
|
end
|
||||||
user_result[user.id] = data
|
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