You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
300 lines
12 KiB
300 lines
12 KiB
<?php
|
|
|
|
class ReportModel {
|
|
|
|
const PERM_AGENTS = 'stats.agents';
|
|
|
|
static protected $perms = array(
|
|
self::PERM_AGENTS => array(
|
|
'title' =>
|
|
/* @trans */ 'Stats',
|
|
'desc' =>
|
|
/* @trans */ 'Ability to view stats of other agents in allowed departments',
|
|
'primary' => true,
|
|
));
|
|
|
|
static function getPermissions() {
|
|
return self::$perms;
|
|
}
|
|
}
|
|
|
|
RolePermission::register(/* @trans */ 'Miscellaneous', ReportModel::getPermissions());
|
|
|
|
class OverviewReport {
|
|
var $start;
|
|
var $end;
|
|
|
|
var $format;
|
|
|
|
function __construct($start, $end='now', $format=null) {
|
|
global $cfg;
|
|
|
|
$this->start = $start;
|
|
$this->end = $end;
|
|
$this->format = $format ?: $cfg->getDateFormat(true);
|
|
}
|
|
|
|
|
|
function getStartDate($format=null, $translate=true) {
|
|
|
|
if (!$this->start)
|
|
return '';
|
|
|
|
$format = $format ?: $this->format;
|
|
if ($translate) {
|
|
$format = str_replace(
|
|
array('y', 'Y', 'm'),
|
|
array('y', 'yy', 'mm'),
|
|
$format);
|
|
}
|
|
|
|
return Format::date(Misc::dbtime($this->start), false, $format);
|
|
}
|
|
|
|
|
|
function getDateRange() {
|
|
global $cfg;
|
|
|
|
$start = $this->start ?: 'last month';
|
|
$stop = $this->end ?: 'now';
|
|
|
|
// Convert user time to db time
|
|
$start = Misc::dbtime($start);
|
|
// Stop time can be relative.
|
|
if ($stop[0] == '+') {
|
|
// $start time + time(X days)
|
|
$now = time();
|
|
$stop = $start + (strtotime($stop, $now)-$now);
|
|
} else {
|
|
$stop = Misc::dbtime($stop);
|
|
}
|
|
|
|
$start = 'FROM_UNIXTIME('.$start.')';
|
|
$stop = 'FROM_UNIXTIME('.$stop.')';
|
|
|
|
return array($start, $stop);
|
|
}
|
|
|
|
function getPlotData() {
|
|
list($start, $stop) = $this->getDateRange();
|
|
$states = array("created", "closed", "reopened", "assigned", "overdue", "transferred");
|
|
$event_ids = Event::getIds();
|
|
|
|
# Fetch all types of events over the timeframe
|
|
$res = db_query('SELECT DISTINCT(E.name) FROM '.THREAD_EVENT_TABLE
|
|
.' T JOIN '.EVENT_TABLE . ' E ON E.id = T.event_id'
|
|
.' WHERE timestamp BETWEEN '.$start.' AND '.$stop
|
|
.' AND T.event_id IN ('.implode(",",$event_ids).') AND T.thread_type = "T"'
|
|
.' ORDER BY 1');
|
|
$events = array();
|
|
while ($row = db_fetch_row($res)) $events[] = __($row[0]);
|
|
|
|
# TODO: Handle user => db timezone offset
|
|
# XXX: Implement annulled column from the %ticket_event table
|
|
$res = db_query('SELECT H.name, DATE_FORMAT(timestamp, \'%Y-%m-%d\'), '
|
|
.'COUNT(DISTINCT E.id)'
|
|
.' FROM '.THREAD_EVENT_TABLE. ' E '
|
|
. ' LEFT JOIN '.EVENT_TABLE. ' H
|
|
ON (E.event_id = H.id)'
|
|
.' WHERE E.timestamp BETWEEN '.$start.' AND '.$stop
|
|
.' AND NOT annulled'
|
|
.' AND E.event_id IN ('.implode(",",$event_ids).')'
|
|
.' GROUP BY E.event_id, DATE_FORMAT(E.timestamp, \'%Y-%m-%d\')'
|
|
.' ORDER BY 2, 1');
|
|
# Initialize array of plot values
|
|
$plots = array();
|
|
foreach ($events as $e) { $plots[$e] = array(); }
|
|
|
|
$time = null; $times = array();
|
|
# Iterate over result set, adding zeros for missing ticket events
|
|
$slots = array();
|
|
while ($row = db_fetch_row($res)) {
|
|
$row_time = strtotime($row[1]);
|
|
if ($time != $row_time) {
|
|
# New time (and not the first), figure out which events did
|
|
# not have any tickets associated for this time slot
|
|
if ($time !== null) {
|
|
# Not the first record -- add zeros all the arrays that
|
|
# did not have at least one entry for the timeframe
|
|
foreach (array_diff($events, $slots) as $slot)
|
|
$plots[$slot][] = 0;
|
|
}
|
|
$slots = array();
|
|
$times[] = $time = $row_time;
|
|
}
|
|
# Keep track of states for this timeframe
|
|
$slots[] = __($row[0]);
|
|
$plots[__($row[0])][] = (int)$row[2];
|
|
}
|
|
foreach (array_diff($events, $slots) as $slot)
|
|
$plots[$slot][] = 0;
|
|
|
|
return array("times" => $times, "plots" => $plots, "events" => $events);
|
|
}
|
|
|
|
function enumTabularGroups() {
|
|
return array("dept"=>__("Department"), "topic"=>__("Topics"),
|
|
# XXX: This will be relative to permissions based on the
|
|
# logged-in-staff. For basic staff, this will be 'My Stats'
|
|
"staff"=>__("Agent"));
|
|
}
|
|
|
|
function getTabularData($group='dept') {
|
|
global $thisstaff;
|
|
|
|
$event_ids = Event::getIds();
|
|
$event = function ($name) use ($event_ids) {
|
|
return $event_ids[$name];
|
|
};
|
|
$dash_headers = array(__('Opened'),__('Assigned'),__('Overdue'),__('Closed'),__('Reopened'),
|
|
__('Deleted'),__('Service Time'),__('Response Time'));
|
|
|
|
list($start, $stop) = $this->getDateRange();
|
|
$times = ThreadEvent::objects()
|
|
->constrain(array(
|
|
'thread__entries' => array(
|
|
'thread__entries__type' => 'R',
|
|
),
|
|
))
|
|
->constrain(array(
|
|
'thread__events' => array(
|
|
'thread__events__event_id' => $event('created'),
|
|
'event_id' => $event('closed'),
|
|
'annulled' => 0,
|
|
),
|
|
))
|
|
->filter(array(
|
|
'timestamp__range' => array($start, $stop, true),
|
|
))
|
|
->aggregate(array(
|
|
'ServiceTime' => SqlAggregate::AVG(SqlFunction::timestampdiff(
|
|
new SqlCode('HOUR'), new SqlField('thread__events__timestamp'), new SqlField('timestamp'))
|
|
),
|
|
'ResponseTime' => SqlAggregate::AVG(SqlFunction::timestampdiff(
|
|
new SqlCode('HOUR'),new SqlField('thread__entries__parent__created'), new SqlField('thread__entries__created')
|
|
)),
|
|
));
|
|
|
|
$stats = ThreadEvent::objects()
|
|
->filter(array(
|
|
'annulled' => 0,
|
|
'timestamp__range' => array($start, $stop, true),
|
|
'thread_type' => 'T',
|
|
))
|
|
->aggregate(array(
|
|
'Opened' => SqlAggregate::COUNT(
|
|
SqlCase::N()
|
|
->when(new Q(array('event_id' => $event('created'))), 1)
|
|
),
|
|
'Assigned' => SqlAggregate::COUNT(
|
|
SqlCase::N()
|
|
->when(new Q(array('event_id' => $event('assigned'))), 1)
|
|
),
|
|
'Overdue' => SqlAggregate::COUNT(
|
|
SqlCase::N()
|
|
->when(new Q(array('event_id' => $event('overdue'))), 1)
|
|
),
|
|
'Closed' => SqlAggregate::COUNT(
|
|
SqlCase::N()
|
|
->when(new Q(array('event_id' => $event('closed'))), 1)
|
|
),
|
|
'Reopened' => SqlAggregate::COUNT(
|
|
SqlCase::N()
|
|
->when(new Q(array('event_id' => $event('reopened'))), 1)
|
|
),
|
|
'Deleted' => SqlAggregate::COUNT(
|
|
SqlCase::N()
|
|
->when(new Q(array('event_id' => $event('deleted'))), 1)
|
|
),
|
|
));
|
|
|
|
switch ($group) {
|
|
case 'dept':
|
|
$headers = array(__('Department'));
|
|
$header = function($row) { return Dept::getLocalNameById($row['dept_id'], $row['dept__name']); };
|
|
$pk = 'dept__id';
|
|
$stats = $stats
|
|
->filter(array('dept_id__in' => $thisstaff->getDepts()))
|
|
->values('dept__id', 'dept__name', 'dept__flags')
|
|
->distinct('dept__id');
|
|
$times = $times
|
|
->filter(array('dept_id__in' => $thisstaff->getDepts()))
|
|
->values('dept__id')
|
|
->distinct('dept__id');
|
|
break;
|
|
case 'topic':
|
|
$headers = array(__('Help Topic'));
|
|
$header = function($row) { return Topic::getLocalNameById($row['topic_id'], $row['topic__topic']); };
|
|
$pk = 'topic_id';
|
|
$topics = $thisstaff->getTopicNames(false, Topic::DISPLAY_DISABLED);
|
|
if (empty($topics))
|
|
return array("columns" => array_merge($headers, $dash_headers),
|
|
"data" => array());
|
|
$stats = $stats
|
|
->values('topic_id', 'topic__topic', 'topic__flags')
|
|
->filter(array('dept_id__in' => $thisstaff->getDepts(), 'topic_id__gt' => 0, 'topic_id__in' => array_keys($topics)))
|
|
->distinct('topic_id');
|
|
$times = $times
|
|
->values('topic_id')
|
|
->filter(array('topic_id__gt' => 0))
|
|
->distinct('topic_id');
|
|
break;
|
|
case 'staff':
|
|
$headers = array(__('Agent'));
|
|
$header = function($row) { return new AgentsName(array(
|
|
'first' => $row['staff__firstname'], 'last' => $row['staff__lastname'])); };
|
|
$pk = 'staff_id';
|
|
$staff = Staff::getStaffMembers();
|
|
$stats = $stats
|
|
->values('staff_id', 'staff__firstname', 'staff__lastname')
|
|
->filter(array('staff_id__in' => array_keys($staff)))
|
|
->distinct('staff_id');
|
|
$times = $times->values('staff_id')->distinct('staff_id');
|
|
$depts = $thisstaff->getManagedDepartments();
|
|
if ($thisstaff->hasPerm(ReportModel::PERM_AGENTS))
|
|
$depts = array_merge($depts, $thisstaff->getDepts());
|
|
$Q = Q::any(array(
|
|
'staff_id' => $thisstaff->getId(),
|
|
));
|
|
if ($depts)
|
|
$Q->add(array('dept_id__in' => $depts));
|
|
$stats = $stats->filter(array('staff_id__gt'=>0))->filter($Q);
|
|
$times = $times->filter(array('staff_id__gt'=>0))->filter($Q);
|
|
break;
|
|
default:
|
|
# XXX: Die if $group not in $groups
|
|
}
|
|
|
|
$timings = array();
|
|
foreach ($times as $T) {
|
|
$timings[$T[$pk]] = $T;
|
|
}
|
|
$rows = array();
|
|
foreach ($stats as $R) {
|
|
if (isset($R['dept__flags'])) {
|
|
if ($R['dept__flags'] & Dept::FLAG_ARCHIVED)
|
|
$status = ' - '.__('Archived');
|
|
elseif ($R['dept__flags'] & Dept::FLAG_ACTIVE)
|
|
$status = '';
|
|
else
|
|
$status = ' - '.__('Disabled');
|
|
}
|
|
if (isset($R['topic__flags'])) {
|
|
if ($R['topic__flags'] & Topic::FLAG_ARCHIVED)
|
|
$status = ' - '.__('Archived');
|
|
elseif ($R['topic__flags'] & Topic::FLAG_ACTIVE)
|
|
$status = '';
|
|
else
|
|
$status = ' - '.__('Disabled');
|
|
}
|
|
|
|
$T = $timings[$R[$pk]];
|
|
$rows[] = array($header($R) . $status, $R['Opened'], $R['Assigned'],
|
|
$R['Overdue'], $R['Closed'], $R['Reopened'], $R['Deleted'],
|
|
number_format($T['ServiceTime'], 1),
|
|
number_format($T['ResponseTime'], 1));
|
|
}
|
|
return array("columns" => array_merge($headers, $dash_headers),
|
|
"data" => $rows);
|
|
}
|
|
}
|