mirror of
https://github.com/itflow-org/itflow
synced 2026-08-16 20:45:12 +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>
|
||||
</a>
|
||||
</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 if ($config_module_enable_itdoc) { ?>
|
||||
<!-- 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);
|
||||
}
|
||||
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -727,8 +727,57 @@ if (isset($_GET['ticket_id'])) {
|
||||
<?php } ?>
|
||||
</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">
|
||||
<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 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
|
||||
const replyFilter = document.getElementById('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;
|
||||
/*!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`
|
||||
--
|
||||
@@ -3151,4 +3171,4 @@ CREATE TABLE `vendors` (
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!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