Init version of dashboard stats.
This commit is contained in:
parent
f527a058c6
commit
aee48e22c2
14 changed files with 729 additions and 157 deletions
|
@ -0,0 +1,106 @@
|
|||
class App.DashboardStats extends App.Controller
|
||||
constructor: ->
|
||||
super
|
||||
|
||||
stats_store = App.StatsStore.first()
|
||||
if stats_store
|
||||
@render(stats_store.data)
|
||||
else
|
||||
@render()
|
||||
|
||||
# bind to rebuild view event
|
||||
@bind('dashboard_stats_rebuild', @render)
|
||||
|
||||
render: (data = {}) ->
|
||||
if !data.StatsTicketWaitingTime
|
||||
data.StatsTicketWaitingTime =
|
||||
handling_time: 0
|
||||
average: 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: 0
|
||||
if !data.StatsTicketInProcess
|
||||
data.StatsTicketInProcess =
|
||||
state: 'supergood'
|
||||
percent: 0
|
||||
average_percent: 0
|
||||
if !data.StatsTicketReopen
|
||||
data.StatsTicketReopen =
|
||||
state: 'supergood'
|
||||
percent: 0
|
||||
average_percent: 0
|
||||
|
||||
@html App.view('dashboard/stats')(data)
|
||||
|
||||
if data.StatsTicketWaitingTime
|
||||
@renderWidgetClockFace data.StatsTicketWaitingTime.handling_time
|
||||
|
||||
renderWidgetClockFace: (time) =>
|
||||
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
|
||||
|
||||
time = 60 if time > 60
|
||||
|
||||
handlingTimeColors =
|
||||
5: '#38AE6A' # supergood
|
||||
10: '#A9AC41' # good
|
||||
15: '#FAAB00' # ok
|
||||
20: '#F6820B' # bad
|
||||
25: '#F35910' # superbad
|
||||
|
||||
for handlingTime, timeColor of handlingTimeColors
|
||||
if time <= handlingTime
|
||||
backgroundColor = timeColor
|
||||
break
|
||||
|
||||
# 30% background
|
||||
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 * time/60
|
||||
ctx.arc radius, radius, radius, -Math.PI/2, arcsector - Math.PI/2, false
|
||||
ctx.lineTo radius, radius
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
|
@ -7,7 +7,7 @@ class App.Dashboard extends App.Controller
|
|||
super
|
||||
|
||||
if @isRole('Customer')
|
||||
@navigate '#'
|
||||
@navigate '#', true
|
||||
return
|
||||
|
||||
# render page
|
||||
|
@ -25,62 +25,21 @@ class App.Dashboard extends App.Controller
|
|||
isAdmin: @isRole('Admin')
|
||||
)
|
||||
|
||||
new App.DashboardStats(
|
||||
el: @$('.stat-widgets')
|
||||
)
|
||||
|
||||
new App.DashboardActivityStream(
|
||||
el: @$('.sidebar')
|
||||
limit: 25
|
||||
)
|
||||
|
||||
@renderWidgetClockFace 25
|
||||
|
||||
clues: (e) =>
|
||||
e.preventDefault()
|
||||
new App.FirstStepsClues(
|
||||
el: @el
|
||||
)
|
||||
|
||||
renderWidgetClockFace: (time) =>
|
||||
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
|
||||
|
||||
time = 60 if time > 60
|
||||
|
||||
handlingTimeColors =
|
||||
5: '#38AE6A' # supergood
|
||||
10: '#A9AC41' # good
|
||||
15: '#FAAB00' # ok
|
||||
20: '#F6820B' # bad
|
||||
25: '#F35910' # superbad
|
||||
|
||||
for handlingTime, timeColor of handlingTimeColors
|
||||
if time <= handlingTime
|
||||
backgroundColor = timeColor
|
||||
break
|
||||
|
||||
# 30% background
|
||||
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 * time/60
|
||||
ctx.arc radius, radius, radius, -Math.PI/2, arcsector - Math.PI/2, false
|
||||
ctx.lineTo radius, radius
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
active: (state) =>
|
||||
@activeState = state
|
||||
|
||||
|
|
|
@ -33,117 +33,7 @@
|
|||
|
||||
</div>
|
||||
|
||||
<div class="tab-content stat-widgets three-columns horizontal">
|
||||
<div class="column">
|
||||
<div class="time stat-widget vertical">
|
||||
<h3>∅ Waiting time today</h3>
|
||||
<div class="stat-graphic">
|
||||
<div class="stat-stopwatch centered">
|
||||
<svg class="stat-icon stopwatch-icon"><use xlink:href="#icon-stopwatch" /></svg>
|
||||
<canvas class="stat-dial"></canvas>
|
||||
<div class="stat-amount"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-label">My handling time: 25 minutes</div>
|
||||
<div class="stat-detail">Average: 13%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="mood stat-widget vertical">
|
||||
<h3>Mood</h3>
|
||||
<div class="stat-graphic">
|
||||
<svg class="stat-icon mood-icon"><use xlink:href="#icon-mood-supergood" /></svg>
|
||||
</div>
|
||||
<div class="stat-label">3% of my tickets escalated.</div>
|
||||
<div class="stat-detail">Average: 17%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="channel-distribution stat-widget vertical centered">
|
||||
<h3>Channel Distribution</h3>
|
||||
<div class="stat-graphic">
|
||||
<div class="stats-row email-channel">
|
||||
<svg class="stat-channel-icon"><use xlink:href="#icon-email" /></svg>
|
||||
<div class="stat-bars">
|
||||
<div class="stat-bar primary" style="height: 80%"></div>
|
||||
<div class="stat-bar secondary" style="height: 100%"></div>
|
||||
</div>
|
||||
<div class="stat-label">34%</div>
|
||||
</div>
|
||||
<div class="stats-row received-calls-channel">
|
||||
<svg class="stat-channel-icon"><use xlink:href="#icon-received-calls" /></svg>
|
||||
<div class="stat-bars">
|
||||
<div class="stat-bar primary" style="height: 53%"></div>
|
||||
<div class="stat-bar secondary" style="height: 47%"></div>
|
||||
</div>
|
||||
<div class="stat-label">26%</div>
|
||||
</div>
|
||||
<div class="stats-row outbound-calls-channel">
|
||||
<svg class="stat-channel-icon"><use xlink:href="#icon-outbound-calls" /></svg>
|
||||
<div class="stat-bars">
|
||||
<div class="stat-bar primary" style="height: 46%"></div>
|
||||
<div class="stat-bar secondary" style="height: 53%"></div>
|
||||
</div>
|
||||
<div class="stat-label">24%</div>
|
||||
</div>
|
||||
<div class="stats-row facebook-channel">
|
||||
<svg class="stat-channel-icon"><use xlink:href="#icon-facebook" /></svg>
|
||||
<div class="stat-bars">
|
||||
<div class="stat-bar primary" style="height: 24%"></div>
|
||||
<div class="stat-bar secondary" style="height: 18%"></div>
|
||||
</div>
|
||||
<div class="stat-label">12%</div>
|
||||
</div>
|
||||
<div class="stats-row twitter-channel">
|
||||
<svg class="stat-channel-icon"><use xlink:href="#icon-twitter" /></svg>
|
||||
<div class="stat-bars">
|
||||
<div class="stat-bar primary" style="height: 13%"></div>
|
||||
<div class="stat-bar secondary" style="height: 16%"></div>
|
||||
</div>
|
||||
<div class="stat-label">4%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="status stat-widget vertical">
|
||||
<h3>Status</h3>
|
||||
<div class="stat-graphic">
|
||||
<div class="stat-tickets vertical reverse end">
|
||||
<svg class="one-ticket state-color supergood-state"><use xlink:href="#icon-one-ticket" /></svg>
|
||||
<svg class="one-ticket state-color supergood-state"><use xlink:href="#icon-one-ticket" /></svg>
|
||||
<svg class="one-ticket state-color supergood-state"><use xlink:href="#icon-one-ticket" /></svg>
|
||||
<svg class="one-ticket state-color supergood-state"><use xlink:href="#icon-one-ticket" /></svg>
|
||||
<svg class="one-ticket state-color supergood-state"><use xlink:href="#icon-one-ticket" /></svg>
|
||||
<svg class="one-ticket state-color supergood-state"><use xlink:href="#icon-one-ticket" /></svg>
|
||||
</div>
|
||||
<svg class="total-tickets"><use xlink:href="#icon-total-tickets" /></svg>
|
||||
</div>
|
||||
<div class="stat-label">Tickets of my Group: 78 of 234</div>
|
||||
<div class="stat-detail">Average: 22%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="in-process stat-widget vertical">
|
||||
<h3>Tickets in process</h3>
|
||||
<div class="stat-graphic">
|
||||
<svg class="in-process-icon state-color supergood-state"><use xlink:href="#icon-in-process" /></svg>
|
||||
</div>
|
||||
<div class="stat-label">74% are currently in process</div>
|
||||
<div class="stat-detail">Average: 62%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="reopening stat-widget vertical">
|
||||
<h3>Reopening rate</h3>
|
||||
<div class="stat-graphic">
|
||||
<svg class="reopening-icon state-color ok-state"><use xlink:href="#icon-reopening" /></svg>
|
||||
</div>
|
||||
<div class="stat-label">7% are being reopened</div>
|
||||
<div class="stat-detail">Average: 6%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content stat-widgets three-columns horizontal"></div>
|
||||
|
||||
</div>
|
||||
<div class="sidebar optional">
|
||||
|
|
77
app/assets/javascripts/app/views/dashboard/stats.jst.eco
Normal file
77
app/assets/javascripts/app/views/dashboard/stats.jst.eco
Normal file
|
@ -0,0 +1,77 @@
|
|||
<div class="column">
|
||||
<div class="time stat-widget vertical">
|
||||
<h3><%- @T('∅ Waiting time today') %></h3>
|
||||
<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) %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="mood stat-widget vertical">
|
||||
<h3><%- @T('Mood') %></h3>
|
||||
<div class="stat-graphic">
|
||||
<%- @Icon("mood-#{@StatsTicketEscalation.state}", 'stat-icon mood-icon') %>
|
||||
</div>
|
||||
<div class="stat-label"><%- @StatsTicketEscalation.own %> of my tickets escalated.</div>
|
||||
<div class="stat-detail"><%- @T('Total: %s', @StatsTicketEscalation.total) %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="channel-distribution stat-widget vertical centered">
|
||||
<h3><%- @T('Channel Distribution') %></h3>
|
||||
<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="<%- @T('Inbound') %>: <%- channel.inbound_in_percent %>% (<%- channel.inbound %>)"></div>
|
||||
<div class="stat-bar secondary" style="height: <%- channel.outbound_in_percent %>%" title="<%- @T('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">
|
||||
<h3><%- @T('Assigned') %></h3>
|
||||
<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 state-color #{@StatsTicketLoadMeasure.state}-state") %>
|
||||
<% 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) %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="in-process stat-widget vertical">
|
||||
<h3><%- @T('Tickets in process') %></h3>
|
||||
<div class="stat-graphic">
|
||||
<%- @Icon('in-process', "in-process-icon state-color #{@StatsTicketInProcess.state}-state") %>
|
||||
</div>
|
||||
<div class="stat-label"><%- @T('%s% are currently in process', @StatsTicketInProcess.percent) %></div>
|
||||
<div class="stat-detail"><%- @T('Average: %s%', @StatsTicketInProcess.average_percent) %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="reopening stat-widget vertical">
|
||||
<h3><%- @T('Reopening rate') %></h3>
|
||||
<div class="stat-graphic">
|
||||
<%- @Icon('reopening', "reopening-icon state-color #{@StatsTicketReopen.state}-state") %>
|
||||
</div>
|
||||
<div class="stat-label"><%- @T('%s% are being reopened', @StatsTicketReopen.percent) %></div>
|
||||
<div class="stat-detail"><%- @T('Average: %s%', @StatsTicketReopen.average_percent) %></div>
|
||||
</div>
|
||||
</div>
|
134
app/models/stats_store.rb
Normal file
134
app/models/stats_store.rb
Normal file
|
@ -0,0 +1,134 @@
|
|||
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class StatsStore < ApplicationModel
|
||||
store :data
|
||||
|
||||
=begin
|
||||
|
||||
count = StatsStore.count_by_search(
|
||||
object: 'User',
|
||||
o_id: ticket.owner_id,
|
||||
key: 'ticket:reopen',
|
||||
start: Time.zone.now - 7.days,
|
||||
end: Time.zone.now,
|
||||
)
|
||||
|
||||
=end
|
||||
|
||||
def self.count_by_search(data)
|
||||
|
||||
# lookups
|
||||
if data[:object]
|
||||
object_id = ObjectLookup.by_name( data[:object] )
|
||||
end
|
||||
|
||||
StatsStore.where(stats_store_object_id: object_id, o_id: data[:o_id], key: data[:key])
|
||||
.where('created_at < ? AND created_at > ?', data[:start], data[:end]).count
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
item = StatsStore.search(
|
||||
object: 'User',
|
||||
o_id: current_user.id,
|
||||
key: 'dashboard',
|
||||
)
|
||||
|
||||
=end
|
||||
|
||||
def self.search(data)
|
||||
|
||||
# lookups
|
||||
if data[:object]
|
||||
data[:stats_store_object_id] = ObjectLookup.by_name( data[:object] )
|
||||
data.delete(:object)
|
||||
end
|
||||
|
||||
find_by(data)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
item = StatsStore.snyc(
|
||||
object: 'User',
|
||||
o_id: current_user.id,
|
||||
key: 'dashboard',
|
||||
data: {some data},
|
||||
)
|
||||
|
||||
=end
|
||||
|
||||
def self.sync(params)
|
||||
|
||||
data = params[:data]
|
||||
params.delete(:data)
|
||||
|
||||
item = search(params)
|
||||
|
||||
if item
|
||||
item.data = data
|
||||
item.save
|
||||
return true
|
||||
end
|
||||
|
||||
# lookups
|
||||
if data[:object]
|
||||
data[:stats_store_object_id] = ObjectLookup.by_name( data[:object] )
|
||||
data.delete(:object)
|
||||
end
|
||||
|
||||
params[:data] = data
|
||||
params[:created_by_id] = 1
|
||||
create(params)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
StatsStore.add(
|
||||
object: 'User',
|
||||
o_id: ticket.owner_id,
|
||||
key: 'ticket:reopen',
|
||||
data: { ticket_id: ticket.id },
|
||||
created_at: Time.zone.now,
|
||||
)
|
||||
|
||||
=end
|
||||
|
||||
def self.add(data)
|
||||
|
||||
# lookups
|
||||
if data[:object]
|
||||
object_id = ObjectLookup.by_name( data[:object] )
|
||||
end
|
||||
|
||||
# create history
|
||||
record = {
|
||||
stats_store_object_id: object_id,
|
||||
o_id: data[:o_id],
|
||||
key: data[:key],
|
||||
data: data[:data],
|
||||
created_at: data[:created_at],
|
||||
created_by_id: data[:created_by_id],
|
||||
}
|
||||
|
||||
StatsStore.create(record)
|
||||
end
|
||||
|
||||
=begin
|
||||
|
||||
cleanup old stats store
|
||||
|
||||
StatsStore.cleanup
|
||||
|
||||
optional you can parse the max oldest stats store entries
|
||||
|
||||
StatsStore.cleanup(3.months)
|
||||
|
||||
=end
|
||||
|
||||
def self.cleanup(diff = 3.months)
|
||||
StatsStore.where('created_at < ?', Time.zone.now - diff).delete_all
|
||||
true
|
||||
end
|
||||
|
||||
end
|
43
db/migrate/20150967000001_create_stats_store.rb
Normal file
43
db/migrate/20150967000001_create_stats_store.rb
Normal file
|
@ -0,0 +1,43 @@
|
|||
class CreateStatsStore < ActiveRecord::Migration
|
||||
def up
|
||||
create_table :stats_stores do |t|
|
||||
t.references :stats_store_object, null: false
|
||||
t.integer :o_id, null: false
|
||||
t.string :key, limit: 250, null: true
|
||||
t.integer :related_o_id, null: true
|
||||
t.integer :related_stats_store_object_id, null: true
|
||||
t.string :data, limit: 2500, null: true
|
||||
t.integer :created_by_id, null: false
|
||||
t.timestamps
|
||||
end
|
||||
add_index :stats_stores, [:o_id]
|
||||
add_index :stats_stores, [:key]
|
||||
add_index :stats_stores, [:stats_store_object_id]
|
||||
add_index :stats_stores, [:created_by_id]
|
||||
add_index :stats_stores, [:created_at]
|
||||
|
||||
|
||||
Scheduler.create_or_update(
|
||||
name: 'Generate user based stats.',
|
||||
method: 'Stats.generate',
|
||||
period: 11.minutes,
|
||||
prio: 2,
|
||||
active: true,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
Scheduler.create_or_update(
|
||||
name: 'Delete old stats store entries.',
|
||||
method: 'StatsStore.cleanup',
|
||||
period: 31.days,
|
||||
prio: 2,
|
||||
active: true,
|
||||
updated_by_id: 1,
|
||||
created_by_id: 1,
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :stats_stores
|
||||
end
|
||||
end
|
45
lib/stats.rb
Normal file
45
lib/stats.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
require 'stats_store'
|
||||
|
||||
class Stats
|
||||
|
||||
=begin
|
||||
|
||||
generate stats for user
|
||||
|
||||
Stats.generate
|
||||
|
||||
returns
|
||||
|
||||
result = true # if generation was successfully
|
||||
|
||||
=end
|
||||
|
||||
def self.generate
|
||||
|
||||
backends = [
|
||||
Stats::TicketChannelDistribution,
|
||||
Stats::TicketInProcess,
|
||||
Stats::TicketLoadMeasure,
|
||||
Stats::TicketEscalation,
|
||||
Stats::TicketReopen,
|
||||
]
|
||||
|
||||
users = User.of_role('Agent')
|
||||
users.each {|user|
|
||||
data = {}
|
||||
backends.each {|backend|
|
||||
data[backend.to_app_model] = backend.generate(user)
|
||||
}
|
||||
StatsStore.sync(
|
||||
object: 'User',
|
||||
o_id: user.id,
|
||||
key: 'dashboard',
|
||||
data: data,
|
||||
)
|
||||
}
|
||||
true
|
||||
end
|
||||
|
||||
end
|
89
lib/stats/ticket_channel_distribution.rb
Normal file
89
lib/stats/ticket_channel_distribution.rb
Normal file
|
@ -0,0 +1,89 @@
|
|||
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class Stats::TicketChannelDistribution
|
||||
|
||||
def self.generate(user)
|
||||
|
||||
# which time range?
|
||||
time_range = 7.days
|
||||
|
||||
# get users groups
|
||||
group_ids = user.groups.map(&:id)
|
||||
|
||||
# get channels
|
||||
channels = [
|
||||
{
|
||||
sender: 'email',
|
||||
icon: 'email',
|
||||
},
|
||||
{
|
||||
sender: 'phone',
|
||||
icon: 'phone',
|
||||
},
|
||||
{
|
||||
sender: 'twitter',
|
||||
icon: 'twitter',
|
||||
},
|
||||
{
|
||||
sender: 'facebook',
|
||||
icon: 'facebook',
|
||||
},
|
||||
]
|
||||
|
||||
# calcualte
|
||||
result = {}
|
||||
total_in = 0
|
||||
total_out = 0
|
||||
channels.each {|channel|
|
||||
result[channel[:sender].to_sym] = {
|
||||
icon: channel[:icon]
|
||||
}
|
||||
type_ids = []
|
||||
Ticket::Article::Type.all.each {|type|
|
||||
next if type.name !~ /^#{channel[:sender]}/i
|
||||
type_ids.push type.id
|
||||
}
|
||||
|
||||
sender = Ticket::Article::Sender.lookup( name: 'Customer' )
|
||||
count = Ticket.where(group_id: group_ids).joins(:articles).where(
|
||||
ticket_articles: { sender_id: sender, type_id: type_ids }
|
||||
).where(
|
||||
'ticket_articles.created_at > ?', Time.zone.now - time_range
|
||||
).count
|
||||
result[channel[:sender].to_sym][:inbound] = count
|
||||
total_in += count
|
||||
|
||||
sender = Ticket::Article::Sender.lookup( name: 'Agent' )
|
||||
count = Ticket.where(group_id: group_ids).joins(:articles).where(
|
||||
ticket_articles: { sender_id: sender, type_id: type_ids }
|
||||
).where(
|
||||
'ticket_articles.created_at > ?', Time.zone.now - time_range
|
||||
).count
|
||||
result[channel[:sender].to_sym][:outbound] = count
|
||||
total_out += count
|
||||
}
|
||||
|
||||
# append in percent
|
||||
channels.each {|channel|
|
||||
count = result[channel[:sender].to_sym][:inbound]
|
||||
#puts "#{channel.inspect}:in/#{result.inspect}:#{count}"
|
||||
if count == 0
|
||||
in_process_precent = 0
|
||||
else
|
||||
in_process_precent = (count * 1000) / ((total_in * 1000) / 100)
|
||||
end
|
||||
result[channel[:sender].to_sym][:inbound_in_percent] = in_process_precent
|
||||
|
||||
count = result[channel[:sender].to_sym][:outbound]
|
||||
if count == 0
|
||||
out_process_precent = 0
|
||||
else
|
||||
out_process_precent = (count * 1000) / ((total_out * 1000) / 100)
|
||||
end
|
||||
result[channel[:sender].to_sym][:outbound_in_percent] = out_process_precent
|
||||
}
|
||||
|
||||
{ channels: result }
|
||||
end
|
||||
|
||||
end
|
48
lib/stats/ticket_escalation.rb
Normal file
48
lib/stats/ticket_escalation.rb
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class Stats::TicketEscalation
|
||||
|
||||
def self.generate(user)
|
||||
|
||||
# get users groups
|
||||
group_ids = user.groups.map(&:id)
|
||||
|
||||
# owned tickets
|
||||
own_escalated = Ticket.where(
|
||||
owner_id: user.id,
|
||||
group_id: group_ids,
|
||||
escalation_time: Time.zone.now,
|
||||
state_id: Ticket::State.by_category('open').map(&:id)
|
||||
).count
|
||||
|
||||
# all tickets
|
||||
all_escalated = Ticket.where(
|
||||
owner_id: user.id,
|
||||
group_id: group_ids,
|
||||
escalation_time: Time.zone.now,
|
||||
state_id: Ticket::State.by_category('open').map(&:id)
|
||||
).count
|
||||
|
||||
average = '-'
|
||||
state = 'supergood'
|
||||
# if in_process_precent > 80
|
||||
# state = 'supergood'
|
||||
# elsif in_process_precent > 60
|
||||
# state = 'good'
|
||||
# elsif in_process_precent > 40
|
||||
# state = 'ok'
|
||||
# elsif in_process_precent > 20
|
||||
# state = 'bad'
|
||||
# elsif in_process_precent > 5
|
||||
# state = 'superbad'
|
||||
# end
|
||||
|
||||
{
|
||||
average: average,
|
||||
state: state,
|
||||
own: own_escalated,
|
||||
total: all_escalated,
|
||||
}
|
||||
end
|
||||
|
||||
end
|
45
lib/stats/ticket_in_process.rb
Normal file
45
lib/stats/ticket_in_process.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class Stats::TicketInProcess
|
||||
|
||||
def self.generate(user)
|
||||
|
||||
# get history entries of tickets worked on today
|
||||
history_object = History::Object.lookup(name: 'Ticket')
|
||||
|
||||
own_ticket_ids = Ticket.select('id').where(owner_id: user.id).map(&:id)
|
||||
|
||||
count = History.select('DISTINCT(o_id)').where(
|
||||
'histories.created_at >= ? AND histories.history_object_id = ? AND histories.created_by_id = ? AND histories.o_id IN (?)', Time.zone.now - 1.day, history_object.id, user.id, own_ticket_ids
|
||||
).count
|
||||
|
||||
total = own_ticket_ids.count
|
||||
in_process_precent = 0
|
||||
state = 'supergood'
|
||||
average_in_percent = '-'
|
||||
|
||||
if count != 0 && total != 0
|
||||
in_process_precent = (count * 1000) / ((total * 1000) / 100)
|
||||
if in_process_precent > 80
|
||||
state = 'supergood'
|
||||
elsif in_process_precent > 60
|
||||
state = 'good'
|
||||
elsif in_process_precent > 40
|
||||
state = 'ok'
|
||||
elsif in_process_precent > 20
|
||||
state = 'bad'
|
||||
elsif in_process_precent > 5
|
||||
state = 'superbad'
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
state: state,
|
||||
in_process: count,
|
||||
percent: in_process_precent,
|
||||
average_percent: average_in_percent,
|
||||
total: total,
|
||||
}
|
||||
end
|
||||
|
||||
end
|
41
lib/stats/ticket_load_measure.rb
Normal file
41
lib/stats/ticket_load_measure.rb
Normal file
|
@ -0,0 +1,41 @@
|
|||
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class Stats::TicketLoadMeasure
|
||||
|
||||
def self.generate(user)
|
||||
|
||||
# owned tickets
|
||||
count = Ticket.where(owner_id: user.id).count
|
||||
|
||||
# get total open
|
||||
total = Ticket.where(group_id: user.groups.map(&:id), state_id: Ticket::State.by_category('open').map(&:id) ).count
|
||||
|
||||
average = '-'
|
||||
state = 'good'
|
||||
# if in_process_precent > 80
|
||||
# state = 'supergood'
|
||||
# elsif in_process_precent > 60
|
||||
# state = 'good'
|
||||
# elsif in_process_precent > 40
|
||||
# state = 'ok'
|
||||
# elsif in_process_precent > 20
|
||||
# state = 'bad'
|
||||
# elsif in_process_precent > 5
|
||||
# state = 'superbad'
|
||||
# end
|
||||
|
||||
if count > total
|
||||
total = count
|
||||
end
|
||||
load_measure_precent = (count * 1000) / ((total * 1000) / 100)
|
||||
|
||||
{
|
||||
average: average,
|
||||
percent: load_measure_precent,
|
||||
state: state,
|
||||
own: count,
|
||||
total: total,
|
||||
}
|
||||
end
|
||||
|
||||
end
|
51
lib/stats/ticket_reopen.rb
Normal file
51
lib/stats/ticket_reopen.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class Stats::TicketReopen
|
||||
|
||||
def self.generate(user)
|
||||
count = StatsStore.count_by_search(
|
||||
object: 'User',
|
||||
o_id: user.id,
|
||||
key: 'ticket:reopen',
|
||||
start: Time.zone.now - 7.days,
|
||||
end: Time.zone.now,
|
||||
)
|
||||
{
|
||||
state: 'good',
|
||||
own: count,
|
||||
total: 0,
|
||||
percent: 0,
|
||||
average_percent: '-',
|
||||
}
|
||||
end
|
||||
|
||||
def self.log(object, o_id, changes, updated_by_id)
|
||||
return if object != 'Ticket'
|
||||
ticket = Ticket.lookup(id: o_id)
|
||||
|
||||
# check if close_time is already set / if not, ticket is not reopend
|
||||
return if !ticket.close_time
|
||||
|
||||
return if !changes['state_id']
|
||||
return if ticket.owner_id == 1
|
||||
|
||||
state_before = Ticket::State.lookup(id: changes['state_id'][0])
|
||||
state_type_before = Ticket::StateType.lookup(id: state_before.state_type_id)
|
||||
return if state_type_before.name != 'closed'
|
||||
|
||||
state_now = Ticket::State.lookup(id: changes['state_id'][1])
|
||||
state_type_now = Ticket::StateType.lookup(id: state_now.state_type_id)
|
||||
return if state_type_now.name == 'closed'
|
||||
|
||||
StatsStore.add(
|
||||
object: 'User',
|
||||
o_id: ticket.owner_id,
|
||||
key: 'ticket:reopen',
|
||||
data: { ticket_id: ticket.id },
|
||||
created_at: Time.zone.now,
|
||||
created_by_id: updated_by_id,
|
||||
updated_by_id: updated_by_id,
|
||||
)
|
||||
end
|
||||
|
||||
end
|
44
lib/stats/ticket_response_time.rb
Normal file
44
lib/stats/ticket_response_time.rb
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Copyright (C) 2012-2013 Zammad Foundation, http://zammad-foundation.org/
|
||||
|
||||
class Stats::TicketResponseTime
|
||||
|
||||
def self.log(object, o_id)
|
||||
return if object != 'Ticket'
|
||||
|
||||
ticket = Ticket.lookup(id: o_id)
|
||||
|
||||
article_created_by_id = 3
|
||||
|
||||
# check if response was sent by owner
|
||||
return if ticket.owner_id != 1 && ticket.owner_id != article_created_by_id
|
||||
|
||||
# return if customer send at least
|
||||
return if ticket.last_contact_customer > ticket.last_contact_agent
|
||||
|
||||
# TODO: only business hours
|
||||
response_time_taken = ticket.last_contact_agent - ticket.last_contact_customer
|
||||
|
||||
(response_time_taken / 60).round
|
||||
end
|
||||
|
||||
def self.generate(user)
|
||||
items = Stats.where('action_at > ? AND action_at < ?', Time.zone.now - 7.days, Time.zone.now).where(key: 'ticket:response_time')
|
||||
count_total = items.count
|
||||
total = 0
|
||||
count_own = 0
|
||||
own = 0
|
||||
items.each {|_item|
|
||||
ticket = Ticket.lookup(id: data[:ticket_id])
|
||||
if ticket.owner_id == user.id
|
||||
count_own += 1
|
||||
own += data[:time]
|
||||
end
|
||||
total += data[:time]
|
||||
}
|
||||
{
|
||||
own: (own / count_own).round,
|
||||
total: (total / count_total).round,
|
||||
}
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in a new issue