diff --git a/admin/post/sla.php b/admin/post/sla.php index 837fb71a..efcf30a9 100644 --- a/admin/post/sla.php +++ b/admin/post/sla.php @@ -109,6 +109,9 @@ if (isset($_POST['edit_sla_settings'])) { mysqli_query($mysqli, "UPDATE settings SET config_business_days = '$business_days', config_business_hours_start = '$business_hours_start', config_business_hours_end = '$business_hours_end', config_sla_warning_percent = $warning_percent, config_sla_notification_email = '$notification_email' WHERE company_id = 1"); + // Drop the cached copy so the restamp below uses the hours just saved + getSlaSettings(true); + // Business hours feed the due date math - re-stamp open SLA tickets $restamped = 0; $sql_tickets = mysqli_query($mysqli, "SELECT ticket_id, ticket_sla_id FROM tickets WHERE ticket_sla_id > 0 AND ticket_closed_at IS NULL AND ticket_archived_at IS NULL"); diff --git a/agent/reports/sla_by_client.php b/agent/reports/sla_by_client.php index ee4f7962..5bbe800e 100644 --- a/agent/reports/sla_by_client.php +++ b/agent/reports/sla_by_client.php @@ -27,18 +27,31 @@ $sql_ticket_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(ticket_created_a $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 "-"; + +// Clock time spent on resolved tickets, gathered for every client at once +// rather than per ticket. Tickets with no interval rows - response-only plans, +// or anything resolved before SLA pausing existed - fall back to the business +// time that elapsed between being raised and resolved. +$resolve_totals = []; +$sql_resolve_times = mysqli_query($mysqli, "SELECT ticket_client_id, ticket_id, ticket_created_at, ticket_resolved_at, SUM(sla_history_minutes) AS logged_minutes + FROM tickets + LEFT JOIN sla_history ON sla_history_ticket_id = ticket_id + WHERE ticket_sla_id > 0 + AND ticket_resolved_at IS NOT NULL + AND $period_query + GROUP BY ticket_id, ticket_client_id, ticket_created_at, ticket_resolved_at" +); +while ($resolve_row = mysqli_fetch_assoc($sql_resolve_times)) { + $resolve_client_id = intval($resolve_row['ticket_client_id']); + $resolve_minutes = is_null($resolve_row['logged_minutes']) + ? businessMinutesBetween($resolve_row['ticket_created_at'], $resolve_row['ticket_resolved_at']) + : intval($resolve_row['logged_minutes']); + + if (!isset($resolve_totals[$resolve_client_id])) { + $resolve_totals[$resolve_client_id] = ['minutes' => 0, 'count' => 0]; } - if ($percent >= 95) { - return "$percent%"; - } - if ($percent >= 80) { - return "$percent%"; - } - return "$percent%"; + $resolve_totals[$resolve_client_id]['minutes'] += $resolve_minutes; + $resolve_totals[$resolve_client_id]['count']++; } ?> @@ -125,18 +138,11 @@ function slaPercentDisplay($percent) $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 + // Clock time actually spent - paused spells excluded, which is + // what the SLA itself 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); + if (!empty($resolve_totals[$client_id]['count'])) { + $avg_time_to_resolve = secondsToTime(($resolve_totals[$client_id]['minutes'] / $resolve_totals[$client_id]['count']) * 60); } ?> diff --git a/agent/reports/sla_summary.php b/agent/reports/sla_summary.php index 272879fc..dce6e010 100644 --- a/agent/reports/sla_summary.php +++ b/agent/reports/sla_summary.php @@ -50,20 +50,6 @@ function getSlaCompliance($where) return $compliance; } -// Colour the headline figures the way the ticket list colours rows -function slaPercentDisplay($percent) -{ - if (is_null($percent)) { - return "-"; - } - if ($percent >= 95) { - return "$percent%"; - } - if ($percent >= 80) { - return "$percent%"; - } - return "$percent%"; -} $overall = getSlaCompliance("AND YEAR(ticket_created_at) = $year"); diff --git a/functions/sla.php b/functions/sla.php index 4bc32677..9abf7a63 100644 --- a/functions/sla.php +++ b/functions/sla.php @@ -17,12 +17,19 @@ */ // Business hours + SLA settings, fetched once per request -function getSlaSettings() +function getSlaSettings($refresh = false) { global $mysqli; static $sla_settings = null; + // Callers that have just written to the settings row pass true - otherwise + // they would restamp tickets using the business hours this request started + // with rather than the ones just saved + if ($refresh) { + $sla_settings = null; + } + if (!is_null($sla_settings)) { return $sla_settings; } @@ -166,16 +173,24 @@ function businessMinutesBetween($start_datetime, $end_datetime) } // Business minutes already spent on a ticket's resolution clock, including the -// interval currently running +// interval currently running. +// +// Tickets with no clock history at all fall back to the business time elapsed +// since they were raised. Only tickets carrying a resolution target get +// intervals, so this covers response-only plans, and tickets that were already +// resolved when SLA pausing was introduced. Without the fallback both report +// zero time spent, which reads as instant resolution. function getTicketSlaConsumedMinutes($ticket_id) { global $mysqli; $ticket_id = intval($ticket_id); $consumed = 0; + $has_history = false; $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)) { + $has_history = true; if (!is_null($row['sla_history_ended_at'])) { $consumed += intval($row['sla_history_minutes']); } else { @@ -183,6 +198,16 @@ function getTicketSlaConsumedMinutes($ticket_id) } } + if (!$has_history) { + $ticket_sql = mysqli_query($mysqli, "SELECT ticket_created_at, ticket_resolved_at, ticket_closed_at FROM tickets WHERE ticket_id = $ticket_id LIMIT 1"); + if (!$ticket_sql || !mysqli_num_rows($ticket_sql)) { + return 0; + } + $ticket = mysqli_fetch_assoc($ticket_sql); + $ended_at = $ticket['ticket_resolved_at'] ?: ($ticket['ticket_closed_at'] ?: date('Y-m-d H:i:s')); + return businessMinutesBetween($ticket['ticket_created_at'], $ended_at); + } + return $consumed; } @@ -199,6 +224,22 @@ function getTicketSlaPausedCount($ticket_id) return intval($row['paused_count']); } +// Colour an SLA compliance percentage for the reports. Null means nothing has +// been judged yet, which is not the same as zero. +function slaPercentDisplay($percent) +{ + if (is_null($percent)) { + return "-"; + } + if ($percent >= 95) { + return "$percent%"; + } + if ($percent >= 80) { + return "$percent%"; + } + return "$percent%"; +} + // 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