mirror of
https://github.com/itflow-org/itflow
synced 2026-08-05 07:07:14 +00:00
Add optional ticket SLAs
Response/resolution targets stamped at creation from per-client/priority assignments, business-hours due date math, warn/breach alert stages via cron/ticket_sla.php, ticket list coloring, per-ticket SLA override, admin page for plans/assignments/business hours. DB update 2.5.0. No behavior change unless SLAs are assigned. Bulk reply now only counts Public replies as first response.
This commit is contained in:
274
admin/sla.php
Normal file
274
admin/sla.php
Normal file
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
|
||||
require_once "includes/inc_all_admin.php";
|
||||
|
||||
// Current SLA / business hours settings
|
||||
$sql_sla_settings = mysqli_query($mysqli, "SELECT config_business_days, config_business_hours_start, config_business_hours_end, config_sla_warning_percent, config_sla_notification_email FROM settings WHERE company_id = 1");
|
||||
$row = mysqli_fetch_assoc($sql_sla_settings);
|
||||
$config_business_days = explode(',', $row['config_business_days']);
|
||||
$config_business_hours_start = substr($row['config_business_hours_start'], 0, 5);
|
||||
$config_business_hours_end = substr($row['config_business_hours_end'], 0, 5);
|
||||
$config_sla_warning_percent = intval($row['config_sla_warning_percent']);
|
||||
$config_sla_notification_email = escapeHtml($row['config_sla_notification_email']);
|
||||
|
||||
// SLA plans (active first)
|
||||
$sql_slas = mysqli_query($mysqli, "SELECT * FROM slas ORDER BY sla_archived_at IS NOT NULL, sla_name ASC");
|
||||
|
||||
// Active plans for the assignment dropdowns
|
||||
$active_slas = [];
|
||||
$sql_active_slas = mysqli_query($mysqli, "SELECT sla_id, sla_name FROM slas WHERE sla_archived_at IS NULL ORDER BY sla_name ASC");
|
||||
while ($active_sla_row = mysqli_fetch_assoc($sql_active_slas)) {
|
||||
$active_slas[intval($active_sla_row['sla_id'])] = $active_sla_row['sla_name'];
|
||||
}
|
||||
|
||||
// Assignments, keyed for easy lookup: [client_id][priority] = sla_id
|
||||
$assignments = [];
|
||||
$assignment_client_names = [];
|
||||
$sql_assignments = mysqli_query($mysqli, "SELECT sla_assignment_client_id, sla_assignment_priority, sla_assignment_sla_id, client_name FROM sla_assignments
|
||||
LEFT JOIN clients ON sla_assignment_client_id = client_id
|
||||
ORDER BY client_name ASC"
|
||||
);
|
||||
while ($assignment_row = mysqli_fetch_assoc($sql_assignments)) {
|
||||
$assignment_client_id = intval($assignment_row['sla_assignment_client_id']);
|
||||
$assignments[$assignment_client_id][$assignment_row['sla_assignment_priority']] = intval($assignment_row['sla_assignment_sla_id']);
|
||||
if ($assignment_client_id) {
|
||||
$assignment_client_names[$assignment_client_id] = $assignment_row['client_name'];
|
||||
}
|
||||
}
|
||||
|
||||
// Wording for an assignment cell: mapped plan, explicit none, or global default
|
||||
function slaAssignmentWording($assignments, $active_slas, $client_id, $priority)
|
||||
{
|
||||
if (!isset($assignments[$client_id][$priority])) {
|
||||
return "<span class='text-secondary'>Default</span>";
|
||||
}
|
||||
$sla_id = $assignments[$client_id][$priority];
|
||||
if ($sla_id == 0) {
|
||||
return "<span class='text-secondary'>None</span>";
|
||||
}
|
||||
if (isset($active_slas[$sla_id])) {
|
||||
return escapeHtml($active_slas[$sla_id]);
|
||||
}
|
||||
return "<span class='text-secondary'>None (archived)</span>";
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-stopwatch mr-2"></i>SLAs</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/sla/sla_add.php"><i class="fas fa-plus mr-2"></i>New SLA</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<p class="text-muted">SLAs are optional. Tickets only get response/resolution targets when an assignment below matches their client and priority - with nothing assigned, nothing changes.</p>
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped table-borderless table-hover">
|
||||
<thead class="text-dark">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Response</th>
|
||||
<th>Resolution</th>
|
||||
<th>Status</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php while ($sla_row = mysqli_fetch_assoc($sql_slas)) {
|
||||
|
||||
$sla_id = intval($sla_row['sla_id']);
|
||||
$sla_name = escapeHtml($sla_row['sla_name']);
|
||||
$sla_description = escapeHtml($sla_row['sla_description']);
|
||||
$sla_response_minutes = intval($sla_row['sla_response_minutes']);
|
||||
$sla_resolution_minutes = intval($sla_row['sla_resolution_minutes']);
|
||||
$sla_archived_at = $sla_row['sla_archived_at'];
|
||||
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="#" class="ajax-modal" data-modal-url="modals/sla/sla_edit.php?id=<?= $sla_id ?>">
|
||||
<?= $sla_name ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= $sla_description ?></td>
|
||||
<td><?= $sla_response_minutes ?> min</td>
|
||||
<td><?= $sla_resolution_minutes ? "$sla_resolution_minutes min" : "-" ?></td>
|
||||
<td>
|
||||
<?php if ($sla_archived_at) { ?>
|
||||
<div class="text-secondary">Archived</div>
|
||||
<?php } else { ?>
|
||||
<div class="text-success text-bold">Active</div>
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown dropleft text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-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/sla/sla_edit.php?id=<?= $sla_id ?>">
|
||||
<i class="fas fa-fw fa-edit mr-2"></i>Edit
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<?php if ($sla_archived_at) { ?>
|
||||
<a class="dropdown-item" href="post.php?unarchive_sla=<?= $sla_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-undo mr-2"></i>Restore
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?archive_sla=<?= $sla_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-archive mr-2"></i>Archive
|
||||
</a>
|
||||
<?php } ?>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-random mr-2"></i>SLA Assignments</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-primary ajax-modal" data-modal-url="modals/sla/sla_assignment_client.php"><i class="fas fa-plus mr-2"></i>Client Override</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<strong>Default (all clients)</strong>
|
||||
<div class="form-row mt-2">
|
||||
<?php foreach (['Low', 'Medium', 'High'] as $priority) {
|
||||
$selected_sla_id = $assignments[0][$priority] ?? 0;
|
||||
?>
|
||||
<div class="form-group col-md-3">
|
||||
<label><?= $priority ?></label>
|
||||
<select class="form-control" name="global_sla_<?= strtolower($priority) ?>">
|
||||
<option value="0">None</option>
|
||||
<?php foreach ($active_slas as $active_sla_id => $active_sla_name) { ?>
|
||||
<option value="<?= $active_sla_id ?>" <?php if ($selected_sla_id == $active_sla_id) { echo "selected"; } ?>><?= escapeHtml($active_sla_name) ?></option>
|
||||
<?php } ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<div class="form-group col-md-3 d-flex align-items-end">
|
||||
<button type="submit" name="save_sla_assignments" class="btn btn-primary"><i class="fas fa-check mr-2"></i>Save Defaults</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<strong>Client overrides</strong>
|
||||
<div class="table-responsive-sm mt-2">
|
||||
<table class="table table-striped table-borderless table-hover">
|
||||
<thead class="text-dark <?php if (empty($assignment_client_names)) { echo "d-none"; } ?>">
|
||||
<tr>
|
||||
<th>Client</th>
|
||||
<th>Low</th>
|
||||
<th>Medium</th>
|
||||
<th>High</th>
|
||||
<th class="text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($assignment_client_names)) { ?>
|
||||
<tr><td colspan="5" class="text-secondary">No client overrides - everyone follows the defaults above.</td></tr>
|
||||
<?php } ?>
|
||||
<?php foreach ($assignment_client_names as $override_client_id => $override_client_name) { ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="#" class="ajax-modal" data-modal-url="modals/sla/sla_assignment_client.php?client_id=<?= $override_client_id ?>">
|
||||
<?= escapeHtml($override_client_name) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= slaAssignmentWording($assignments, $active_slas, $override_client_id, 'Low') ?></td>
|
||||
<td><?= slaAssignmentWording($assignments, $active_slas, $override_client_id, 'Medium') ?></td>
|
||||
<td><?= slaAssignmentWording($assignments, $active_slas, $override_client_id, 'High') ?></td>
|
||||
<td>
|
||||
<div class="dropdown dropleft text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button" data-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/sla/sla_assignment_client.php?client_id=<?= $override_client_id ?>">
|
||||
<i class="fas fa-fw fa-edit mr-2"></i>Edit
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_client_sla_assignments=<?= $override_client_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
|
||||
<i class="fas fa-fw fa-trash mr-2"></i>Remove
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-dark">
|
||||
<div class="card-header py-2">
|
||||
<h3 class="card-title mt-2"><i class="fas fa-fw fa-business-time mr-2"></i>Business Hours & Notifications</h3>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<form action="post.php" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label>Business days</label>
|
||||
<div>
|
||||
<?php foreach ([1 => 'Mon', 2 => 'Tue', 3 => 'Wed', 4 => 'Thu', 5 => 'Fri', 6 => 'Sat', 7 => 'Sun'] as $day_number => $day_name) { ?>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" name="business_days[]" value="<?= $day_number ?>" id="business_day_<?= $day_number ?>" <?php if (in_array($day_number, $config_business_days)) { echo "checked"; } ?>>
|
||||
<label class="form-check-label" for="business_day_<?= $day_number ?>"><?= $day_name ?></label>
|
||||
</div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
<small class="text-muted">SLA clocks only run during business hours on these days. Unchecking everything makes SLAs count 24x7.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-3">
|
||||
<label>Business hours start</label>
|
||||
<input type="time" class="form-control" name="business_hours_start" value="<?= $config_business_hours_start ?>" required>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label>Business hours end</label>
|
||||
<input type="time" class="form-control" name="business_hours_end" value="<?= $config_business_hours_end ?>" required>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label>Warn at % of target</label>
|
||||
<input type="number" class="form-control" name="warning_percent" min="1" max="99" value="<?= $config_sla_warning_percent ?>" required>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label>Notification email <small class="text-muted">(optional)</small></label>
|
||||
<input type="email" class="form-control" name="notification_email" placeholder="dispatch@example.com" value="<?= $config_sla_notification_email ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<small class="text-muted d-block mb-3">Warnings and breach alerts also notify the assigned agent in-app and by email. Requires cron/ticket_sla.php running every minute.</small>
|
||||
|
||||
<button type="submit" name="edit_sla_settings" class="btn btn-primary"><i class="fas fa-check mr-2"></i>Save Settings</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once "../includes/footer.php";
|
||||
Reference in New Issue
Block a user