mirror of
https://github.com/itflow-org/itflow
synced 2026-08-04 22:57: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:
41
admin/database_updates/2.5.1.php
Normal file
41
admin/database_updates/2.5.1.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 2.5.1 (from 2.5.0)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
// SLA pausing. sla_history records the intervals during which a ticket's
|
||||
// resolution clock was actually running - one open row (ended_at NULL) per
|
||||
// running ticket. Statuses flagged below stop the clock, and the ticket's
|
||||
// resolution due date is recomputed from the remaining budget on resume.
|
||||
mysqli_query($mysqli, "CREATE TABLE `sla_history` (
|
||||
`sla_history_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`sla_history_started_at` datetime NOT NULL,
|
||||
`sla_history_ended_at` datetime DEFAULT NULL,
|
||||
`sla_history_minutes` int(11) DEFAULT NULL,
|
||||
`sla_history_ticket_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`sla_history_id`),
|
||||
KEY `sla_history_ticket_id` (`sla_history_ticket_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||
|
||||
// Which statuses pause the resolution clock. Nothing pauses by default, so
|
||||
// SLA behaviour is unchanged until an admin opts a status in.
|
||||
mysqli_query($mysqli, "ALTER TABLE `ticket_statuses`
|
||||
ADD COLUMN `ticket_status_pauses_sla` tinyint(1) NOT NULL DEFAULT 0 AFTER `ticket_status_active`");
|
||||
|
||||
// Backfill an open interval for every ticket already running a resolution
|
||||
// clock, anchored at creation. Without this their consumed time would read
|
||||
// as zero and the first pause/resume would hand back the full budget.
|
||||
mysqli_query($mysqli, "INSERT INTO sla_history (sla_history_started_at, sla_history_ticket_id)
|
||||
SELECT ticket_created_at, ticket_id
|
||||
FROM tickets
|
||||
LEFT JOIN slas ON ticket_sla_id = sla_id
|
||||
WHERE ticket_sla_id > 0
|
||||
AND sla_resolution_minutes > 0
|
||||
AND ticket_resolution_due_at IS NOT NULL
|
||||
AND ticket_resolved_at IS NULL
|
||||
AND ticket_closed_at IS NULL
|
||||
AND ticket_archived_at IS NULL");
|
||||
@@ -33,6 +33,20 @@ ob_start();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label>SLA</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-stopwatch"></i></span>
|
||||
</div>
|
||||
<select class="form-control select2" name="pauses_sla">
|
||||
<option value="0">Resolution clock keeps running</option>
|
||||
<option value="1">Pause the resolution clock</option>
|
||||
</select>
|
||||
</div>
|
||||
<small class="text-muted">Tickets sitting in a paused status never warn or breach on resolution. Time already spent is kept and the deadline moves out when the ticket comes back.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_ticket_status" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Create</button>
|
||||
|
||||
@@ -10,6 +10,7 @@ $ticket_status_name = escapeHtml($row['ticket_status_name']);
|
||||
$ticket_status_color = escapeHtml($row['ticket_status_color']);
|
||||
$ticket_status_order = intval($row['ticket_status_order']);
|
||||
$ticket_status_active = intval($row['ticket_status_active']);
|
||||
$ticket_status_pauses_sla = intval($row['ticket_status_pauses_sla']);
|
||||
|
||||
// Generate the HTML form content using output buffering.
|
||||
ob_start();
|
||||
@@ -70,6 +71,20 @@ ob_start();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>SLA</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-stopwatch"></i></span>
|
||||
</div>
|
||||
<select class="form-control select2" name="pauses_sla">
|
||||
<option <?php if ($ticket_status_pauses_sla == 0) { echo "selected "; } ?>value="0">Resolution clock keeps running</option>
|
||||
<option <?php if ($ticket_status_pauses_sla == 1) { echo "selected "; } ?>value="1">Pause the resolution clock</option>
|
||||
</select>
|
||||
</div>
|
||||
<small class="text-muted">Tickets sitting in a paused status never warn or breach on resolution. Time already spent is kept and the deadline moves out when the ticket comes back.</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="edit_ticket_status" class="btn btn-primary text-bold"><i class="fas fa-check mr-2"></i>Save</button>
|
||||
|
||||
@@ -9,7 +9,9 @@ if (isset($_POST['add_ticket_status'])) {
|
||||
$name = escapeSql($_POST['name']);
|
||||
$color = escapeSql($_POST['color']);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_statuses SET ticket_status_name = '$name', ticket_status_color = '$color'");
|
||||
$pauses_sla = intval($_POST['pauses_sla'] ?? 0);
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_statuses SET ticket_status_name = '$name', ticket_status_color = '$color', ticket_status_pauses_sla = $pauses_sla");
|
||||
|
||||
$ticket_status_id = mysqli_insert_id($mysqli);
|
||||
|
||||
@@ -31,7 +33,15 @@ if (isset($_POST['edit_ticket_status'])) {
|
||||
$order = intval($_POST['order']);
|
||||
$status = intval($_POST['status']);
|
||||
|
||||
mysqli_query($mysqli, "UPDATE ticket_statuses SET ticket_status_name = '$name', ticket_status_color = '$color', ticket_status_order = $order, ticket_status_active = $status WHERE ticket_status_id = $ticket_status_id");
|
||||
$pauses_sla = intval($_POST['pauses_sla'] ?? 0);
|
||||
|
||||
mysqli_query($mysqli, "UPDATE ticket_statuses SET ticket_status_name = '$name', ticket_status_color = '$color', ticket_status_order = $order, ticket_status_active = $status, ticket_status_pauses_sla = $pauses_sla WHERE ticket_status_id = $ticket_status_id");
|
||||
|
||||
// Tickets already sitting in this status need their clock reconciled
|
||||
$sql_status_tickets = mysqli_query($mysqli, "SELECT ticket_id FROM tickets WHERE ticket_status = $ticket_status_id AND ticket_closed_at IS NULL AND ticket_archived_at IS NULL");
|
||||
while ($status_ticket_row = mysqli_fetch_assoc($sql_status_tickets)) {
|
||||
syncTicketSlaClock($status_ticket_row['ticket_id']);
|
||||
}
|
||||
|
||||
logAudit("Ticket Status", "Edit", "$session_name edited custom ticket status $name", 0, $ticket_status_id);
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
Status <?php if ($sort == 'ticket_status_active') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>SLA</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -72,6 +73,7 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
$ticket_status_name = escapeHtml($row['ticket_status_name']);
|
||||
$ticket_status_color = escapeHtml($row['ticket_status_color']);
|
||||
$ticket_status_active = intval($row['ticket_status_active']);
|
||||
$ticket_status_pauses_sla = intval($row['ticket_status_pauses_sla']);
|
||||
if ($ticket_status_active) {
|
||||
$ticket_status_display = "<div class='text-success text-bold'>Active</div>";
|
||||
} else {
|
||||
@@ -92,6 +94,13 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
<td>
|
||||
<span class='badge badge-pill text-light p-2' style="background-color: <?= $ticket_status_color ?>"><?= $ticket_status_name ?></span>
|
||||
<td><?= $ticket_status_display ?></td>
|
||||
<td>
|
||||
<?php if ($ticket_status_pauses_sla) { ?>
|
||||
<span class="text-warning"><i class="fas fa-fw fa-pause-circle mr-1"></i>Paused</span>
|
||||
<?php } else { ?>
|
||||
<span class="text-secondary">Running</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown dropleft text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-toggle="dropdown">
|
||||
|
||||
@@ -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()">
|
||||
|
||||
@@ -30,6 +30,7 @@ if (!empty($ticket_id)) {
|
||||
|
||||
// Resolve
|
||||
$update_sql = mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 4, ticket_resolved_at = NOW() WHERE ticket_id = $ticket_id AND ticket_client_id = $client_id LIMIT 1");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
setTicketResolutionSlaMet($ticket_id);
|
||||
|
||||
// Check insert & get insert ID
|
||||
|
||||
@@ -106,6 +106,7 @@ if (isset($_POST['add_ticket_comment'])) {
|
||||
|
||||
// Update Ticket Last Response Field & set ticket to open as client has replied
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2 WHERE ticket_id = $ticket_id AND ticket_client_id = $session_client_id LIMIT 1");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
|
||||
|
||||
// Get ticket details & Notify the assigned tech (if any)
|
||||
@@ -278,6 +279,8 @@ if (isset($_GET['resolve_ticket'])) {
|
||||
|
||||
// Resolve the ticket
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 4, ticket_resolved_at = NOW() WHERE ticket_id = $ticket_id AND ticket_client_id = $session_client_id");
|
||||
setTicketResolutionSlaMet($ticket_id);
|
||||
syncTicketSlaClock($ticket_id);
|
||||
|
||||
// Add reply
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket resolved by $session_contact_name.', ticket_reply_type = 'Client', ticket_reply_by = $session_contact_id, ticket_reply_ticket_id = $ticket_id");
|
||||
@@ -313,6 +316,8 @@ if (isset($_GET['reopen_ticket'])) {
|
||||
|
||||
// Re-open ticket
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2, ticket_resolved_at = NULL WHERE ticket_id = $ticket_id AND ticket_client_id = $session_client_id");
|
||||
resetTicketResolutionSla($ticket_id);
|
||||
syncTicketSlaClock($ticket_id);
|
||||
|
||||
// Add reply
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket reopened by $session_contact_name.', ticket_reply_type = 'Client', ticket_reply_by = $session_contact_id, ticket_reply_ticket_id = $ticket_id");
|
||||
@@ -348,6 +353,7 @@ if (isset($_GET['close_ticket'])) {
|
||||
|
||||
// Fully close ticket
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 5, ticket_closed_at = NOW() WHERE ticket_id = $ticket_id AND ticket_client_id = $session_client_id");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
|
||||
// Add reply
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_replies SET ticket_reply = 'Ticket closed by $session_contact_name.', ticket_reply_type = 'Client', ticket_reply_by = $session_contact_id, ticket_reply_ticket_id = $ticket_id");
|
||||
|
||||
@@ -484,6 +484,7 @@ while ($row = mysqli_fetch_assoc($sql_resolved_tickets_to_close)) {
|
||||
$client_id = intval($row['ticket_client_id']);
|
||||
|
||||
mysqli_query($mysqli,"UPDATE tickets SET ticket_status = 5, ticket_closed_at = NOW(), ticket_closed_by = $ticket_assigned_to WHERE ticket_id = $ticket_id");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
|
||||
//Logging
|
||||
logAudit("Ticket", "Closed", "$ticket_prefix$ticket_number auto closed", $client_id, $ticket_id);
|
||||
|
||||
@@ -370,6 +370,7 @@ function addReply($from_email, $date, $subject, $ticket_number, $message, $attac
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_status = 2, ticket_resolved_at = NULL WHERE ticket_id = $ticket_id AND ticket_client_id = $client_id LIMIT 1");
|
||||
resetTicketResolutionSla($ticket_id);
|
||||
|
||||
logAudit("Ticket", "Edit", "Email parser: Client contact $from_email_esc updated ticket $config_ticket_prefix$ticket_number_esc ($subject)", $client_id, $ticket_id);
|
||||
triggerCustomAction('ticket_reply_client', $ticket_id);
|
||||
|
||||
@@ -28,6 +28,10 @@ require_once "../functions.php";
|
||||
* they make every notification fire exactly once. Tickets without an SLA
|
||||
* (ticket_sla_id = 0) are never selected, so with no SLAs assigned this cron
|
||||
* is a no-op.
|
||||
*
|
||||
* Tickets sitting in a status flagged as pausing the SLA are skipped on the
|
||||
* resolution track: their clock is not running, so they can neither warn nor
|
||||
* breach until someone moves them back to a running status.
|
||||
*/
|
||||
|
||||
$sla_settings = getSlaSettings();
|
||||
@@ -135,7 +139,9 @@ $sql_resolution = mysqli_query($mysqli, "SELECT ticket_id, ticket_prefix, ticket
|
||||
FROM tickets
|
||||
LEFT JOIN slas ON ticket_sla_id = sla_id
|
||||
LEFT JOIN users ON ticket_assigned_to = user_id
|
||||
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
|
||||
WHERE ticket_sla_id > 0
|
||||
AND COALESCE(ticket_status_pauses_sla, 0) = 0
|
||||
AND ticket_resolution_due_at IS NOT NULL
|
||||
AND ticket_resolved_at IS NULL
|
||||
AND ticket_closed_at IS NULL
|
||||
@@ -154,8 +160,9 @@ while ($ticket = mysqli_fetch_assoc($sql_resolution)) {
|
||||
sendSlaAlert($ticket, "Resolution SLA breached", "The resolution SLA on this ticket was missed (due {$ticket['ticket_resolution_due_at']}).");
|
||||
|
||||
} elseif ($stage < 1 && $warning_percent) {
|
||||
$warn_at = strtotime(addBusinessMinutes($ticket['ticket_created_at'], floor(intval($ticket['sla_resolution_minutes']) * $warning_percent / 100)));
|
||||
if ($now >= $warn_at) {
|
||||
// Measured against consumed clock time, so paused spells don't warn early
|
||||
$warn_after_minutes = floor(intval($ticket['sla_resolution_minutes']) * $warning_percent / 100);
|
||||
if (getTicketSlaConsumedMinutes($ticket_id) >= $warn_after_minutes) {
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_resolution_sla_alert_stage = 1 WHERE ticket_id = $ticket_id");
|
||||
sendSlaAlert($ticket, "Resolution SLA at risk", "This ticket is approaching its resolution SLA (due {$ticket['ticket_resolution_due_at']}).");
|
||||
}
|
||||
|
||||
16
db.sql
16
db.sql
@@ -2271,6 +2271,21 @@ CREATE TABLE `sla_assignments` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `sla_history`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `sla_history`;
|
||||
CREATE TABLE `sla_history` (
|
||||
`sla_history_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`sla_history_started_at` datetime NOT NULL,
|
||||
`sla_history_ended_at` datetime DEFAULT NULL,
|
||||
`sla_history_minutes` int(11) DEFAULT NULL,
|
||||
`sla_history_ticket_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`sla_history_id`),
|
||||
KEY `sla_history_ticket_id` (`sla_history_ticket_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
--
|
||||
-- Table structure for table `slas`
|
||||
--
|
||||
@@ -2667,6 +2682,7 @@ CREATE TABLE `ticket_statuses` (
|
||||
`ticket_status_name` varchar(200) NOT NULL,
|
||||
`ticket_status_color` varchar(200) NOT NULL,
|
||||
`ticket_status_active` tinyint(1) NOT NULL DEFAULT 1,
|
||||
`ticket_status_pauses_sla` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`ticket_status_order` int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`ticket_status_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
@@ -114,6 +114,160 @@ function addBusinessMinutes($start_datetime, $minutes)
|
||||
return $cursor->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
// Business minutes elapsed between two datetimes - the inverse of
|
||||
// addBusinessMinutes, used to measure how much of a resolution budget an
|
||||
// interval actually consumed.
|
||||
function businessMinutesBetween($start_datetime, $end_datetime)
|
||||
{
|
||||
$start = new DateTime($start_datetime);
|
||||
$end = new DateTime($end_datetime);
|
||||
|
||||
if ($end <= $start) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$sla_settings = getSlaSettings();
|
||||
$business_days = $sla_settings['business_days'];
|
||||
$day_start = $sla_settings['business_hours_start'];
|
||||
$day_end = $sla_settings['business_hours_end'];
|
||||
|
||||
if (empty($business_days) || empty($day_start) || empty($day_end) || $day_start >= $day_end) {
|
||||
return intval(floor(($end->getTimestamp() - $start->getTimestamp()) / 60));
|
||||
}
|
||||
|
||||
$seconds = 0;
|
||||
$cursor = clone $start;
|
||||
|
||||
// Sum the overlap between the interval and each day's business window
|
||||
for ($i = 0; $i < 731; $i++) {
|
||||
|
||||
if ($cursor > $end) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (in_array(intval($cursor->format('N')), $business_days)) {
|
||||
|
||||
$window_start = new DateTime($cursor->format('Y-m-d') . " $day_start");
|
||||
$window_end = new DateTime($cursor->format('Y-m-d') . " $day_end");
|
||||
|
||||
$from = $cursor > $window_start ? $cursor : $window_start;
|
||||
$to = $end < $window_end ? $end : $window_end;
|
||||
|
||||
if ($to > $from) {
|
||||
$seconds += $to->getTimestamp() - $from->getTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
$cursor = new DateTime($cursor->format('Y-m-d') . ' 00:00:00');
|
||||
$cursor->modify('+1 day');
|
||||
}
|
||||
|
||||
return intval(floor($seconds / 60));
|
||||
}
|
||||
|
||||
// Business minutes already spent on a ticket's resolution clock, including the
|
||||
// interval currently running
|
||||
function getTicketSlaConsumedMinutes($ticket_id)
|
||||
{
|
||||
global $mysqli;
|
||||
|
||||
$ticket_id = intval($ticket_id);
|
||||
$consumed = 0;
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT sla_history_started_at, sla_history_ended_at, sla_history_minutes FROM sla_history WHERE sla_history_ticket_id = $ticket_id");
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
if (!is_null($row['sla_history_ended_at'])) {
|
||||
$consumed += intval($row['sla_history_minutes']);
|
||||
} else {
|
||||
$consumed += businessMinutesBetween($row['sla_history_started_at'], date('Y-m-d H:i:s'));
|
||||
}
|
||||
}
|
||||
|
||||
return $consumed;
|
||||
}
|
||||
|
||||
// How many times a ticket's clock has been stopped. Zero means it has run
|
||||
// continuously since creation, so its deadline is still simply creation plus
|
||||
// the budget - the same answer pausing-free installs have always had.
|
||||
function getTicketSlaPausedCount($ticket_id)
|
||||
{
|
||||
global $mysqli;
|
||||
|
||||
$ticket_id = intval($ticket_id);
|
||||
$row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(sla_history_id) AS paused_count FROM sla_history WHERE sla_history_ticket_id = $ticket_id AND sla_history_ended_at IS NOT NULL"));
|
||||
|
||||
return intval($row['paused_count']);
|
||||
}
|
||||
|
||||
// Reconcile a ticket's resolution clock with its current status. Safe to call
|
||||
// after any status change (and after applyTicketSla) - it opens an interval
|
||||
// when the clock should be running, closes it when it should not, and on
|
||||
// resume re-bases the resolution deadline on the remaining budget. The response
|
||||
// clock is deliberately never paused: a ticket cannot wait on a client before
|
||||
// anyone has replied to it.
|
||||
function syncTicketSlaClock($ticket_id)
|
||||
{
|
||||
global $mysqli;
|
||||
|
||||
$ticket_id = intval($ticket_id);
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT ticket_sla_id, ticket_created_at, ticket_resolved_at, ticket_closed_at, ticket_archived_at, ticket_resolution_sla_alert_stage, sla_resolution_minutes, ticket_status_pauses_sla
|
||||
FROM tickets
|
||||
LEFT JOIN slas ON ticket_sla_id = sla_id
|
||||
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
|
||||
WHERE ticket_id = $ticket_id
|
||||
LIMIT 1"
|
||||
);
|
||||
if (!$sql || !mysqli_num_rows($sql)) {
|
||||
return;
|
||||
}
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
|
||||
$open_sql = mysqli_query($mysqli, "SELECT sla_history_id, sla_history_started_at FROM sla_history WHERE sla_history_ticket_id = $ticket_id AND sla_history_ended_at IS NULL ORDER BY sla_history_id DESC LIMIT 1");
|
||||
$open_interval = mysqli_num_rows($open_sql) ? mysqli_fetch_assoc($open_sql) : null;
|
||||
|
||||
// Only tickets carrying a resolution target have a clock to track
|
||||
$resolution_minutes = intval($row['sla_resolution_minutes']);
|
||||
$tracked = intval($row['ticket_sla_id']) > 0 && $resolution_minutes > 0;
|
||||
|
||||
$should_run = $tracked
|
||||
&& empty($row['ticket_resolved_at'])
|
||||
&& empty($row['ticket_closed_at'])
|
||||
&& empty($row['ticket_archived_at'])
|
||||
&& intval($row['ticket_status_pauses_sla']) == 0;
|
||||
|
||||
if ($should_run && is_null($open_interval)) {
|
||||
|
||||
// Prior intervals mean this is a resume, so the deadline moves out by
|
||||
// whatever budget is left rather than staying where it was
|
||||
$had_history = getTicketSlaPausedCount($ticket_id) > 0;
|
||||
$consumed = $had_history ? getTicketSlaConsumedMinutes($ticket_id) : 0;
|
||||
|
||||
// The very first interval is anchored to the ticket's creation time, not
|
||||
// to now - otherwise a backdated ticket would start life with its whole
|
||||
// budget intact and re-stamping it would keep handing that budget back
|
||||
$interval_start = $had_history ? date('Y-m-d H:i:s') : escapeSql($row['ticket_created_at']);
|
||||
mysqli_query($mysqli, "INSERT INTO sla_history SET sla_history_started_at = '$interval_start', sla_history_ticket_id = $ticket_id");
|
||||
|
||||
if ($had_history) {
|
||||
$remaining = $resolution_minutes - $consumed;
|
||||
if ($remaining < 0) {
|
||||
$remaining = 0;
|
||||
}
|
||||
$resolution_due_at = addBusinessMinutes(date('Y-m-d H:i:s'), $remaining);
|
||||
// A ticket that already breached stays breached
|
||||
$alert_stage = intval($row['ticket_resolution_sla_alert_stage']) == 2 ? 2 : 0;
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_resolution_due_at = '$resolution_due_at', ticket_resolution_sla_alert_stage = $alert_stage WHERE ticket_id = $ticket_id");
|
||||
}
|
||||
|
||||
} elseif (!$should_run && !is_null($open_interval)) {
|
||||
|
||||
$interval_id = intval($open_interval['sla_history_id']);
|
||||
$minutes = businessMinutesBetween($open_interval['sla_history_started_at'], date('Y-m-d H:i:s'));
|
||||
mysqli_query($mysqli, "UPDATE sla_history SET sla_history_ended_at = NOW(), sla_history_minutes = $minutes WHERE sla_history_id = $interval_id");
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve which SLA (if any) applies to a client + priority combination.
|
||||
// Returns an sla_id, or 0 when no SLA applies.
|
||||
function getTicketSlaId($client_id, $priority)
|
||||
@@ -180,6 +334,7 @@ function applyTicketSla($ticket_id, $forced_sla_id = null)
|
||||
// No SLA applies - clear any previous targets
|
||||
if ($sla_id == 0) {
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_sla_id = 0, ticket_response_due_at = NULL, ticket_resolution_due_at = NULL, ticket_response_sla_met = NULL, ticket_resolution_sla_met = NULL, ticket_response_sla_alert_stage = 0, ticket_resolution_sla_alert_stage = 0 WHERE ticket_id = $ticket_id");
|
||||
syncTicketSlaClock($ticket_id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -196,7 +351,19 @@ function applyTicketSla($ticket_id, $forced_sla_id = null)
|
||||
$resolution_due_at = null;
|
||||
$resolution_due_at_set = "NULL";
|
||||
if (intval($sla['sla_resolution_minutes']) > 0) {
|
||||
$resolution_due_at = addBusinessMinutes($created_at, $sla['sla_resolution_minutes']);
|
||||
// Once a ticket has been paused the budget is measured from what was
|
||||
// actually consumed, so paused time is not silently handed back. A
|
||||
// ticket that has only ever run keeps its original creation-anchored
|
||||
// deadline, including when that deadline is already in the past.
|
||||
if (getTicketSlaPausedCount($ticket_id) > 0) {
|
||||
$remaining = intval($sla['sla_resolution_minutes']) - getTicketSlaConsumedMinutes($ticket_id);
|
||||
if ($remaining < 0) {
|
||||
$remaining = 0;
|
||||
}
|
||||
$resolution_due_at = addBusinessMinutes(date('Y-m-d H:i:s'), $remaining);
|
||||
} else {
|
||||
$resolution_due_at = addBusinessMinutes($created_at, $sla['sla_resolution_minutes']);
|
||||
}
|
||||
$resolution_due_at_set = "'$resolution_due_at'";
|
||||
}
|
||||
|
||||
@@ -213,6 +380,8 @@ function applyTicketSla($ticket_id, $forced_sla_id = null)
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_sla_id = $sla_id, ticket_response_due_at = '$response_due_at', ticket_resolution_due_at = $resolution_due_at_set, ticket_response_sla_met = $response_met_set, ticket_resolution_sla_met = $resolution_met_set, ticket_response_sla_alert_stage = 0, ticket_resolution_sla_alert_stage = 0 WHERE ticket_id = $ticket_id");
|
||||
|
||||
syncTicketSlaClock($ticket_id);
|
||||
}
|
||||
|
||||
// Record the ticket's first response (if not already recorded) and judge the
|
||||
@@ -266,10 +435,13 @@ function setTicketResolutionSlaMet($ticket_id)
|
||||
$resolution_met = $ended_at <= strtotime($row['ticket_resolution_due_at']) ? 1 : 0;
|
||||
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_resolution_sla_met = $resolution_met WHERE ticket_id = $ticket_id");
|
||||
|
||||
syncTicketSlaClock($ticket_id);
|
||||
}
|
||||
|
||||
// A reopened ticket goes back on the resolution clock (original due date - no
|
||||
// pause/extend logic yet, that arrives with SLA pausing)
|
||||
// A reopened ticket goes back on the resolution clock. syncTicketSlaClock
|
||||
// reopens an interval and re-bases the deadline on the budget that is left, so
|
||||
// a ticket that was resolved with time to spare gets that remainder back.
|
||||
function resetTicketResolutionSla($ticket_id)
|
||||
{
|
||||
global $mysqli;
|
||||
@@ -277,4 +449,6 @@ function resetTicketResolutionSla($ticket_id)
|
||||
$ticket_id = intval($ticket_id);
|
||||
|
||||
mysqli_query($mysqli, "UPDATE tickets SET ticket_resolution_sla_met = NULL, ticket_resolution_sla_alert_stage = 0 WHERE ticket_id = $ticket_id");
|
||||
|
||||
syncTicketSlaClock($ticket_id);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user