From ac175e7dc046d36f7f741ebf44d01945a284be75 Mon Sep 17 00:00:00 2001 From: johnnyq Date: Thu, 27 Aug 2026 15:03:31 -0400 Subject: [PATCH] Add SLA response target to ticket created emails, with a call-in note on High and Urgent --- agent/post/recurring_ticket.php | 8 +++- agent/post/ticket.php | 12 +++-- agent/ticket.php | 2 +- cron/nightly_tasks.php | 4 +- cron/ticket_email_parser.php | 4 +- functions/sla.php | 81 +++++++++++++++++++++++++++++++++ 6 files changed, 103 insertions(+), 8 deletions(-) diff --git a/agent/post/recurring_ticket.php b/agent/post/recurring_ticket.php index 78589b190..115b663ec 100644 --- a/agent/post/recurring_ticket.php +++ b/agent/post/recurring_ticket.php @@ -190,7 +190,9 @@ if (isset($_POST['bulk_force_recurring_tickets'])) { if (!empty($config_smtp_provider) && $config_ticket_client_general_notifications == 1 && filter_var($contact_email, FILTER_VALIDATE_EMAIL)) { $email_subject = "Ticket Created - [$ticket_prefix$ticket_number] - $ticket_subject (scheduled)"; - $email_body = "##- Please type your reply above this line -##

Hello $contact_name,

A ticket regarding \"$ticket_subject\" has been automatically created for you.

--------------------------------
$ticket_details--------------------------------

Ticket: $ticket_prefix$ticket_number
Subject: $ticket_subject
Status: Open
Portal: https://$config_base_url/client/ticket.php?id=$id

--
$company_name - Support
$config_ticket_from_email
$company_phone"; + // SLA response commitment for this client + priority, empty when no SLA applies + $sla_notice = escapeSql(getTicketSlaEmailNotice($id, $company_phone)); + $email_body = "##- Please type your reply above this line -##

Hello $contact_name,

A ticket regarding \"$ticket_subject\" has been automatically created for you.

--------------------------------
$ticket_details--------------------------------

Ticket: $ticket_prefix$ticket_number
Subject: $ticket_subject
Status: Open
Portal: https://$config_base_url/client/ticket.php?id=$id$sla_notice

--
$company_name - Support
$config_ticket_from_email
$company_phone"; $email = [ 'from' => $config_ticket_from_email, @@ -337,7 +339,9 @@ if (isset($_GET['force_recurring_ticket'])) { if (!empty($config_smtp_provider) && $config_ticket_client_general_notifications == 1 && filter_var($contact_email, FILTER_VALIDATE_EMAIL)) { $email_subject = "Ticket created - [$ticket_prefix$ticket_number] - $ticket_subject (scheduled)"; - $email_body = "##- Please type your reply above this line -##

Hello $contact_name,

A ticket regarding \"$ticket_subject\" has been automatically created for you.

--------------------------------
$ticket_details--------------------------------

Ticket: $ticket_prefix$ticket_number
Subject: $ticket_subject
Status: Open
Portal: https://$config_base_url/client/ticket.php?id=$id

--
$company_name - Support
$config_ticket_from_email
$company_phone"; + // SLA response commitment for this client + priority, empty when no SLA applies + $sla_notice = escapeSql(getTicketSlaEmailNotice($id, $company_phone)); + $email_body = "##- Please type your reply above this line -##

Hello $contact_name,

A ticket regarding \"$ticket_subject\" has been automatically created for you.

--------------------------------
$ticket_details--------------------------------

Ticket: $ticket_prefix$ticket_number
Subject: $ticket_subject
Status: Open
Portal: https://$config_base_url/client/ticket.php?id=$id$sla_notice

--
$company_name - Support
$config_ticket_from_email
$company_phone"; $email = [ 'from' => $config_ticket_from_email, diff --git a/agent/post/ticket.php b/agent/post/ticket.php index 8cc594e00..5bad5f6f6 100644 --- a/agent/post/ticket.php +++ b/agent/post/ticket.php @@ -140,7 +140,9 @@ if (isset($_POST['add_ticket'])) { // EMAILING $subject = "Ticket Created [$ticket_prefix$ticket_number] - $ticket_subject"; - $body = "##- Please type your reply above this line -##

Hello $contact_name,

A ticket regarding \"$ticket_subject\" has been created for you.

--------------------------------
$ticket_details--------------------------------

Ticket: $ticket_prefix$ticket_number
Subject: $ticket_subject
Status: Open
Portal: View ticket

--
$company_name - Support
$config_ticket_from_email
$company_phone"; + // SLA response commitment for this client + priority, empty when no SLA applies + $sla_notice = escapeSql(getTicketSlaEmailNotice($ticket_id, $company_phone)); + $body = "##- Please type your reply above this line -##

Hello $contact_name,

A ticket regarding \"$ticket_subject\" has been created for you.

--------------------------------
$ticket_details--------------------------------

Ticket: $ticket_prefix$ticket_number
Subject: $ticket_subject
Status: Open
Portal: View ticket$sla_notice

--
$company_name - Support
$config_ticket_from_email
$company_phone"; // Verify contact email is valid if (filter_var($contact_email, FILTER_VALIDATE_EMAIL)) { @@ -315,7 +317,9 @@ if (isset($_POST['edit_ticket'])) { $data = []; // Queue array $subject = "Ticket Created - [$ticket_prefix$ticket_number] - $ticket_subject"; - $body = "##- Please type your reply above this line -##

Hello $contact_name,

A ticket regarding \"$ticket_subject\" has been created for you.

--------------------------------
$ticket_details--------------------------------

Ticket: $ticket_prefix$ticket_number
Subject: $ticket_subject
Status: $ticket_status
Portal: View ticket

--
$company_name - Support
$config_ticket_from_email
$company_phone"; + // SLA response commitment for this client + priority, empty when no SLA applies + $sla_notice = escapeSql(getTicketSlaEmailNotice($ticket_id, $company_phone)); + $body = "##- Please type your reply above this line -##

Hello $contact_name,

A ticket regarding \"$ticket_subject\" has been created for you.

--------------------------------
$ticket_details--------------------------------

Ticket: $ticket_prefix$ticket_number
Subject: $ticket_subject
Status: $ticket_status
Portal: View ticket$sla_notice

--
$company_name - Support
$config_ticket_from_email
$company_phone"; // Only add contact to email queue if email is valid @@ -511,7 +515,9 @@ if (isset($_POST['edit_ticket_contact'])) { $data = []; // Queue array $subject = "Ticket Created - [$ticket_prefix$ticket_number] - $ticket_subject"; - $body = "##- Please type your reply above this line -##

Hello $contact_name,

A ticket regarding \"$ticket_subject\" has been created for you.

--------------------------------
$ticket_details--------------------------------

Ticket: $ticket_prefix$ticket_number
Subject: $ticket_subject
Status: $ticket_status
Portal: View ticket

--
$company_name - Support
$config_ticket_from_email
$company_phone"; + // SLA response commitment for this client + priority, empty when no SLA applies + $sla_notice = escapeSql(getTicketSlaEmailNotice($ticket_id, $company_phone)); + $body = "##- Please type your reply above this line -##

Hello $contact_name,

A ticket regarding \"$ticket_subject\" has been created for you.

--------------------------------
$ticket_details--------------------------------

Ticket: $ticket_prefix$ticket_number
Subject: $ticket_subject
Status: $ticket_status
Portal: View ticket$sla_notice

--
$company_name - Support
$config_ticket_from_email
$company_phone"; $data[] = [ 'from' => $config_ticket_from_email, diff --git a/agent/ticket.php b/agent/ticket.php index b823efb11..e412f0104 100644 --- a/agent/ticket.php +++ b/agent/ticket.php @@ -717,7 +717,7 @@ if (isset($_GET['ticket_id'])) {
- + diff --git a/cron/nightly_tasks.php b/cron/nightly_tasks.php index 91b9f3a6c..5990489dd 100644 --- a/cron/nightly_tasks.php +++ b/cron/nightly_tasks.php @@ -441,7 +441,9 @@ if (mysqli_num_rows($sql_recurring_tickets) > 0) { if (!empty($config_smtp_provider) && $config_ticket_client_general_notifications == 1 && filter_var($contact_email, FILTER_VALIDATE_EMAIL)) { $email_subject = "Ticket created - [$ticket_prefix$ticket_number] - $ticket_subject (scheduled)"; - $email_body = "##- Please type your reply above this line -##

Hello $contact_name,

A ticket regarding \"$ticket_subject\" has been automatically created for you.

--------------------------------
$ticket_details--------------------------------

Ticket: $ticket_prefix$ticket_number
Subject: $ticket_subject
Status: Open
Portal: https://$config_base_url/client/ticket.php?id=$id

--
$company_name - Support
$config_ticket_from_email
$company_phone"; + // SLA response commitment for this client + priority, empty when no SLA applies + $sla_notice = escapeSql(getTicketSlaEmailNotice($id, $company_phone)); + $email_body = "##- Please type your reply above this line -##

Hello $contact_name,

A ticket regarding \"$ticket_subject\" has been automatically created for you.

--------------------------------
$ticket_details--------------------------------

Ticket: $ticket_prefix$ticket_number
Subject: $ticket_subject
Status: Open
Portal: https://$config_base_url/client/ticket.php?id=$id$sla_notice

--
$company_name - Support
$config_ticket_from_email
$company_phone"; $email = [ 'from' => $config_ticket_from_email, diff --git a/cron/ticket_email_parser.php b/cron/ticket_email_parser.php index cd852f9e9..9a430b9e6 100644 --- a/cron/ticket_email_parser.php +++ b/cron/ticket_email_parser.php @@ -161,7 +161,9 @@ function addTicket($contact_id, $contact_name, $contact_email, $client_id, $date $data = []; if ($config_ticket_client_general_notifications == 1 && !preg_match($bad_pattern, $contact_email)) { $subject_email = "Ticket created - [$config_ticket_prefix$ticket_number] - $subject"; - $body = "##- Please type your reply above this line -##

Hello $contact_name,

Thank you for your email. A ticket regarding \"$subject\" has been automatically created for you.

Ticket: $config_ticket_prefix$ticket_number
Subject: $subject
Status: New
Portal: View ticket

--
$company_name - Support
$config_ticket_from_email
$company_phone"; + // SLA response commitment for this client + priority, empty when no SLA applies + $sla_notice = getTicketSlaEmailNotice($id, $company_phone); + $body = "##- Please type your reply above this line -##

Hello $contact_name,

Thank you for your email. A ticket regarding \"$subject\" has been automatically created for you.

Ticket: $config_ticket_prefix$ticket_number
Subject: $subject
Status: New
Portal: View ticket$sla_notice

--
$company_name - Support
$config_ticket_from_email
$company_phone"; $data[] = [ 'from' => $config_ticket_from_email, 'from_name' => $config_ticket_from_name, diff --git a/functions/sla.php b/functions/sla.php index 28c2aafc8..7edb4bf0d 100644 --- a/functions/sla.php +++ b/functions/sla.php @@ -425,6 +425,87 @@ function applyTicketSla($ticket_id, $forced_sla_id = null) syncTicketSlaClock($ticket_id); } +// Human-readable SLA target, e.g. "2 business hours" or "45 minutes". The +// "business" qualifier is dropped when no business calendar is configured, +// because addBusinessMinutes runs 24x7 in that case and the promise would +// otherwise be misleading. The condition mirrors that function's own guard. +function formatSlaMinutes($minutes) +{ + $minutes = intval($minutes); + + $sla_settings = getSlaSettings(); + $day_start = $sla_settings['business_hours_start']; + $day_end = $sla_settings['business_hours_end']; + + $qualifier = ''; + if (!empty($sla_settings['business_days']) && !empty($day_start) && !empty($day_end) && $day_start < $day_end) { + $qualifier = 'business '; + } + + if ($minutes < 60) { + return $minutes . ' minute' . ($minutes == 1 ? '' : 's'); + } + + $hours = intdiv($minutes, 60); + $remainder = $minutes % 60; + + $text = $hours . ' ' . $qualifier . 'hour' . ($hours == 1 ? '' : 's'); + if ($remainder > 0) { + $text .= ' ' . $remainder . ' minute' . ($remainder == 1 ? '' : 's'); + } + + return $text; +} + +// Client-facing SLA block for a "ticket created" email. Returns '' when no SLA +// applies to the ticket or the plan carries no response target, so callers can +// append it unconditionally. +// +// Call this AFTER applyTicketSla(), which is what stamps ticket_response_due_at. +// +// The returned HTML deliberately contains NO single or double quotes. Most of +// these email bodies are assembled pre-escaped and handed to addToMailQueue, +// which interpolates the body straight into its INSERT without escaping it, so +// a stray quote here would break the query at those call sites. +function getTicketSlaEmailNotice($ticket_id, $company_phone = '') +{ + global $mysqli; + + $ticket_id = intval($ticket_id); + + $sql = mysqli_query($mysqli, "SELECT ticket_priority, ticket_response_due_at, sla_response_minutes + FROM tickets + LEFT JOIN slas ON ticket_sla_id = sla_id + WHERE ticket_id = $ticket_id LIMIT 1"); + if (!$sql || !mysqli_num_rows($sql)) { + return ''; + } + $row = mysqli_fetch_assoc($sql); + + $response_minutes = intval($row['sla_response_minutes']); + + if (empty($row['ticket_response_due_at']) || $response_minutes <= 0) { + return ''; + } + + $priority = $row['ticket_priority']; + $target = formatSlaMinutes($response_minutes); + $due = date('D j M, g:i A', strtotime($row['ticket_response_due_at'])); + + $notice = "

Priority: $priority
Target response: within $target (by $due)"; + + // Higher priorities get told to phone rather than sit on an email thread + if (!empty($company_phone) && ($priority == 'Urgent' || $priority == 'High')) { + if ($priority == 'Urgent') { + $notice .= "

This ticket is marked Urgent. If the issue is stopping work right now, please call us on $company_phone rather than waiting for a reply to this email."; + } else { + $notice .= "

This ticket is marked High priority. If it is business-impacting, calling us on $company_phone will get you the fastest response."; + } + } + + return $notice; +} + // Record the ticket's first response (if not already recorded) and judge the // response SLA against the stored due date. Replaces the previous inline // ticket_first_response_at updates so the SLA verdict can never drift from