mirror of
https://github.com/itflow-org/itflow
synced 2026-08-05 15:17:17 +00:00
Fix SLA resolve-time reporting, stale settings cache on save, and report N+1
This commit is contained in:
@@ -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");
|
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
|
// Business hours feed the due date math - re-stamp open SLA tickets
|
||||||
$restamped = 0;
|
$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");
|
$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");
|
||||||
|
|||||||
@@ -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");
|
$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)
|
|
||||||
{
|
// Clock time spent on resolved tickets, gathered for every client at once
|
||||||
if (is_null($percent)) {
|
// rather than per ticket. Tickets with no interval rows - response-only plans,
|
||||||
return "<span class='text-secondary'>-</span>";
|
// 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) {
|
$resolve_totals[$resolve_client_id]['minutes'] += $resolve_minutes;
|
||||||
return "<span class='text-success text-bold'>$percent%</span>";
|
$resolve_totals[$resolve_client_id]['count']++;
|
||||||
}
|
|
||||||
if ($percent >= 80) {
|
|
||||||
return "<span class='text-warning text-bold'>$percent%</span>";
|
|
||||||
}
|
|
||||||
return "<span class='text-danger text-bold'>$percent%</span>";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
@@ -125,18 +138,11 @@ function slaPercentDisplay($percent)
|
|||||||
|
|
||||||
$avg_time_to_respond = is_null($stats['avg_response_seconds']) ? '-' : secondsToTime($stats['avg_response_seconds']);
|
$avg_time_to_respond = is_null($stats['avg_response_seconds']) ? '-' : secondsToTime($stats['avg_response_seconds']);
|
||||||
|
|
||||||
// Resolution time is measured in clock time actually spent -
|
// Clock time actually spent - paused spells excluded, which is
|
||||||
// paused spells are excluded, which is what the SLA judged on
|
// what the SLA itself judged on
|
||||||
$avg_time_to_resolve = '-';
|
$avg_time_to_resolve = '-';
|
||||||
$resolved_minutes_total = 0;
|
if (!empty($resolve_totals[$client_id]['count'])) {
|
||||||
$resolved_count = 0;
|
$avg_time_to_resolve = secondsToTime(($resolve_totals[$client_id]['minutes'] / $resolve_totals[$client_id]['count']) * 60);
|
||||||
$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>
|
<tr>
|
||||||
|
|||||||
@@ -50,20 +50,6 @@ function getSlaCompliance($where)
|
|||||||
return $compliance;
|
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");
|
$overall = getSlaCompliance("AND YEAR(ticket_created_at) = $year");
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Business hours + SLA settings, fetched once per request
|
// Business hours + SLA settings, fetched once per request
|
||||||
function getSlaSettings()
|
function getSlaSettings($refresh = false)
|
||||||
{
|
{
|
||||||
global $mysqli;
|
global $mysqli;
|
||||||
|
|
||||||
static $sla_settings = null;
|
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)) {
|
if (!is_null($sla_settings)) {
|
||||||
return $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
|
// 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)
|
function getTicketSlaConsumedMinutes($ticket_id)
|
||||||
{
|
{
|
||||||
global $mysqli;
|
global $mysqli;
|
||||||
|
|
||||||
$ticket_id = intval($ticket_id);
|
$ticket_id = intval($ticket_id);
|
||||||
$consumed = 0;
|
$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");
|
$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)) {
|
while ($row = mysqli_fetch_assoc($sql)) {
|
||||||
|
$has_history = true;
|
||||||
if (!is_null($row['sla_history_ended_at'])) {
|
if (!is_null($row['sla_history_ended_at'])) {
|
||||||
$consumed += intval($row['sla_history_minutes']);
|
$consumed += intval($row['sla_history_minutes']);
|
||||||
} else {
|
} 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;
|
return $consumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +224,22 @@ function getTicketSlaPausedCount($ticket_id)
|
|||||||
return intval($row['paused_count']);
|
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 "<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>";
|
||||||
|
}
|
||||||
|
|
||||||
// Reconcile a ticket's resolution clock with its current status. Safe to call
|
// Reconcile a ticket's resolution clock with its current status. Safe to call
|
||||||
// after any status change (and after applyTicketSla) - it opens an interval
|
// 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
|
// when the clock should be running, closes it when it should not, and on
|
||||||
|
|||||||
Reference in New Issue
Block a user