Files
itflow/agent/reports/sla_by_client.php

178 lines
9.2 KiB
PHP

<?php
require_once "includes/inc_all_reports.php";
enforceUserPermission('module_support');
if (isset($_GET['year'])) {
$year = intval($_GET['year']);
} else {
$year = date('Y');
}
if (isset($_GET['month'])) {
$month = intval($_GET['month']);
} else {
$month = 0; // 0 = whole year
}
$period_query = "YEAR(ticket_created_at) = $year";
$period_label = $year;
if ($month) {
$period_query .= " AND MONTH(ticket_created_at) = $month";
$period_label = date("F", mktime(1, 1, 1, $month, 1)) . " $year";
}
$sql_ticket_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(ticket_created_at) AS ticket_year FROM tickets ORDER BY ticket_year DESC");
$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC");
// Clock time spent on resolved tickets, gathered for every client at once
// rather than per ticket. Tickets with no interval rows - response-only plans,
// or anything resolved before SLA pausing existed - fall back to the business
// time that elapsed between being raised and resolved.
$resolve_totals = [];
$sql_resolve_times = mysqli_query($mysqli, "SELECT ticket_client_id, ticket_id, ticket_created_at, ticket_resolved_at, SUM(sla_history_minutes) AS logged_minutes
FROM tickets
LEFT JOIN sla_history ON sla_history_ticket_id = ticket_id
WHERE ticket_sla_id > 0
AND ticket_resolved_at IS NOT NULL
AND $period_query
GROUP BY ticket_id, ticket_client_id, ticket_created_at, ticket_resolved_at"
);
while ($resolve_row = mysqli_fetch_assoc($sql_resolve_times)) {
$resolve_client_id = intval($resolve_row['ticket_client_id']);
$resolve_minutes = is_null($resolve_row['logged_minutes'])
? businessMinutesBetween($resolve_row['ticket_created_at'], $resolve_row['ticket_resolved_at'])
: intval($resolve_row['logged_minutes']);
if (!isset($resolve_totals[$resolve_client_id])) {
$resolve_totals[$resolve_client_id] = ['minutes' => 0, 'count' => 0];
}
$resolve_totals[$resolve_client_id]['minutes'] += $resolve_minutes;
$resolve_totals[$resolve_client_id]['count']++;
}
?>
<div class="card card-dark">
<div class="card-header py-2">
<h3 class="card-title mt-2"><i class="fas fa-fw fa-stopwatch mr-2"></i>SLA by Client</h3>
<div class="card-tools">
<button type="button" class="btn btn-primary d-print-none" onclick="window.print();"><i class="fas fa-fw fa-print mr-2"></i>Print</button>
</div>
</div>
<div class="card-body">
<form class="mb-3">
<select onchange="this.form.submit()" class="form-control" name="year">
<?php
while ($row = mysqli_fetch_assoc($sql_ticket_years)) {
$ticket_year = intval($row['ticket_year']); ?>
<option <?php if ($year == $ticket_year) { ?> selected <?php } ?> > <?= $ticket_year ?></option>
<?php } ?>
</select>
<select onchange="this.form.submit()" class="form-control" name="month">
<option <?php if ($month == 0) { echo 'selected'; } ?> value="0">Whole year</option>
<?php for ($m = 1; $m <= 12; $m++) { ?>
<option <?php if ($month == $m) { echo 'selected'; } ?> value="<?= $m ?>"><?= date("F", mktime(1, 1, 1, $m, 1)) ?></option>
<?php } ?>
</select>
</form>
<div class="card card-dark mb-3">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-fw fa-chart-area mr-2"></i><?= $period_label ?></h3>
</div>
<div class="card-body">
<div class="table-responsive-sm">
<table class="table table-striped">
<thead>
<tr>
<th>Client</th>
<th class="text-right">Tickets</th>
<th class="text-right">Response met</th>
<th class="text-right">Response missed</th>
<th class="text-right">Response %</th>
<th class="text-right">Resolution met</th>
<th class="text-right">Resolution missed</th>
<th class="text-right">Resolution %</th>
<th class="text-right">Avg time to respond</th>
<th class="text-right">Avg clock to resolve</th>
</tr>
</thead>
<tbody>
<?php
$any_rows = false;
while ($row = mysqli_fetch_assoc($sql_clients)) {
$client_id = intval($row['client_id']);
$client_name = escapeHtml($row['client_name']);
$stats = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT
COUNT(ticket_id) AS ticket_count,
SUM(ticket_response_sla_met = 1) AS response_met,
SUM(ticket_response_sla_met = 0) AS response_missed,
SUM(ticket_resolution_sla_met = 1) AS resolution_met,
SUM(ticket_resolution_sla_met = 0) AS resolution_missed,
AVG(CASE WHEN ticket_first_response_at IS NOT NULL THEN TIMESTAMPDIFF(SECOND, ticket_created_at, ticket_first_response_at) END) AS avg_response_seconds
FROM tickets
WHERE ticket_sla_id > 0 AND ticket_client_id = $client_id AND $period_query"
));
$ticket_count = intval($stats['ticket_count']);
if ($ticket_count == 0) {
continue;
}
$any_rows = true;
$response_met = intval($stats['response_met']);
$response_missed = intval($stats['response_missed']);
$resolution_met = intval($stats['resolution_met']);
$resolution_missed = intval($stats['resolution_missed']);
$response_judged = $response_met + $response_missed;
$response_percent = $response_judged ? round($response_met / $response_judged * 100, 1) : null;
$resolution_judged = $resolution_met + $resolution_missed;
$resolution_percent = $resolution_judged ? round($resolution_met / $resolution_judged * 100, 1) : null;
$avg_time_to_respond = is_null($stats['avg_response_seconds']) ? '-' : secondsToTime($stats['avg_response_seconds']);
// Clock time actually spent - paused spells excluded, which is
// what the SLA itself judged on
$avg_time_to_resolve = '-';
if (!empty($resolve_totals[$client_id]['count'])) {
$avg_time_to_resolve = secondsToTime(($resolve_totals[$client_id]['minutes'] / $resolve_totals[$client_id]['count']) * 60);
}
?>
<tr>
<td><?= $client_name ?></td>
<td class="text-right"><?= $ticket_count ?></td>
<td class="text-right"><?= $response_met ?></td>
<td class="text-right"><?= $response_missed ?></td>
<td class="text-right"><?= slaPercentDisplay($response_percent) ?></td>
<td class="text-right"><?= $resolution_met ?></td>
<td class="text-right"><?= $resolution_missed ?></td>
<td class="text-right"><?= slaPercentDisplay($resolution_percent) ?></td>
<td class="text-right"><?= $avg_time_to_respond ?></td>
<td class="text-right"><?= $avg_time_to_resolve ?></td>
</tr>
<?php }
if (!$any_rows) { ?>
<tr><td colspan="10" class="text-secondary">No tickets with an SLA in this period.</td></tr>
<?php } ?>
</tbody>
</table>
</div>
<small class="text-muted">Time to respond is wall-clock from ticket creation. Time to resolve counts only business hours with the SLA clock running, so paused time is excluded.</small>
</div>
</div>
</div>
</div>
<?php
require_once "../../includes/footer.php";