mirror of
https://github.com/itflow-org/itflow
synced 2026-08-17 04:55:13 +00:00
Feature: Add Ticket Canned Responses
This commit is contained in:
135
admin/canned_responses.php
Normal file
135
admin/canned_responses.php
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Default Column Sortby Filter
|
||||||
|
$sort = "canned_response_name";
|
||||||
|
$order = "ASC";
|
||||||
|
|
||||||
|
require_once "includes/inc_all_admin.php";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A canned response either belongs to one ticket category or, with category id 0, offers
|
||||||
|
* itself on every ticket. The LEFT JOIN is what puts the category name on the row; a
|
||||||
|
* response whose category was deleted since falls back to the general case below.
|
||||||
|
*/
|
||||||
|
$sql = mysqli_query(
|
||||||
|
$mysqli,
|
||||||
|
"SELECT SQL_CALC_FOUND_ROWS canned_response_id, canned_response_name, canned_response_body,
|
||||||
|
canned_response_category_id, category_name
|
||||||
|
FROM canned_responses
|
||||||
|
LEFT JOIN categories ON canned_response_category_id = category_id AND category_type = 'Ticket'
|
||||||
|
WHERE (canned_response_name LIKE '%$q%' OR canned_response_body LIKE '%$q%')
|
||||||
|
AND canned_response_archived_at IS NULL
|
||||||
|
ORDER BY $sort $order
|
||||||
|
LIMIT $record_from, $record_to"
|
||||||
|
);
|
||||||
|
|
||||||
|
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="card card-dark">
|
||||||
|
<div class="card-header py-2">
|
||||||
|
<h3 class="card-title mt-2"><i class="fas fa-fw fa-comment-dots me-2"></i>Canned Responses</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/canned_response/canned_response_add.php" data-modal-size="lg"><i class="fas fa-plus me-2"></i>New Canned Response</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form autocomplete="off">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="input-group mb-3 mb-md-0">
|
||||||
|
<input type="search" class="form-control" name="q" value="<?php if(isset($q)){ echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search Canned Responses">
|
||||||
|
<button class="btn btn-dark"><i class="fa fa-search"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<hr>
|
||||||
|
<div class="table-responsive-sm">
|
||||||
|
<table class="table table-striped table-borderless table-hover">
|
||||||
|
<thead class="text-dark <?php if($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=canned_response_name&order=<?= $disp ?>">
|
||||||
|
Response <?php if ($sort == 'canned_response_name') { echo $order_icon; } ?>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a class="text-secondary" href="?<?= $url_query_strings_sort ?>&sort=category_name&order=<?= $disp ?>">
|
||||||
|
Ticket Category <?php if ($sort == 'category_name') { echo $order_icon; } ?>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th class="text-center">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
while($row = mysqli_fetch_assoc($sql)){
|
||||||
|
$canned_response_id = intval($row['canned_response_id']);
|
||||||
|
$canned_response_name = escapeHtml($row['canned_response_name']);
|
||||||
|
$canned_response_category_id = intval($row['canned_response_category_id']);
|
||||||
|
$canned_response_category_name = escapeHtml($row['category_name']);
|
||||||
|
|
||||||
|
// The body is HTML from TinyMCE. This is a preview, not a render - tags
|
||||||
|
// are stripped rather than escaped so the column reads as the sentence
|
||||||
|
// the agent will actually be inserting
|
||||||
|
$canned_response_preview = escapeHtml(mb_strimwidth(trim(html_entity_decode(strip_tags($row['canned_response_body']), ENT_QUOTES, 'UTF-8')), 0, 120, '...'));
|
||||||
|
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex">
|
||||||
|
<i class="fa fa-fw fa-2x fa-comment-dots me-3"></i>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div>
|
||||||
|
<a class="ajax-modal" href="#" data-modal-url="modals/canned_response/canned_response_edit.php?id=<?= $canned_response_id ?>" data-modal-size="lg">
|
||||||
|
<?= $canned_response_name ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div><small class="text-secondary"><?= $canned_response_preview ?></small></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php if ($canned_response_category_id && $canned_response_category_name) { ?>
|
||||||
|
<span class="badge bg-secondary"><?= $canned_response_category_name ?></span>
|
||||||
|
<?php } else { ?>
|
||||||
|
<span class="badge bg-dark">All ticket categories</span>
|
||||||
|
<?php } ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="dropdown dropstart text-center">
|
||||||
|
<button class="btn btn-secondary btn-sm" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-ellipsis-h"></i>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/canned_response/canned_response_edit.php?id=<?= $canned_response_id ?>" data-modal-size="lg">
|
||||||
|
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_canned_response=<?= $canned_response_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||||
|
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php require_once "../includes/filter_footer.php";
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once "../includes/footer.php";
|
||||||
27
admin/database_updates/2.6.9.php
Normal file
27
admin/database_updates/2.6.9.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ITFlow - Database update to version 2.6.9 (from 2.6.8)
|
||||||
|
* Included by admin/database_updates.php - do not access directly
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||||
|
|
||||||
|
// Canned responses for ticket replies, managed from Admin > Canned Responses and
|
||||||
|
// picked from a dropdown on the ticket reply form.
|
||||||
|
//
|
||||||
|
// canned_response_category_id is a ticket category (categories.category_id where
|
||||||
|
// category_type = 'Ticket'), or 0 for one that offers itself on every ticket
|
||||||
|
// whatever its category.
|
||||||
|
|
||||||
|
mysqli_query($mysqli, "CREATE TABLE IF NOT EXISTS `canned_responses` (
|
||||||
|
`canned_response_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`canned_response_name` varchar(200) NOT NULL,
|
||||||
|
`canned_response_body` longtext DEFAULT NULL,
|
||||||
|
`canned_response_category_id` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`canned_response_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`canned_response_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
|
||||||
|
`canned_response_archived_at` datetime DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`canned_response_id`),
|
||||||
|
KEY `canned_response_category_id` (`canned_response_category_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||||
@@ -124,6 +124,12 @@
|
|||||||
<p>Ticket Templates</p>
|
<p>Ticket Templates</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/canned_responses.php" class="nav-link <?= (basename($_SERVER['PHP_SELF']) == 'canned_responses.php' ? 'active' : '') ?>">
|
||||||
|
<i class="nav-icon fas fa-comment-dots"></i>
|
||||||
|
<p>Canned Responses</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
<?php if ($config_module_enable_itdoc) { ?>
|
<?php if ($config_module_enable_itdoc) { ?>
|
||||||
<!-- 2025-11-16 JQ - Hide Contracts not yet ready
|
<!-- 2025-11-16 JQ - Hide Contracts not yet ready
|
||||||
|
|||||||
58
admin/modals/canned_response/canned_response_add.php
Normal file
58
admin/modals/canned_response/canned_response_add.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once '../../includes/modal_header.php';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="modal-header bg-dark">
|
||||||
|
<h5 class="modal-title"><i class="fa fa-fw fa-comment-dots me-2"></i>New Canned Response</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<form action="post.php" method="post" autocomplete="off">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Name <strong class="text-danger">*</strong></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-comment-dots"></i></span>
|
||||||
|
<input type="text" class="form-control" name="name" placeholder="What an agent picks it by" maxlength="200" required autofocus>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Ticket Category</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||||
|
<select class="form-select select2" name="category">
|
||||||
|
<option value="0">- All ticket categories -</option>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$sql_categories = mysqli_query($mysqli, "SELECT category_id, category_name FROM categories WHERE category_type = 'Ticket' AND category_archived_at IS NULL ORDER BY category_name ASC");
|
||||||
|
while ($row = mysqli_fetch_assoc($sql_categories)) {
|
||||||
|
$category_id_select = intval($row['category_id']);
|
||||||
|
$category_name_select = escapeHtml($row['category_name']); ?>
|
||||||
|
<option value="<?= $category_id_select ?>"><?= $category_name_select ?></option>
|
||||||
|
|
||||||
|
<?php } ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<small class="text-secondary">Leave this on all categories for a response that should be offered on every ticket.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Response</label>
|
||||||
|
<textarea class="form-control tinymceTicket" name="body"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" name="add_canned_response" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Create Canned Response</button>
|
||||||
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once '../../../includes/modal_footer.php';
|
||||||
72
admin/modals/canned_response/canned_response_edit.php
Normal file
72
admin/modals/canned_response/canned_response_edit.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once '../../includes/modal_header.php';
|
||||||
|
|
||||||
|
$canned_response_id = intval($_GET['id']);
|
||||||
|
|
||||||
|
$sql = mysqli_query($mysqli, "SELECT canned_response_name, canned_response_body, canned_response_category_id
|
||||||
|
FROM canned_responses WHERE canned_response_id = $canned_response_id LIMIT 1");
|
||||||
|
$row = mysqli_fetch_assoc($sql);
|
||||||
|
|
||||||
|
$canned_response_name = escapeHtml($row['canned_response_name']);
|
||||||
|
$canned_response_category_id = intval($row['canned_response_category_id']);
|
||||||
|
|
||||||
|
// Into a textarea, so escaped rather than purified - it is markup being edited, not rendered
|
||||||
|
$canned_response_body = escapeHtml($row['canned_response_body']);
|
||||||
|
|
||||||
|
// Generate the HTML form content using output buffering.
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="modal-header bg-dark">
|
||||||
|
<h5 class="modal-title"><i class="fa fa-fw fa-comment-dots me-2"></i>Editing Canned Response: <strong><?= $canned_response_name ?></strong></h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<form action="post.php" method="post" autocomplete="off">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||||
|
<input type="hidden" name="canned_response_id" value="<?= $canned_response_id ?>">
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Name <strong class="text-danger">*</strong></label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-comment-dots"></i></span>
|
||||||
|
<input type="text" class="form-control" name="name" maxlength="200" value="<?= $canned_response_name ?>" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Ticket Category</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||||
|
<select class="form-select select2" name="category">
|
||||||
|
<option value="0" <?php if ($canned_response_category_id == 0) { echo "selected"; } ?>>- All ticket categories -</option>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$sql_categories = mysqli_query($mysqli, "SELECT category_id, category_name FROM categories WHERE category_type = 'Ticket' AND category_archived_at IS NULL ORDER BY category_name ASC");
|
||||||
|
while ($category_row = mysqli_fetch_assoc($sql_categories)) {
|
||||||
|
$category_id_select = intval($category_row['category_id']);
|
||||||
|
$category_name_select = escapeHtml($category_row['category_name']); ?>
|
||||||
|
<option value="<?= $category_id_select ?>" <?php if ($canned_response_category_id == $category_id_select) { echo "selected"; } ?>><?= $category_name_select ?></option>
|
||||||
|
|
||||||
|
<?php } ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<small class="text-secondary">Leave this on all categories for a response that should be offered on every ticket.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label>Response</label>
|
||||||
|
<textarea class="form-control tinymceTicket" name="body"><?= $canned_response_body ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" name="edit_canned_response" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Save</button>
|
||||||
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fa fa-times me-2"></i>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
require_once '../../../includes/modal_footer.php';
|
||||||
67
admin/post/canned_response.php
Normal file
67
admin/post/canned_response.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Canned Responses
|
||||||
|
|
||||||
|
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||||
|
|
||||||
|
if (isset($_POST['add_canned_response'])) {
|
||||||
|
|
||||||
|
validateCSRFToken();
|
||||||
|
|
||||||
|
$name = escapeSql($_POST['name']);
|
||||||
|
|
||||||
|
// The body is TinyMCE HTML, so it goes in the way ticket replies and ticket template
|
||||||
|
// details do - escaped for the query only, and purified where it is rendered
|
||||||
|
$body = mysqli_real_escape_string($mysqli, $_POST['body']);
|
||||||
|
|
||||||
|
// 0 is the general case: offered on every ticket whatever its category
|
||||||
|
$category_id = intval($_POST['category']);
|
||||||
|
|
||||||
|
mysqli_query($mysqli, "INSERT INTO canned_responses SET canned_response_name = '$name', canned_response_body = '$body', canned_response_category_id = $category_id");
|
||||||
|
|
||||||
|
$canned_response_id = mysqli_insert_id($mysqli);
|
||||||
|
|
||||||
|
logAudit("Canned Response", "Create", "$session_name created canned response $name", 0, $canned_response_id);
|
||||||
|
|
||||||
|
flashAlert("Canned Response <strong>$name</strong> created");
|
||||||
|
|
||||||
|
redirect();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_POST['edit_canned_response'])) {
|
||||||
|
|
||||||
|
validateCSRFToken();
|
||||||
|
|
||||||
|
$canned_response_id = intval($_POST['canned_response_id']);
|
||||||
|
$name = escapeSql($_POST['name']);
|
||||||
|
$body = mysqli_real_escape_string($mysqli, $_POST['body']);
|
||||||
|
$category_id = intval($_POST['category']);
|
||||||
|
|
||||||
|
mysqli_query($mysqli, "UPDATE canned_responses SET canned_response_name = '$name', canned_response_body = '$body', canned_response_category_id = $category_id WHERE canned_response_id = $canned_response_id");
|
||||||
|
|
||||||
|
logAudit("Canned Response", "Edit", "$session_name edited canned response $name", 0, $canned_response_id);
|
||||||
|
|
||||||
|
flashAlert("Canned Response <strong>$name</strong> edited");
|
||||||
|
|
||||||
|
redirect();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['delete_canned_response'])) {
|
||||||
|
|
||||||
|
validateCSRFToken();
|
||||||
|
|
||||||
|
$canned_response_id = intval($_GET['delete_canned_response']);
|
||||||
|
|
||||||
|
$name = escapeSql(getFieldById('canned_responses', $canned_response_id, 'canned_response_name'));
|
||||||
|
|
||||||
|
mysqli_query($mysqli, "DELETE FROM canned_responses WHERE canned_response_id = $canned_response_id");
|
||||||
|
|
||||||
|
logAudit("Canned Response", "Delete", "$session_name deleted canned response $name", 0, $canned_response_id);
|
||||||
|
|
||||||
|
flashAlert("Canned Response <strong>$name</strong> deleted", 'error');
|
||||||
|
|
||||||
|
redirect();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -382,6 +382,37 @@ if (isset($_GET['get_active_clients'])) {
|
|||||||
echo json_encode($response);
|
echo json_encode($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns the body of one canned response, for the picker on the ticket reply form.
|
||||||
|
* Fetched on demand rather than rendered into every ticket page, because the bodies are
|
||||||
|
* full HTML replies and an install can have a lot of them.
|
||||||
|
*/
|
||||||
|
if (isset($_GET['get_canned_response'])) {
|
||||||
|
enforceUserPermission('module_support');
|
||||||
|
|
||||||
|
$canned_response_id = intval($_GET['get_canned_response']);
|
||||||
|
|
||||||
|
$canned_sql = mysqli_query($mysqli, "SELECT canned_response_body FROM canned_responses
|
||||||
|
WHERE canned_response_id = $canned_response_id AND canned_response_archived_at IS NULL LIMIT 1");
|
||||||
|
|
||||||
|
$canned_row = mysqli_fetch_assoc($canned_sql);
|
||||||
|
|
||||||
|
// Purified here rather than at save time, the same way ticket replies and ticket
|
||||||
|
// template details are - what lands in the editor is what would have been rendered
|
||||||
|
require_once "../libs/htmlpurifier/HTMLPurifier.standalone.php";
|
||||||
|
|
||||||
|
$canned_purifier_config = HTMLPurifier_Config::createDefault();
|
||||||
|
$canned_purifier_config->set('Cache.DefinitionImpl', null);
|
||||||
|
$canned_purifier_config->set('URI.AllowedSchemes', ['data' => true, 'src' => true, 'http' => true, 'https' => true]);
|
||||||
|
$canned_purifier = new HTMLPurifier($canned_purifier_config);
|
||||||
|
|
||||||
|
$response = [];
|
||||||
|
$response['body'] = $canned_row ? $canned_purifier->purify($canned_row['canned_response_body']) : '';
|
||||||
|
|
||||||
|
echo json_encode($response);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns ordered list of active contacts for a specified client
|
* Returns ordered list of active contacts for a specified client
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -727,8 +727,57 @@ if (isset($_GET['ticket_id'])) {
|
|||||||
<?php } ?>
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Canned responses offered on this ticket: the ones tied to its
|
||||||
|
* category, plus the general ones that are offered everywhere.
|
||||||
|
* Names only - the body is fetched when one is picked, so a
|
||||||
|
* shelf of long responses does not ride along with every ticket.
|
||||||
|
*/
|
||||||
|
$sql_canned_responses = mysqli_query($mysqli, "SELECT canned_response_id, canned_response_name, canned_response_category_id
|
||||||
|
FROM canned_responses
|
||||||
|
WHERE canned_response_archived_at IS NULL
|
||||||
|
AND (canned_response_category_id = 0 OR canned_response_category_id = $ticket_category)
|
||||||
|
ORDER BY canned_response_name ASC");
|
||||||
|
|
||||||
|
$canned_responses_for_category = [];
|
||||||
|
$canned_responses_general = [];
|
||||||
|
|
||||||
|
while ($canned_row = mysqli_fetch_assoc($sql_canned_responses)) {
|
||||||
|
if (intval($canned_row['canned_response_category_id']) === 0) {
|
||||||
|
$canned_responses_general[] = $canned_row;
|
||||||
|
} else {
|
||||||
|
$canned_responses_for_category[] = $canned_row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($canned_responses_for_category || $canned_responses_general) { ?>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<select class="form-select" id="canned_response_picker">
|
||||||
|
<option value="">- Insert a canned response -</option>
|
||||||
|
<?php if ($canned_responses_for_category) { ?>
|
||||||
|
<optgroup label="<?= $ticket_category_display ?>">
|
||||||
|
<?php foreach ($canned_responses_for_category as $canned_row) { ?>
|
||||||
|
<option value="<?= intval($canned_row['canned_response_id']) ?>"><?= escapeHtml($canned_row['canned_response_name']) ?></option>
|
||||||
|
<?php } ?>
|
||||||
|
</optgroup>
|
||||||
|
<?php } ?>
|
||||||
|
<?php if ($canned_responses_general) { ?>
|
||||||
|
<optgroup label="All categories">
|
||||||
|
<?php foreach ($canned_responses_general as $canned_row) { ?>
|
||||||
|
<option value="<?= intval($canned_row['canned_response_id']) ?>"><?= escapeHtml($canned_row['canned_response_name']) ?></option>
|
||||||
|
<?php } ?>
|
||||||
|
</optgroup>
|
||||||
|
<?php } ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php } ?>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<textarea class="form-control tinymceTicket" name="ticket_reply" placeholder="Type a response"></textarea>
|
<textarea class="form-control tinymceTicket" id="ticket_reply" name="ticket_reply" placeholder="Type a response"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -1424,6 +1473,43 @@ require_once "../includes/footer.php";
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Canned responses - insert at the cursor rather than replacing, so picking one into a
|
||||||
|
// half-written reply adds to it. The body is fetched on demand; TinyMCE owns the field
|
||||||
|
// by now, so setting the textarea value directly would do nothing visible.
|
||||||
|
const cannedPicker = document.getElementById('canned_response_picker');
|
||||||
|
if (cannedPicker) {
|
||||||
|
cannedPicker.addEventListener('change', function () {
|
||||||
|
const cannedId = cannedPicker.value;
|
||||||
|
if (!cannedId) return;
|
||||||
|
|
||||||
|
cannedPicker.disabled = true;
|
||||||
|
|
||||||
|
fetch('ajax.php?get_canned_response=' + encodeURIComponent(cannedId))
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const editor = window.tinymce ? tinymce.get('ticket_reply') : null;
|
||||||
|
|
||||||
|
if (editor) {
|
||||||
|
editor.insertContent(data.body);
|
||||||
|
editor.focus();
|
||||||
|
} else {
|
||||||
|
// TinyMCE failed to load - fall back to the plain textarea
|
||||||
|
const textarea = document.getElementById('ticket_reply');
|
||||||
|
if (textarea) {
|
||||||
|
textarea.value += data.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert('Could not load that canned response.');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
cannedPicker.disabled = false;
|
||||||
|
cannedPicker.value = '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Conversation filter - show everything, only what the client can see, or only internal notes
|
// Conversation filter - show everything, only what the client can see, or only internal notes
|
||||||
const replyFilter = document.getElementById('replyFilter');
|
const replyFilter = document.getElementById('replyFilter');
|
||||||
if (replyFilter) {
|
if (replyFilter) {
|
||||||
|
|||||||
22
db.sql
22
db.sql
@@ -471,6 +471,26 @@ CREATE TABLE `calendars` (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Table structure for table `canned_responses`
|
||||||
|
--
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `canned_responses`;
|
||||||
|
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||||
|
/*!40101 SET character_set_client = utf8mb4 */;
|
||||||
|
CREATE TABLE `canned_responses` (
|
||||||
|
`canned_response_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`canned_response_name` varchar(200) NOT NULL,
|
||||||
|
`canned_response_body` longtext DEFAULT NULL,
|
||||||
|
`canned_response_category_id` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`canned_response_created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`canned_response_updated_at` datetime DEFAULT NULL ON UPDATE current_timestamp(),
|
||||||
|
`canned_response_archived_at` datetime DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`canned_response_id`),
|
||||||
|
KEY `canned_response_category_id` (`canned_response_category_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Table structure for table `categories`
|
-- Table structure for table `categories`
|
||||||
--
|
--
|
||||||
@@ -3151,4 +3171,4 @@ CREATE TABLE `vendors` (
|
|||||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
|
|
||||||
-- Dump completed on 2026-08-15 16:40:00
|
-- Dump completed on 2026-08-15 17:28:46
|
||||||
|
|||||||
Reference in New Issue
Block a user