mirror of
https://github.com/itflow-org/itflow
synced 2026-08-05 07:07:14 +00:00
Add SLA pausing, SLA reports, SLA filtering and kanban SLA state (phase 3)
Statuses can be flagged to pause the resolution clock; sla_history records the intervals a ticket's clock actually ran and the deadline is re-based on remaining budget when it resumes. Adds SLA Summary and SLA by Client reports, an SLA state filter on the ticket list, SLA colouring on kanban cards, and an Urgent column on the Tickets by Client report. DB update 2.5.1. Also fixes resolution SLA verdicts being skipped when resolving via kanban or the client portal.
This commit is contained in:
@@ -502,10 +502,14 @@ if (isset($_POST['update_kanban_ticket'])) {
|
||||
// If the ticket was moved from a resolved status to another status, we need to update ticket_resolved_at
|
||||
if ($oldStatus === $statuses['Resolved']) {
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_order = $kanban, ticket_status = $status, ticket_resolved_at = NULL WHERE ticket_id = $ticket_id");
|
||||
resetTicketResolutionSla($ticket_id);
|
||||
syncTicketSlaClock($ticket_id);
|
||||
triggerCustomAction('ticket_update', $ticket_id);
|
||||
} elseif ($status === $statuses['Resolved']) {
|
||||
// If the ticket was moved to a resolved status, we need to update ticket_resolved_at
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_order = $kanban, ticket_status = $status, ticket_resolved_at = NOW() WHERE ticket_id = $ticket_id");
|
||||
setTicketResolutionSlaMet($ticket_id);
|
||||
syncTicketSlaClock($ticket_id);
|
||||
triggerCustomAction('ticket_update', $ticket_id);
|
||||
|
||||
// Client notification email
|
||||
@@ -586,6 +590,7 @@ if (isset($_POST['update_kanban_ticket'])) {
|
||||
} else {
|
||||
// If the ticket was moved from any status to another status
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_order = $kanban, ticket_status = $status WHERE ticket_id = $ticket_id");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
triggerCustomAction('ticket_update', $ticket_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,6 +863,7 @@ if (isset($_POST['assign_ticket'])) {
|
||||
|
||||
// Update ticket & insert reply
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_assigned_to = $assigned_to, ticket_status = '$ticket_status' WHERE ticket_id = $ticket_id");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = '$ticket_reply', ticket_reply_type = 'Internal', ticket_reply_time_worked = '00:01:00', ticket_reply_by = $session_user_id, ticket_reply_ticket_id = $ticket_id");
|
||||
|
||||
@@ -1070,6 +1071,7 @@ if (isset($_POST['bulk_assign_ticket'])) {
|
||||
|
||||
// Update ticket & insert reply
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_assigned_to = $assign_to, ticket_status = $ticket_status WHERE ticket_id = $ticket_id");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = '$ticket_reply', ticket_reply_type = 'Internal', ticket_reply_time_worked = '00:01:00', ticket_reply_by = $session_user_id, ticket_reply_ticket_id = $ticket_id");
|
||||
|
||||
@@ -1276,6 +1278,7 @@ if (isset($_POST['bulk_merge_tickets'])) {
|
||||
}
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket $ticket_prefix$ticket_number bulk merged into <a href=\"ticket.php?ticket_id=$merge_into_ticket_id\">$ticket_prefix$merge_into_ticket_number</a>. Comment: $merge_comment', ticket_reply_time_worked = '00:01:00', ticket_reply_type = '$ticket_reply_type', ticket_reply_by = $session_user_id, ticket_reply_ticket_id = $ticket_id");
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = '5', ticket_resolved_at = NOW(), ticket_closed_at = NOW(), ticket_closed_by = $session_user_id WHERE ticket_id = $ticket_id") or die(mysqli_error($mysqli));
|
||||
syncTicketSlaClock($ticket_id);
|
||||
setTicketResolutionSlaMet($ticket_id);
|
||||
|
||||
// Update new parent ticket
|
||||
@@ -1356,6 +1359,7 @@ if (isset($_POST['bulk_resolve_tickets'])) {
|
||||
|
||||
// Update ticket & insert reply
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 4, ticket_resolved_at = NOW() WHERE ticket_id = $ticket_id");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
setTicketResolutionSlaMet($ticket_id);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = '$details', ticket_reply_type = '$ticket_reply_type', ticket_reply_time_worked = '$ticket_reply_time_worked', ticket_reply_by = $session_user_id, ticket_reply_ticket_id = $ticket_id");
|
||||
@@ -1504,6 +1508,7 @@ if (isset($_POST['bulk_ticket_reply'])) {
|
||||
|
||||
// Update Ticket Status
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = '$ticket_status' WHERE ticket_id = $ticket_id");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
|
||||
logAudit("Ticket", "Reply", "$session_name replied to ticket $ticket_prefix$ticket_number - $ticket_subject and was a $ticket_reply_type reply", $client_id, $ticket_id);
|
||||
|
||||
@@ -1833,6 +1838,7 @@ if (isset($_POST['add_ticket_reply'])) {
|
||||
|
||||
// Update Ticket Status & updated at (in case status didn't change)
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = $ticket_status, ticket_updated_at = NOW() WHERE ticket_id = $ticket_id");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
|
||||
// Resolve the ticket, if set
|
||||
if ($ticket_status == 4) {
|
||||
@@ -2134,6 +2140,7 @@ if (isset($_POST['merge_ticket'])) {
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket $ticket_prefix$ticket_number merged into <a href=\"ticket.php?ticket_id=$merge_into_ticket_id\">$ticket_prefix$merge_into_ticket_number</a>. Comment: $merge_comment', ticket_reply_time_worked = '00:01:00', ticket_reply_type = '$ticket_reply_type', ticket_reply_by = $session_user_id, ticket_reply_ticket_id = $ticket_id");
|
||||
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = '5', ticket_resolved_at = NOW(), ticket_closed_at = NOW(), ticket_closed_by = $session_user_id WHERE ticket_id = $ticket_id") or die(mysqli_error($mysqli));
|
||||
syncTicketSlaClock($ticket_id);
|
||||
setTicketResolutionSlaMet($ticket_id);
|
||||
|
||||
//Update new parent ticket
|
||||
@@ -2210,6 +2217,7 @@ if (isset($_GET['resolve_ticket'])) {
|
||||
|
||||
// Resolve
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 4, ticket_resolved_at = NOW() WHERE ticket_id = $ticket_id");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
setTicketResolutionSlaMet($ticket_id);
|
||||
|
||||
logAudit("Ticket", "Resolved", "$session_name resolved ticket $ticket_prefix$ticket_number (ID: $ticket_id)", $client_id, $ticket_id);
|
||||
@@ -2311,6 +2319,7 @@ if (isset($_GET['close_ticket'])) {
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 5, ticket_closed_at = NOW(), ticket_closed_by = $session_user_id WHERE ticket_id = $ticket_id") or die(mysqli_error($mysqli));
|
||||
syncTicketSlaClock($ticket_id);
|
||||
setTicketResolutionSlaMet($ticket_id);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket closed.', ticket_reply_type = 'Internal', ticket_reply_time_worked = '00:01:00', ticket_reply_by = $session_user_id, ticket_reply_ticket_id = $ticket_id");
|
||||
@@ -2412,6 +2421,7 @@ if (isset($_GET['reopen_ticket'])) {
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2, ticket_resolved_at = NULL WHERE ticket_id = $ticket_id");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
resetTicketResolutionSla($ticket_id);
|
||||
|
||||
logAudit("Ticket", "Reopened", "$session_name reopened ticket ID $ticket_id", $client_id, $ticket_id);
|
||||
|
||||
@@ -114,6 +114,20 @@
|
||||
<p>Tickets by Client</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="/agent/reports/sla_summary.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "sla_summary.php") { echo "active"; } ?>">
|
||||
<i class="nav-icon fas fa-stopwatch"></i>
|
||||
<p>SLA Summary</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="/agent/reports/sla_by_client.php" class="nav-link <?php if (basename($_SERVER["PHP_SELF"]) == "sla_by_client.php") { echo "active"; } ?>">
|
||||
<i class="nav-icon fas fa-stopwatch"></i>
|
||||
<p>SLA by Client</p>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if (lookupUserPermission("module_credential") >= 1) { ?>
|
||||
<li class="nav-item">
|
||||
|
||||
171
agent/reports/sla_by_client.php
Normal file
171
agent/reports/sla_by_client.php
Normal file
@@ -0,0 +1,171 @@
|
||||
<?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");
|
||||
|
||||
function slaPercentDisplay($percent)
|
||||
{
|
||||
if (is_null($percent)) {
|
||||
return "<span class='text-secondary'>-</span>";
|
||||
}
|
||||
if ($percent >= 95) {
|
||||
return "<span class='text-success text-bold'>$percent%</span>";
|
||||
}
|
||||
if ($percent >= 80) {
|
||||
return "<span class='text-warning text-bold'>$percent%</span>";
|
||||
}
|
||||
return "<span class='text-danger text-bold'>$percent%</span>";
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<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']);
|
||||
|
||||
// Resolution time is measured in clock time actually spent -
|
||||
// paused spells are excluded, which is what the SLA judged on
|
||||
$avg_time_to_resolve = '-';
|
||||
$resolved_minutes_total = 0;
|
||||
$resolved_count = 0;
|
||||
$sql_resolved = mysqli_query($mysqli, "SELECT ticket_id FROM tickets WHERE ticket_sla_id > 0 AND ticket_client_id = $client_id AND ticket_resolved_at IS NOT NULL AND $period_query");
|
||||
while ($resolved_row = mysqli_fetch_assoc($sql_resolved)) {
|
||||
$resolved_minutes_total += getTicketSlaConsumedMinutes($resolved_row['ticket_id']);
|
||||
$resolved_count++;
|
||||
}
|
||||
if ($resolved_count > 0) {
|
||||
$avg_time_to_resolve = secondsToTime(($resolved_minutes_total / $resolved_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";
|
||||
212
agent/reports/sla_summary.php
Normal file
212
agent/reports/sla_summary.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
require_once "includes/inc_all_reports.php";
|
||||
|
||||
enforceUserPermission('module_support');
|
||||
|
||||
if (isset($_GET['year'])) {
|
||||
$year = intval($_GET['year']);
|
||||
} else {
|
||||
$year = date('Y');
|
||||
}
|
||||
|
||||
$sql_ticket_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(ticket_created_at) AS ticket_year FROM tickets ORDER BY ticket_year DESC");
|
||||
|
||||
// Compliance figures for a slice of tickets carrying an SLA
|
||||
function getSlaCompliance($where)
|
||||
{
|
||||
global $mysqli;
|
||||
|
||||
$row = 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_response_sla_met IS NULL) AS response_pending,
|
||||
SUM(ticket_resolution_sla_met = 1) AS resolution_met,
|
||||
SUM(ticket_resolution_sla_met = 0) AS resolution_missed,
|
||||
SUM(ticket_resolution_sla_met IS NULL AND ticket_resolution_due_at IS NOT NULL) AS resolution_pending
|
||||
FROM tickets
|
||||
WHERE ticket_sla_id > 0 $where"
|
||||
));
|
||||
|
||||
$compliance = [
|
||||
'ticket_count' => intval($row['ticket_count']),
|
||||
'response_met' => intval($row['response_met']),
|
||||
'response_missed' => intval($row['response_missed']),
|
||||
'response_pending' => intval($row['response_pending']),
|
||||
'resolution_met' => intval($row['resolution_met']),
|
||||
'resolution_missed' => intval($row['resolution_missed']),
|
||||
'resolution_pending' => intval($row['resolution_pending']),
|
||||
];
|
||||
|
||||
// Percentages count only judged tickets - a ticket still in flight is
|
||||
// neither a hit nor a miss
|
||||
$response_judged = $compliance['response_met'] + $compliance['response_missed'];
|
||||
$compliance['response_percent'] = $response_judged ? round($compliance['response_met'] / $response_judged * 100, 1) : null;
|
||||
|
||||
$resolution_judged = $compliance['resolution_met'] + $compliance['resolution_missed'];
|
||||
$compliance['resolution_percent'] = $resolution_judged ? round($compliance['resolution_met'] / $resolution_judged * 100, 1) : null;
|
||||
|
||||
return $compliance;
|
||||
}
|
||||
|
||||
// Colour the headline figures the way the ticket list colours rows
|
||||
function slaPercentDisplay($percent)
|
||||
{
|
||||
if (is_null($percent)) {
|
||||
return "<span class='text-secondary'>-</span>";
|
||||
}
|
||||
if ($percent >= 95) {
|
||||
return "<span class='text-success text-bold'>$percent%</span>";
|
||||
}
|
||||
if ($percent >= 80) {
|
||||
return "<span class='text-warning text-bold'>$percent%</span>";
|
||||
}
|
||||
return "<span class='text-danger text-bold'>$percent%</span>";
|
||||
}
|
||||
|
||||
$overall = getSlaCompliance("AND YEAR(ticket_created_at) = $year");
|
||||
|
||||
?>
|
||||
|
||||
<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 Summary</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>
|
||||
</form>
|
||||
|
||||
<?php if ($overall['ticket_count'] == 0) { ?>
|
||||
<p class="text-secondary">No tickets raised in <?= $year ?> carried an SLA. Assign SLAs under Admin > SLAs to start tracking.</p>
|
||||
<?php } else { ?>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-4">
|
||||
<div class="info-box">
|
||||
<span class="info-box-icon bg-secondary"><i class="fas fa-life-ring"></i></span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">Tickets with an SLA</span>
|
||||
<span class="info-box-number"><?= $overall['ticket_count'] ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="info-box">
|
||||
<span class="info-box-icon bg-secondary"><i class="fas fa-reply"></i></span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">Response compliance</span>
|
||||
<span class="info-box-number"><?= slaPercentDisplay($overall['response_percent']) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="info-box">
|
||||
<span class="info-box-icon bg-secondary"><i class="fas fa-flag-checkered"></i></span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">Resolution compliance</span>
|
||||
<span class="info-box-number"><?= slaPercentDisplay($overall['resolution_percent']) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>By Priority (<?= $year ?>)</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Priority</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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach (['Urgent', 'High', 'Medium', 'Low'] as $priority) {
|
||||
$stats = getSlaCompliance("AND YEAR(ticket_created_at) = $year AND ticket_priority = '$priority'");
|
||||
if ($stats['ticket_count'] == 0) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?= $priority ?></td>
|
||||
<td class="text-right"><?= $stats['ticket_count'] ?></td>
|
||||
<td class="text-right"><?= $stats['response_met'] ?></td>
|
||||
<td class="text-right"><?= $stats['response_missed'] ?></td>
|
||||
<td class="text-right"><?= slaPercentDisplay($stats['response_percent']) ?></td>
|
||||
<td class="text-right"><?= $stats['resolution_met'] ?></td>
|
||||
<td class="text-right"><?= $stats['resolution_missed'] ?></td>
|
||||
<td class="text-right"><?= slaPercentDisplay($stats['resolution_percent']) ?></td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-dark mb-3">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fas fa-fw fa-calendar mr-2"></i>By Month (<?= $year ?>)</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Month</th>
|
||||
<th class="text-right">Tickets</th>
|
||||
<th class="text-right">Response %</th>
|
||||
<th class="text-right">Resolution %</th>
|
||||
<th class="text-right">Still open</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php for ($month = 1; $month <= 12; $month++) {
|
||||
$stats = getSlaCompliance("AND YEAR(ticket_created_at) = $year AND MONTH(ticket_created_at) = $month");
|
||||
if ($stats['ticket_count'] == 0) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td><?= date("F", mktime(1, 1, 1, $month, 1)) ?></td>
|
||||
<td class="text-right"><?= $stats['ticket_count'] ?></td>
|
||||
<td class="text-right"><?= slaPercentDisplay($stats['response_percent']) ?></td>
|
||||
<td class="text-right"><?= slaPercentDisplay($stats['resolution_percent']) ?></td>
|
||||
<td class="text-right"><?= $stats['resolution_pending'] ?></td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<small class="text-muted">Percentages count only tickets whose targets have been judged - tickets still awaiting a response or resolution are excluded until their outcome is known. Tickets raised before SLAs were assigned carry no SLA and are not counted.</small>
|
||||
|
||||
<?php } ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
require_once "../../includes/footer.php";
|
||||
@@ -68,6 +68,7 @@ $sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients
|
||||
<th class="text-right">Priority: Low</th>
|
||||
<th class="text-right">Priority: Med</th>
|
||||
<th class="text-right">Priority: High</th>
|
||||
<th class="text-right">Priority: Urgent</th>
|
||||
<th class="text-right">Resolved</th>
|
||||
<th class="text-right">Total Time worked <i>(H:M:S)</i></th>
|
||||
<th class="text-right">Avg time to respond</th>
|
||||
@@ -105,6 +106,11 @@ $sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients
|
||||
$row = mysqli_fetch_assoc($sql_high_ticket_count);
|
||||
$high_ticket_count = intval($row['high_ticket_count']);
|
||||
|
||||
// Breakdown tickets for each priority - Urgent
|
||||
$sql_urgent_ticket_count = mysqli_query($mysqli, "SELECT COUNT(ticket_id) AS urgent_ticket_count FROM tickets WHERE YEAR(ticket_created_at) = $year AND ticket_client_id = $client_id AND ticket_priority = 'Urgent'");
|
||||
$row = mysqli_fetch_assoc($sql_urgent_ticket_count);
|
||||
$urgent_ticket_count = intval($row['urgent_ticket_count']);
|
||||
|
||||
// Used to calculate average time to respond to tickets that were raised in period specified
|
||||
$sql_tickets_respond = mysqli_query($mysqli, "SELECT ticket_created_at, ticket_first_response_at FROM tickets WHERE YEAR(ticket_created_at) = $year AND ticket_client_id = $client_id");
|
||||
|
||||
@@ -158,6 +164,7 @@ $sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients
|
||||
<td class="text-right"><?= $low_ticket_count ?></td>
|
||||
<td class="text-right"><?= $med_ticket_count ?></td>
|
||||
<td class="text-right"><?= $high_ticket_count ?></td>
|
||||
<td class="text-right"><?= $urgent_ticket_count ?></td>
|
||||
<td class="text-right"><?= $ticket_resolved_count ?></td>
|
||||
<td class="text-right"><?= $ticket_total_time_worked ?></td>
|
||||
<td class="text-right"><?= $avg_time_to_respond ?></td>
|
||||
@@ -189,6 +196,7 @@ $sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients
|
||||
<th class="text-right">Priority: Low</th>
|
||||
<th class="text-right">Priority: Med</th>
|
||||
<th class="text-right">Priority: High</th>
|
||||
<th class="text-right">Priority: Urgent</th>
|
||||
<th class="text-right">Resolved</th>
|
||||
<th class="text-right">Total Time worked <i>(H:M:S)</i></th>
|
||||
<th class="text-right">Avg time to respond</th>
|
||||
@@ -227,6 +235,11 @@ $sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients
|
||||
$row = mysqli_fetch_assoc($sql_high_ticket_count);
|
||||
$high_ticket_count = intval($row['high_ticket_count']);
|
||||
|
||||
// Breakdown tickets for each priority - Urgent
|
||||
$sql_urgent_ticket_count = mysqli_query($mysqli, "SELECT COUNT(ticket_id) AS urgent_ticket_count FROM tickets WHERE YEAR(ticket_created_at) = $year AND MONTH(ticket_created_at) = $month AND ticket_client_id = $client_id AND ticket_priority = 'Urgent'");
|
||||
$row = mysqli_fetch_assoc($sql_urgent_ticket_count);
|
||||
$urgent_ticket_count = intval($row['urgent_ticket_count']);
|
||||
|
||||
// Used to calculate average time to respond to tickets that were raised in period specified
|
||||
$sql_tickets_respond = mysqli_query($mysqli, "SELECT ticket_created_at, ticket_first_response_at FROM tickets WHERE YEAR(ticket_created_at) = $year AND MONTH(ticket_created_at) = $month AND ticket_client_id = $client_id");
|
||||
|
||||
@@ -279,6 +292,7 @@ $sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients
|
||||
<td class="text-right"><?= $low_ticket_count ?></td>
|
||||
<td class="text-right"><?= $med_ticket_count ?></td>
|
||||
<td class="text-right"><?= $high_ticket_count ?></td>
|
||||
<td class="text-right"><?= $urgent_ticket_count ?></td>
|
||||
<td class="text-right"><?= $ticket_resolved_count ?></td>
|
||||
<td class="text-right"><?= $ticket_total_time_worked ?></td>
|
||||
<td class="text-right"><?= $avg_time_to_respond ?></td>
|
||||
|
||||
@@ -117,6 +117,7 @@ if (isset($_GET['ticket_id'])) {
|
||||
$ticket_resolution_due_at = escapeHtml($row['ticket_resolution_due_at']);
|
||||
$ticket_response_sla_met = $row['ticket_response_sla_met'];
|
||||
$ticket_resolution_sla_met = $row['ticket_resolution_sla_met'];
|
||||
$ticket_sla_paused = intval($row['ticket_status_pauses_sla']);
|
||||
$ticket_sla_name = "None";
|
||||
if ($ticket_sla_id) {
|
||||
$sla_name_sql = mysqli_query($mysqli, "SELECT sla_name FROM slas WHERE sla_id = $ticket_sla_id");
|
||||
@@ -911,7 +912,11 @@ if (isset($_GET['ticket_id'])) {
|
||||
<?php } ?>
|
||||
<?php if ($ticket_sla_id && $ticket_resolution_due_at) { ?>
|
||||
<div class="mt-2">
|
||||
<i class="fas fa-fw fa-flag-checkered text-secondary mr-1"></i><strong class="mr-1">Resolve by:</strong><?= date('M d • g:i A', strtotime($ticket_resolution_due_at)) ?>
|
||||
<i class="fas fa-fw fa-flag-checkered text-secondary mr-1"></i><strong class="mr-1">Resolve by:</strong><?php if ($ticket_sla_paused && empty($ticket_resolved_at) && empty($ticket_closed_at)) { ?>
|
||||
<span class="text-warning"><i class="fas fa-fw fa-pause-circle mr-1"></i>Paused</span>
|
||||
<?php } else { ?>
|
||||
<?= date('M d • g:i A', strtotime($ticket_resolution_due_at)) ?>
|
||||
<?php } ?>
|
||||
<?php if (!is_null($ticket_resolution_sla_met)) { echo $ticket_resolution_sla_met ? "<i class='fas fa-fw fa-check text-success ml-1' title='Resolution SLA met'></i>" : "<i class='fas fa-fw fa-exclamation-triangle text-danger ml-1' title='Resolution SLA missed'></i>"; } ?>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
@@ -108,9 +108,22 @@ $kanban = array_values($statuses);
|
||||
} else {
|
||||
$ticket_priority_color = "info";
|
||||
}
|
||||
|
||||
// SLA state, same stages the ticket list colours on. A paused
|
||||
// ticket drops its at-risk flag but keeps a recorded breach.
|
||||
$ticket_sla_alert_stage = max(intval($item['ticket_response_sla_alert_stage']), intval($item['ticket_resolution_sla_alert_stage']));
|
||||
if (intval($item['ticket_status_pauses_sla']) && $ticket_sla_alert_stage < 2) {
|
||||
$ticket_sla_alert_stage = 0;
|
||||
}
|
||||
$ticket_sla_class = '';
|
||||
if ($ticket_sla_alert_stage == 2) {
|
||||
$ticket_sla_class = ' border-danger';
|
||||
} elseif ($ticket_sla_alert_stage == 1) {
|
||||
$ticket_sla_class = ' border-warning';
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="task grab-cursor"
|
||||
<div class="task grab-cursor<?= $ticket_sla_class ?>"
|
||||
data-ticket-id="<?= $item['ticket_id'] ?>"
|
||||
data-ticket-status-id="<?= $item['ticket_status_id'] ?>"
|
||||
ondblclick="window.location.href='ticket.php?ticket_id=<?= $item['ticket_id'] ?>'">
|
||||
@@ -119,6 +132,12 @@ $kanban = array_values($statuses);
|
||||
<?= $item['ticket_priority'] ?>
|
||||
</span>
|
||||
|
||||
<?php if ($ticket_sla_alert_stage == 2) { ?>
|
||||
<span class="badge badge-danger" title="SLA breached"><i class="fas fa-fw fa-stopwatch"></i></span>
|
||||
<?php } elseif ($ticket_sla_alert_stage == 1) { ?>
|
||||
<span class="badge badge-warning" title="SLA at risk"><i class="fas fa-fw fa-stopwatch"></i></span>
|
||||
<?php } ?>
|
||||
|
||||
<span class="badge badge-secondary">
|
||||
<?= $item['category_name'] ?>
|
||||
</span>
|
||||
|
||||
@@ -92,6 +92,11 @@
|
||||
$ticket_closed_at = escapeHtml($row['ticket_closed_at']);
|
||||
// SLA alert stages are maintained by cron/ticket_sla.php (1 = warned, 2 = breached)
|
||||
$ticket_sla_alert_stage = max(intval($row['ticket_response_sla_alert_stage']), intval($row['ticket_resolution_sla_alert_stage']));
|
||||
// A paused ticket isn't running down its clock, so drop the
|
||||
// at-risk warning - a breach already recorded still stands
|
||||
if (intval($row['ticket_status_pauses_sla']) && $ticket_sla_alert_stage < 2) {
|
||||
$ticket_sla_alert_stage = 0;
|
||||
}
|
||||
if (empty($ticket_updated_at)) {
|
||||
if (!empty($ticket_closed_at)) {
|
||||
$ticket_updated_at_display = "<p>Never</p>";
|
||||
|
||||
@@ -69,6 +69,24 @@ if (isset($_GET['category']) & !empty($_GET['category'])) {
|
||||
// Default - any
|
||||
$ticket_assigned_query = '';
|
||||
$ticket_assigned_filter_id = '';
|
||||
// SLA state filter - breached / at risk / met / no SLA
|
||||
$ticket_sla_query = '';
|
||||
$ticket_sla_filter = '';
|
||||
if (isset($_GET['sla']) && !empty($_GET['sla'])) {
|
||||
$ticket_sla_filter = $_GET['sla'];
|
||||
if ($ticket_sla_filter == 'breached') {
|
||||
$ticket_sla_query = 'AND ticket_sla_id > 0 AND (ticket_response_sla_alert_stage = 2 OR ticket_resolution_sla_alert_stage = 2 OR ticket_response_sla_met = 0 OR ticket_resolution_sla_met = 0)';
|
||||
} elseif ($ticket_sla_filter == 'at_risk') {
|
||||
$ticket_sla_query = 'AND ticket_sla_id > 0 AND COALESCE(ticket_status_pauses_sla, 0) = 0 AND (ticket_response_sla_alert_stage = 1 OR ticket_resolution_sla_alert_stage = 1)';
|
||||
} elseif ($ticket_sla_filter == 'paused') {
|
||||
$ticket_sla_query = 'AND ticket_sla_id > 0 AND ticket_status_pauses_sla = 1';
|
||||
} elseif ($ticket_sla_filter == 'met') {
|
||||
$ticket_sla_query = 'AND ticket_sla_id > 0 AND ticket_response_sla_met = 1 AND (ticket_resolution_sla_met = 1 OR ticket_resolution_due_at IS NULL)';
|
||||
} elseif ($ticket_sla_filter == 'none') {
|
||||
$ticket_sla_query = 'AND ticket_sla_id = 0';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_GET['assigned']) & !empty($_GET['assigned'])) {
|
||||
if ($_GET['assigned'] == 'unassigned') {
|
||||
$ticket_assigned_query = 'AND ticket_assigned_to = 0';
|
||||
@@ -109,6 +127,7 @@ $query =
|
||||
$category_query
|
||||
AND DATE(ticket_created_at) BETWEEN '$dtf' AND '$dtt'
|
||||
AND (CONCAT(ticket_prefix,ticket_number) LIKE '%$q%' OR client_name LIKE '%$q%' OR ticket_subject LIKE '%$q%' OR ticket_status_name LIKE '%$q%' OR ticket_priority LIKE '%$q%' OR user_name LIKE '%$q%' OR contact_name LIKE '%$q%' OR asset_name LIKE '%$q%' OR vendor_name LIKE '%$q%' OR ticket_vendor_ticket_number LIKE '%q%')
|
||||
$ticket_sla_query
|
||||
$ticket_billable_snippet
|
||||
$ticket_project_snippet
|
||||
$access_permission_query_overide
|
||||
@@ -211,6 +230,24 @@ $sql_categories_filter = mysqli_query(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Only offer the SLA filter once SLAs are actually in use
|
||||
$sla_filter_in_use = mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(sla_id) FROM slas WHERE sla_archived_at IS NULL"))[0] > 0;
|
||||
if ($sla_filter_in_use) { ?>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group">
|
||||
<select class="form-control select2" name="sla" onchange="this.form.submit()">
|
||||
<option value="">- All SLA States -</option>
|
||||
<option <?php if ($ticket_sla_filter == 'breached') { echo "selected"; } ?> value="breached">SLA breached</option>
|
||||
<option <?php if ($ticket_sla_filter == 'at_risk') { echo "selected"; } ?> value="at_risk">SLA at risk</option>
|
||||
<option <?php if ($ticket_sla_filter == 'paused') { echo "selected"; } ?> value="paused">SLA paused</option>
|
||||
<option <?php if ($ticket_sla_filter == 'met') { echo "selected"; } ?> value="met">SLA met</option>
|
||||
<option <?php if ($ticket_sla_filter == 'none') { echo "selected"; } ?> value="none">No SLA</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<?php } ?>
|
||||
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group">
|
||||
<select class="form-control select2" name="category" onchange="this.form.submit()">
|
||||
|
||||
Reference in New Issue
Block a user