mirror of
https://github.com/itflow-org/itflow
synced 2026-11-11 07:01:16 +00:00
Rebuild Admin > Ticket Statuses: list on the standard pattern with In use and Switched off tabs, search, sortable headers, ticket counts linking to the tickets, SLA clock and client-reply columns and drag ordering for the pickers and board; one New/Edit window with badge preview, colour swatches and a description shown on hover across the app; per-status choice of whether a client reply by email or portal moves the ticket back to Open or leaves it; switch off and delete move a status's tickets to a chosen status with ticket history and SLA sync, and clear canned responses that set it; built-in statuses keep their names, stay on and have fixed SLA and reply behaviour enforced server-side; badges use readable text colour; the tickets filter keeps a switched-off status; database 3.4.0 adds ticket_status_description and ticket_status_client_reply; new functions/ticket_status.php
This commit is contained in:
48
admin/database_updates/3.4.0.php
Normal file
48
admin/database_updates/3.4.0.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Database update to version 3.4.0 (from 3.3.9)
|
||||
* Included by admin/database_updates.php - do not access directly
|
||||
*/
|
||||
|
||||
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
|
||||
|
||||
$itflow_column = function ($table, $column) use ($mysqli) {
|
||||
$result = mysqli_query($mysqli, "SELECT 1 FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '$table' AND COLUMN_NAME = '$column' LIMIT 1");
|
||||
return $result && mysqli_num_rows($result) > 0;
|
||||
};
|
||||
|
||||
// What a ticket status is for - shown when it is hovered, in the pickers and on the board
|
||||
if (!$itflow_column('ticket_statuses', 'ticket_status_description')) {
|
||||
mysqli_query($mysqli, "ALTER TABLE `ticket_statuses` ADD `ticket_status_description` varchar(255) DEFAULT NULL AFTER `ticket_status_color`");
|
||||
}
|
||||
|
||||
// What a client's reply does to a ticket in the status - 1 moves it back to Open (as every reply always has), 0 leaves it there
|
||||
if (!$itflow_column('ticket_statuses', 'ticket_status_client_reply')) {
|
||||
mysqli_query($mysqli, "ALTER TABLE `ticket_statuses` ADD `ticket_status_client_reply` tinyint(1) NOT NULL DEFAULT 1 AFTER `ticket_status_pauses_sla`");
|
||||
}
|
||||
|
||||
// The five built-in statuses say what they are for - only where nothing is written yet
|
||||
$itflow_descriptions = [
|
||||
1 => 'Just come in - nobody has picked it up yet.',
|
||||
2 => 'Being worked on.',
|
||||
3 => 'Waiting on someone else - the client, a vendor or a part.',
|
||||
4 => 'Fixed, and waiting to be closed - a reply from the client reopens it.',
|
||||
5 => 'Finished - it can\'t be replied to.',
|
||||
];
|
||||
foreach ($itflow_descriptions as $itflow_status_id => $itflow_description) {
|
||||
mysqli_query($mysqli, "UPDATE ticket_statuses SET ticket_status_description = '" . mysqli_real_escape_string($mysqli, $itflow_description) . "'
|
||||
WHERE ticket_status_id = $itflow_status_id AND (ticket_status_description IS NULL OR ticket_status_description = '')");
|
||||
}
|
||||
|
||||
// Number the statuses 1, 2, 3... in the order they already show in (most are all 0, and ties go by id),
|
||||
// so they can be dragged into another
|
||||
$itflow_position = 0;
|
||||
$itflow_statuses = mysqli_query($mysqli, "SELECT ticket_status_id FROM ticket_statuses ORDER BY ticket_status_order ASC, ticket_status_id ASC");
|
||||
while ($itflow_status = mysqli_fetch_row($itflow_statuses)) {
|
||||
$itflow_position++;
|
||||
mysqli_query($mysqli, "UPDATE ticket_statuses SET ticket_status_order = $itflow_position WHERE ticket_status_id = " . intval($itflow_status[0]));
|
||||
}
|
||||
|
||||
unset($itflow_column, $itflow_descriptions, $itflow_status_id, $itflow_description, $itflow_position, $itflow_statuses, $itflow_status);
|
||||
@@ -1,5 +1,15 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* New ticket status - its name, what it is for, its colour, whether the SLA
|
||||
* clock runs in it and what a client's reply does. It goes at the end of the
|
||||
* order. The fields are ticket_status_form.php.
|
||||
*/
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$ticket_status = null;
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
@@ -11,34 +21,7 @@ ob_start();
|
||||
<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-info-circle"></i></span>
|
||||
<input type="text" class="form-control" name="name" placeholder="Ticket Status name" maxlength="200" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Color <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-paint-brush"></i></span>
|
||||
<input type="color" class="form-control form-control-color" name="color" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3">
|
||||
<label>SLA</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-stopwatch"></i></span>
|
||||
<select class="form-select select2" name="pauses_sla">
|
||||
<option value="0">Resolution clock keeps running</option>
|
||||
<option value="1">Pause the resolution clock</option>
|
||||
</select>
|
||||
</div>
|
||||
<small class="text-muted">Tickets sitting in a paused status never warn or breach on resolution. Time already spent is kept and the deadline moves out when the ticket comes back.</small>
|
||||
</div>
|
||||
<?php require __DIR__ . '/ticket_status_form.php'; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="add_ticket_status" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Create</button>
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Edit a ticket status. The fields are ticket_status_form.php - a built-in
|
||||
* status shows its fixed answers rather than asking.
|
||||
*/
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$ticket_status_id = intval($_GET['id']);
|
||||
$ticket_status = ticketStatusRow(intval($_GET['id'] ?? 0));
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT ticket_status_active, ticket_status_color, ticket_status_name, ticket_status_order,
|
||||
ticket_status_pauses_sla FROM ticket_statuses WHERE ticket_status_id = $ticket_status_id LIMIT 1");
|
||||
$row = mysqli_fetch_assoc($sql);
|
||||
$ticket_status_name = escapeHtml($row['ticket_status_name']);
|
||||
$ticket_status_color = escapeHtml($row['ticket_status_color']);
|
||||
$ticket_status_order = intval($row['ticket_status_order']);
|
||||
$ticket_status_active = intval($row['ticket_status_active']);
|
||||
$ticket_status_pauses_sla = intval($row['ticket_status_pauses_sla']);
|
||||
// Null for a custom status (the admin picks), 1 or 0 for the five built-in ones
|
||||
$ticket_status_sla_lock = getTicketStatusSlaLock($ticket_status_id);
|
||||
|
||||
// Generate the HTML form content using output buffering.
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
if (!$ticket_status) { ?>
|
||||
<div class="modal-body text-center text-secondary py-5">That status is gone - it may have been deleted.</div>
|
||||
<?php } else {
|
||||
$ticket_status_id = intval($ticket_status['ticket_status_id']); ?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-info-circle me-2"></i>Editing Ticket Status: <strong><?= $ticket_status_name ?></strong></h5>
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-info-circle me-2"></i>Ticket Status: <strong><?= escapeHtml($ticket_status['ticket_status_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">
|
||||
@@ -28,70 +25,16 @@ ob_start();
|
||||
<input type="hidden" name="ticket_status_id" value="<?= $ticket_status_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-tag"></i></span>
|
||||
<input type="text" class="form-control" name="name" maxlength="200" value="<?= $ticket_status_name ?>" required <?php if ($ticket_status_id <= 5) { echo "readonly"; } ?>>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Color <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-paint-brush"></i></span>
|
||||
<input type="color" class="form-control form-control-color" name="color" value="<?= $ticket_status_color ?>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Order</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-sort-numeric-down"></i></span>
|
||||
<input type="number" class="form-control" name="order" placeholder="Leave blank for no order" value="<?= $ticket_status_order ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>Status <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-info-circle"></i></span>
|
||||
<select class="form-select select2" name="status" required>
|
||||
<option <?php if ($ticket_status_active == 1) { echo "selected"; } ?> value="1">Active</option>
|
||||
<option <?php if ($ticket_status_active == 0) { echo "selected"; } ?> value="0" <?php if ($ticket_status_id <= 5) { echo "disabled"; } ?>>Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label>SLA</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-stopwatch"></i></span>
|
||||
<?php if (is_null($ticket_status_sla_lock)) { ?>
|
||||
<select class="form-select select2" name="pauses_sla">
|
||||
<option <?php if ($ticket_status_pauses_sla == 0) { echo "selected "; } ?>value="0">Resolution clock keeps running</option>
|
||||
<option <?php if ($ticket_status_pauses_sla == 1) { echo "selected "; } ?>value="1">Pause the resolution clock</option>
|
||||
</select>
|
||||
<?php } else { ?>
|
||||
<input type="text" class="form-control" value="<?= $ticket_status_sla_lock ? 'Pause the resolution clock' : 'Resolution clock keeps running' ?>" readonly>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if (is_null($ticket_status_sla_lock)) { ?>
|
||||
<small class="text-muted">Tickets sitting in a paused status never warn or breach on resolution. Time already spent is kept and the deadline moves out when the ticket comes back.</small>
|
||||
<?php } elseif ($ticket_status_sla_lock) { ?>
|
||||
<small class="text-muted">Fixed for built-in statuses. Tickets here never warn or breach on resolution - time already spent is kept and the deadline moves out when the ticket comes back.</small>
|
||||
<?php } else { ?>
|
||||
<small class="text-muted">Fixed for built-in statuses. Tickets here are being worked, so their resolution clock runs.</small>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<?php if (intval($ticket_status['ticket_status_active']) !== 1) { ?>
|
||||
<div class="alert alert-secondary small py-2"><i class="fas fa-fw fa-power-off me-1"></i>Switched off - it can't be picked, and isn't on the board. Switch it on from the list.</div>
|
||||
<?php } ?>
|
||||
<?php require __DIR__ . '/ticket_status_form.php'; ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" name="edit_ticket_status" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save</button>
|
||||
<button type="submit" name="edit_ticket_status" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save changes</button>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
<?php }
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
|
||||
112
admin/modals/ticket_status/ticket_status_form.php
Normal file
112
admin/modals/ticket_status/ticket_status_form.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* A ticket status's fields - for ticket_status_add.php and
|
||||
* ticket_status_edit.php, which set $ticket_status (a ticketStatusRow(), or
|
||||
* null when new). A built-in status keeps its name and has fixed answers for
|
||||
* the SLA clock and a client's reply - shown, not asked. The preview of its
|
||||
* badge and the colour swatches are js/ticket_status_form.js. The server
|
||||
* checks it all again (ticketStatusFromInput(), ticketStatusProblem()).
|
||||
*/
|
||||
|
||||
isset($mysqli) || die("Direct file access is not allowed");
|
||||
|
||||
$s = $ticket_status ?? null;
|
||||
$s_id = $s ? intval($s['ticket_status_id']) : 0;
|
||||
$s_builtin = ticketStatusBuiltIns()[$s_id] ?? null;
|
||||
$s_name = (string) ($s['ticket_status_name'] ?? '');
|
||||
$s_color = $s ? ticketStatusColor($s['ticket_status_color']) : ticketStatusNextColor();
|
||||
$s_description = (string) ($s['ticket_status_description'] ?? '');
|
||||
$s_paused = $s ? intval($s['ticket_status_pauses_sla']) === 1 : false;
|
||||
$s_reply = $s ? (intval($s['ticket_status_client_reply'] ?? 1) === 0 ? 0 : 1) : 1;
|
||||
|
||||
?>
|
||||
|
||||
<div data-ticket-status-form>
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mb-3 p-2 border rounded bg-body-tertiary">
|
||||
<span class="small text-secondary">Looks like</span>
|
||||
<span data-ticket-status-preview><?= ticketStatusBadgeHtml($s_name !== '' ? $s_name : 'New status', $s_color, 'list') ?></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name <strong class="text-danger">*</strong></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-tag"></i></span>
|
||||
<input type="text" class="form-control" name="name" maxlength="200" placeholder="Waiting on Vendor, Scheduled, Parts Ordered" value="<?= escapeHtml($s_name) ?>" required<?= $s_builtin ? ' readonly' : ($s ? '' : ' autofocus') ?> data-ticket-status-name>
|
||||
</div>
|
||||
<?php if ($s_builtin) { ?>
|
||||
<small class="text-secondary"><i class="fas fa-fw fa-lock me-1"></i>ITFlow's own statuses keep their names. <?= escapeHtml($s_builtin['set_by']) ?></small>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-align-left"></i></span>
|
||||
<input type="text" class="form-control" name="description" maxlength="255" placeholder="<?= escapeHtml($s_builtin ? $s_builtin['about'] : 'What it means, and when to use it') ?>" value="<?= escapeHtml($s_description) ?>">
|
||||
</div>
|
||||
<small class="text-secondary">Shown when the status is hovered - in the pickers, on the board and on tickets.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Color</label>
|
||||
<input type="hidden" name="color" value="<?= $s_color ?>" data-ticket-status-color>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<div class="d-flex flex-wrap gap-1" role="group" aria-label="Colors">
|
||||
<?php foreach (CATEGORY_COLORS as $swatch) { ?>
|
||||
<button type="button" class="itflow-tag-swatch<?= $swatch === $s_color ? ' active' : '' ?>" style="--itflow-tag-color: <?= $swatch ?>; --itflow-tag-text: <?= tagTextColor($swatch) ?>" data-ticket-status-swatch="<?= $swatch ?>" title="<?= $swatch ?>" aria-label="Color <?= $swatch ?>" aria-pressed="<?= $swatch === $s_color ? 'true' : 'false' ?>"><i class="fas fa-check" aria-hidden="true"></i></button>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="input-group input-group-sm w-auto" title="Any other color">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-paint-brush"></i></span>
|
||||
<input type="color" class="form-control form-control-color" value="<?= $s_color ?>" aria-label="Custom color" data-ticket-status-color-pick>
|
||||
<input type="text" class="form-control font-monospace" style="width: 6rem" maxlength="7" value="<?= $s_color ?>" pattern="#?[0-9a-fA-F]{6}" aria-label="Color as hex" data-ticket-status-color-hex>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">SLA resolution clock</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-stopwatch"></i></span>
|
||||
<?php if ($s_builtin) { ?>
|
||||
<input type="text" class="form-control" value="<?= $s_id === TICKET_STATUS_CLOSED ? 'Stopped - the ticket is finished' : ($s_paused ? 'Paused while a ticket is in it' : 'Keeps running') ?>" readonly>
|
||||
<?php } else { ?>
|
||||
<select class="form-select" name="pauses_sla">
|
||||
<option value="0"<?= $s_paused ? '' : ' selected' ?>>Keeps running - the ticket is being worked</option>
|
||||
<option value="1"<?= $s_paused ? ' selected' : '' ?>>Paused - waiting on someone else</option>
|
||||
</select>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<small class="text-secondary">
|
||||
<?php if ($s_builtin) { ?><i class="fas fa-fw fa-lock me-1"></i>Fixed for ITFlow's own statuses. <?php } ?>
|
||||
A ticket in a paused status never warns or breaches on resolution - the time already spent is kept, and the deadline moves out when it comes back. The response clock always runs.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-1">
|
||||
<label class="form-label">When the client replies</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-reply"></i></span>
|
||||
<?php if ($s_builtin) { ?>
|
||||
<input type="text" class="form-control" value="<?= [TICKET_STATUS_OPEN => 'It stays Open', TICKET_STATUS_RESOLVED => 'It is reopened - back to Open', TICKET_STATUS_CLOSED => 'They can\'t - they are told to raise a new ticket'][$s_id] ?? 'It moves back to Open' ?>" readonly>
|
||||
<?php } else { ?>
|
||||
<select class="form-select" name="client_reply">
|
||||
<?php foreach (TICKET_STATUS_CLIENT_REPLY as $value => $reply) { ?>
|
||||
<option value="<?= $value ?>"<?= $value === $s_reply ? ' selected' : '' ?>><?= escapeHtml($reply['label']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<small class="text-secondary">
|
||||
<?php if ($s_builtin) { ?>
|
||||
<i class="fas fa-fw fa-lock me-1"></i>Fixed for ITFlow's own statuses.
|
||||
<?php } else { ?>
|
||||
By email or in the portal. Back to Open suits a status that waits on the client; leave it for one like Scheduled, where a "thanks, see you then" shouldn't put it back in the queue. Whoever it is assigned to is told either way.
|
||||
<?php } ?>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/ticket_status_form.js"></script>
|
||||
92
admin/modals/ticket_status/ticket_status_remove.php
Normal file
92
admin/modals/ticket_status/ticket_status_remove.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* Switch off or delete one of your own ticket statuses (?action=off or
|
||||
* delete) - saying first how many tickets are in it, and where they go:
|
||||
* another status, picked here. Switching off can leave them where they are
|
||||
* (they stay in it, but it can't be picked again); deleting can't. Canned
|
||||
* responses that set it are said too - after a delete they leave the status
|
||||
* as it is. The work is ticketStatusSetActive() and ticketStatusDelete().
|
||||
*/
|
||||
|
||||
require_once '../../includes/modal_header.php';
|
||||
|
||||
$ticket_status = ticketStatusRow(intval($_GET['id'] ?? 0));
|
||||
$delete = ($_GET['action'] ?? '') === 'delete';
|
||||
|
||||
ob_start();
|
||||
|
||||
if (!$ticket_status || ticketStatusIsBuiltIn($ticket_status['ticket_status_id'])) { ?>
|
||||
<div class="modal-body text-center text-secondary py-5"><?= $ticket_status ? 'ITFlow\'s own statuses can\'t be switched off or deleted.' : 'That status is gone - it may have been deleted.' ?></div>
|
||||
<?php } else {
|
||||
$ticket_status_id = intval($ticket_status['ticket_status_id']);
|
||||
$name = escapeHtml($ticket_status['ticket_status_name']);
|
||||
$counts = ticketStatusTicketCounts()[$ticket_status_id] ?? ['all' => 0, 'open' => 0, 'shown' => 0];
|
||||
$canned = ticketStatusCannedCount($ticket_status_id);
|
||||
$targets = ticketStatusMoveTargets($ticket_status_id);
|
||||
$tickets = $counts['all'];
|
||||
$plural = function ($n, $one, $many) {
|
||||
return number_format($n) . ' ' . ($n === 1 ? $one : $many);
|
||||
}; ?>
|
||||
|
||||
<div class="modal-header bg-dark">
|
||||
<h5 class="modal-title"><i class="fas fa-fw fa-<?= $delete ? 'trash' : 'power-off' ?> me-2"></i><?= $delete ? 'Delete' : 'Switch off' ?> <strong><?= $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="ticket_status_id" value="<?= $ticket_status_id ?>">
|
||||
|
||||
<div class="modal-body">
|
||||
<p class="mb-3">
|
||||
<?= ticketStatusBadgeHtml($ticket_status['ticket_status_name'], $ticket_status['ticket_status_color'], 'detail') ?>
|
||||
<?php if ($delete) { ?>
|
||||
goes for good. Tickets' history keeps its name.
|
||||
<?php } else { ?>
|
||||
stays here, switched off - it can't be picked, and comes off the board. You can switch it on again.
|
||||
<?php } ?>
|
||||
</p>
|
||||
|
||||
<?php if ($tickets) { ?>
|
||||
<div class="alert alert-<?= $delete ? 'warning' : 'secondary' ?> small py-2">
|
||||
<i class="fas fa-fw fa-life-ring me-1"></i><strong><?= $plural($tickets, 'ticket is', 'tickets are') ?></strong> in it<?= $counts['open'] !== $tickets ? ' - ' . $plural($counts['open'], 'open one', 'open ones') : '' ?>.
|
||||
<a href="/agent/tickets.php?status[]=<?= $ticket_status_id ?>" target="_blank">See them</a>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Move <?= $tickets === 1 ? 'it' : 'them' ?> to <?php if ($delete) { ?><strong class="text-danger">*</strong><?php } ?></label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-fw fa-exchange-alt"></i></span>
|
||||
<select class="form-select" name="move_to"<?= $delete ? ' required' : '' ?>>
|
||||
<?php if (!$delete) { ?>
|
||||
<option value="0">Leave them in <?= $name ?></option>
|
||||
<?php } ?>
|
||||
<?php foreach ($targets as $target_id => $target) { ?>
|
||||
<option value="<?= $target_id ?>"<?= $target_id === TICKET_STATUS_OPEN && $delete ? ' selected' : '' ?> title="<?= escapeHtml(ticketStatusAbout($target)) ?>"><?= escapeHtml($target['ticket_status_name']) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<small class="text-secondary">Each says so in its history, and its SLA clock follows the status it goes to.</small>
|
||||
</div>
|
||||
<?php } else { ?>
|
||||
<p class="small text-secondary"><i class="fas fa-fw fa-check me-1"></i>No tickets are in it.</p>
|
||||
<?php } ?>
|
||||
|
||||
<?php if ($canned) { ?>
|
||||
<p class="small text-secondary mb-0">
|
||||
<i class="fas fa-fw fa-comment-dots me-1"></i><?= $plural($canned, 'canned response sets', 'canned responses set') ?> this status.
|
||||
<?= $delete ? 'They will leave the status as it is instead.' : 'Until it is switched on again, they leave the status as it is.' ?>
|
||||
</p>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<?php if ($delete) { ?>
|
||||
<button type="submit" name="delete_ticket_status" class="btn btn-danger text-bold"><i class="fas fa-trash me-2"></i>Delete</button>
|
||||
<?php } else { ?>
|
||||
<button type="submit" name="switch_off_ticket_status" class="btn btn-primary text-bold"><i class="fas fa-power-off me-2"></i>Switch off</button>
|
||||
<?php } ?>
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php }
|
||||
require_once '../../../includes/modal_footer.php';
|
||||
@@ -1,80 +1,168 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* ITFlow - Admin > Ticket Statuses: add and change a status, put them in
|
||||
* order (dragged, or up and down), switch one off or on, and delete one of
|
||||
* your own - moving its tickets first. The five ITFlow has keep their names,
|
||||
* stay on, and have fixed SLA and client-reply behaviour, whatever a request
|
||||
* says. The work is in functions/ticket_status.php.
|
||||
*/
|
||||
|
||||
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
||||
|
||||
if (isset($_POST['add_ticket_status'])) {
|
||||
if (isset($_POST['add_ticket_status']) || isset($_POST['edit_ticket_status'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$name = escapeSql($_POST['name']);
|
||||
$color = escapeSql($_POST['color']);
|
||||
$row = isset($_POST['edit_ticket_status']) ? ticketStatusRow($_POST['ticket_status_id'] ?? 0) : null;
|
||||
if (isset($_POST['edit_ticket_status']) && !$row) {
|
||||
flashAlert("That status is gone - it may have been deleted.", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$pauses_sla = intval($_POST['pauses_sla'] ?? 0);
|
||||
$fields = ticketStatusFromInput($_POST, $row);
|
||||
$problem = ticketStatusProblem($fields, $row ? $row['ticket_status_id'] : 0);
|
||||
if ($problem !== '') {
|
||||
flashAlert(escapeHtml($problem) . ' The status was not saved.', 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "INSERT INTO ticket_statuses SET ticket_status_name = '$name', ticket_status_color = '$color', ticket_status_pauses_sla = $pauses_sla");
|
||||
|
||||
$ticket_status_id = mysqli_insert_id($mysqli);
|
||||
|
||||
logAudit("Ticket Status", "Create", "$session_name created custom ticket status $name", 0, $ticket_status_id);
|
||||
|
||||
flashAlert("Custom Ticket Status <strong>$name</strong> created");
|
||||
if ($row) {
|
||||
$ticket_status_id = intval($row['ticket_status_id']);
|
||||
$changes = ticketStatusUpdate($row, $fields);
|
||||
if ($changes) {
|
||||
logAudit("Ticket Status", "Edit", "$session_name changed ticket status " . escapeSql($row['ticket_status_name']) . ": " . escapeSql(implode('; ', $changes)), 0, $ticket_status_id);
|
||||
}
|
||||
flashAlert("Status <strong>" . escapeHtml($fields['name']) . "</strong> saved");
|
||||
} else {
|
||||
$ticket_status_id = ticketStatusCreate($fields);
|
||||
logAudit("Ticket Status", "Create", "$session_name added ticket status " . escapeSql($fields['name']) . ($fields['pauses_sla'] ? ' (pauses the resolution clock)' : ''), 0, $ticket_status_id);
|
||||
flashAlert("Status <strong>" . escapeHtml($fields['name']) . "</strong> added - at the end of the order");
|
||||
}
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['edit_ticket_status'])) {
|
||||
if (isset($_GET['move_ticket_status'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$ticket_status_id = intval($_POST['ticket_status_id']);
|
||||
$name = escapeSql($_POST['name']);
|
||||
$color = escapeSql($_POST['color']);
|
||||
$order = intval($_POST['order']);
|
||||
$status = intval($_POST['status']);
|
||||
|
||||
// Built-in statuses have fixed SLA behaviour. The modal renders it read-only, and a
|
||||
// read-only field submits nothing - so the value is decided here rather than taken
|
||||
// from the POST, which also means a hand-made request cannot change it.
|
||||
$pauses_sla = getTicketStatusSlaLock($ticket_status_id);
|
||||
if (is_null($pauses_sla)) {
|
||||
$pauses_sla = intval($_POST['pauses_sla'] ?? 0);
|
||||
// One place up or down among the ones in use
|
||||
$row = ticketStatusRow($_GET['move_ticket_status']);
|
||||
if ($row && intval($row['ticket_status_active']) === 1) {
|
||||
$ids = array_keys(array_filter(ticketStatusesAll(true), function ($status) {
|
||||
return intval($status['ticket_status_active']) === 1;
|
||||
}));
|
||||
$at = array_search(intval($row['ticket_status_id']), $ids, true);
|
||||
$to = ($_GET['direction'] ?? '') === 'up' ? $at - 1 : $at + 1;
|
||||
if ($at !== false && isset($ids[$to])) {
|
||||
[$ids[$at], $ids[$to]] = [$ids[$to], $ids[$at]];
|
||||
ticketStatusReorder($ids);
|
||||
logAudit("Ticket Status", "Edit", "$session_name moved ticket status " . escapeSql($row['ticket_status_name']) . " " . (($_GET['direction'] ?? '') === 'up' ? 'up' : 'down'), 0, intval($row['ticket_status_id']));
|
||||
}
|
||||
}
|
||||
|
||||
mysqli_query($mysqli, "UPDATE ticket_statuses SET ticket_status_name = '$name', ticket_status_color = '$color', ticket_status_order = $order, ticket_status_active = $status, ticket_status_pauses_sla = $pauses_sla WHERE ticket_status_id = $ticket_status_id");
|
||||
|
||||
// Tickets already sitting in this status need their clock reconciled
|
||||
$sql_status_tickets = mysqli_query($mysqli, "SELECT ticket_id FROM tickets WHERE ticket_status = $ticket_status_id AND ticket_closed_at IS NULL AND ticket_archived_at IS NULL");
|
||||
while ($status_ticket_row = mysqli_fetch_assoc($sql_status_tickets)) {
|
||||
syncTicketSlaClock($status_ticket_row['ticket_id']);
|
||||
}
|
||||
|
||||
logAudit("Ticket Status", "Edit", "$session_name edited custom ticket status $name", 0, $ticket_status_id);
|
||||
|
||||
flashAlert("Custom Ticket Status <strong>$name</strong> edited");
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_GET['delete_ticket_status'])) {
|
||||
if (isset($_POST['reorder_ticket_statuses'])) {
|
||||
|
||||
// Dragged into order on Admin > Ticket Statuses - answers with JSON, as the page saves it in the background
|
||||
header('Content-Type: application/json');
|
||||
if (!hash_equals((string) ($_SESSION['csrf_token'] ?? ''), (string) ($_POST['csrf_token'] ?? ''))) {
|
||||
echo json_encode(['ok' => false, 'error' => 'The page is out of date - reload it']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// The ones dragged, a page of the ones in use, in their place among the others in use - offset is how many come before them
|
||||
$current = array_keys(array_filter(ticketStatusesAll(true), function ($status) {
|
||||
return intval($status['ticket_status_active']) === 1;
|
||||
}));
|
||||
$ids = array_values(array_intersect(array_unique(array_map('intval', (array) ($_POST['ticket_status_ids'] ?? []))), $current));
|
||||
$new_order = array_values(array_diff($current, $ids));
|
||||
array_splice($new_order, min(max(0, intval($_POST['offset'] ?? 0)), count($new_order)), 0, $ids);
|
||||
if (ticketStatusReorder($new_order)) {
|
||||
logAudit("Ticket Status", "Edit", "$session_name reordered the ticket statuses");
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
|
||||
}
|
||||
|
||||
if (isset($_GET['switch_on_ticket_status'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$ticket_status_id = intval($_GET['delete_ticket_status']);
|
||||
|
||||
if ($ticket_status_id <= 5) {
|
||||
exit("Can't delete built-in statuses");
|
||||
$row = ticketStatusRow($_GET['switch_on_ticket_status']);
|
||||
if ($row && intval($row['ticket_status_active']) !== 1) {
|
||||
ticketStatusSetActive($row, true);
|
||||
logAudit("Ticket Status", "Edit", "$session_name switched on ticket status " . escapeSql($row['ticket_status_name']), 0, intval($row['ticket_status_id']));
|
||||
flashAlert("Status <strong>" . escapeHtml($row['ticket_status_name']) . "</strong> switched on - it can be picked again");
|
||||
}
|
||||
|
||||
$ticket_status_name = escapeSql(getFieldById('ticket_statuses', $ticket_status_id, 'ticket_status_name'));
|
||||
|
||||
mysqli_query($mysqli, "DELETE FROM ticket_statuses WHERE ticket_status_id = $ticket_status_id");
|
||||
|
||||
logAudit("Ticket Status", "Delete", "$session_name deleted custom ticket status $ticket_status_name");
|
||||
|
||||
flashAlert("Custom Ticket Status <strong>$ticket_status_name</strong> Deleted", 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['switch_off_ticket_status'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$row = ticketStatusRow($_POST['ticket_status_id'] ?? 0);
|
||||
if (!$row) {
|
||||
flashAlert("That status is gone - it may have been deleted.", 'error');
|
||||
redirect();
|
||||
}
|
||||
$name = $row['ticket_status_name'];
|
||||
$move_to = intval($_POST['move_to'] ?? 0);
|
||||
if ($move_to && !isset(ticketStatusMoveTargets($row['ticket_status_id'])[$move_to])) {
|
||||
flashAlert("Its tickets can't be moved there. Nothing was changed.", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
[$ok, $moved] = ticketStatusSetActive($row, false, $move_to);
|
||||
if (!$ok) {
|
||||
flashAlert("ITFlow's own statuses can't be switched off.", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$to_name = $moved ? ticketStatusesAll()[$move_to]['ticket_status_name'] : '';
|
||||
logAudit("Ticket Status", "Edit", "$session_name switched off ticket status " . escapeSql($name) . ($moved ? ", moving $moved tickets to " . escapeSql($to_name) : ''), 0, intval($row['ticket_status_id']));
|
||||
flashAlert("Status <strong>" . escapeHtml($name) . "</strong> switched off" . ($moved ? " - $moved " . ($moved === 1 ? 'ticket' : 'tickets') . " moved to <strong>" . escapeHtml($to_name) . "</strong>" : ''));
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
if (isset($_POST['delete_ticket_status'])) {
|
||||
|
||||
validateCSRFToken();
|
||||
|
||||
$row = ticketStatusRow($_POST['ticket_status_id'] ?? 0);
|
||||
if (!$row) {
|
||||
flashAlert("That status is gone - it may have been deleted.", 'error');
|
||||
redirect();
|
||||
}
|
||||
if (ticketStatusIsBuiltIn($row['ticket_status_id'])) {
|
||||
flashAlert("ITFlow's own statuses can't be deleted.", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$name = $row['ticket_status_name'];
|
||||
$move_to = intval($_POST['move_to'] ?? 0);
|
||||
[$ok, $moved, $canned] = ticketStatusDelete($row, $move_to);
|
||||
if (!$ok) {
|
||||
flashAlert("Pick where its tickets go. Nothing was deleted.", 'error');
|
||||
redirect();
|
||||
}
|
||||
|
||||
$to_name = $moved ? (ticketStatusesAll()[$move_to]['ticket_status_name'] ?? '') : '';
|
||||
logAudit("Ticket Status", "Delete", "$session_name deleted ticket status " . escapeSql($name) . ($moved ? ", moving $moved tickets to " . escapeSql($to_name) : '') . ($canned ? "; $canned canned responses no longer set a status" : ''), 0, intval($row['ticket_status_id']));
|
||||
flashAlert("Status <strong>" . escapeHtml($name) . "</strong> deleted" . ($moved ? " - $moved " . ($moved === 1 ? 'ticket' : 'tickets') . " moved to <strong>" . escapeHtml($to_name) . "</strong>" : ''), 'error');
|
||||
|
||||
redirect();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,118 +1,205 @@
|
||||
<?php
|
||||
|
||||
// Default Column Sortby Filter
|
||||
/*
|
||||
* Admin > Ticket Statuses - the steps a ticket goes through. The five ITFlow
|
||||
* has (New, Open, On Hold, Resolved, Closed) and your own - Waiting on
|
||||
* Vendor, Scheduled, Parts Ordered. Each row shows the status as it looks,
|
||||
* what it is for, how many tickets are in it, whether the SLA's resolution
|
||||
* clock runs in it and what a client's reply does to a ticket in it. In use
|
||||
* are in the order the pickers and the board show them, and can be dragged
|
||||
* into another; switched off ones are kept, but can't be picked. The work is
|
||||
* in functions/ticket_status.php; the handlers are admin/post/ticket_status.php.
|
||||
*/
|
||||
|
||||
// Default Column Sortby Filter - the order the pickers and the board show them in
|
||||
$sort = "ticket_status_order";
|
||||
$order = "ASC";
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
$sql = mysqli_query(
|
||||
$mysqli,
|
||||
"SELECT SQL_CALC_FOUND_ROWS ticket_status_active, ticket_status_color, ticket_status_id, ticket_status_name,
|
||||
ticket_status_pauses_sla FROM ticket_statuses
|
||||
WHERE ticket_status_name LIKE '%$q%'
|
||||
ORDER BY $sort $order LIMIT $record_from, $record_to"
|
||||
);
|
||||
$builtins = ticketStatusBuiltIns();
|
||||
|
||||
$views = [
|
||||
'' => ['label' => 'In use', 'icon' => 'fa-check-circle', 'where' => 'ticket_status_active = 1', 'title' => 'In the pickers and on the board'],
|
||||
'off' => ['label' => 'Switched off', 'icon' => 'fa-power-off', 'where' => 'ticket_status_active = 0', 'title' => 'Kept, but can\'t be picked'],
|
||||
];
|
||||
$show = (string) ($_GET['show'] ?? '');
|
||||
$show = isset($views[$show]) ? $show : '';
|
||||
|
||||
// Only these sort - an unknown column would take the page down
|
||||
$sortable_columns = [
|
||||
'ticket_status_order' => "ticket_status_order $order, ticket_status_id ASC",
|
||||
'ticket_status_name' => "ticket_status_name $order",
|
||||
'tickets' => "n_tickets $order, ticket_status_order ASC",
|
||||
'ticket_status_pauses_sla' => "ticket_status_pauses_sla $order, ticket_status_order ASC",
|
||||
];
|
||||
if (!isset($sortable_columns[$sort])) {
|
||||
$sort = 'ticket_status_order';
|
||||
}
|
||||
|
||||
$filters = "(ticket_status_name LIKE '%$q%'" . (ticketStatusColumnsReady() ? " OR ticket_status_description LIKE '%$q%'" : '') . ")";
|
||||
|
||||
$sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS " . ticketStatusColumnsSql() . ",
|
||||
(SELECT COUNT(*) FROM tickets WHERE ticket_status = ticket_status_id AND ticket_archived_at IS NULL) AS n_tickets
|
||||
FROM ticket_statuses
|
||||
WHERE $filters AND {$views[$show]['where']}
|
||||
ORDER BY {$sortable_columns[$sort]}
|
||||
LIMIT $record_from, $record_to");
|
||||
|
||||
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
|
||||
$counts = mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', array_map(function ($view) {
|
||||
return "COALESCE(SUM({$view['where']}), 0)";
|
||||
}, $views)) . " FROM ticket_statuses WHERE $filters"));
|
||||
$view_counts = array_combine(array_keys($views), $counts);
|
||||
|
||||
// Dragging into order: the ones in use, in their order, with nothing searched
|
||||
$can_reorder = $show === '' && $sort === 'ticket_status_order' && $order === 'ASC' && $q === '' && $num_rows[0] > 1;
|
||||
|
||||
$sort_link = function ($column, $label) use ($url_query_strings_sort, $sort, $disp, $order_icon) {
|
||||
return '<a class="text-reset text-decoration-none" href="?' . escapeHtml($url_query_strings_sort) . '&sort=' . $column . '&order=' . $disp . '">'
|
||||
. $label . ($sort === $column ? " $order_icon" : '') . '</a>';
|
||||
};
|
||||
$pill_url = function ($view) {
|
||||
return '?' . http_build_query(array_filter(['q' => $_GET['q'] ?? '', 'show' => $view]));
|
||||
};
|
||||
|
||||
$csrf = $_SESSION['csrf_token'];
|
||||
|
||||
?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-info-circle me-2"></i>Tickets Statuses</h3>
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-info-circle me-2"></i>Ticket Statuses</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/ticket_status/ticket_status_add.php"><i class="fas fa-plus me-2"></i>New Ticket Status</button>
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/ticket_status/ticket_status_add.php">
|
||||
<i class="fas fa-plus"></i><span class="d-none d-lg-inline ms-2">New Status</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-header py-3">
|
||||
<form autocomplete="off">
|
||||
<?php if ($show !== '') { ?><input type="hidden" name="show" value="<?= escapeHtml($show) ?>"><?php } ?>
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-md-4">
|
||||
<div class="col-lg-5 col-md-6">
|
||||
<div class="input-group">
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search Ticket Statuses">
|
||||
<button class="btn btn-primary" title="Search"><i class="fa fa-search"></i></button>
|
||||
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search statuses..." title="Name or what it is for">
|
||||
<button class="btn btn-dark" title="Search"><i class="fa fa-search"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<nav class="nav nav-pills itflow-asset-tabs mt-2">
|
||||
<?php foreach ($views as $view_key => $view) { ?>
|
||||
<a class="nav-link <?= $show === (string) $view_key ? 'active' : '' ?>" href="<?= escapeHtml($pill_url($view_key)) ?>" title="<?= escapeHtml($view['title']) ?>"><i class="fas fa-fw <?= $view['icon'] ?> me-1"></i><?= escapeHtml($view['label']) ?> <span class="badge rounded-pill ms-1"><?= intval($view_counts[$view_key]) ?></span></a>
|
||||
<?php } ?>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped table-borderless table-hover mb-0">
|
||||
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light text-nowrap <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th class="ps-3">
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ticket_status_name&order=<?= $disp ?>">
|
||||
Name <?php if ($sort == 'ticket_status_name') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ticket_status_color&order=<?= $disp ?>">
|
||||
Color <?php if ($sort == 'ticket_status_color') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=ticket_status_active&order=<?= $disp ?>">
|
||||
Status <?php if ($sort == 'ticket_status_active') { echo $order_icon; } ?>
|
||||
</a>
|
||||
</th>
|
||||
<th>SLA</th>
|
||||
<th class="text-center">Action</th>
|
||||
<?php if ($can_reorder) { ?><th style="width: 1%"><span class="visually-hidden">Order</span></th><?php } ?>
|
||||
<th class="<?= $can_reorder ? 'ps-0' : 'ps-3' ?>"><?= $sort_link('ticket_status_name', 'Status') ?></th>
|
||||
<th class="text-end"><?= $sort_link('tickets', 'Tickets') ?></th>
|
||||
<th class="d-none d-md-table-cell"><?= $sort_link('ticket_status_pauses_sla', 'SLA clock') ?></th>
|
||||
<th class="d-none d-lg-table-cell">When the client replies</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody<?= $can_reorder ? ' data-ticket-status-sortable data-ticket-status-offset="' . intval($record_from) . '"' : '' ?>>
|
||||
<?php
|
||||
|
||||
$position = $record_from - 1;
|
||||
while ($row = mysqli_fetch_assoc($sql)) {
|
||||
$position++;
|
||||
$ticket_status_id = intval($row['ticket_status_id']);
|
||||
$ticket_status_name = escapeHtml($row['ticket_status_name']);
|
||||
$ticket_status_color = escapeHtml($row['ticket_status_color']);
|
||||
$ticket_status_active = intval($row['ticket_status_active']);
|
||||
$ticket_status_pauses_sla = intval($row['ticket_status_pauses_sla']);
|
||||
if ($ticket_status_active) {
|
||||
$ticket_status_display = "<div class='text-success text-bold'>Active</div>";
|
||||
} else {
|
||||
$ticket_status_display = "<div class='text-secondary'>Inactive</div>";
|
||||
}
|
||||
|
||||
$name = escapeHtml($row['ticket_status_name']);
|
||||
$builtin = $builtins[$ticket_status_id] ?? null;
|
||||
$about = ticketStatusAbout($row);
|
||||
$active = intval($row['ticket_status_active']) === 1;
|
||||
$paused = intval($row['ticket_status_pauses_sla']) === 1;
|
||||
$reply = TICKET_STATUS_CLIENT_REPLY[intval($row['ticket_status_client_reply']) === 0 ? 0 : 1];
|
||||
$tickets = intval($row['n_tickets']);
|
||||
$edit_url = "modals/ticket_status/ticket_status_edit.php?id=$ticket_status_id";
|
||||
$tickets_url = "/agent/tickets.php?status[]=$ticket_status_id";
|
||||
$fixed = $builtin ? ' <i class="fas fa-lock small text-body-tertiary" title="Fixed for the statuses ITFlow has"></i>' : '';
|
||||
$clock_html = $ticket_status_id === TICKET_STATUS_CLOSED
|
||||
? '<span class="text-body-secondary">Stopped</span>'
|
||||
: ($paused ? '<span class="text-warning-emphasis"><i class="fas fa-fw fa-pause-circle me-1"></i>Paused</span>' : '<span class="text-body-secondary"><i class="fas fa-fw fa-play-circle me-1"></i>Runs</span>');
|
||||
$reply_html = $ticket_status_id === TICKET_STATUS_CLOSED
|
||||
? '<span class="text-body-secondary" title="A closed ticket can\'t be replied to - the client is told to raise a new one">Can\'t reply</span>'
|
||||
: '<span class="text-body-secondary"><i class="fas fa-fw ' . $reply['icon'] . ' me-1"></i>' . ([TICKET_STATUS_NEW => 'Opens it', TICKET_STATUS_OPEN => 'Stays Open', TICKET_STATUS_RESOLVED => 'Reopens it'][$ticket_status_id] ?? $reply['short']) . '</span>';
|
||||
?>
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<a href="#"
|
||||
<?php if ( $ticket_status_id > 5 ) { ?>
|
||||
class="ajax-modal" data-modal-url="modals/ticket_status/ticket_status_edit.php?id=<?= $ticket_status_id ?>"
|
||||
<?php } ?>
|
||||
>
|
||||
<?= $ticket_status_name ?>
|
||||
</a>
|
||||
<tr data-ticket-status-id="<?= $ticket_status_id ?>">
|
||||
<?php if ($can_reorder) { ?>
|
||||
<td class="pe-0"><span class="itflow-category-grip text-body-secondary" title="Drag to move" data-ticket-status-grip><i class="fas fa-grip-vertical" aria-hidden="true"></i></span></td>
|
||||
<?php } ?>
|
||||
<td class="<?= $can_reorder ? 'ps-0' : 'ps-3' ?>">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<span class="itflow-asset-icon itflow-asset-tone-other d-none d-sm-inline-flex" aria-hidden="true"><i class="fas fa-fw <?= $builtin ? $builtin['icon'] : 'fa-tag' ?>"></i></span>
|
||||
<div class="min-w-0">
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<a class="ajax-modal text-decoration-none" href="#" data-modal-url="<?= $edit_url ?>" title="Edit <?= $name ?>"><?= ticketStatusBadgeHtml($row['ticket_status_name'], $row['ticket_status_color'], 'list') ?></a>
|
||||
<?php if ($builtin) { ?>
|
||||
<span class="badge rounded-pill text-bg-light border fw-normal" title="<?= escapeHtml($builtin['set_by']) ?>"><i class="fas fa-fw fa-lock text-secondary me-1"></i>Built in</span>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<?php if ($about !== '') { ?>
|
||||
<div class="small text-body-secondary mt-1"><?= escapeHtml($about) ?></div>
|
||||
<?php } ?>
|
||||
<!-- On smaller screens the clock and replies come along under the name -->
|
||||
<div class="d-lg-none mt-1 small d-flex flex-wrap gap-3">
|
||||
<span class="d-md-none"><?= $clock_html ?></span>
|
||||
<span title="When the client replies"><?= $reply_html ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class='badge rounded-pill text-light p-2' style="background-color: <?= $ticket_status_color ?>"><?= $ticket_status_name ?></span>
|
||||
<td><?= $ticket_status_display ?></td>
|
||||
<td>
|
||||
<?php if ($ticket_status_pauses_sla) { ?>
|
||||
<span class="text-warning"><i class="fas fa-fw fa-pause-circle me-1"></i>Paused</span>
|
||||
<td class="text-end text-nowrap">
|
||||
<?php if ($tickets) { ?>
|
||||
<a href="<?= escapeHtml($tickets_url) ?>" title="See the tickets in <?= $name ?>"><?= number_format($tickets) ?></a>
|
||||
<?php } else { ?>
|
||||
<span class="text-secondary">Running</span>
|
||||
<?php } ?>
|
||||
<?php if (!is_null(getTicketStatusSlaLock($ticket_status_id))) { ?>
|
||||
<small class="text-muted ms-1" title="Built-in statuses have fixed SLA behaviour">(fixed)</small>
|
||||
<span class="text-body-tertiary">0</span>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown dropstart text-center">
|
||||
<button class="btn btn-secondary btn-sm" title="More actions" type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<td class="d-none d-md-table-cell text-nowrap"><?= $clock_html . $fixed ?></td>
|
||||
<td class="d-none d-lg-table-cell text-nowrap"><?= $reply_html . $fixed ?></td>
|
||||
<td class="text-end">
|
||||
<div class="dropdown dropstart">
|
||||
<button class="btn btn-sm btn-light" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-fw fa-ellipsis-v"></i></button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/ticket_status/ticket_status_edit.php?id=<?= $ticket_status_id ?>">
|
||||
<i class="fas fa-fw fa-edit me-2"></i>Edit
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="<?= $edit_url ?>">
|
||||
<i class="fas fa-fw fa-edit text-secondary me-2"></i>Edit
|
||||
</a>
|
||||
<?php if (!$ticket_status_active) { ?>
|
||||
<?php if ($tickets) { ?>
|
||||
<a class="dropdown-item" href="<?= escapeHtml($tickets_url) ?>">
|
||||
<i class="fas fa-fw fa-life-ring text-secondary me-2"></i>See its tickets
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php if ($can_reorder) { ?>
|
||||
<?php if ($position > 0) { ?>
|
||||
<a class="dropdown-item" href="post.php?move_ticket_status=<?= $ticket_status_id ?>&direction=up&csrf_token=<?= $csrf ?>">
|
||||
<i class="fas fa-fw fa-arrow-up text-secondary me-2"></i>Move up
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php if ($position < $num_rows[0] - 1) { ?>
|
||||
<a class="dropdown-item" href="post.php?move_ticket_status=<?= $ticket_status_id ?>&direction=down&csrf_token=<?= $csrf ?>">
|
||||
<i class="fas fa-fw fa-arrow-down text-secondary me-2"></i>Move down
|
||||
</a>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
<?php if (!$builtin) { ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_ticket_status=<?= $ticket_status_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<?php if ($active) { ?>
|
||||
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/ticket_status/ticket_status_remove.php?id=<?= $ticket_status_id ?>&action=off" title="Kept, but it can't be picked - and it comes off the board">
|
||||
<i class="fas fa-fw fa-power-off text-secondary me-2"></i>Switch off
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
<a class="dropdown-item" href="post.php?switch_on_ticket_status=<?= $ticket_status_id ?>&csrf_token=<?= $csrf ?>" title="Back in the pickers and on the board">
|
||||
<i class="fas fa-fw fa-power-off text-secondary me-2"></i>Switch on
|
||||
</a>
|
||||
<?php } ?>
|
||||
<a class="dropdown-item text-danger ajax-modal" href="#" data-modal-url="modals/ticket_status/ticket_status_remove.php?id=<?= $ticket_status_id ?>&action=delete">
|
||||
<i class="fas fa-fw fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
<?php } ?>
|
||||
@@ -120,24 +207,66 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if ($can_reorder) { ?>
|
||||
<div class="small text-body-secondary px-3 py-2 border-top"><i class="fas fa-fw fa-grip-vertical me-1"></i>Drag them into the order the status pickers and the ticket board show them in.</div>
|
||||
<?php } elseif ($show === '' && $q !== '') { ?>
|
||||
<div class="small text-body-secondary px-3 py-2 border-top">Clear the search to drag them into order.</div>
|
||||
<?php } ?>
|
||||
<?php
|
||||
$empty_state = [
|
||||
'thing' => 'ticket statuses',
|
||||
'icon' => 'fa-info-circle',
|
||||
'actions' => [['label' => 'New Ticket Status', 'modal' => 'modals/ticket_status/ticket_status_add.php']],
|
||||
'filtered' => $q !== '',
|
||||
'views' => ['show'],
|
||||
'view' => $show === 'off' ? [
|
||||
'title' => 'None switched off',
|
||||
'icon' => 'fa-power-off',
|
||||
'text' => 'A status you switch off is kept here - it can\'t be picked, and comes off the board.',
|
||||
] : [],
|
||||
'text' => 'The steps a ticket goes through - add your own, like Waiting on Vendor or Scheduled.',
|
||||
'actions' => [['label' => 'New Status', 'modal' => 'modals/ticket_status/ticket_status_add.php']],
|
||||
];
|
||||
require_once "../includes/filter_footer.php";
|
||||
?>
|
||||
<?php require_once "../includes/filter_footer.php"; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($can_reorder) { ?>
|
||||
<script src="/libs/SortableJS/Sortable.min.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
// Dragged into order - saved as soon as one is dropped, in their place among the rest (offset - the ones on the pages before);
|
||||
// a failure reloads the page to show the order kept
|
||||
const body = document.querySelector('[data-ticket-status-sortable]');
|
||||
new Sortable(body, {
|
||||
handle: '[data-ticket-status-grip]',
|
||||
animation: 150,
|
||||
onEnd: function (event) {
|
||||
if (event.oldIndex === event.newIndex) {
|
||||
return;
|
||||
}
|
||||
const ids = Array.from(body.querySelectorAll('tr[data-ticket-status-id]')).map(function (row) {
|
||||
return row.dataset.ticketStatusId;
|
||||
});
|
||||
itflowPost('post.php', { reorder_ticket_statuses: 1, csrf_token: '<?= $csrf ?>', offset: body.dataset.ticketStatusOffset, ticket_status_ids: ids })
|
||||
.then(function (text) {
|
||||
const result = JSON.parse(text);
|
||||
if (!result.ok) {
|
||||
throw new Error(result.error || 'Not saved');
|
||||
}
|
||||
itflowToast('Order saved', 'success');
|
||||
})
|
||||
.catch(function () {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php } ?>
|
||||
|
||||
<?php
|
||||
require_once "../includes/footer.php";
|
||||
|
||||
Reference in New Issue
Block a user