Ticket Templates makes them (admin/ticket_templates.php, * admin/ticket_template.php, admin/post/ticket_template.php). They are picked * on New Ticket, the New Tickets windows of the assets and clients lists and * recurring tickets (agent/includes/inc_ticket_template_select.php, which * fills in the form - agent/js/ticket_tasks_modal.js), and a project template * raises its ticket templates as they are (projectApplyTemplate()). * * Words in square brackets - [Client], [Contact], [Asset], [Site], [Agent], * [Project], [Ticket], [Date], [Month], [Quarter] and [Year] - are filled in * from the ticket when it is raised (ticketPlaceholdersFill()). Anything else * in brackets, [Employee Name] say, stays for whoever picks it up. * * A ticket keeps which template it came from (tickets.ticket_template_id), so * a template shows how often it is used and its recent tickets. */ /** The placeholders filled in from the ticket - what each becomes. */ const TICKET_PLACEHOLDERS = [ 'Client' => "the client's name", 'Contact' => "the contact's name", 'Asset' => "the asset's name", 'Site' => "the site's name", 'Agent' => 'the agent it is assigned to, or who raised it', 'Project' => "the project's name", 'Ticket' => 'its number - TCK-123', 'Date' => "today's date", 'Month' => 'the month - October 2026', 'Quarter' => 'the quarter - Q4 2026', 'Year' => 'the year', ]; /** One template with its category and the agent it goes to, or null. Archived ones too. */ function ticketTemplateRow($ticket_template_id) { global $mysqli; $ticket_template_id = intval($ticket_template_id); if (!$ticket_template_id) { return null; } $row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT ticket_templates.*, category_name, category_color, category_icon, category_type, category_description, user_name AS assignee_name, user_avatar AS assignee_avatar FROM ticket_templates LEFT JOIN categories ON category_id = ticket_template_category_id AND ticket_template_category_id > 0 LEFT JOIN users ON user_id = ticket_template_assigned_to AND ticket_template_assigned_to > 0 WHERE ticket_template_id = $ticket_template_id LIMIT 1")); return $row ?: null; } /** A template's tasks, in order - [['id', 'name', 'estimate'], ...]. */ function ticketTemplateTasks($ticket_template_id) { global $mysqli; $tasks = []; $sql = mysqli_query($mysqli, "SELECT task_template_id, task_template_name, task_template_completion_estimate FROM task_templates WHERE task_template_ticket_template_id = " . intval($ticket_template_id) . " ORDER BY task_template_order ASC, task_template_id ASC"); while ($row = mysqli_fetch_assoc($sql)) { $tasks[] = ['id' => intval($row['task_template_id']), 'name' => (string) $row['task_template_name'], 'estimate' => intval($row['task_template_completion_estimate'])]; } return $tasks; } /** The ticket tags a template gives its tickets - ids, the archived ones too. */ function ticketTemplateTagIds($ticket_template_id) { global $mysqli; $ids = []; $sql = mysqli_query($mysqli, "SELECT tag_id FROM ticket_template_tags WHERE ticket_template_id = " . intval($ticket_template_id) . " ORDER BY tag_id"); while ($row = mysqli_fetch_row($sql)) { $ids[] = intval($row[0]); } return $ids; } /** "4 hours", "1 day", "3 days" - when a template's tickets fall due; '' for not set. */ function ticketTemplateDueText($hours) { $hours = intval($hours); if ($hours <= 0) { return ''; } if ($hours % 24 === 0) { $days = intdiv($hours, 24); return $days === 1 ? '1 day' : "$days days"; } return $hours === 1 ? '1 hour' : "$hours hours"; } /** Tasks' time in words - "45m", "2h 30m". */ function ticketTemplateMinutesText($minutes) { $minutes = max(0, intval($minutes)); return formatDuration(sprintf('%02d:%02d:00', intdiv($minutes, 60), $minutes % 60)); } /** Square-bracket words in some text - [Client], [Employee Name] - each once, as written. */ function ticketTemplatePlaceholdersIn($text) { preg_match_all('/\[([A-Za-z][A-Za-z0-9 _\-\/]{0,40})\]/', strip_tags((string) $text), $matches); $found = []; foreach ($matches[1] as $name) { $found[strtolower(trim($name))] = $found[strtolower(trim($name))] ?? trim($name); } return array_values($found); } /** When a ticket raised now from a template that is due in $hours is due - 'Y-m-d H:i:s', or null for not set. */ function ticketTemplateDueAt($hours) { $hours = intval($hours); return $hours > 0 ? date('Y-m-d H:i:s', time() + $hours * 3600) : null; } /* * A template's form, posted (admin/modals/ticket_template/ticket_template_form.php), * checked - its fields raw, or null with $error saying why. The due time is * due_amount in due_unit (hours or days). */ function ticketTemplateFields(array $post, &$error = '') { $name = trim(mb_substr(preg_replace('/\s+/', ' ', strip_tags((string) ($post['name'] ?? ''))), 0, 200)); if ($name === '') { $error = 'A template needs a name.'; return null; } $priority = (string) ($post['priority'] ?? ''); $billable = (string) ($post['billable'] ?? ''); $amount = max(0, intval($post['due_amount'] ?? 0)); $hours = ($post['due_unit'] ?? 'days') === 'hours' ? $amount : $amount * 24; return [ 'name' => $name, 'description' => trim(mb_substr(strip_tags((string) ($post['description'] ?? '')), 0, 500)), 'subject' => trim(mb_substr(strip_tags((string) ($post['subject'] ?? '')), 0, 500)), 'details' => (string) ($post['details'] ?? ''), 'category_id' => ticketCategoryId($post['category_id'] ?? 0), 'priority' => in_array($priority, TICKET_PRIORITIES, true) ? $priority : null, 'billable' => $billable === '1' ? 1 : ($billable === '0' ? 0 : null), 'assigned_to' => ticketAgentId($post['assigned_to'] ?? 0), 'due_hours' => min($hours, 24 * 366), 'tags' => (array) ($post['tags'] ?? []), ]; } /** Save a template's fields (ticketTemplateFields()) - a new one when $ticket_template_id is 0. Its id. */ function ticketTemplateSave($ticket_template_id, array $f) { global $mysqli; $ticket_template_id = intval($ticket_template_id); $set = "ticket_template_name = '" . escapeSql($f['name']) . "', ticket_template_description = '" . escapeSql($f['description']) . "', ticket_template_subject = '" . escapeSql($f['subject']) . "', ticket_template_details = '" . mysqli_real_escape_string($mysqli, $f['details']) . "', ticket_template_category_id = " . intval($f['category_id']) . ", ticket_template_priority = " . ($f['priority'] === null ? 'NULL' : "'" . escapeSql($f['priority']) . "'") . ", ticket_template_billable = " . ($f['billable'] === null ? 'NULL' : intval($f['billable'])) . ", ticket_template_assigned_to = " . intval($f['assigned_to']) . ", ticket_template_due_hours = " . intval($f['due_hours']); if ($ticket_template_id) { mysqli_query($mysqli, "UPDATE ticket_templates SET $set WHERE ticket_template_id = $ticket_template_id"); } else { mysqli_query($mysqli, "INSERT INTO ticket_templates SET $set"); $ticket_template_id = intval(mysqli_insert_id($mysqli)); } ticketTemplateSetTags($ticket_template_id, $f['tags']); return $ticket_template_id; } /* Set a template's tags from a Tags field - ticket tags, as tagSetForRecord() takes them. The ids it has now. */ function ticketTemplateSetTags($ticket_template_id, array $values) { global $mysqli; $ticket_template_id = intval($ticket_template_id); $had = ticketTemplateTagIds($ticket_template_id); $keep = tagKeepIds('ticket', tagIdsFromValues('ticket', $values), $had); $remove = array_diff($had, $keep); if ($remove) { mysqli_query($mysqli, "DELETE FROM ticket_template_tags WHERE ticket_template_id = $ticket_template_id AND tag_id IN (" . implode(',', $remove) . ")"); } foreach (array_diff($keep, $had) as $tag_id) { mysqli_query($mysqli, "INSERT IGNORE INTO ticket_template_tags SET ticket_template_id = $ticket_template_id, tag_id = " . intval($tag_id)); } return $keep; } /** A copy of a template - "Copy of" its name, its tasks and tags - not archived. The new one's id, or 0. */ function ticketTemplateCopy($ticket_template_id) { global $mysqli; $row = ticketTemplateRow($ticket_template_id); if (!$row) { return 0; } $new_id = ticketTemplateSave(0, [ 'name' => mb_substr('Copy of ' . $row['ticket_template_name'], 0, 200), 'description' => (string) $row['ticket_template_description'], 'subject' => (string) $row['ticket_template_subject'], 'details' => (string) $row['ticket_template_details'], 'category_id' => intval($row['ticket_template_category_id']), 'priority' => $row['ticket_template_priority'], 'billable' => $row['ticket_template_billable'] === null ? null : intval($row['ticket_template_billable']), 'assigned_to' => intval($row['ticket_template_assigned_to']), 'due_hours' => intval($row['ticket_template_due_hours']), 'tags' => [], ]); foreach (ticketTemplateTagIds($ticket_template_id) as $tag_id) { mysqli_query($mysqli, "INSERT IGNORE INTO ticket_template_tags SET ticket_template_id = $new_id, tag_id = $tag_id"); } foreach (ticketTemplateTasks($ticket_template_id) as $order => $task) { mysqli_query($mysqli, "INSERT INTO task_templates SET task_template_name = '" . escapeSql($task['name']) . "', task_template_order = " . ($order + 1) . ", task_template_completion_estimate = " . intval($task['estimate']) . ", task_template_ticket_template_id = $new_id"); } return $new_id; } /** How templates are used - [id => ['count' => tickets raised, 'open' => still open, 'last' => when the last was raised]]. */ function ticketTemplateUsage(array $ticket_template_ids) { global $mysqli; $ids = array_values(array_filter(array_map('intval', $ticket_template_ids))); $usage = []; if (!$ids) { return $usage; } $sql = mysqli_query($mysqli, "SELECT ticket_template_id, COUNT(*) AS used, SUM(ticket_closed_at IS NULL AND ticket_resolved_at IS NULL) AS still_open, MAX(ticket_created_at) AS last_used FROM tickets WHERE ticket_template_id IN (" . implode(',', $ids) . ") AND ticket_archived_at IS NULL GROUP BY ticket_template_id"); while ($row = mysqli_fetch_assoc($sql)) { $usage[intval($row['ticket_template_id'])] = ['count' => intval($row['used']), 'open' => intval($row['still_open']), 'last' => $row['last_used']]; } return $usage; } /* * How a template sets up its tickets, as little badges - its priority, * category, billing, agent and due time. HTML; '' when it sets none of them. */ function ticketTemplateDefaultsHtml(array $row) { $chips = []; if (!empty($row['ticket_template_priority'])) { $chips[] = ticketPriorityHtml($row['ticket_template_priority'], 'detail'); } if (intval($row['ticket_template_category_id']) && !empty($row['category_name'])) { $chips[] = '' . categoryChip(['category_name' => $row['category_name'], 'category_type' => 'Ticket', 'category_color' => $row['category_color'] ?? '', 'category_icon' => $row['category_icon'] ?? '']) . ''; } if ($row['ticket_template_billable'] !== null) { $chips[] = '' . (intval($row['ticket_template_billable']) ? 'Billable' : 'Not billable') . ''; } if (intval($row['ticket_template_assigned_to']) && !empty($row['assignee_name'])) { $chips[] = '' . escapeHtml($row['assignee_name']) . ''; } if (intval($row['ticket_template_due_hours'])) { $chips[] = 'Due in ' . ticketTemplateDueText($row['ticket_template_due_hours']) . ''; } return $chips ? '' . implode('', $chips) . '' : ''; } /* * What picking a template puts into a ticket form - the data attributes of * its