set('Cache.DefinitionImpl', null); // Disable cache by setting a non-existent directory or an invalid one
$purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]);
$purifier = new HTMLPurifier($purifier_config);
if (isset($_GET['ticket_id'])) {
$ticket_id = intval($_GET['ticket_id']);
$sql = mysqli_query(
$mysqli,
"SELECT * FROM tickets
LEFT JOIN clients ON ticket_client_id = client_id
LEFT JOIN contacts ON ticket_contact_id = contact_id
LEFT JOIN users ON ticket_assigned_to = user_id
LEFT JOIN locations ON ticket_location_id = location_id
LEFT JOIN assets ON ticket_asset_id = asset_id
LEFT JOIN asset_interfaces ON interface_asset_id = asset_id AND interface_primary = 1
LEFT JOIN vendors ON ticket_vendor_id = vendor_id
LEFT JOIN projects ON ticket_project_id = project_id
LEFT JOIN quotes ON ticket_quote_id = quote_id
LEFT JOIN invoices ON ticket_invoice_id = invoice_id
LEFT JOIN ticket_statuses ON ticket_status = ticket_status_id
LEFT JOIN categories ON ticket_category = category_id
WHERE ticket_id = $ticket_id
$access_permission_query_overide
LIMIT 1"
);
if (mysqli_num_rows($sql) == 0) {
echo "
";
require_once "../includes/footer.php";
} else {
$ticket = mysqli_fetch_assoc($sql);
$client_id = intval($ticket['client_id']);
$client_name = escapeHtml($ticket['client_name']);
$ticket_prefix = escapeHtml($ticket['ticket_prefix']);
$ticket_number = intval($ticket['ticket_number']);
$ticket_reference = "$ticket_prefix$ticket_number";
$ticket_source = escapeHtml($ticket['ticket_source']);
$ticket_category = intval($ticket['ticket_category']);
$ticket_category_display = escapeHtml($ticket['category_name']);
$ticket_subject = escapeHtml($ticket['ticket_subject']);
$ticket_details = $purifier->purify($ticket['ticket_details']);
$ticket_priority = escapeHtml($ticket['ticket_priority']);
$ticket_billable = intval($ticket['ticket_billable']);
$ticket_scheduled_for = escapeHtml($ticket['ticket_schedule']);
$ticket_onsite = intval($ticket['ticket_onsite']);
if ($ticket_scheduled_for) {
$ticket_scheduled_wording = date('M d, Y • g:i A', strtotime($ticket_scheduled_for));
} else {
$ticket_scheduled_wording = "Not scheduled";
}
// Set Ticket Badge Color based of priority
if ($ticket_priority == "Urgent") {
$ticket_priority_color = "dark";
} elseif ($ticket_priority == "High") {
$ticket_priority_color = "danger";
} elseif ($ticket_priority == "Medium") {
$ticket_priority_color = "warning";
} else {
$ticket_priority_color = "info";
}
$ticket_feedback = escapeHtml($ticket['ticket_feedback']);
$ticket_status = intval($ticket['ticket_status_id']);
$ticket_status_id = intval($ticket['ticket_status_id']);
$ticket_status_name = escapeHtml($ticket['ticket_status_name']);
$ticket_status_color = escapeHtml($ticket['ticket_status_color']);
$ticket_vendor_ticket_number = escapeHtml($ticket['ticket_vendor_ticket_number']);
$ticket_created_at = escapeHtml($ticket['ticket_created_at']);
$ticket_created_at_ago = timeAgo($ticket['ticket_created_at']);
$ticket_created_by = intval($ticket['ticket_created_by']);
$ticket_date = date('Y-m-d', strtotime($ticket_created_at));
$ticket_updated_at = escapeHtml($ticket['ticket_updated_at']);
$ticket_updated_at_ago = timeAgo($ticket['ticket_updated_at']);
$ticket_first_response_at = escapeHtml($ticket['ticket_first_response_at']);
$ticket_sla_id = intval($ticket['ticket_sla_id']);
$ticket_response_due_at = escapeHtml($ticket['ticket_response_due_at']);
$ticket_resolution_due_at = escapeHtml($ticket['ticket_resolution_due_at']);
$ticket_response_sla_met = $ticket['ticket_response_sla_met'];
$ticket_resolution_sla_met = $ticket['ticket_resolution_sla_met'];
$ticket_sla_paused = intval($ticket['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");
if (mysqli_num_rows($sla_name_sql)) {
$ticket_sla_name = escapeHtml(mysqli_fetch_assoc($sla_name_sql)['sla_name']);
}
}
$ticket_resolved_at = escapeHtml($ticket['ticket_resolved_at']);
$ticket_resolved_at_ago = timeAgo($ticket['ticket_resolved_at']);
$ticket_closed_at = escapeHtml($ticket['ticket_closed_at']);
$ticket_closed_at_ago = timeAgo($ticket['ticket_closed_at']);
$ticket_closed_by = intval($ticket['ticket_closed_by']);
// The three states the page keys its controls off
$ticket_is_closed = !empty($ticket_closed_at);
$ticket_is_resolved = !empty($ticket_resolved_at);
$ticket_is_open = !$ticket_is_resolved && !$ticket_is_closed;
$can_edit_ticket = lookupUserPermission("module_support") >= 2;
$ticket_assigned_to_display = formatAssignee($ticket['ticket_assigned_to'], $ticket['user_name'], $ticket['user_avatar'], $ticket_is_open);
// Tab Title // No Sanitizing needed
$page_title = $ticket['ticket_subject'];
$tab_title = "{$ticket['ticket_prefix']}{$ticket['ticket_number']}";
$contact_id = intval($ticket['contact_id']);
$contact_client_id = intval($ticket['contact_client_id']);
$contact_name = escapeHtml($ticket['contact_name']);
$contact_photo = escapeHtml($ticket['contact_photo']);
$contact_title = escapeHtml($ticket['contact_title']);
$contact_email = escapeHtml($ticket['contact_email']);
$contact_phone_country_code = escapeHtml($ticket['contact_phone_country_code']);
$contact_phone = escapeHtml(formatPhoneNumber($ticket['contact_phone'], $contact_phone_country_code));
$contact_extension = escapeHtml($ticket['contact_extension']);
$contact_mobile_country_code = escapeHtml($ticket['contact_mobile_country_code']);
$contact_mobile = escapeHtml(formatPhoneNumber($ticket['contact_mobile'], $contact_mobile_country_code));
$asset_id = intval($ticket['asset_id']);
$asset_name = escapeHtml($ticket['asset_name']);
$asset_type = escapeHtml($ticket['asset_type']);
$asset_icon = getAssetIcon($asset_type);
$vendor_id = intval($ticket['ticket_vendor_id']);
$vendor_name = escapeHtml($ticket['vendor_name']);
$vendor_contact_name = escapeHtml($ticket['vendor_contact_name']);
$vendor_phone_country_code = escapeHtml($ticket['vendor_phone_country_code']);
$vendor_phone = escapeHtml(formatPhoneNumber($ticket['vendor_phone'], $vendor_phone_country_code));
$vendor_email = escapeHtml($ticket['vendor_email']);
$vendor_website = escapeHtml($ticket['vendor_website']);
$location_id = intval($ticket['location_id']);
$location_name = escapeHtml($ticket['location_name']);
$location_phone = formatPhoneNumber($ticket['location_phone']);
$quote_id = intval($ticket['ticket_quote_id']);
$quote_prefix = escapeHtml($ticket['quote_prefix']);
$quote_number = intval($ticket['quote_number']);
// The quote row may have been deleted out from under the ticket
$quote_reference = $quote_number ? "$quote_prefix$quote_number" : "#$quote_id";
$invoice_id = intval($ticket['ticket_invoice_id']);
$invoice_prefix = escapeHtml($ticket['invoice_prefix']);
$invoice_number = intval($ticket['invoice_number']);
// Same for the invoice - fall back to the id rather than rendering a bare 0
$invoice_reference = $invoice_number ? "$invoice_prefix$invoice_number" : "#$invoice_id";
$project_id = intval($ticket['project_id']);
$project_name = escapeHtml($ticket['project_name']);
$project_manager = intval($ticket['project_manager']);
$project_manager_name = '';
if ($project_manager) {
$project_manager_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_name FROM users WHERE user_id = $project_manager"));
$project_manager_name = escapeHtml($project_manager_row['user_name']);
}
// How far along the project it is part of is - and where its page is
$project_stats = $project_id ? projectStats([$project_id])[$project_id] : null;
$project_url = $project_id ? escapeHtml(projectUrl($project_id, $ticket['project_client_id'])) : '';
$ticket_created_by_display = '';
if ($ticket_created_by) {
$created_by_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_name FROM users WHERE user_id = $ticket_created_by"));
$ticket_created_by_display = escapeHtml($created_by_row['user_name']);
}
$ticket_closed_by_display = '';
if ($ticket_is_closed) {
$ticket_closed_by_display = 'User';
if (!empty($ticket_closed_by)) {
$closed_by_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_name FROM users WHERE user_id = $ticket_closed_by"));
$ticket_closed_by_display = escapeHtml($closed_by_row['user_name']);
}
}
/*
* How much else this contact has open. The old queries compared the
* integer ticket_status column against the string 'Closed', which MySQL
* casts to 0 - so "open" matched every ticket and "closed" matched none.
* Resolution state lives in ticket_resolved_at, same as the ticket list.
*/
// The other open tickets for the same person or device - a duplicate, or a
// problem with something bigger behind it, shows up while working this one
$also_open = empty($ticket['ticket_resolved_at']) && empty($ticket['ticket_closed_at'])
? ticketAlsoOpen($contact_id, intval($ticket['ticket_asset_id']), $ticket_id, 6) : [];
// Reply counts, one pass - these drive the conversation filter
$reply_counts = array('total' => 0, 'public' => 0, 'internal' => 0);
$sql_reply_counts = mysqli_query(
$mysqli,
"SELECT ticket_reply_type, COUNT(ticket_reply_id) AS reply_count
FROM ticket_replies
WHERE ticket_reply_archived_at IS NULL AND ticket_reply_ticket_id = $ticket_id
GROUP BY ticket_reply_type"
);
while ($reply_count_row = mysqli_fetch_assoc($sql_reply_counts)) {
$reply_count = intval($reply_count_row['reply_count']);
$reply_counts['total'] += $reply_count;
if ($reply_count_row['ticket_reply_type'] == 'Internal') {
$reply_counts['internal'] += $reply_count;
} else {
$reply_counts['public'] += $reply_count;
}
}
// Get Total Ticket Time
$ticket_total_reply_time = escapeHtml(mysqli_fetch_assoc(mysqli_query(
$mysqli,
"SELECT SEC_TO_TIME(SUM(TIME_TO_SEC(ticket_reply_time_worked))) AS ticket_total_reply_time
FROM ticket_replies WHERE ticket_reply_archived_at IS NULL AND ticket_reply_ticket_id = $ticket_id"
))['ticket_total_reply_time']);
/*
* Agents who have worked this ticket. The old query joined every reply's
* ticket_reply_by against users, but a Client reply stores a CONTACT id
* there - so any contact whose id collided with a user id showed up as a
* collaborator. That is the bug the old commented-out block ran into.
*/
$ticket_collaborators = escapeHtml(mysqli_fetch_assoc(mysqli_query(
$mysqli,
"SELECT GROUP_CONCAT(DISTINCT user_name ORDER BY user_name SEPARATOR ', ') AS user_names
FROM ticket_replies
INNER JOIN users ON ticket_reply_by = user_id
WHERE ticket_reply_archived_at IS NULL
AND ticket_reply_ticket_id = $ticket_id
AND ticket_reply_type != 'Client'"
))['user_names']);
// Get ticket replies - with whether each of yours was emailed, and who to
$sql_ticket_replies = mysqli_query($mysqli, "SELECT contact_name, contact_photo, ticket_reply, ticket_reply_by, ticket_reply_created_at, ticket_reply_id,
ticket_reply_time_worked, ticket_reply_type, ticket_reply_updated_at, user_avatar, user_id, user_name,
" . (ticketReplyEmailReady() ? 'ticket_reply_emailed, ticket_reply_emailed_to' : 'NULL AS ticket_reply_emailed, NULL AS ticket_reply_emailed_to') . "
FROM ticket_replies
LEFT JOIN users ON ticket_reply_by = user_id
LEFT JOIN contacts ON ticket_reply_by = contact_id
WHERE ticket_reply_ticket_id = $ticket_id
AND ticket_reply_archived_at IS NULL
ORDER BY ticket_reply_id DESC"
);
$ticket_reply_rows = mysqli_fetch_all($sql_ticket_replies, MYSQLI_ASSOC);
// Who each internal note @mentions - "Mentions you" on yours (functions/ticket_mention.php)
$reply_mentions = ticketMentionsForTicket($ticket_id);
// Whether each email went, from the mail queue while it has them
$reply_email_states = ticketReplyEmailStates($ticket_reply_rows);
// Which replies were texts, either way, and whether the ones out went (functions/sms.php)
$reply_sms_states = smsRepliesForTicket($ticket_id);
$reply_email_queue_url = $session_is_admin ? '/admin/mail_queue.php?q=' . rawurlencode('[' . ticketReference($ticket) . ']') : '';
/*
* Every attachment on the ticket in one query, split by reply. The page
* used to fire one attachment query per reply as it rendered.
*/
$ticket_attachments = array(); // attachments on the ticket itself
$reply_attachments = array(); // attachments keyed by reply id
$sql_all_attachments = mysqli_query(
$mysqli,
"SELECT ticket_attachment_id, ticket_attachment_name, ticket_attachment_reply_id
FROM ticket_attachments WHERE ticket_attachment_ticket_id = $ticket_id"
);
while ($attachment_row = mysqli_fetch_assoc($sql_all_attachments)) {
$attachment_reply_id = intval($attachment_row['ticket_attachment_reply_id']);
if ($attachment_reply_id) {
$reply_attachments[$attachment_reply_id][] = $attachment_row;
} else {
$ticket_attachments[] = $attachment_row;
}
}
// Get Watchers
$sql_ticket_watchers = mysqli_query($mysqli, "SELECT watcher_email, watcher_id FROM ticket_watchers WHERE watcher_ticket_id = $ticket_id ORDER BY watcher_email DESC");
$watcher_count = mysqli_num_rows($sql_ticket_watchers);
// Get Additional Assets
$sql_additional_assets = mysqli_query($mysqli, "SELECT assets.asset_id, asset_name, asset_type FROM assets, ticket_assets
WHERE assets.asset_id = ticket_assets.asset_id
AND ticket_id = $ticket_id
AND assets.asset_id != $asset_id"
);
// Get Tasks
$sql_tasks = mysqli_query($mysqli, "SELECT task_completed_at, task_completion_estimate, task_id, task_name FROM tasks WHERE task_ticket_id = $ticket_id ORDER BY task_order ASC, task_id ASC");
$task_count = mysqli_num_rows($sql_tasks);
$completed_task_count = intval(mysqli_fetch_row(mysqli_query(
$mysqli,
"SELECT COUNT(task_id) FROM tasks WHERE task_ticket_id = $ticket_id AND task_completed_at IS NOT NULL"
))[0]);
// Tasks Completed Percent
$tasks_completed_percent = 0;
if ($task_count) {
$tasks_completed_percent = round(($completed_task_count / $task_count) * 100);
}
// Tasks still open block resolving the ticket - the page says so rather than just hiding the button
$tasks_block_resolve = $task_count !== $completed_task_count;
/*
* All approvals for this ticket's tasks in one query. The task loop used
* to run two approval queries per row.
*/
$task_approvals = array();
if ($task_count) {
$sql_task_approvals = mysqli_query(
$mysqli,
"SELECT approval_id, approval_task_id, approval_scope, approval_type,
approval_required_user_id, approval_created_by, approval_status
FROM task_approvals
INNER JOIN tasks ON approval_task_id = task_id
WHERE task_ticket_id = $ticket_id
AND approval_status IN ('pending','declined')"
);
while ($approval_row = mysqli_fetch_assoc($sql_task_approvals)) {
$task_approvals[intval($approval_row['approval_task_id'])][] = $approval_row;
}
}
/*
* Ticket history. agent/post/ticket.php has been writing priority, SLA,
* contact, watcher and asset changes here all along - the page fetched
* the rows and then never rendered them.
*/
$sql_ticket_history = mysqli_query($mysqli, "SELECT ticket_history_description, ticket_history_created_at
FROM ticket_history
WHERE ticket_history_ticket_id = $ticket_id
ORDER BY ticket_history_id DESC"
);
$ticket_history_count = mysqli_num_rows($sql_ticket_history);
/*
* The single most useful thing on the page: which clock is running and
* how long is left. Everything else about the SLA is detail.
*/
$sla_state = ''; // breached | at_risk | paused | met | running
$sla_label = '';
$sla_class = '';
$sla_icon = 'fa-stopwatch';
/*
* Only surface the SLA field when SLAs are actually in play - either this
* ticket has one, or plans exist to assign. Mirrors $sla_filter_in_use on
* agent/tickets.php so an install that does not use SLAs never sees them.
* The count is only queried when the ticket has no SLA of its own.
*/
$sla_in_use = $ticket_sla_id > 0;
if (!$sla_in_use && $can_edit_ticket) {
$sla_in_use = mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(sla_id) FROM slas WHERE sla_archived_at IS NULL"))[0] > 0;
}
if ($ticket_sla_id) {
$sla_next_due = empty($ticket_first_response_at) ? $ticket_response_due_at : $ticket_resolution_due_at;
$sla_next_label = empty($ticket_first_response_at) ? 'First response due' : 'Resolution due';
$ticket_sla_alert_stage = max(intval($ticket['ticket_response_sla_alert_stage']), intval($ticket['ticket_resolution_sla_alert_stage']));
$sla_breached = (!is_null($ticket_response_sla_met) && !$ticket_response_sla_met)
|| (!is_null($ticket_resolution_sla_met) && !$ticket_resolution_sla_met)
|| $ticket_sla_alert_stage == 2;
if ($sla_breached) {
$sla_state = 'breached';
$sla_label = 'SLA breached';
$sla_class = 'text-bg-danger';
$sla_icon = 'fa-exclamation-triangle';
} elseif (!$ticket_is_open) {
$sla_state = 'met';
$sla_label = 'SLA met';
$sla_class = 'text-bg-success';
$sla_icon = 'fa-check';
} elseif ($ticket_sla_paused) {
$sla_state = 'paused';
$sla_label = 'SLA paused';
$sla_class = 'text-bg-secondary';
$sla_icon = 'fa-pause';
} elseif (!empty($sla_next_due)) {
$sla_overdue = strtotime($sla_next_due) < time();
$sla_state = $sla_overdue ? 'breached' : 'running';
$sla_label = $sla_next_label . ' ' . timeAgo($sla_next_due);
$sla_class = $sla_overdue ? 'text-bg-danger' : 'text-bg-light border';
}
}
// The Details panel already says "SLA", so the state badge drops the word - and an
// overdue clock says so plainly rather than "Resolution due 1 minute ago"
$sla_state_label = str_replace(array('SLA breached', 'SLA met', 'SLA paused'), array('Breached', 'Met', 'Paused'), $sla_label);
if ($sla_state == 'breached' && !empty($sla_overdue)) {
$sla_state_label = (empty($ticket_first_response_at) ? 'First response' : 'Resolution') . ' overdue';
}
// Due date from the edit modal - it was saved and never shown anywhere
$ticket_due_at = escapeHtml($ticket['ticket_due_at']);
$ticket_due_overdue = $ticket_due_at && $ticket_is_open && strtotime($ticket_due_at) < time();
// How the ticket arrived, in words rather than the raw source value ("via Agent")
if ($ticket_source == 'Email') {
$ticket_opened_wording = "Came in by email $ticket_created_at_ago";
} elseif ($ticket_source == 'Portal') {
$ticket_opened_wording = "Submitted in the client portal $ticket_created_at_ago";
} elseif ($ticket_source == 'Recurring') {
$ticket_opened_wording = "Created $ticket_created_at_ago from a recurring ticket";
} elseif ($ticket_source == 'API') {
$ticket_opened_wording = "Created $ticket_created_at_ago through the API";
} elseif ($ticket_source == 'Text') {
$ticket_opened_wording = "Came in by text $ticket_created_at_ago";
} elseif ($ticket_source && $ticket_source !== 'Agent') {
$ticket_opened_wording = "Opened $ticket_created_at_ago via $ticket_source";
} else {
$ticket_opened_wording = "Opened $ticket_created_at_ago" . ($ticket_created_by_display ? " by $ticket_created_by_display" : '');
}
/*
* Statuses an agent can pick by hand, shared by the status menu in the
* Details panel and the reply form. New and Closed are system-managed.
*/
$ticket_status_options = array();
if ($can_edit_ticket && $ticket_is_open) {
$sql_ticket_status_options = mysqli_query($mysqli, "SELECT ticket_status_id, ticket_status_name, ticket_status_color FROM ticket_statuses WHERE ticket_status_id != 1 AND ticket_status_id != 5 AND ticket_status_active = 1 ORDER BY ticket_status_order, ticket_status_id");
while ($status_option_row = mysqli_fetch_assoc($sql_ticket_status_options)) {
$ticket_status_options[] = $status_option_row;
}
}
// An emailed reply needs an address to go to and mail set up to send it -
// without mail configured the option used to save as public and email nobody
$can_email_reply = !empty($contact_email) && !empty($config_smtp_provider);
// A text reply - to its contact's mobile, or the number texting on it (functions/sms.php)
$sms_recipient = smsTicketRecipient($ticket);
// The Resolve confirmation says what resolving does - resolve_ticket emails the
// contact when client notifications are on, and the nightly job closes it later
$resolve_confirm_text = '';
if (!empty($config_smtp_provider) && $config_ticket_client_general_notifications == 1 && $contact_email) {
$resolve_confirm_text .= "$contact_name is emailed that it is resolved. ";
}
if ($config_ticket_autoclose_hours > 0) {
$resolve_confirm_text .= "It closes on its own after $config_ticket_autoclose_hours hours unless the client replies.";
}
// AI - offered only when a model is set up for it and the role may use AI (functions/ai_ticket.php)
$ai_summary_available = aiReady('Tickets');
$ai_triage_available = $ai_summary_available && $can_edit_ticket && $ticket_is_open && aiTriageReady();
// A ticket with no client is written up in your own company's documents, when there is one
$ai_document_client_id = $client_id ?: intval($config_internal_client_id ?? 0);
$ai_document_available = $can_edit_ticket && $ai_document_client_id > 0 && aiReady('Documentation');
// What AI triage suggests that the ticket doesn't have yet
$ai_triage = $ai_summary_available && aiTriageReady() ? aiTriageRow($ticket_id) : null;
$ai_triage_changes = $ai_triage && $ai_triage['ai_triage_state'] === 'suggested' && $ticket_is_open ? aiTriageChanges($ai_triage, $ticket) : [];
// Billing actions, decided once - the header shows Invoice once the work is done
$show_quote_button = $can_edit_ticket && $config_module_enable_accounting && $ticket_billable == 1 && empty($quote_id) && empty($invoice_id) && lookupUserPermission("module_sales") >= 2;
// Prepaid hours on a contract it drew from - all of it covered leaves nothing to invoice
$ticket_prepaid_kept_out = false;
$ticket_prepaid_uses = $config_module_enable_accounting && lookupUserPermission("module_sales") >= 1 ? contractHoursTicketUses($ticket_id, $ticket_prepaid_kept_out) : [];
$ticket_prepaid_hours = round(array_sum(array_column($ticket_prepaid_uses, 'contract_block_use_hours')), 2);
$ticket_prepaid_full = in_array('1', array_map('strval', array_column($ticket_prepaid_uses, 'contract_block_use_full')), true);
$show_invoice_button = $can_edit_ticket && $config_module_enable_accounting && $ticket_billable == 1 && empty($invoice_id) && !$ticket_prepaid_full && lookupUserPermission("module_sales") >= 2;
// Trips - the driving done for it: anyone working it can log theirs (functions/trips.php)
$show_trips = $config_module_enable_accounting && $client_id > 0;
$ticket_trips = $show_trips ? tripsFor('trip_ticket_id', $ticket_id, 5) : [];
[$ticket_trip_count, $ticket_trip_miles] = $show_trips ? tripTotalsFor('trip_ticket_id', $ticket_id) : [0, 0.0];
// Values in the Details panel are links only when they can actually be changed
$can_change_fields = $can_edit_ticket && !$ticket_is_closed;
$ticket_tags_display = tagPills('ticket', $ticket_id, tagsForRecord('ticket', $ticket_id), $client_id ? "tickets.php?client_id=$client_id&" : 'tickets.php?', $can_change_fields);
?>
Fill-ins from = escapeHtml($fill_in_template['ticket_template_name']) ?>
= escapeHtml($answer['name']) ?>
= escapeHtml($answer_text) ?>
Not answered= $answer['required'] ? ' yet' : '' ?>
Conversation · newest first
No replies yet.
purify($reply_row['ticket_reply']);
$ticket_reply_type = escapeHtml($reply_row['ticket_reply_type']);
// A note that mentions you says so, and your name in it stands out
$reply_mentions_me = in_array(intval($session_user_id), $reply_mentions[$ticket_reply_id] ?? [], true);
if ($reply_mentions_me && $ticket_reply_type == 'Internal') {
$ticket_reply = str_replace('itflow-mention-u' . intval($session_user_id) . '"', 'itflow-mention-u' . intval($session_user_id) . ' is-me"', $ticket_reply);
}
$ticket_reply_created_at = escapeHtml($reply_row['ticket_reply_created_at']);
$ticket_reply_created_at_ago = timeAgo($reply_row['ticket_reply_created_at']);
$ticket_reply_updated_at = escapeHtml($reply_row['ticket_reply_updated_at']);
$ticket_reply_time_worked = '';
// A client's reply wears their contact avatar - their own colour,
// the same as everywhere else they appear
$reply_contact_avatar = '';
if ($ticket_reply_type == "Client") {
$ticket_reply_by_display = escapeHtml($reply_row['contact_name']);
$reply_contact_avatar = contactAvatar($client_id, $reply_row['ticket_reply_by'], $reply_row['contact_name'], $reply_row['contact_photo'], 'md');
} else {
$ticket_reply_by_display = escapeHtml($reply_row['user_name']);
$user_id = intval($reply_row['user_id']);
$user_avatar = escapeHtml($reply_row['user_avatar']);
$user_initials = initials($reply_row['user_name']);
$avatar_link = "../uploads/users/$user_id/$user_avatar";
$ticket_reply_time_worked = $reply_row['ticket_reply_time_worked'];
}
// Internal notes are the one thing that must never be mistaken for
// something the client saw, so they get a label, not just a colour
if ($ticket_reply_type == 'Internal') {
$reply_border = 'dark';
$reply_badge = "Internal";
$reply_group = 'internal';
} elseif ($ticket_reply_type == 'Client') {
$reply_border = 'warning';
$reply_badge = "From client";
$reply_group = 'public';
} elseif ($reply_row['ticket_reply_emailed'] === null) {
// From before ITFlow kept whether a reply was emailed
$reply_border = 'info';
$reply_badge = "Public";
$reply_group = 'public';
} elseif (intval($reply_row['ticket_reply_emailed']) === 1) {
$reply_border = 'info';
$reply_badge = "Email reply";
$reply_group = 'public';
} else {
$reply_border = 'info';
$reply_badge = "Public note";
$reply_group = 'public';
}
// Who it was emailed to, and whether it went
$reply_emailed = isset($reply_email_states[$ticket_reply_id]) ? ticketReplyEmailHtml($ticket_reply_id, $reply_email_states[$ticket_reply_id], $reply_email_queue_url) : null;
// A text - the one it was texted to and whether it went, or that it came in by text
$reply_sms = $reply_sms_states[$ticket_reply_id] ?? null;
$reply_sms_line = '';
if ($reply_sms && $reply_sms['direction'] === 'out') {
[$sms_icon, $sms_words, $sms_tone] = smsReplyStateText($reply_sms);
$reply_sms_line = "" . escapeHtml($sms_words) . '';
if ($ticket_reply_type == 'Public') {
$reply_badge = "Text reply";
}
} elseif ($reply_sms && $ticket_reply_type == 'Client') {
$reply_badge .= " By text";
if (trim((string) $reply_row['contact_name']) === '' || !intval($reply_row['ticket_reply_by'])) {
$ticket_reply_by_display = escapeHtml(smsNumberText($reply_sms['number']));
$reply_contact_avatar = '';
}
}
?>
>
= $reply_contact_avatar ?>= $user_initials ?>
= $ticket_reply_by_display ?>
= $reply_badge ?>
Mentions you
$sla_state_label";
} elseif ($ticket_sla_id) {
$sla_value = $ticket_sla_name;
} else {
$sla_value = "None";
}
// The plan name goes underneath whenever the badge took the main line
$sla_plan_line = ($sla_state_label && $ticket_sla_id) ? "$ticket_sla_name" : '';
?>