style: auto-fix 1340 phpcs PSR-12 violations via phpcbf; exclude MissingNamespace and SideEffects
This commit is contained in:
+17
-16
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* CreateTicketView.php — New ticket creation form, redesigned for TDS v1.2
|
||||
* Variables: $templates (array), $allUsers (array), $error (string|null)
|
||||
@@ -40,7 +41,7 @@ include __DIR__ . '/layout_header.php';
|
||||
value="<?= htmlspecialchars(CsrfMiddleware::getToken(), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<input type="hidden" name="link_duplicate_of" id="linkDuplicateOf" value="">
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<?php if (isset($error)) : ?>
|
||||
<div class="lt-msg lt-msg-danger lt-mb-md" role="alert">
|
||||
<strong>Error:</strong> <?= htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
|
||||
</div>
|
||||
@@ -55,12 +56,12 @@ include __DIR__ . '/layout_header.php';
|
||||
<label class="lt-label" for="templateSelect">Use a Template</label>
|
||||
<select id="templateSelect" class="lt-select" data-action="load-template">
|
||||
<option value="">— No Template —</option>
|
||||
<?php if (!empty($templates)): ?>
|
||||
<?php foreach ($templates as $tpl): ?>
|
||||
<?php if (!empty($templates)) : ?>
|
||||
<?php foreach ($templates as $tpl) : ?>
|
||||
<option value="<?= (int)$tpl['template_id'] ?>">
|
||||
<?= htmlspecialchars($tpl['template_name']) ?>
|
||||
<?= htmlspecialchars($tpl['template_name']) ?>
|
||||
</option>
|
||||
<?php endforeach ?>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
</select>
|
||||
<p class="lt-form-hint">Selecting a template pre-fills the form fields.</p>
|
||||
@@ -157,12 +158,12 @@ include __DIR__ . '/layout_header.php';
|
||||
<label class="lt-label" for="assigned_to">Assign To</label>
|
||||
<select id="assigned_to" name="assigned_to" class="lt-select">
|
||||
<option value="">— Unassigned —</option>
|
||||
<?php if (!empty($allUsers)): ?>
|
||||
<?php foreach ($allUsers as $u): ?>
|
||||
<?php if (!empty($allUsers)) : ?>
|
||||
<?php foreach ($allUsers as $u) : ?>
|
||||
<option value="<?= (int)$u['user_id'] ?>">
|
||||
<?= htmlspecialchars($u['display_name'] ?? $u['username']) ?>
|
||||
<?= htmlspecialchars($u['display_name'] ?? $u['username']) ?>
|
||||
</option>
|
||||
<?php endforeach ?>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
</select>
|
||||
<p class="lt-form-hint">Leave blank to create as unassigned.</p>
|
||||
@@ -189,19 +190,19 @@ include __DIR__ . '/layout_header.php';
|
||||
<label class="lt-label lt-text-cyan">Allowed Groups</label>
|
||||
<div class="visibility-groups-list lt-flex lt-flex-wrap lt-flex-gap-sm">
|
||||
<?php
|
||||
require_once __DIR__ . '/../models/UserModel.php';
|
||||
$userModel = new UserModel($conn);
|
||||
$allGroups = $userModel->getAllGroups();
|
||||
foreach ($allGroups as $group):
|
||||
?>
|
||||
require_once __DIR__ . '/../models/UserModel.php';
|
||||
$userModel = new UserModel($conn);
|
||||
$allGroups = $userModel->getAllGroups();
|
||||
foreach ($allGroups as $group) :
|
||||
?>
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox visibility-group-checkbox"
|
||||
name="visibility_groups[]"
|
||||
value="<?= htmlspecialchars($group, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<span class="lt-badge"><?= htmlspecialchars($group) ?></span>
|
||||
</label>
|
||||
<?php endforeach ?>
|
||||
<?php if (empty($allGroups)): ?>
|
||||
<?php endforeach ?>
|
||||
<?php if (empty($allGroups)) : ?>
|
||||
<span class="lt-text-muted lt-text-sm">No groups available</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
|
||||
+157
-136
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* DashboardView.php — Main ticket dashboard, redesigned for TDS v1.2.
|
||||
*
|
||||
@@ -53,21 +54,26 @@ if (!empty($_GET['type'])) {
|
||||
$activeFilters[] = ['type' => 'type', 'value' => $_GET['type'], 'label' => 'Type: ' . htmlspecialchars($_GET['type'])];
|
||||
}
|
||||
if (!empty($_GET['assigned_to'])) {
|
||||
$label = match($_GET['assigned_to']) { 'unassigned' => 'Unassigned', 'me' => 'Me', default => 'User #' . htmlspecialchars($_GET['assigned_to']) };
|
||||
$label = match ($_GET['assigned_to']) {
|
||||
'unassigned' => 'Unassigned', 'me' => 'Me', default => 'User #' . htmlspecialchars($_GET['assigned_to'])
|
||||
};
|
||||
$activeFilters[] = ['type' => 'assigned_to', 'value' => $_GET['assigned_to'], 'label' => 'Assigned: ' . $label];
|
||||
}
|
||||
if (!empty($_GET['created_from']) || !empty($_GET['created_to'])) {
|
||||
$from = $_GET['created_from'] ?? ''; $to = $_GET['created_to'] ?? '';
|
||||
$from = $_GET['created_from'] ?? '';
|
||||
$to = $_GET['created_to'] ?? '';
|
||||
$label = $from === $to && $from ? 'Created: ' . $from : 'Created: ' . ($from ?: '…') . ' – ' . ($to ?: '…');
|
||||
$activeFilters[] = ['type' => 'created_from', 'value' => $from, 'label' => $label];
|
||||
}
|
||||
if (!empty($_GET['updated_from']) || !empty($_GET['updated_to'])) {
|
||||
$from = $_GET['updated_from'] ?? ''; $to = $_GET['updated_to'] ?? '';
|
||||
$from = $_GET['updated_from'] ?? '';
|
||||
$to = $_GET['updated_to'] ?? '';
|
||||
$label = $from === $to && $from ? 'Updated: ' . $from : 'Updated: ' . ($from ?: '…') . ' – ' . ($to ?: '…');
|
||||
$activeFilters[] = ['type' => 'updated_from', 'value' => $from, 'label' => $label];
|
||||
}
|
||||
if (!empty($_GET['closed_from']) || !empty($_GET['closed_to'])) {
|
||||
$from = $_GET['closed_from'] ?? ''; $to = $_GET['closed_to'] ?? '';
|
||||
$from = $_GET['closed_from'] ?? '';
|
||||
$to = $_GET['closed_to'] ?? '';
|
||||
$label = $from === $to && $from ? 'Closed: ' . $from : 'Closed: ' . ($from ?: '…') . ' – ' . ($to ?: '…');
|
||||
$activeFilters[] = ['type' => 'closed_from', 'value' => $from, 'label' => $label];
|
||||
}
|
||||
@@ -99,19 +105,19 @@ include __DIR__ . '/layout_header.php';
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
STATS GRID
|
||||
═══════════════════════════════════════════════════════════ -->
|
||||
<?php if (isset($stats)): ?>
|
||||
<?php if (isset($stats)) : ?>
|
||||
<div class="lt-stats-grid" id="statsGrid">
|
||||
|
||||
<?php
|
||||
<?php
|
||||
// Trend indicators — derived from existing stats without extra DB query
|
||||
// Logic: if more closed today than created → improving (green), if more created → warn, else idle
|
||||
$trendOpen = ($stats['closed_today'] > $stats['created_today']) ? 'lt-dot-up' :
|
||||
$trendOpen = ($stats['closed_today'] > $stats['created_today']) ? 'lt-dot-up' :
|
||||
($stats['created_today'] > $stats['closed_today'] ? 'lt-dot-warn' : 'lt-dot-idle');
|
||||
$trendCrit = ($stats['critical'] > 0) ? 'lt-dot-warn' : 'lt-dot-up';
|
||||
$trendUnassi = ($stats['unassigned'] > 0) ? 'lt-dot-warn' : 'lt-dot-idle';
|
||||
$trendToday = ($stats['created_today'] > 0) ? 'lt-dot-warn' : 'lt-dot-idle';
|
||||
$trendClosed = ($stats['closed_today'] > 0) ? 'lt-dot-up' : 'lt-dot-idle';
|
||||
?>
|
||||
$trendCrit = ($stats['critical'] > 0) ? 'lt-dot-warn' : 'lt-dot-up';
|
||||
$trendUnassi = ($stats['unassigned'] > 0) ? 'lt-dot-warn' : 'lt-dot-idle';
|
||||
$trendToday = ($stats['created_today'] > 0) ? 'lt-dot-warn' : 'lt-dot-idle';
|
||||
$trendClosed = ($stats['closed_today'] > 0) ? 'lt-dot-up' : 'lt-dot-idle';
|
||||
?>
|
||||
|
||||
<div class="lt-stat-card stat-open" role="button" tabindex="0"
|
||||
data-filter-key="status" data-filter-val="Open,Pending,In Progress"
|
||||
@@ -177,21 +183,26 @@ include __DIR__ . '/layout_header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$avgHours = $stats['avg_resolution_hours'] ?? 0;
|
||||
if ($avgHours <= 0) {
|
||||
$avgDisplay = '—'; $avgUnit = '';
|
||||
} elseif ($avgHours < 1) {
|
||||
$avgDisplay = (string)max(1, (int)round($avgHours * 60)); $avgUnit = 'min';
|
||||
} elseif ($avgHours < 48) {
|
||||
$avgDisplay = (string)(int)round($avgHours); $avgUnit = 'hr';
|
||||
} elseif ($avgHours < 336) { // <14 days
|
||||
$avgDisplay = number_format($avgHours / 24, 1); $avgUnit = 'days';
|
||||
} else {
|
||||
$avgDisplay = number_format($avgHours / 168, 1); $avgUnit = 'wks';
|
||||
}
|
||||
$avgTitle = $avgHours > 0 ? number_format($avgHours, 1) . ' hours' : 'No data';
|
||||
?>
|
||||
<?php
|
||||
$avgHours = $stats['avg_resolution_hours'] ?? 0;
|
||||
if ($avgHours <= 0) {
|
||||
$avgDisplay = '—';
|
||||
$avgUnit = '';
|
||||
} elseif ($avgHours < 1) {
|
||||
$avgDisplay = (string)max(1, (int)round($avgHours * 60));
|
||||
$avgUnit = 'min';
|
||||
} elseif ($avgHours < 48) {
|
||||
$avgDisplay = (string)(int)round($avgHours);
|
||||
$avgUnit = 'hr';
|
||||
} elseif ($avgHours < 336) { // <14 days
|
||||
$avgDisplay = number_format($avgHours / 24, 1);
|
||||
$avgUnit = 'days';
|
||||
} else {
|
||||
$avgDisplay = number_format($avgHours / 168, 1);
|
||||
$avgUnit = 'wks';
|
||||
}
|
||||
$avgTitle = $avgHours > 0 ? number_format($avgHours, 1) . ' hours' : 'No data';
|
||||
?>
|
||||
<div class="lt-stat-card stat-time" title="Average resolution time: <?= htmlspecialchars($avgTitle) ?>" aria-label="Avg resolution time">
|
||||
<div class="lt-stat-icon lt-text-muted">⏱</div>
|
||||
<div class="lt-stat-info">
|
||||
@@ -332,7 +343,7 @@ include __DIR__ . '/layout_header.php';
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php if (!empty($stats['by_assignee'])): ?>
|
||||
<?php if (!empty($stats['by_assignee'])) : ?>
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
TEAM WORKLOAD PANEL
|
||||
═══════════════════════════════════════════════════════════ -->
|
||||
@@ -347,7 +358,7 @@ include __DIR__ . '/layout_header.php';
|
||||
$maxLoad = max(array_column($byAssignee, 'open_count') ?: [1]);
|
||||
?>
|
||||
<div class="workload-grid">
|
||||
<?php foreach ($byAssignee as $a):
|
||||
<?php foreach ($byAssignee as $a) :
|
||||
$count = (int)$a['open_count'];
|
||||
$name = $a['display_name'] ?? $a['username'] ?? 'Unknown';
|
||||
$pct = $maxLoad > 0 ? round(($count / $maxLoad) * 100) : 0;
|
||||
@@ -357,10 +368,10 @@ include __DIR__ . '/layout_header.php';
|
||||
$avatarColors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', ''];
|
||||
$avatarColor = $avatarColors[abs(crc32($name)) % count($avatarColors)];
|
||||
$userId = (int)($a['user_id'] ?? 0);
|
||||
?>
|
||||
?>
|
||||
<div class="workload-item">
|
||||
<div class="lt-avatar lt-avatar--sm <?= $avatarColor ?>" aria-hidden="true" title="<?= htmlspecialchars($name) ?>">
|
||||
<?php if ($userId > 0): ?>
|
||||
<?php if ($userId > 0) : ?>
|
||||
<img src="/api/user_avatar.php?user_id=<?= $userId ?>" alt="" class="lt-avatar-img">
|
||||
<?php endif ?>
|
||||
<span class="lt-avatar-initials"><?= htmlspecialchars($initials) ?></span>
|
||||
@@ -415,7 +426,7 @@ include __DIR__ . '/layout_header.php';
|
||||
<!-- Status Filter -->
|
||||
<fieldset class="lt-filter-group">
|
||||
<legend class="lt-filter-label">Status</legend>
|
||||
<?php foreach ($GLOBALS['config']['TICKET_STATUSES'] as $s): ?>
|
||||
<?php foreach ($GLOBALS['config']['TICKET_STATUSES'] as $s) : ?>
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox sidebar-filter"
|
||||
name="status" value="<?= htmlspecialchars($s) ?>"
|
||||
@@ -426,32 +437,32 @@ include __DIR__ . '/layout_header.php';
|
||||
</fieldset>
|
||||
|
||||
<!-- Category Filter -->
|
||||
<?php if (!empty($categories)): ?>
|
||||
<?php if (!empty($categories)) : ?>
|
||||
<fieldset class="lt-filter-group">
|
||||
<legend class="lt-filter-label">Category</legend>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<?php foreach ($categories as $cat) : ?>
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox sidebar-filter"
|
||||
name="category" value="<?= htmlspecialchars($cat) ?>"
|
||||
<?= in_array($cat, $currentCategories) ? 'checked' : '' ?>>
|
||||
<?= htmlspecialchars($cat) ?>
|
||||
<?= htmlspecialchars($cat) ?>
|
||||
</label>
|
||||
<?php endforeach ?>
|
||||
<?php endforeach ?>
|
||||
</fieldset>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- Type Filter -->
|
||||
<?php if (!empty($types)): ?>
|
||||
<?php if (!empty($types)) : ?>
|
||||
<fieldset class="lt-filter-group">
|
||||
<legend class="lt-filter-label">Type</legend>
|
||||
<?php foreach ($types as $type): ?>
|
||||
<?php foreach ($types as $type) : ?>
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox sidebar-filter"
|
||||
name="type" value="<?= htmlspecialchars($type) ?>"
|
||||
<?= in_array($type, $currentTypes) ? 'checked' : '' ?>>
|
||||
<?= htmlspecialchars($type) ?>
|
||||
<?= htmlspecialchars($type) ?>
|
||||
</label>
|
||||
<?php endforeach ?>
|
||||
<?php endforeach ?>
|
||||
</fieldset>
|
||||
<?php endif ?>
|
||||
|
||||
@@ -503,10 +514,10 @@ include __DIR__ . '/layout_header.php';
|
||||
<button type="button" id="lt-sidebar-toggle-btn" class="lt-btn lt-btn-ghost lt-btn-sm"
|
||||
aria-label="Toggle filter sidebar" title="Toggle filters">⋮⋮ Filters</button>
|
||||
<form method="GET" action="" class="lt-search-form" role="search">
|
||||
<?php foreach (['status','category','type','sort','dir'] as $p): ?>
|
||||
<?php if (isset($_GET[$p])): ?>
|
||||
<?php foreach (['status','category','type','sort','dir'] as $p) : ?>
|
||||
<?php if (isset($_GET[$p])) : ?>
|
||||
<input type="hidden" name="<?= $p ?>" value="<?= htmlspecialchars($_GET[$p]) ?>">
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
<?php endforeach ?>
|
||||
<div class="lt-search">
|
||||
<input type="text" name="search" class="lt-input lt-search-input"
|
||||
@@ -519,7 +530,7 @@ include __DIR__ . '/layout_header.php';
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost" data-action="open-advanced-search">
|
||||
FILTER
|
||||
</button>
|
||||
<?php if (!empty($_GET['search'])): ?>
|
||||
<?php if (!empty($_GET['search'])) : ?>
|
||||
<a href="?" class="lt-btn lt-btn-sm lt-btn-ghost" aria-label="Clear search">✕</a>
|
||||
<?php endif ?>
|
||||
</form>
|
||||
@@ -529,7 +540,7 @@ include __DIR__ . '/layout_header.php';
|
||||
<?= $totalTickets ?> ticket<?= $totalTickets !== 1 ? 's' : '' ?>
|
||||
</span>
|
||||
<!-- Export dropdown (admin + selection) -->
|
||||
<?php if ($isAdmin): ?>
|
||||
<?php if ($isAdmin) : ?>
|
||||
<div class="lt-dropdown-wrap" id="exportDropdown" style="display:none">
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-ghost lt-dropdown-trigger"
|
||||
id="exportDropdownTrigger"
|
||||
@@ -554,28 +565,28 @@ include __DIR__ . '/layout_header.php';
|
||||
<div id="savedFilterPills" class="saved-filter-pills lt-flex lt-flex-wrap lt-flex-gap-sm" style="display:none;padding:0.35rem 0 0.1rem" aria-label="Saved filters"></div>
|
||||
|
||||
<!-- Active filters bar -->
|
||||
<?php if (!empty($activeFilters)): ?>
|
||||
<?php if (!empty($activeFilters)) : ?>
|
||||
<div class="active-filters-bar lt-flex lt-flex-wrap lt-flex-gap-sm" role="group" aria-label="Active filters">
|
||||
<span class="lt-text-xs lt-text-muted">Active:</span>
|
||||
<?php foreach ($activeFilters as $f): ?>
|
||||
<?php foreach ($activeFilters as $f) : ?>
|
||||
<span class="lt-badge filter-badge"
|
||||
data-filter-type="<?= htmlspecialchars($f['type']) ?>"
|
||||
data-filter-value="<?= htmlspecialchars($f['value']) ?>">
|
||||
<?= htmlspecialchars($f['label']) ?>
|
||||
<?= htmlspecialchars($f['label']) ?>
|
||||
<button type="button" class="filter-remove"
|
||||
data-action="remove-filter"
|
||||
data-filter-type="<?= htmlspecialchars($f['type']) ?>"
|
||||
data-filter-value="<?= htmlspecialchars($f['value']) ?>"
|
||||
aria-label="Remove <?= htmlspecialchars($f['label']) ?> filter">✕</button>
|
||||
</span>
|
||||
<?php endforeach ?>
|
||||
<?php endforeach ?>
|
||||
<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm"
|
||||
data-action="clear-all-filters">CLEAR ALL</button>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- Search results info -->
|
||||
<?php if (!empty($_GET['search'])): ?>
|
||||
<?php if (!empty($_GET['search'])) : ?>
|
||||
<div class="lt-msg lt-msg-info">
|
||||
Showing results for: <strong><?= htmlspecialchars($_GET['search']) ?></strong>
|
||||
— <?= $totalTickets ?> ticket<?= $totalTickets !== 1 ? 's' : '' ?> found
|
||||
@@ -588,7 +599,7 @@ include __DIR__ . '/layout_header.php';
|
||||
<div id="tab-table" class="lt-tab-panel active" role="tabpanel" aria-labelledby="tableViewBtn">
|
||||
|
||||
<!-- Bulk actions (admin only, shown when tickets selected) -->
|
||||
<?php if ($isAdmin): ?>
|
||||
<?php if ($isAdmin) : ?>
|
||||
<div class="bulk-actions-inline" style="display:none" aria-live="polite">
|
||||
<span id="selected-count" class="lt-text-amber lt-text-sm">0</span>
|
||||
<span class="lt-text-xs lt-text-muted"> tickets selected</span>
|
||||
@@ -616,7 +627,7 @@ include __DIR__ . '/layout_header.php';
|
||||
<div id="colTogglePanel" class="col-toggle-panel" aria-hidden="true" role="dialog" aria-label="Column visibility">
|
||||
<div class="col-toggle-title">Visible Columns</div>
|
||||
<?php
|
||||
$toggleableCols = [
|
||||
$toggleableCols = [
|
||||
'ticket_id' => 'Ticket ID',
|
||||
'category' => 'Category',
|
||||
'type' => 'Type',
|
||||
@@ -624,14 +635,14 @@ include __DIR__ . '/layout_header.php';
|
||||
'assigned_to' => 'Assigned To',
|
||||
'created_at' => 'Created',
|
||||
'updated_at' => 'Updated',
|
||||
];
|
||||
foreach ($toggleableCols as $colKey => $colName): ?>
|
||||
];
|
||||
foreach ($toggleableCols as $colKey => $colName) : ?>
|
||||
<label class="col-toggle-row">
|
||||
<input type="checkbox" class="lt-checkbox col-toggle-cb"
|
||||
data-col="<?= $colKey ?>" checked>
|
||||
<span><?= $colName ?></span>
|
||||
</label>
|
||||
<?php endforeach ?>
|
||||
<?php endforeach ?>
|
||||
<div class="col-toggle-footer">
|
||||
<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm lt-w-full" id="colToggleReset">Reset</button>
|
||||
</div>
|
||||
@@ -643,7 +654,7 @@ include __DIR__ . '/layout_header.php';
|
||||
<caption class="lt-sr-only">Ticket queue sorted by <?= htmlspecialchars($currentSort) ?> <?= $currentDir ?></caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<?php if ($isAdmin): ?>
|
||||
<?php if ($isAdmin) : ?>
|
||||
<th scope="col" class="col-checkbox">
|
||||
<input type="checkbox" class="lt-checkbox" id="selectAllCheckbox"
|
||||
data-action="toggle-select-all" aria-label="Select all tickets">
|
||||
@@ -663,16 +674,16 @@ include __DIR__ . '/layout_header.php';
|
||||
'updated_at' => 'Updated',
|
||||
'_actions' => 'Actions',
|
||||
];
|
||||
foreach ($columns as $col => $label):
|
||||
if ($col === '_actions'): ?>
|
||||
foreach ($columns as $col => $label) :
|
||||
if ($col === '_actions') : ?>
|
||||
<th scope="col" class="col-actions" data-col="_actions">Actions</th>
|
||||
<?php else:
|
||||
$newDir = ($currentSort === $col && $currentDir === 'asc') ? 'desc' : 'asc';
|
||||
$sortClass = ($currentSort === $col) ? 'sort-' . $currentDir : '';
|
||||
$ariaSort = ($currentSort === $col) ? 'aria-sort="' . ($currentDir === 'asc' ? 'ascending' : 'descending') . '"' : '';
|
||||
$sortParams = array_merge($_GET, ['sort' => $col, 'dir' => $newDir, 'page' => 1]);
|
||||
$sortUrl = htmlspecialchars('?' . http_build_query($sortParams), ENT_QUOTES, 'UTF-8');
|
||||
?>
|
||||
<?php else :
|
||||
$newDir = ($currentSort === $col && $currentDir === 'asc') ? 'desc' : 'asc';
|
||||
$sortClass = ($currentSort === $col) ? 'sort-' . $currentDir : '';
|
||||
$ariaSort = ($currentSort === $col) ? 'aria-sort="' . ($currentDir === 'asc' ? 'ascending' : 'descending') . '"' : '';
|
||||
$sortParams = array_merge($_GET, ['sort' => $col, 'dir' => $newDir, 'page' => 1]);
|
||||
$sortUrl = htmlspecialchars('?' . http_build_query($sortParams), ENT_QUOTES, 'UTF-8');
|
||||
?>
|
||||
<th scope="col" class="<?= $sortClass ?>" data-col="<?= $col ?>"
|
||||
data-action="navigate" data-url="<?= $sortUrl ?>"
|
||||
<?= $ariaSort ?>
|
||||
@@ -682,45 +693,47 @@ include __DIR__ . '/layout_header.php';
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($tickets)): ?>
|
||||
<?php if (empty($tickets)) : ?>
|
||||
<tr>
|
||||
<td colspan="<?= $colCount ?>" class="lt-empty">
|
||||
<div class="lt-empty-state">
|
||||
<div class="lt-empty-state-icon">📭</div>
|
||||
<div class="lt-empty-state-title">No Tickets Found</div>
|
||||
<div class="lt-empty-state-body">No tickets match your current filters.</div>
|
||||
<?php if (!empty($activeFilters) || !empty($_GET['search'])): ?>
|
||||
<?php if (!empty($activeFilters) || !empty($_GET['search'])) : ?>
|
||||
<a href="?" class="lt-btn lt-btn-sm lt-btn-ghost">Clear Filters</a>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($tickets as $row):
|
||||
$creator = htmlspecialchars($row['creator_display_name'] ?? $row['creator_username'] ?? 'System');
|
||||
$assignedTo = htmlspecialchars($row['assigned_display_name'] ?? $row['assigned_username'] ?? 'Unassigned');
|
||||
$pNum = (int)$row['priority'];
|
||||
$rowStatusSlug = strtolower(str_replace(' ', '-', $row['status']));
|
||||
$critClass = ($pNum === 1) ? ' lt-row-critical' : '';
|
||||
$warnClass = ($pNum === 2) ? ' lt-row-warning' : '';
|
||||
$createdFmt = date('Y-m-d H:i', strtotime($row['created_at']));
|
||||
$updatedFmt = date('Y-m-d H:i', strtotime($row['updated_at']));
|
||||
?>
|
||||
<?php else : ?>
|
||||
<?php foreach ($tickets as $row) :
|
||||
$creator = htmlspecialchars($row['creator_display_name'] ?? $row['creator_username'] ?? 'System');
|
||||
$assignedTo = htmlspecialchars($row['assigned_display_name'] ?? $row['assigned_username'] ?? 'Unassigned');
|
||||
$pNum = (int)$row['priority'];
|
||||
$rowStatusSlug = strtolower(str_replace(' ', '-', $row['status']));
|
||||
$critClass = ($pNum === 1) ? ' lt-row-critical' : '';
|
||||
$warnClass = ($pNum === 2) ? ' lt-row-warning' : '';
|
||||
$createdFmt = date('Y-m-d H:i', strtotime($row['created_at']));
|
||||
$updatedFmt = date('Y-m-d H:i', strtotime($row['updated_at']));
|
||||
?>
|
||||
<tr class="lt-row-p<?= $pNum ?><?= $critClass ?><?= $warnClass ?>">
|
||||
<?php if ($isAdmin): ?>
|
||||
<?php if ($isAdmin) : ?>
|
||||
<td data-label="Select" data-action="toggle-row-checkbox" class="checkbox-cell">
|
||||
<input type="checkbox" class="lt-checkbox ticket-checkbox"
|
||||
value="<?= htmlspecialchars($row['ticket_id']) ?>"
|
||||
data-action="update-selection"
|
||||
aria-label="Select ticket <?= htmlspecialchars($row['ticket_id']) ?>">
|
||||
</td>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
<td data-label="Ticket ID" data-col="ticket_id">
|
||||
<a href="/ticket/<?= htmlspecialchars($row['ticket_id']) ?>"
|
||||
class="ticket-link"><?= htmlspecialchars($row['ticket_id']) ?></a>
|
||||
</td>
|
||||
<td data-label="Priority" data-col="priority">
|
||||
<?php $badgeClass = match($pNum) { 1 => 'lt-badge-p1', 2 => 'lt-badge-p2', 3 => 'lt-badge-p3', default => 'lt-badge-p4' }; ?>
|
||||
<?php $badgeClass = match ($pNum) {
|
||||
1 => 'lt-badge-p1', 2 => 'lt-badge-p2', 3 => 'lt-badge-p3', default => 'lt-badge-p4'
|
||||
}; ?>
|
||||
<span class="lt-badge <?= $badgeClass ?>">P<?= $pNum ?></span>
|
||||
</td>
|
||||
<td data-label="Title" data-col="title" class="col-title">
|
||||
@@ -729,24 +742,24 @@ include __DIR__ . '/layout_header.php';
|
||||
<td data-label="Category" data-col="category" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['category']) ?></td>
|
||||
<td data-label="Type" data-col="type" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['type']) ?></td>
|
||||
<td data-label="Status" data-col="status">
|
||||
<?php $rowDotClass = match($row['status']) {
|
||||
'Open' => 'lt-dot-up',
|
||||
'In Progress' => 'lt-dot-warn',
|
||||
'Pending' => 'lt-dot--orange',
|
||||
'Closed' => 'lt-dot-idle',
|
||||
default => 'lt-dot-idle',
|
||||
}; ?>
|
||||
<?php $rowDotClass = match ($row['status']) {
|
||||
'Open' => 'lt-dot-up',
|
||||
'In Progress' => 'lt-dot-warn',
|
||||
'Pending' => 'lt-dot--orange',
|
||||
'Closed' => 'lt-dot-idle',
|
||||
default => 'lt-dot-idle',
|
||||
}; ?>
|
||||
<span class="lt-dot <?= $rowDotClass ?>" aria-hidden="true" style="vertical-align:middle;margin-right:0.3rem"></span>
|
||||
<span class="lt-status lt-status-<?= $rowStatusSlug ?>"><?= htmlspecialchars($row['status']) ?></span>
|
||||
</td>
|
||||
<td data-label="Created By" data-col="created_by" class="lt-text-xs"><?= $creator ?></td>
|
||||
<td data-label="Assigned To" data-col="assigned_to" class="lt-text-xs">
|
||||
<?php $assigneeDisplay = $row['assigned_display_name'] ?? $row['assigned_username'] ?? null; ?>
|
||||
<?php if ($assigneeDisplay): ?>
|
||||
<?php $assigneeDisplay = $row['assigned_display_name'] ?? $row['assigned_username'] ?? null; ?>
|
||||
<?php if ($assigneeDisplay) : ?>
|
||||
<span data-tooltip="<?= htmlspecialchars($assigneeDisplay, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($assigneeDisplay) ?></span>
|
||||
<?php else: ?>
|
||||
<?php else : ?>
|
||||
<span class="lt-text-muted">Unassigned</span>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td data-label="Created" data-col="created_at" class="lt-text-xs lt-text-muted ts-cell"
|
||||
data-ts="<?= htmlspecialchars($row['created_at'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
@@ -772,7 +785,7 @@ include __DIR__ . '/layout_header.php';
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -780,40 +793,44 @@ include __DIR__ . '/layout_header.php';
|
||||
</div><!-- /.lt-frame -->
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<?php if ($totalPages > 1) : ?>
|
||||
<div class="lt-pagination" role="navigation" aria-label="Ticket pagination">
|
||||
<?php
|
||||
$currentParams = $_GET;
|
||||
if ($page > 1) {
|
||||
$currentParams['page'] = $page - 1;
|
||||
$prevUrl = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
||||
echo '<button class="lt-btn lt-btn-sm" data-action="navigate" data-url="' . $prevUrl . '" aria-label="Previous page">«</button>';
|
||||
}
|
||||
$range = range(max(1, $page - 2), min($totalPages, $page + 2));
|
||||
if (!in_array(1, $range)) {
|
||||
$currentParams['page'] = 1;
|
||||
$url1 = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
||||
echo '<button class="lt-btn lt-btn-sm" data-action="navigate" data-url="' . $url1 . '">1</button>';
|
||||
if ($range[0] > 2) echo '<span class="lt-text-muted lt-text-xs" style="padding:0 0.25rem">…</span>';
|
||||
}
|
||||
foreach ($range as $i) {
|
||||
$currentParams['page'] = $i;
|
||||
$iUrl = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
||||
$activeClass = ($i === $page) ? ' lt-btn-primary' : '';
|
||||
echo '<button class="lt-btn lt-btn-sm' . $activeClass . '" data-action="navigate" data-url="' . $iUrl . '" ' . ($i === $page ? 'aria-current="page"' : '') . '>' . $i . '</button>';
|
||||
}
|
||||
if (!in_array($totalPages, $range)) {
|
||||
if ($range[count($range)-1] < $totalPages - 1) echo '<span class="lt-text-muted lt-text-xs" style="padding:0 0.25rem">…</span>';
|
||||
$currentParams['page'] = $totalPages;
|
||||
$urlLast = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
||||
echo '<button class="lt-btn lt-btn-sm" data-action="navigate" data-url="' . $urlLast . '">' . $totalPages . '</button>';
|
||||
}
|
||||
if ($page < $totalPages) {
|
||||
$currentParams['page'] = $page + 1;
|
||||
$nextUrl = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
||||
echo '<button class="lt-btn lt-btn-sm" data-action="navigate" data-url="' . $nextUrl . '" aria-label="Next page">»</button>';
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
$currentParams = $_GET;
|
||||
if ($page > 1) {
|
||||
$currentParams['page'] = $page - 1;
|
||||
$prevUrl = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
||||
echo '<button class="lt-btn lt-btn-sm" data-action="navigate" data-url="' . $prevUrl . '" aria-label="Previous page">«</button>';
|
||||
}
|
||||
$range = range(max(1, $page - 2), min($totalPages, $page + 2));
|
||||
if (!in_array(1, $range)) {
|
||||
$currentParams['page'] = 1;
|
||||
$url1 = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
||||
echo '<button class="lt-btn lt-btn-sm" data-action="navigate" data-url="' . $url1 . '">1</button>';
|
||||
if ($range[0] > 2) {
|
||||
echo '<span class="lt-text-muted lt-text-xs" style="padding:0 0.25rem">…</span>';
|
||||
}
|
||||
}
|
||||
foreach ($range as $i) {
|
||||
$currentParams['page'] = $i;
|
||||
$iUrl = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
||||
$activeClass = ($i === $page) ? ' lt-btn-primary' : '';
|
||||
echo '<button class="lt-btn lt-btn-sm' . $activeClass . '" data-action="navigate" data-url="' . $iUrl . '" ' . ($i === $page ? 'aria-current="page"' : '') . '>' . $i . '</button>';
|
||||
}
|
||||
if (!in_array($totalPages, $range)) {
|
||||
if ($range[count($range) - 1] < $totalPages - 1) {
|
||||
echo '<span class="lt-text-muted lt-text-xs" style="padding:0 0.25rem">…</span>';
|
||||
}
|
||||
$currentParams['page'] = $totalPages;
|
||||
$urlLast = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
||||
echo '<button class="lt-btn lt-btn-sm" data-action="navigate" data-url="' . $urlLast . '">' . $totalPages . '</button>';
|
||||
}
|
||||
if ($page < $totalPages) {
|
||||
$currentParams['page'] = $page + 1;
|
||||
$nextUrl = htmlspecialchars('?' . http_build_query($currentParams), ENT_QUOTES, 'UTF-8');
|
||||
echo '<button class="lt-btn lt-btn-sm" data-action="navigate" data-url="' . $nextUrl . '" aria-label="Next page">»</button>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
@@ -904,11 +921,11 @@ include __DIR__ . '/layout_header.php';
|
||||
<div class="lt-kv-row">
|
||||
<span class="lt-kv-label">Default status filters</span>
|
||||
<span class="lt-kv-value lt-flex lt-flex-wrap lt-flex-gap-sm">
|
||||
<?php foreach ($_lt_statuses as $sf): ?>
|
||||
<?php foreach ($_lt_statuses as $sf) : ?>
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox" name="defaultFilters" value="<?= htmlspecialchars($sf) ?>"
|
||||
<?= in_array($sf, ['Open','Pending','In Progress']) ? 'checked' : '' ?>>
|
||||
<?= htmlspecialchars($sf) ?>
|
||||
<?= htmlspecialchars($sf) ?>
|
||||
</label>
|
||||
<?php endforeach ?>
|
||||
</span>
|
||||
@@ -986,14 +1003,14 @@ include __DIR__ . '/layout_header.php';
|
||||
<span class="lt-kv-label">Groups</span>
|
||||
<span class="lt-kv-value">
|
||||
<?php
|
||||
$groups = array_filter(array_map('trim', explode(',', $GLOBALS['currentUser']['groups'] ?? '')));
|
||||
if ($groups):
|
||||
foreach ($groups as $g): ?>
|
||||
$groups = array_filter(array_map('trim', explode(',', $GLOBALS['currentUser']['groups'] ?? '')));
|
||||
if ($groups) :
|
||||
foreach ($groups as $g) : ?>
|
||||
<span class="lt-badge lt-badge-sm"><?= htmlspecialchars($g) ?></span>
|
||||
<?php endforeach;
|
||||
else: ?>
|
||||
<?php endforeach;
|
||||
else : ?>
|
||||
<span class="lt-text-muted">No groups assigned</span>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1086,12 +1103,16 @@ include __DIR__ . '/layout_header.php';
|
||||
<span class="lt-kv-value lt-flex lt-flex-gap-sm lt-flex-align-center">
|
||||
<select id="adv-priority-min" class="lt-select lt-select-sm">
|
||||
<option value="">Any</option>
|
||||
<?php foreach (range(1,5) as $p): ?><option value="<?= $p ?>">P<?= $p ?></option><?php endforeach ?>
|
||||
<?php foreach (range(1, 5) as $p) :
|
||||
?><option value="<?= $p ?>">P<?= $p ?></option><?php
|
||||
endforeach ?>
|
||||
</select>
|
||||
<span class="lt-text-xs lt-text-muted">to</span>
|
||||
<select id="adv-priority-max" class="lt-select lt-select-sm">
|
||||
<option value="">Any</option>
|
||||
<?php foreach (range(1,5) as $p): ?><option value="<?= $p ?>">P<?= $p ?></option><?php endforeach ?>
|
||||
<?php foreach (range(1, 5) as $p) :
|
||||
?><option value="<?= $p ?>">P<?= $p ?></option><?php
|
||||
endforeach ?>
|
||||
</select>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
+163
-130
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* TicketView.php — Individual ticket view, redesigned for TDS v1.2
|
||||
* Variables: $ticket, $comments (threaded), $timeline, $allUsers, $allowedTransitions
|
||||
@@ -20,8 +21,9 @@ $pageScripts = [
|
||||
];
|
||||
|
||||
// Helper functions
|
||||
function getEventIcon(string $actionType): string {
|
||||
return match($actionType) {
|
||||
function getEventIcon(string $actionType): string
|
||||
{
|
||||
return match ($actionType) {
|
||||
'create' => '[+]',
|
||||
'update' => '[~]',
|
||||
'comment' => '[>]',
|
||||
@@ -34,16 +36,23 @@ function getEventIcon(string $actionType): string {
|
||||
};
|
||||
}
|
||||
|
||||
function formatAction(array $event): string {
|
||||
function formatAction(array $event): string
|
||||
{
|
||||
$det = $event['details'] ?? [];
|
||||
switch ($event['action_type']) {
|
||||
case 'create':
|
||||
if (($event['entity_type'] ?? '') === 'comment') return 'posted a comment';
|
||||
if (($event['entity_type'] ?? '') === 'comment') {
|
||||
return 'posted a comment';
|
||||
}
|
||||
return 'created this ticket';
|
||||
case 'comment': return 'posted a comment';
|
||||
case 'view': return 'viewed this ticket';
|
||||
case 'attachment': return 'uploaded a file';
|
||||
case 'delete': return 'deleted a comment';
|
||||
case 'comment':
|
||||
return 'posted a comment';
|
||||
case 'view':
|
||||
return 'viewed this ticket';
|
||||
case 'attachment':
|
||||
return 'uploaded a file';
|
||||
case 'delete':
|
||||
return 'deleted a comment';
|
||||
case 'assign':
|
||||
if (is_array($det) && isset($det['assigned_to']['to'])) {
|
||||
$to = $det['assigned_to']['to'] ?: 'Unassigned';
|
||||
@@ -58,7 +67,9 @@ function formatAction(array $event): string {
|
||||
case 'update':
|
||||
if (is_array($det)) {
|
||||
$fields = array_keys(array_filter($det, fn($v) => is_array($v) && isset($v['from'], $v['to'])));
|
||||
if ($fields) return 'updated ' . implode(', ', array_map(fn($f) => str_replace('_', ' ', $f), $fields));
|
||||
if ($fields) {
|
||||
return 'updated ' . implode(', ', array_map(fn($f) => str_replace('_', ' ', $f), $fields));
|
||||
}
|
||||
}
|
||||
return 'updated this ticket';
|
||||
default:
|
||||
@@ -72,8 +83,11 @@ $ageDays = floor($ageSeconds / 86400);
|
||||
$ageHours = floor(($ageSeconds % 86400) / 3600);
|
||||
$ageClass = 'lt-text-muted';
|
||||
if ($ticket['status'] !== 'Closed') {
|
||||
if ($ageDays >= 10) $ageClass = 'lt-text-danger';
|
||||
elseif ($ageDays >= 5) $ageClass = 'lt-text-amber';
|
||||
if ($ageDays >= 10) {
|
||||
$ageClass = 'lt-text-danger';
|
||||
} elseif ($ageDays >= 5) {
|
||||
$ageClass = 'lt-text-amber';
|
||||
}
|
||||
}
|
||||
$ageStr = $ageDays > 0
|
||||
? $ageDays . ' day' . ($ageDays !== 1 ? 's' : '')
|
||||
@@ -93,12 +107,12 @@ $visUserModel = new UserModel($conn);
|
||||
$allAvailableGroups = $visUserModel->getAllGroups();
|
||||
|
||||
// JSON-encode ticket fields for the inline script
|
||||
$json_ticket_id = json_encode($ticket['ticket_id'], JSON_HEX_TAG);
|
||||
$json_title = json_encode($ticket['title'], JSON_HEX_TAG);
|
||||
$json_status = json_encode($ticket['status'], JSON_HEX_TAG);
|
||||
$json_priority = json_encode($ticket['priority'], JSON_HEX_TAG);
|
||||
$json_category = json_encode($ticket['category'], JSON_HEX_TAG);
|
||||
$json_type = json_encode($ticket['type'], JSON_HEX_TAG);
|
||||
$json_ticket_id = json_encode($ticket['ticket_id'], JSON_HEX_TAG);
|
||||
$json_title = json_encode($ticket['title'], JSON_HEX_TAG);
|
||||
$json_status = json_encode($ticket['status'], JSON_HEX_TAG);
|
||||
$json_priority = json_encode($ticket['priority'], JSON_HEX_TAG);
|
||||
$json_category = json_encode($ticket['category'], JSON_HEX_TAG);
|
||||
$json_type = json_encode($ticket['type'], JSON_HEX_TAG);
|
||||
$json_updated_at = json_encode($ticket['updated_at'], JSON_HEX_TAG);
|
||||
$json_total_comments = json_encode((int)$totalComments, JSON_HEX_TAG);
|
||||
$json_comment_page = json_encode((int)$commentPageSize, JSON_HEX_TAG);
|
||||
@@ -150,7 +164,7 @@ include __DIR__ . '/layout_header.php';
|
||||
<div class="lt-btn-group">
|
||||
<!-- Status dot indicator -->
|
||||
<?php
|
||||
$dotClass = match($ticket['status']) {
|
||||
$dotClass = match ($ticket['status']) {
|
||||
'Open' => 'lt-dot-up',
|
||||
'In Progress' => 'lt-dot-warn',
|
||||
'Pending' => 'lt-dot--orange',
|
||||
@@ -168,13 +182,17 @@ include __DIR__ . '/layout_header.php';
|
||||
<option value="<?= htmlspecialchars($ticket['status']) ?>" selected>
|
||||
<?= htmlspecialchars($ticket['status']) ?> (current)
|
||||
</option>
|
||||
<?php foreach ($allowedTransitions as $t): ?>
|
||||
<?php foreach ($allowedTransitions as $t) : ?>
|
||||
<option value="<?= htmlspecialchars($t['to_status']) ?>"
|
||||
data-requires-comment="<?= $t['requires_comment'] ? '1' : '0' ?>"
|
||||
data-requires-admin="<?= $t['requires_admin'] ? '1' : '0' ?>">
|
||||
<?= htmlspecialchars($t['to_status']) ?>
|
||||
<?php if ($t['requires_comment']): ?> *<?php endif ?>
|
||||
<?php if ($t['requires_admin']): ?> (Admin)<?php endif ?>
|
||||
<?= htmlspecialchars($t['to_status']) ?>
|
||||
<?php if ($t['requires_comment']) :
|
||||
?> *<?php
|
||||
endif ?>
|
||||
<?php if ($t['requires_admin']) :
|
||||
?> (Admin)<?php
|
||||
endif ?>
|
||||
</option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
@@ -191,18 +209,20 @@ include __DIR__ . '/layout_header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($priorityNum <= 2 && $ticket['status'] !== 'Closed'): ?>
|
||||
<?php
|
||||
$slaTargetHours = match($priorityNum) { 1 => 8, 2 => 24, default => 72 };
|
||||
$elapsedSeconds = time() - strtotime($ticket['created_at']);
|
||||
$elapsedHours = round($elapsedSeconds / 3600, 1);
|
||||
$slaPct = min(100, round(($elapsedSeconds / ($slaTargetHours * 3600)) * 100));
|
||||
$slaBreached = $elapsedSeconds >= ($slaTargetHours * 3600);
|
||||
$alertClass = $priorityNum === 1 ? 'lt-alert--error' : 'lt-alert--warning';
|
||||
$alertIcon = $priorityNum === 1 ? '[ ! ]' : '[ ~ ]';
|
||||
$alertLabel = $priorityNum === 1 ? 'CRITICAL — P1 Ticket' : 'HIGH PRIORITY — P2 Ticket';
|
||||
$progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progress--red' : 'lt-progress--green');
|
||||
?>
|
||||
<?php if ($priorityNum <= 2 && $ticket['status'] !== 'Closed') : ?>
|
||||
<?php
|
||||
$slaTargetHours = match ($priorityNum) {
|
||||
1 => 8, 2 => 24, default => 72
|
||||
};
|
||||
$elapsedSeconds = time() - strtotime($ticket['created_at']);
|
||||
$elapsedHours = round($elapsedSeconds / 3600, 1);
|
||||
$slaPct = min(100, round(($elapsedSeconds / ($slaTargetHours * 3600)) * 100));
|
||||
$slaBreached = $elapsedSeconds >= ($slaTargetHours * 3600);
|
||||
$alertClass = $priorityNum === 1 ? 'lt-alert--error' : 'lt-alert--warning';
|
||||
$alertIcon = $priorityNum === 1 ? '[ ! ]' : '[ ~ ]';
|
||||
$alertLabel = $priorityNum === 1 ? 'CRITICAL — P1 Ticket' : 'HIGH PRIORITY — P2 Ticket';
|
||||
$progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progress--red' : 'lt-progress--green');
|
||||
?>
|
||||
<!-- Priority alert banner — P1/P2 only, dismissible per session -->
|
||||
<div class="lt-alert <?= $alertClass ?>" id="priorityAlertBanner"
|
||||
role="alert" aria-live="polite"
|
||||
@@ -216,9 +236,9 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<div class="lt-alert-msg">
|
||||
SLA target: <strong><?= $slaTargetHours ?>h</strong> —
|
||||
Elapsed: <strong id="slaElapsedTimer"><?= $elapsedHours ?>h</strong>
|
||||
<?php if (!$slaBreached): ?>
|
||||
<?php if (!$slaBreached) : ?>
|
||||
— Remaining: <strong id="slaCountdownTimer" class="lt-text-cyan"></strong>
|
||||
<?php else: ?>
|
||||
<?php else : ?>
|
||||
— <span class="lt-text-danger" id="slaCountdownTimer">SLA BREACHED (+<strong id="slaOverrunTimer"><?= round(($elapsedSeconds - $slaTargetHours * 3600) / 3600, 1) ?>h</strong>)</span>
|
||||
<?php endif ?>
|
||||
<div class="lt-progress lt-progress--sm <?= $progressClass ?>" id="slaProgress" style="margin-top:0.35rem"
|
||||
@@ -317,7 +337,7 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<span class="lt-kv-label">Priority</span>
|
||||
<span class="lt-kv-value">
|
||||
<select id="prioritySelect" class="lt-select lt-select-sm editable-metadata lt-display-field" aria-label="Priority">
|
||||
<?php foreach ([1=>'P1 - Critical',2=>'P2 - High',3=>'P3 - Medium',4=>'P4 - Low',5=>'P5 - Minimal'] as $v=>$l): ?>
|
||||
<?php foreach ([1 => 'P1 - Critical',2 => 'P2 - High',3 => 'P3 - Medium',4 => 'P4 - Low',5 => 'P5 - Minimal'] as $v => $l) : ?>
|
||||
<option value="<?= $v ?>" <?= (int)$ticket['priority'] === $v ? 'selected' : '' ?>><?= $l ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
@@ -326,13 +346,15 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<div class="lt-kv-row">
|
||||
<span class="lt-kv-label">Category</span>
|
||||
<span class="lt-kv-value">
|
||||
<?php $catColor = match($ticket['category']) { 'Hardware'=>'lt-tag--orange','Software'=>'lt-tag--cyan','Network'=>'lt-tag--purple','Security'=>'lt-tag--red',default=>'' }; ?>
|
||||
<?php $catColor = match ($ticket['category']) {
|
||||
'Hardware'=>'lt-tag--orange','Software'=>'lt-tag--cyan','Network'=>'lt-tag--purple','Security'=>'lt-tag--red',default=>''
|
||||
}; ?>
|
||||
<!-- Read mode tag — hidden in edit mode via CSS -->
|
||||
<span class="lt-tag <?= $catColor ?> read-mode-tag" id="categoryTag"
|
||||
aria-label="Category: <?= htmlspecialchars($ticket['category']) ?>"><?= htmlspecialchars($ticket['category']) ?></span>
|
||||
<!-- Edit mode select — shown only when editing -->
|
||||
<select id="categorySelect" class="lt-select lt-select-sm editable-metadata edit-mode-field" style="display:none" aria-label="Category">
|
||||
<?php foreach (['Hardware','Software','Network','Security','General'] as $c): ?>
|
||||
<?php foreach (['Hardware','Software','Network','Security','General'] as $c) : ?>
|
||||
<option value="<?= $c ?>" <?= $ticket['category'] === $c ? 'selected' : '' ?>><?= $c ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
@@ -341,13 +363,15 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<div class="lt-kv-row">
|
||||
<span class="lt-kv-label">Type</span>
|
||||
<span class="lt-kv-value">
|
||||
<?php $typeColor = match($ticket['type']) { 'Maintenance'=>'lt-tag--orange','Issue'=>'lt-tag--red','Problem'=>'lt-tag--red','Upgrade'=>'lt-tag--purple','Install'=>'lt-tag--cyan',default=>'' }; ?>
|
||||
<?php $typeColor = match ($ticket['type']) {
|
||||
'Maintenance'=>'lt-tag--orange','Issue'=>'lt-tag--red','Problem'=>'lt-tag--red','Upgrade'=>'lt-tag--purple','Install'=>'lt-tag--cyan',default=>''
|
||||
}; ?>
|
||||
<!-- Read mode tag — hidden in edit mode via CSS -->
|
||||
<span class="lt-tag <?= $typeColor ?> read-mode-tag" id="typeTag"
|
||||
aria-label="Type: <?= htmlspecialchars($ticket['type']) ?>"><?= htmlspecialchars($ticket['type']) ?></span>
|
||||
<!-- Edit mode select — shown only when editing -->
|
||||
<select id="typeSelect" class="lt-select lt-select-sm editable-metadata edit-mode-field" style="display:none" aria-label="Type">
|
||||
<?php foreach (['Maintenance','Install','Task','Upgrade','Issue','Problem'] as $t): ?>
|
||||
<?php foreach (['Maintenance','Install','Task','Upgrade','Issue','Problem'] as $t) : ?>
|
||||
<option value="<?= $t ?>" <?= $ticket['type'] === $t ? 'selected' : '' ?>><?= $t ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
@@ -358,7 +382,7 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<span class="lt-kv-value">
|
||||
<select id="assignedToSelect" class="lt-select lt-select-sm" aria-label="Assign ticket">
|
||||
<option value="">Unassigned</option>
|
||||
<?php foreach ($allUsers as $u): ?>
|
||||
<?php foreach ($allUsers as $u) : ?>
|
||||
<option value="<?= (int)$u['user_id'] ?>"
|
||||
<?= ((int)$ticket['assigned_to'] === (int)$u['user_id']) ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($u['display_name'] ?? $u['username']) ?>
|
||||
@@ -387,7 +411,7 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<div class="lt-kv-row">
|
||||
<span class="lt-kv-label">Created By</span>
|
||||
<span class="lt-kv-value"><?= htmlspecialchars($creator) ?>
|
||||
<?php if (!empty($ticket['created_at'])): ?>
|
||||
<?php if (!empty($ticket['created_at'])) : ?>
|
||||
<span class="lt-text-muted lt-text-xs"> —
|
||||
<span class="ts-cell" data-ts="<?= htmlspecialchars($ticket['created_at'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
title="<?= date('Y-m-d H:i T', strtotime($ticket['created_at'])) ?>">
|
||||
@@ -397,19 +421,19 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<?php endif ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php if (!empty($ticket['updater_display_name']) || !empty($ticket['updater_username'])): ?>
|
||||
<?php if (!empty($ticket['updater_display_name']) || !empty($ticket['updater_username'])) : ?>
|
||||
<div class="lt-kv-row">
|
||||
<span class="lt-kv-label">Last Updated</span>
|
||||
<span class="lt-kv-value">
|
||||
<?= htmlspecialchars($ticket['updater_display_name'] ?? $ticket['updater_username']) ?>
|
||||
<?php if (!empty($ticket['updated_at'])): ?>
|
||||
<?= htmlspecialchars($ticket['updater_display_name'] ?? $ticket['updater_username']) ?>
|
||||
<?php if (!empty($ticket['updated_at'])) : ?>
|
||||
<span class="lt-text-muted lt-text-xs"> —
|
||||
<span class="ts-cell" data-ts="<?= htmlspecialchars($ticket['updated_at'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
title="<?= date('Y-m-d H:i T', strtotime($ticket['updated_at'])) ?>">
|
||||
<?= date('M d, Y H:i', strtotime($ticket['updated_at'])) ?>
|
||||
</span>
|
||||
</span>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
@@ -421,8 +445,8 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label lt-text-cyan">Allowed Groups</label>
|
||||
<div class="visibility-groups-edit lt-flex lt-flex-wrap lt-flex-gap-sm">
|
||||
<?php foreach ($allAvailableGroups as $group):
|
||||
$isChecked = in_array($group, $currentVisibilityGroups, true); ?>
|
||||
<?php foreach ($allAvailableGroups as $group) :
|
||||
$isChecked = in_array($group, $currentVisibilityGroups, true); ?>
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox visibility-group-checkbox editable-metadata lt-display-field"
|
||||
value="<?= htmlspecialchars($group, ENT_QUOTES, 'UTF-8') ?>"
|
||||
@@ -430,7 +454,7 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<span class="lt-badge"><?= htmlspecialchars($group) ?></span>
|
||||
</label>
|
||||
<?php endforeach ?>
|
||||
<?php if (empty($allAvailableGroups)): ?>
|
||||
<?php if (empty($allAvailableGroups)) : ?>
|
||||
<span class="lt-text-muted">No groups available</span>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
@@ -451,7 +475,7 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<button type="button" class="lt-tab" id="comments-tab-btn"
|
||||
role="tab" data-tab="comments-panel" aria-selected="false" aria-controls="comments-panel">
|
||||
Comments
|
||||
<?php if (!empty($comments)): ?>
|
||||
<?php if (!empty($comments)) : ?>
|
||||
<span class="lt-badge lt-badge-sm"><?= count($comments) ?></span>
|
||||
<?php endif ?>
|
||||
</button>
|
||||
@@ -541,39 +565,42 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<div class="lt-section-header">Comment History</div>
|
||||
<div class="lt-section-body">
|
||||
<div class="comments-list" id="commentsList">
|
||||
<?php if (empty($comments)): ?>
|
||||
<?php if (empty($comments)) : ?>
|
||||
<div class="lt-empty">No comments yet. Be the first to comment.</div>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
function renderComment(array $comment, ?int $currentUserId, bool $isAdmin, int $depth = 0): void {
|
||||
$displayName = $comment['display_name_formatted'] ?? $comment['user_name'] ?? 'Unknown User';
|
||||
$commentId = (int)$comment['comment_id'];
|
||||
$isOwner = ((int)$comment['user_id'] === (int)$currentUserId);
|
||||
$canModify = $isOwner || $isAdmin;
|
||||
$markdownEnabled = (bool)($comment['markdown_enabled'] ?? false);
|
||||
$threadDepth = (int)($comment['thread_depth'] ?? $depth);
|
||||
$parentId = $comment['parent_comment_id'] ?? null;
|
||||
$depthClass = 'thread-depth-' . min($threadDepth, 3);
|
||||
$threadClass = $parentId ? 'comment-reply' : 'comment-root';
|
||||
$dateStr = date('M d, Y H:i', strtotime($comment['created_at']));
|
||||
$editedIndicator = !empty($comment['updated_at']) ? ' <span class="comment-edited lt-text-xs lt-text-muted">(edited)</span>' : '';
|
||||
// Avatar initials + color (fallback when no photo)
|
||||
$words = array_filter(explode(' ', $displayName));
|
||||
$initials = strtoupper(implode('', array_map(fn($w) => $w[0], array_slice($words, 0, 2))));
|
||||
$avatarColors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', ''];
|
||||
$avatarColor = $avatarColors[abs(crc32($displayName)) % count($avatarColors)];
|
||||
$commentUserId = (int)($comment['user_id'] ?? 0);
|
||||
?>
|
||||
<?php else : ?>
|
||||
<?php
|
||||
function renderComment(array $comment, ?int $currentUserId, bool $isAdmin, int $depth = 0): void
|
||||
{
|
||||
$displayName = $comment['display_name_formatted'] ?? $comment['user_name'] ?? 'Unknown User';
|
||||
$commentId = (int)$comment['comment_id'];
|
||||
$isOwner = ((int)$comment['user_id'] === (int)$currentUserId);
|
||||
$canModify = $isOwner || $isAdmin;
|
||||
$markdownEnabled = (bool)($comment['markdown_enabled'] ?? false);
|
||||
$threadDepth = (int)($comment['thread_depth'] ?? $depth);
|
||||
$parentId = $comment['parent_comment_id'] ?? null;
|
||||
$depthClass = 'thread-depth-' . min($threadDepth, 3);
|
||||
$threadClass = $parentId ? 'comment-reply' : 'comment-root';
|
||||
$dateStr = date('M d, Y H:i', strtotime($comment['created_at']));
|
||||
$editedIndicator = !empty($comment['updated_at']) ? ' <span class="comment-edited lt-text-xs lt-text-muted">(edited)</span>' : '';
|
||||
// Avatar initials + color (fallback when no photo)
|
||||
$words = array_filter(explode(' ', $displayName));
|
||||
$initials = strtoupper(implode('', array_map(fn($w) => $w[0], array_slice($words, 0, 2))));
|
||||
$avatarColors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', ''];
|
||||
$avatarColor = $avatarColors[abs(crc32($displayName)) % count($avatarColors)];
|
||||
$commentUserId = (int)($comment['user_id'] ?? 0);
|
||||
?>
|
||||
<div class="comment <?= $depthClass ?> <?= $threadClass ?>"
|
||||
data-comment-id="<?= $commentId ?>"
|
||||
data-markdown-enabled="<?= $markdownEnabled ? '1' : '0' ?>"
|
||||
data-thread-depth="<?= $threadDepth ?>"
|
||||
data-parent-id="<?= htmlspecialchars((string)($parentId ?? ''), ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?php if ($parentId): ?><div class="thread-line" aria-hidden="true"></div><?php endif ?>
|
||||
<?php if ($parentId) :
|
||||
?><div class="thread-line" aria-hidden="true"></div><?php
|
||||
endif ?>
|
||||
<div class="comment-content">
|
||||
<div class="comment-header lt-flex lt-flex-gap-sm lt-flex-align-center">
|
||||
<div class="lt-avatar lt-avatar--xs <?= $avatarColor ?>" aria-hidden="true">
|
||||
<?php if ($commentUserId > 0): ?>
|
||||
<?php if ($commentUserId > 0) : ?>
|
||||
<img src="/api/user_avatar.php?user_id=<?= $commentUserId ?>"
|
||||
alt=""
|
||||
class="lt-avatar-img">
|
||||
@@ -588,14 +615,14 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<?= $editedIndicator ?>
|
||||
</span>
|
||||
<div class="comment-actions lt-btn-group">
|
||||
<?php if ($threadDepth < 3): ?>
|
||||
<?php if ($threadDepth < 3) : ?>
|
||||
<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm comment-action-btn reply-btn"
|
||||
data-action="reply-comment"
|
||||
data-comment-id="<?= $commentId ?>"
|
||||
data-user="<?= htmlspecialchars($displayName, ENT_QUOTES) ?>"
|
||||
aria-label="Reply to comment">Reply</button>
|
||||
<?php endif ?>
|
||||
<?php if ($canModify): ?>
|
||||
<?php if ($canModify) : ?>
|
||||
<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm comment-action-btn edit-btn"
|
||||
data-action="edit-comment"
|
||||
data-comment-id="<?= $commentId ?>"
|
||||
@@ -617,19 +644,21 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
id="comment-raw-<?= $commentId ?>"
|
||||
aria-hidden="true"><?= htmlspecialchars($comment['comment_text']) ?></textarea>
|
||||
</div>
|
||||
<?php if (!empty($comment['replies'])): ?>
|
||||
<?php if (!empty($comment['replies'])) : ?>
|
||||
<div class="comment-replies">
|
||||
<?php foreach ($comment['replies'] as $reply): ?>
|
||||
<?php renderComment($reply, $currentUserId, $isAdmin, $threadDepth + 1); ?>
|
||||
<?php endforeach ?>
|
||||
<?php foreach ($comment['replies'] as $reply) : ?>
|
||||
<?php renderComment($reply, $currentUserId, $isAdmin, $threadDepth + 1); ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
foreach ($comments as $comment): renderComment($comment, $currentUserId, $isAdmin); endforeach;
|
||||
?>
|
||||
<?php if ($totalComments > $commentPageSize): ?>
|
||||
<?php
|
||||
}
|
||||
foreach ($comments as $comment) :
|
||||
renderComment($comment, $currentUserId, $isAdmin);
|
||||
endforeach;
|
||||
?>
|
||||
<?php if ($totalComments > $commentPageSize) : ?>
|
||||
<div id="loadMoreComments" class="lt-flex lt-flex-center lt-mt-md">
|
||||
<button type="button" id="loadMoreBtn" class="lt-btn lt-btn-ghost lt-btn-sm">
|
||||
Load more comments
|
||||
@@ -638,7 +667,7 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -748,27 +777,27 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<span class="lt-frame-bl">╚</span><span class="lt-frame-br">╝</span>
|
||||
<div class="lt-section-header">Activity Timeline</div>
|
||||
<div class="lt-section-body">
|
||||
<?php if (empty($timeline)): ?>
|
||||
<?php if (empty($timeline)) : ?>
|
||||
<div class="lt-empty">No activity recorded yet.</div>
|
||||
<?php else: ?>
|
||||
<?php else : ?>
|
||||
<div class="lt-timeline">
|
||||
<?php foreach ($timeline as $event): ?>
|
||||
<?php
|
||||
$actor = htmlspecialchars($event['display_name'] ?? $event['username'] ?? 'System');
|
||||
$action = formatAction($event);
|
||||
$icon = getEventIcon($event['action_type']);
|
||||
$evtFmt = date('M d, Y H:i', strtotime($event['created_at']));
|
||||
$tClass = match($event['action_type']) {
|
||||
'create' => 'lt-timeline-item--green',
|
||||
'status_change' => 'lt-timeline-item--cyan',
|
||||
'comment' => 'lt-timeline-item--green',
|
||||
'assign' => 'lt-timeline-item--orange',
|
||||
'attachment' => 'lt-timeline-item--orange',
|
||||
'update' => '',
|
||||
'delete' => 'lt-timeline-item--red',
|
||||
default => 'lt-timeline-item--dim',
|
||||
};
|
||||
?>
|
||||
<?php foreach ($timeline as $event) : ?>
|
||||
<?php
|
||||
$actor = htmlspecialchars($event['display_name'] ?? $event['username'] ?? 'System');
|
||||
$action = formatAction($event);
|
||||
$icon = getEventIcon($event['action_type']);
|
||||
$evtFmt = date('M d, Y H:i', strtotime($event['created_at']));
|
||||
$tClass = match ($event['action_type']) {
|
||||
'create' => 'lt-timeline-item--green',
|
||||
'status_change' => 'lt-timeline-item--cyan',
|
||||
'comment' => 'lt-timeline-item--green',
|
||||
'assign' => 'lt-timeline-item--orange',
|
||||
'attachment' => 'lt-timeline-item--orange',
|
||||
'update' => '',
|
||||
'delete' => 'lt-timeline-item--red',
|
||||
default => 'lt-timeline-item--dim',
|
||||
};
|
||||
?>
|
||||
<div class="lt-timeline-item <?= $tClass ?>">
|
||||
<div class="lt-timeline-meta">
|
||||
<span class="lt-timeline-icon lt-text-xs" aria-hidden="true"><?= $icon ?></span>
|
||||
@@ -778,34 +807,36 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
data-ts="<?= htmlspecialchars($event['created_at'], ENT_QUOTES, 'UTF-8') ?>"
|
||||
title="<?= $evtFmt ?>"><?= $evtFmt ?></span>
|
||||
</div>
|
||||
<?php if (!empty($event['details']) && !in_array($event['action_type'], ['status_change', 'assign', 'comment', 'view'], true)): ?>
|
||||
<?php if (!empty($event['details']) && !in_array($event['action_type'], ['status_change', 'assign', 'comment', 'view'], true)) : ?>
|
||||
<div class="lt-timeline-body lt-text-xs lt-text-muted">
|
||||
<?php
|
||||
$det = $event['details'];
|
||||
if (is_array($det)) {
|
||||
$parts = [];
|
||||
foreach ($det as $k => $v) {
|
||||
if (is_array($v) && isset($v['from'], $v['to'])) {
|
||||
$label = ucfirst(str_replace('_', ' ', $k));
|
||||
$from = mb_strlen((string)$v['from']) > 60
|
||||
<?php
|
||||
$det = $event['details'];
|
||||
if (is_array($det)) {
|
||||
$parts = [];
|
||||
foreach ($det as $k => $v) {
|
||||
if (is_array($v) && isset($v['from'], $v['to'])) {
|
||||
$label = ucfirst(str_replace('_', ' ', $k));
|
||||
$from = mb_strlen((string)$v['from']) > 60
|
||||
? mb_substr((string)$v['from'], 0, 60) . '…'
|
||||
: (string)$v['from'];
|
||||
$to = mb_strlen((string)$v['to']) > 60
|
||||
$to = mb_strlen((string)$v['to']) > 60
|
||||
? mb_substr((string)$v['to'], 0, 60) . '…'
|
||||
: (string)$v['to'];
|
||||
$parts[] = '<strong>' . htmlspecialchars($label) . ':</strong> '
|
||||
$parts[] = '<strong>' . htmlspecialchars($label) . ':</strong> '
|
||||
. '<span class="lt-text-muted">' . htmlspecialchars($from) . '</span>'
|
||||
. ' <span class="lt-text-amber">→</span> '
|
||||
. '<span class="lt-text-cyan">' . htmlspecialchars($to) . '</span>';
|
||||
} elseif (!in_array($k, ['old_value', 'new_value'], true)) {
|
||||
$parts[] = '<strong>' . htmlspecialchars($k) . ':</strong> ' . htmlspecialchars((string)$v);
|
||||
}
|
||||
}
|
||||
if ($parts) echo implode('<br>', $parts);
|
||||
}
|
||||
?>
|
||||
} elseif (!in_array($k, ['old_value', 'new_value'], true)) {
|
||||
$parts[] = '<strong>' . htmlspecialchars($k) . ':</strong> ' . htmlspecialchars((string)$v);
|
||||
}
|
||||
}
|
||||
if ($parts) {
|
||||
echo implode('<br>', $parts);
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
@@ -896,12 +927,14 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<span class="lt-kv-label">Groups</span>
|
||||
<span class="lt-kv-value">
|
||||
<?php
|
||||
$groups = array_filter(array_map('trim', explode(',', $GLOBALS['currentUser']['groups'] ?? '')));
|
||||
if ($groups): foreach ($groups as $g): ?>
|
||||
$groups = array_filter(array_map('trim', explode(',', $GLOBALS['currentUser']['groups'] ?? '')));
|
||||
if ($groups) :
|
||||
foreach ($groups as $g) : ?>
|
||||
<span class="lt-badge lt-badge-sm"><?= htmlspecialchars($g) ?></span>
|
||||
<?php endforeach; else: ?>
|
||||
<?php endforeach;
|
||||
else : ?>
|
||||
<span class="lt-text-muted">None</span>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+14
-12
@@ -72,38 +72,40 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($apiKeys)): ?>
|
||||
<?php if (empty($apiKeys)) : ?>
|
||||
<tr><td colspan="8" class="lt-empty">No API keys found. Generate one above.</td></tr>
|
||||
<?php else: foreach ($apiKeys as $key): ?>
|
||||
<?php $expired = $key['expires_at'] && strtotime($key['expires_at']) < time(); ?>
|
||||
<?php else :
|
||||
foreach ($apiKeys as $key) : ?>
|
||||
<?php $expired = $key['expires_at'] && strtotime($key['expires_at']) < time(); ?>
|
||||
<tr id="key-row-<?= (int)$key['api_key_id'] ?>">
|
||||
<td data-label="Name"><strong><?= htmlspecialchars($key['key_name']) ?></strong></td>
|
||||
<td data-label="Prefix" class="lt-text-xs"><code><?= htmlspecialchars($key['key_prefix']) ?>…</code></td>
|
||||
<td data-label="Created By" class="lt-text-xs"><?= htmlspecialchars($key['display_name'] ?? $key['username'] ?? 'Unknown') ?></td>
|
||||
<td data-label="Created" class="lt-text-xs lt-text-muted"><?= date('Y-m-d H:i', strtotime($key['created_at'])) ?></td>
|
||||
<td data-label="Expires" class="lt-text-xs <?= $expired ? 'lt-text-danger' : 'lt-text-cyan' ?>">
|
||||
<?= $key['expires_at'] ? date('Y-m-d', strtotime($key['expires_at'])) . ($expired ? ' (Expired)' : '') : 'Never' ?>
|
||||
<?= $key['expires_at'] ? date('Y-m-d', strtotime($key['expires_at'])) . ($expired ? ' (Expired)' : '') : 'Never' ?>
|
||||
</td>
|
||||
<td data-label="Last Used" class="lt-text-xs lt-text-muted">
|
||||
<?= $key['last_used'] ? date('Y-m-d H:i', strtotime($key['last_used'])) : 'Never' ?>
|
||||
<?= $key['last_used'] ? date('Y-m-d H:i', strtotime($key['last_used'])) : 'Never' ?>
|
||||
</td>
|
||||
<td data-label="Status">
|
||||
<?php if ($key['is_active']): ?>
|
||||
<?php if ($key['is_active']) : ?>
|
||||
<span class="lt-status lt-status-open">Active</span>
|
||||
<?php else: ?>
|
||||
<?php else : ?>
|
||||
<span class="lt-status lt-status-closed">Revoked</span>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<?php if ($key['is_active']): ?>
|
||||
<?php if ($key['is_active']) : ?>
|
||||
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger"
|
||||
data-action="revoke-key" data-id="<?= (int)$key['api_key_id'] ?>">REVOKE</button>
|
||||
<?php else: ?>
|
||||
<?php else : ?>
|
||||
<span class="lt-text-muted lt-text-xs">—</span>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
<?php endforeach;
|
||||
endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<label class="lt-label" for="action_type">Action Type</label>
|
||||
<select name="action_type" id="action_type" class="lt-select lt-select-sm">
|
||||
<option value="">All Actions</option>
|
||||
<?php foreach (['create','update','delete','comment','assign','status_change','login','security'] as $a): ?>
|
||||
<?php foreach (['create','update','delete','comment','assign','status_change','login','security'] as $a) : ?>
|
||||
<option value="<?= htmlspecialchars($a, ENT_QUOTES, 'UTF-8') ?>" <?= ($filters['action_type'] ?? '') === $a ? 'selected' : '' ?>><?= htmlspecialchars(ucfirst(str_replace('_', ' ', $a)), ENT_QUOTES, 'UTF-8') ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
@@ -38,11 +38,13 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<label class="lt-label" for="user_id">User</label>
|
||||
<select name="user_id" id="user_id" class="lt-select lt-select-sm">
|
||||
<option value="">All Users</option>
|
||||
<?php if (isset($users)): foreach ($users as $u): ?>
|
||||
<?php if (isset($users)) :
|
||||
foreach ($users as $u) : ?>
|
||||
<option value="<?= (int)$u['user_id'] ?>" <?= ($filters['user_id'] ?? '') == $u['user_id'] ? 'selected' : '' ?>>
|
||||
<?= htmlspecialchars($u['display_name'] ?? $u['username']) ?>
|
||||
<?= htmlspecialchars($u['display_name'] ?? $u['username']) ?>
|
||||
</option>
|
||||
<?php endforeach; endif ?>
|
||||
<?php endforeach;
|
||||
endif ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group" style="margin:0">
|
||||
@@ -76,73 +78,79 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($auditLogs)): ?>
|
||||
<?php if (empty($auditLogs)) : ?>
|
||||
<tr><td colspan="7" class="lt-empty">No audit log entries found.</td></tr>
|
||||
<?php else: foreach ($auditLogs as $log): ?>
|
||||
<?php else :
|
||||
foreach ($auditLogs as $log) : ?>
|
||||
<tr>
|
||||
<td data-label="Timestamp" class="lt-text-xs"><?= date('Y-m-d H:i:s', strtotime($log['created_at'])) ?></td>
|
||||
<td data-label="User"><?= htmlspecialchars($log['display_name'] ?? $log['username'] ?? 'System') ?></td>
|
||||
<td data-label="Action"><span class="lt-text-amber"><?= htmlspecialchars($log['action_type']) ?></span></td>
|
||||
<td data-label="Entity" class="lt-text-xs"><?= htmlspecialchars($log['entity_type'] ?? '-') ?></td>
|
||||
<td data-label="Entity ID" class="lt-text-xs">
|
||||
<?php if ($log['entity_type'] === 'ticket' && $log['entity_id']): ?>
|
||||
<?php if ($log['entity_type'] === 'ticket' && $log['entity_id']) : ?>
|
||||
<a href="/ticket/<?= htmlspecialchars($log['entity_id']) ?>"><?= htmlspecialchars($log['entity_id']) ?></a>
|
||||
<?php else: ?>
|
||||
<?= htmlspecialchars($log['entity_id'] ?? '-') ?>
|
||||
<?php endif ?>
|
||||
<?php else : ?>
|
||||
<?= htmlspecialchars($log['entity_id'] ?? '-') ?>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td data-label="Details" class="lt-text-xs lt-text-muted" style="max-width:200px;overflow:hidden;text-overflow:ellipsis">
|
||||
<?php
|
||||
if ($log['details']) {
|
||||
$det = is_string($log['details']) ? json_decode($log['details'], true) : $log['details'];
|
||||
echo '<code>' . htmlspecialchars(is_array($det) ? json_encode($det) : (string)$log['details']) . '</code>';
|
||||
} else {
|
||||
echo '-';
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
if ($log['details']) {
|
||||
$det = is_string($log['details']) ? json_decode($log['details'], true) : $log['details'];
|
||||
echo '<code>' . htmlspecialchars(is_array($det) ? json_encode($det) : (string)$log['details']) . '</code>';
|
||||
} else {
|
||||
echo '-';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td data-label="IP" class="lt-text-xs lt-text-muted"><?= htmlspecialchars($log['ip_address'] ?? '-') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
<?php endforeach;
|
||||
endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php if (($totalPages ?? 1) > 1): ?>
|
||||
<?php if (($totalPages ?? 1) > 1) : ?>
|
||||
<div class="lt-pagination" role="navigation">
|
||||
<?php
|
||||
$params = $_GET;
|
||||
$start = max(1, $page - 2);
|
||||
$end = min($totalPages, $page + 2);
|
||||
if ($page > 1) {
|
||||
$params['page'] = $page - 1;
|
||||
$pUrl = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
||||
echo '<a href="' . $pUrl . '" class="lt-btn lt-btn-sm" aria-label="Previous page">«</a> ';
|
||||
}
|
||||
if ($start > 1) {
|
||||
$params['page'] = 1;
|
||||
echo '<a href="' . htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8') . '" class="lt-btn lt-btn-sm">1</a> ';
|
||||
if ($start > 2) echo '<span class="lt-text-muted lt-text-xs">…</span>';
|
||||
}
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
$params['page'] = $i;
|
||||
$url = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
||||
$class = ($i == $page) ? ' lt-btn-primary' : '';
|
||||
$curr = ($i == $page) ? ' aria-current="page"' : '';
|
||||
echo '<a href="' . $url . '" class="lt-btn lt-btn-sm' . $class . '"' . $curr . '>' . $i . '</a> ';
|
||||
}
|
||||
if ($end < $totalPages) {
|
||||
if ($end < $totalPages - 1) echo '<span class="lt-text-muted lt-text-xs">…</span>';
|
||||
$params['page'] = $totalPages;
|
||||
echo '<a href="' . htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8') . '" class="lt-btn lt-btn-sm">' . $totalPages . '</a> ';
|
||||
}
|
||||
if ($page < $totalPages) {
|
||||
$params['page'] = $page + 1;
|
||||
$nUrl = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
||||
echo '<a href="' . $nUrl . '" class="lt-btn lt-btn-sm" aria-label="Next page">»</a>';
|
||||
}
|
||||
?>
|
||||
<?php
|
||||
$params = $_GET;
|
||||
$start = max(1, $page - 2);
|
||||
$end = min($totalPages, $page + 2);
|
||||
if ($page > 1) {
|
||||
$params['page'] = $page - 1;
|
||||
$pUrl = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
||||
echo '<a href="' . $pUrl . '" class="lt-btn lt-btn-sm" aria-label="Previous page">«</a> ';
|
||||
}
|
||||
if ($start > 1) {
|
||||
$params['page'] = 1;
|
||||
echo '<a href="' . htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8') . '" class="lt-btn lt-btn-sm">1</a> ';
|
||||
if ($start > 2) {
|
||||
echo '<span class="lt-text-muted lt-text-xs">…</span>';
|
||||
}
|
||||
}
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
$params['page'] = $i;
|
||||
$url = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
||||
$class = ($i == $page) ? ' lt-btn-primary' : '';
|
||||
$curr = ($i == $page) ? ' aria-current="page"' : '';
|
||||
echo '<a href="' . $url . '" class="lt-btn lt-btn-sm' . $class . '"' . $curr . '>' . $i . '</a> ';
|
||||
}
|
||||
if ($end < $totalPages) {
|
||||
if ($end < $totalPages - 1) {
|
||||
echo '<span class="lt-text-muted lt-text-xs">…</span>';
|
||||
}
|
||||
$params['page'] = $totalPages;
|
||||
echo '<a href="' . htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8') . '" class="lt-btn lt-btn-sm">' . $totalPages . '</a> ';
|
||||
}
|
||||
if ($page < $totalPages) {
|
||||
$params['page'] = $page + 1;
|
||||
$nUrl = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
||||
echo '<a href="' . $nUrl . '" class="lt-btn lt-btn-sm" aria-label="Next page">»</a>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
|
||||
@@ -41,9 +41,10 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($customFields)): ?>
|
||||
<?php if (empty($customFields)) : ?>
|
||||
<tr><td colspan="8" class="lt-empty">No custom fields defined. Create fields to extend ticket metadata.</td></tr>
|
||||
<?php else: foreach ($customFields as $field): ?>
|
||||
<?php else :
|
||||
foreach ($customFields as $field) : ?>
|
||||
<tr>
|
||||
<td data-label="Order" class="lt-text-xs lt-text-muted"><?= (int)$field['display_order'] ?></td>
|
||||
<td data-label="Field Name"><code class="lt-text-cyan lt-text-xs"><?= htmlspecialchars($field['field_name']) ?></code></td>
|
||||
@@ -51,11 +52,11 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<td data-label="Type" class="lt-text-xs"><?= htmlspecialchars(ucfirst($field['field_type'])) ?></td>
|
||||
<td data-label="Category" class="lt-text-xs"><?= htmlspecialchars($field['category'] ?? 'All') ?></td>
|
||||
<td data-label="Required" class="lt-text-center">
|
||||
<?= $field['is_required'] ? '<span class="lt-text-amber">✓</span>' : '<span class="lt-text-muted">—</span>' ?>
|
||||
<?= $field['is_required'] ? '<span class="lt-text-amber">✓</span>' : '<span class="lt-text-muted">—</span>' ?>
|
||||
</td>
|
||||
<td data-label="Status">
|
||||
<span class="lt-status <?= $field['is_active'] ? 'lt-status-open' : 'lt-status-closed' ?>">
|
||||
<?= $field['is_active'] ? 'Active' : 'Inactive' ?>
|
||||
<?= $field['is_active'] ? 'Active' : 'Inactive' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
@@ -67,7 +68,8 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
<?php endforeach;
|
||||
endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -116,7 +118,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<label class="lt-label" for="cf-category">Category <span class="lt-text-muted lt-text-xs">(empty = all categories)</span></label>
|
||||
<select id="cf-category" name="category" class="lt-select">
|
||||
<option value="">All Categories</option>
|
||||
<?php foreach (['General','Hardware','Software','Network','Security'] as $c): ?>
|
||||
<?php foreach (['General','Hardware','Software','Network','Security'] as $c) : ?>
|
||||
<option value="<?= $c ?>"><?= $c ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
|
||||
@@ -40,32 +40,33 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($recurringTickets)): ?>
|
||||
<?php if (empty($recurringTickets)) : ?>
|
||||
<tr><td colspan="7" class="lt-empty">No recurring tickets configured. Create schedules to auto-generate tickets.</td></tr>
|
||||
<?php else: foreach ($recurringTickets as $rt): ?>
|
||||
<?php
|
||||
$schedule = ucfirst($rt['schedule_type']);
|
||||
if ($rt['schedule_type'] === 'weekly') {
|
||||
$days = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
$schedule .= ' (' . ($days[$rt['schedule_day']] ?? '?') . ')';
|
||||
} elseif ($rt['schedule_type'] === 'monthly') {
|
||||
$schedule .= ' (Day ' . $rt['schedule_day'] . ')';
|
||||
}
|
||||
$schedule .= ' @ ' . substr($rt['schedule_time'], 0, 5);
|
||||
?>
|
||||
<?php else :
|
||||
foreach ($recurringTickets as $rt) : ?>
|
||||
<?php
|
||||
$schedule = ucfirst($rt['schedule_type']);
|
||||
if ($rt['schedule_type'] === 'weekly') {
|
||||
$days = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
$schedule .= ' (' . ($days[$rt['schedule_day']] ?? '?') . ')';
|
||||
} elseif ($rt['schedule_type'] === 'monthly') {
|
||||
$schedule .= ' (Day ' . $rt['schedule_day'] . ')';
|
||||
}
|
||||
$schedule .= ' @ ' . substr($rt['schedule_time'], 0, 5);
|
||||
?>
|
||||
<tr>
|
||||
<td data-label="Title"><strong><?= htmlspecialchars($rt['title_template']) ?></strong></td>
|
||||
<td data-label="Schedule" class="lt-text-xs lt-text-cyan"><?= htmlspecialchars($schedule) ?></td>
|
||||
<td data-label="Category" class="lt-text-xs"><?= htmlspecialchars($rt['category']) ?></td>
|
||||
<td data-label="Assigned To" class="lt-text-xs">
|
||||
<?= htmlspecialchars($rt['assigned_name'] ?? $rt['assigned_username'] ?? 'Unassigned') ?>
|
||||
<?= htmlspecialchars($rt['assigned_name'] ?? $rt['assigned_username'] ?? 'Unassigned') ?>
|
||||
</td>
|
||||
<td data-label="Next Run" class="lt-text-xs lt-text-muted">
|
||||
<?= date('M d, Y H:i', strtotime($rt['next_run_at'])) ?>
|
||||
<?= date('M d, Y H:i', strtotime($rt['next_run_at'])) ?>
|
||||
</td>
|
||||
<td data-label="Status">
|
||||
<span class="lt-status <?= $rt['is_active'] ? 'lt-status-open' : 'lt-status-closed' ?>">
|
||||
<?= $rt['is_active'] ? 'Active' : 'Inactive' ?>
|
||||
<?= $rt['is_active'] ? 'Active' : 'Inactive' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
@@ -81,7 +82,8 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
<?php endforeach;
|
||||
endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -131,7 +133,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="rec-category">Category</label>
|
||||
<select id="rec-category" name="category" class="lt-select">
|
||||
<?php foreach (['General','Hardware','Software','Network','Security'] as $c): ?>
|
||||
<?php foreach (['General','Hardware','Software','Network','Security'] as $c) : ?>
|
||||
<option value="<?= $c ?>"><?= $c ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
@@ -139,7 +141,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="rec-type">Type</label>
|
||||
<select id="rec-type" name="type" class="lt-select">
|
||||
<?php foreach (['Issue','Maintenance','Install','Task','Upgrade','Problem'] as $t): ?>
|
||||
<?php foreach (['Issue','Maintenance','Install','Task','Upgrade','Problem'] as $t) : ?>
|
||||
<option value="<?= $t ?>"><?= $t ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
|
||||
@@ -39,14 +39,18 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($templates)): ?>
|
||||
<?php if (empty($templates)) : ?>
|
||||
<tr><td colspan="6" class="lt-empty">No templates defined. Create templates to speed up ticket creation.</td></tr>
|
||||
<?php else: foreach ($templates as $tpl): ?>
|
||||
<?php else :
|
||||
foreach ($templates as $tpl) : ?>
|
||||
<tr>
|
||||
<td data-label="Name"><strong><?= htmlspecialchars($tpl['template_name']) ?></strong></td>
|
||||
<td data-label="Category" class="lt-text-xs"><?= htmlspecialchars($tpl['category'] ?? 'Any') ?></td>
|
||||
<td data-label="Type" class="lt-text-xs"><?= htmlspecialchars($tpl['type'] ?? 'Any') ?></td>
|
||||
<?php $tp = (int)($tpl['default_priority'] ?? 4); $tBadge = match($tp) { 1 => 'lt-badge-p1', 2 => 'lt-badge-p2', 3 => 'lt-badge-p3', default => 'lt-badge-p4' }; ?>
|
||||
<?php $tp = (int)($tpl['default_priority'] ?? 4);
|
||||
$tBadge = match ($tp) {
|
||||
1 => 'lt-badge-p1', 2 => 'lt-badge-p2', 3 => 'lt-badge-p3', default => 'lt-badge-p4'
|
||||
}; ?>
|
||||
<td data-label="Priority"><span class="lt-badge <?= $tBadge ?>">P<?= $tp ?></span></td>
|
||||
<td data-label="Status">
|
||||
<span class="lt-status <?= ($tpl['is_active'] ?? 1) ? 'lt-status-open' : 'lt-status-closed' ?>">
|
||||
@@ -62,7 +66,8 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
<?php endforeach;
|
||||
endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -99,7 +104,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<label class="lt-label" for="tpl-category">Category</label>
|
||||
<select id="tpl-category" name="category" class="lt-select">
|
||||
<option value="">Any</option>
|
||||
<?php foreach (['General','Hardware','Software','Network','Security'] as $c): ?>
|
||||
<?php foreach (['General','Hardware','Software','Network','Security'] as $c) : ?>
|
||||
<option value="<?= $c ?>"><?= $c ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
@@ -108,7 +113,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<label class="lt-label" for="tpl-type">Type</label>
|
||||
<select id="tpl-type" name="type" class="lt-select">
|
||||
<option value="">Any</option>
|
||||
<?php foreach (['Maintenance','Install','Task','Upgrade','Issue','Problem'] as $t): ?>
|
||||
<?php foreach (['Maintenance','Install','Task','Upgrade','Issue','Problem'] as $t) : ?>
|
||||
<option value="<?= $t ?>"><?= $t ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
@@ -116,7 +121,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<div class="lt-form-group">
|
||||
<label class="lt-label" for="tpl-priority">Priority</label>
|
||||
<select id="tpl-priority" name="priority" class="lt-select">
|
||||
<?php foreach ([1=>'P1',2=>'P2',3=>'P3',4=>'P4 (default)',5=>'P5'] as $v=>$l): ?>
|
||||
<?php foreach ([1 => 'P1',2 => 'P2',3 => 'P3',4 => 'P4 (default)',5 => 'P5'] as $v => $l) : ?>
|
||||
<option value="<?= $v ?>" <?= $v === 4 ? 'selected' : '' ?>><?= $l ?></option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
|
||||
@@ -42,7 +42,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
</form>
|
||||
|
||||
<!-- Summary stats -->
|
||||
<?php if (!empty($userStats)): ?>
|
||||
<?php if (!empty($userStats)) : ?>
|
||||
<div class="lt-stats-grid lt-mb-md">
|
||||
<div class="lt-stat-card">
|
||||
<div class="lt-stat-icon lt-text-cyan">[ # ]</div>
|
||||
@@ -89,25 +89,27 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($userStats)): ?>
|
||||
<?php if (empty($userStats)) : ?>
|
||||
<tr><td colspan="6" class="lt-empty">No user activity data available.</td></tr>
|
||||
<?php else: foreach ($userStats as $u): ?>
|
||||
<?php else :
|
||||
foreach ($userStats as $u) : ?>
|
||||
<tr>
|
||||
<td data-label="User">
|
||||
<strong><?= htmlspecialchars($u['display_name'] ?? $u['username']) ?></strong>
|
||||
<?php if ($u['is_admin']): ?>
|
||||
<?php if ($u['is_admin']) : ?>
|
||||
<span class="lt-badge lt-badge-admin lt-text-xs">ADMIN</span>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td data-label="Created"><span class="lt-text-cyan"><?= (int)($u['tickets_created'] ?? 0) ?></span></td>
|
||||
<td data-label="Resolved"><span class="lt-text-muted"><?= (int)($u['tickets_resolved'] ?? 0) ?></span></td>
|
||||
<td data-label="Comments"><span class="lt-text-amber"><?= (int)($u['comments_added'] ?? 0) ?></span></td>
|
||||
<td data-label="Assigned"><?= (int)($u['tickets_assigned'] ?? 0) ?></td>
|
||||
<td data-label="Last Activity" class="lt-text-xs lt-text-muted">
|
||||
<?= $u['last_activity'] ? date('M d, Y H:i', strtotime($u['last_activity'])) : 'Never' ?>
|
||||
<?= $u['last_activity'] ? date('M d, Y H:i', strtotime($u['last_activity'])) : 'Never' ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
<?php endforeach;
|
||||
endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -25,17 +25,23 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<div class="lt-section-body">
|
||||
<div class="lt-grid-4">
|
||||
<?php
|
||||
$statuses = ['Open', 'Pending', 'In Progress', 'Closed'];
|
||||
foreach ($statuses as $status):
|
||||
$slug = strtolower(str_replace(' ', '-', $status));
|
||||
$toCount = 0;
|
||||
if (isset($workflows)) { foreach ($workflows as $w) { if ($w['from_status'] === $status) $toCount++; } }
|
||||
?>
|
||||
$statuses = ['Open', 'Pending', 'In Progress', 'Closed'];
|
||||
foreach ($statuses as $status) :
|
||||
$slug = strtolower(str_replace(' ', '-', $status));
|
||||
$toCount = 0;
|
||||
if (isset($workflows)) {
|
||||
foreach ($workflows as $w) {
|
||||
if ($w['from_status'] === $status) {
|
||||
$toCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="lt-card lt-text-center">
|
||||
<span class="lt-status lt-status-<?= $slug ?>"><?= $status ?></span>
|
||||
<div class="lt-text-xs lt-text-muted lt-mt-sm">→ <?= $toCount ?> transition<?= $toCount !== 1 ? 's' : '' ?></div>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
<p class="lt-text-xs lt-text-muted lt-mt-sm">
|
||||
Define which status transitions are allowed. This controls what options appear in the status dropdown on tickets.
|
||||
@@ -61,10 +67,12 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($workflows)): ?>
|
||||
<?php if (empty($workflows)) : ?>
|
||||
<tr><td colspan="7" class="lt-empty">No transitions defined. Add transitions to enable status changes.</td></tr>
|
||||
<?php else: foreach ($workflows as $wf): ?>
|
||||
<?php $fromSlug = preg_replace('/[^a-z-]/', '', strtolower(str_replace(' ', '-', $wf['from_status']))); $toSlug = preg_replace('/[^a-z-]/', '', strtolower(str_replace(' ', '-', $wf['to_status']))); ?>
|
||||
<?php else :
|
||||
foreach ($workflows as $wf) : ?>
|
||||
<?php $fromSlug = preg_replace('/[^a-z-]/', '', strtolower(str_replace(' ', '-', $wf['from_status'])));
|
||||
$toSlug = preg_replace('/[^a-z-]/', '', strtolower(str_replace(' ', '-', $wf['to_status']))); ?>
|
||||
<tr>
|
||||
<td data-label="From">
|
||||
<span class="lt-status lt-status-<?= $fromSlug ?>"><?= htmlspecialchars($wf['from_status']) ?></span>
|
||||
@@ -74,15 +82,15 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<span class="lt-status lt-status-<?= $toSlug ?>"><?= htmlspecialchars($wf['to_status']) ?></span>
|
||||
</td>
|
||||
<td data-label="Req. Comment" class="lt-text-center">
|
||||
<?= $wf['requires_comment'] ? '<span class="lt-text-cyan">✓</span>' : '<span class="lt-text-muted">—</span>' ?>
|
||||
<?= $wf['requires_comment'] ? '<span class="lt-text-cyan">✓</span>' : '<span class="lt-text-muted">—</span>' ?>
|
||||
</td>
|
||||
<td data-label="Req. Admin" class="lt-text-center">
|
||||
<?= $wf['requires_admin'] ? '<span class="lt-text-amber">✓</span>' : '<span class="lt-text-muted">—</span>' ?>
|
||||
<?= $wf['requires_admin'] ? '<span class="lt-text-amber">✓</span>' : '<span class="lt-text-muted">—</span>' ?>
|
||||
</td>
|
||||
<td data-label="Active" class="lt-text-center">
|
||||
<?= $wf['is_active']
|
||||
? '<span class="lt-text-cyan">✓</span>'
|
||||
: '<span class="lt-text-danger">✗</span>' ?>
|
||||
<?= $wf['is_active']
|
||||
? '<span class="lt-text-cyan">✓</span>'
|
||||
: '<span class="lt-text-danger">✗</span>' ?>
|
||||
</td>
|
||||
<td data-label="Actions">
|
||||
<div class="lt-btn-group">
|
||||
@@ -93,7 +101,8 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif ?>
|
||||
<?php endforeach;
|
||||
endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
+14
-13
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* layout_footer.php — Shared bottom-of-page partial for all views.
|
||||
*
|
||||
@@ -23,12 +24,12 @@
|
||||
================================================================ -->
|
||||
<?php
|
||||
// Context-sensitive keyboard hints based on active nav
|
||||
$_ltf_nav = $activeNav ?? 'dashboard';
|
||||
$_ltf_isTicket = str_starts_with($pageTitle ?? '', 'Ticket #');
|
||||
?>
|
||||
$_ltf_nav = $activeNav ?? 'dashboard';
|
||||
$_ltf_isTicket = str_starts_with($pageTitle ?? '', 'Ticket #');
|
||||
?>
|
||||
<footer class="lt-footer" role="contentinfo" aria-label="Keyboard shortcuts and app info">
|
||||
<nav class="lt-footer-hints" aria-label="Keyboard shortcuts">
|
||||
<?php if ($_ltf_isTicket): ?>
|
||||
<?php if ($_ltf_isTicket) : ?>
|
||||
<a href="/" class="lt-footer-hint" title="Go to dashboard"><span class="lt-footer-key">[ ← ]</span> BACK</a>
|
||||
<span class="lt-footer-sep">|</span>
|
||||
<span class="lt-footer-hint" title="Press 1–4 to change status"><span class="lt-footer-key">[ 1-4 ]</span> STATUS</span>
|
||||
@@ -36,11 +37,11 @@
|
||||
<span class="lt-footer-hint" title="Press C to jump to comment box"><span class="lt-footer-key">[ C ]</span> COMMENT</span>
|
||||
<span class="lt-footer-sep">|</span>
|
||||
<button type="button" class="lt-footer-hint" data-action="open-settings" title="Open settings"><span class="lt-footer-key">[ * ]</span> CFG</button>
|
||||
<?php elseif (str_starts_with($_ltf_nav, 'admin')): ?>
|
||||
<?php elseif (str_starts_with($_ltf_nav, 'admin')) : ?>
|
||||
<a href="/" class="lt-footer-hint" title="Go to dashboard"><span class="lt-footer-key">[ ~ ]</span> HOME</a>
|
||||
<span class="lt-footer-sep">|</span>
|
||||
<button type="button" class="lt-footer-hint" data-action="open-settings" title="Open settings"><span class="lt-footer-key">[ * ]</span> CFG</button>
|
||||
<?php else: ?>
|
||||
<?php else : ?>
|
||||
<a href="/" class="lt-footer-hint" title="Go to dashboard (G then D)"><span class="lt-footer-key">[ ~ ]</span> HOME</a>
|
||||
<span class="lt-footer-sep">|</span>
|
||||
<span class="lt-footer-hint" title="Press / or Ctrl+K to search"><span class="lt-footer-key">[ / ]</span> SEARCH</span>
|
||||
@@ -114,17 +115,17 @@
|
||||
|
||||
<!-- base.js + utils.js + globals already loaded in <head> via layout_header.php -->
|
||||
|
||||
<?php if (!empty($pageScripts)): ?>
|
||||
<?php if (!empty($pageScripts)) : ?>
|
||||
<!-- PAGE-SPECIFIC SCRIPTS -->
|
||||
<?php foreach ($pageScripts as $_ltf_script): ?>
|
||||
<?php foreach ($pageScripts as $_ltf_script) : ?>
|
||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="<?= htmlspecialchars($_ltf_script, ENT_QUOTES, 'UTF-8') ?>"></script>
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($pageInlineScript)): ?>
|
||||
<?php if (!empty($pageInlineScript)) : ?>
|
||||
<!-- PAGE INLINE SCRIPT -->
|
||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?= $pageInlineScript ?>
|
||||
<?= $pageInlineScript ?>
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -144,7 +145,7 @@
|
||||
{ id: 'help-shortcuts', group: 'Help', icon: '?', label: 'Keyboard Shortcuts', kbd: '?', action: function() { lt.modal.open('lt-keys-help'); } },
|
||||
{ id: 'help-theme', group: 'Help', icon: '*', label: 'Toggle Theme', action: function() { lt.theme.toggle(); } },
|
||||
];
|
||||
<?php if (!empty($GLOBALS['currentUser']['is_admin'])): ?>
|
||||
<?php if (!empty($GLOBALS['currentUser']['is_admin'])) : ?>
|
||||
_cpCmds = _cpCmds.concat([
|
||||
{ id: 'admin-templates', group: 'Admin', icon: 'T', label: 'Templates', action: function() { window.location.href = '/admin/templates'; } },
|
||||
{ id: 'admin-workflow', group: 'Admin', icon: 'W', label: 'Workflow', action: function() { window.location.href = '/admin/workflow'; } },
|
||||
@@ -173,7 +174,7 @@
|
||||
}
|
||||
|
||||
// ── Notification Bell ─────────────────────────────────────────────
|
||||
<?php if (!empty($GLOBALS['currentUser'])): ?>
|
||||
<?php if (!empty($GLOBALS['currentUser'])) : ?>
|
||||
(function() {
|
||||
var bell = document.getElementById('lt-notif-bell');
|
||||
var panel = document.getElementById('lt-notif-panel');
|
||||
|
||||
+13
-12
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* layout_header.php — Shared top-of-page partial for all views.
|
||||
*
|
||||
@@ -33,10 +34,10 @@ $_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=VT323&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/assets/css/base.css?v=<?= $_lt_assetVer ?>">
|
||||
<?php if (!empty($pageStyles)): ?>
|
||||
<?php foreach ($pageStyles as $_lt_css): ?>
|
||||
<?php if (!empty($pageStyles)) : ?>
|
||||
<?php foreach ($pageStyles as $_lt_css) : ?>
|
||||
<link rel="stylesheet" href="<?= htmlspecialchars($_lt_css, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?php endforeach; ?>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<link rel="icon" href="/assets/images/favicon.png" type="image/png">
|
||||
<!-- Base JS loaded in head so lt.* is available for inline view scripts -->
|
||||
@@ -50,7 +51,7 @@ $_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
|
||||
window.APP_TIMEZONE_OFFSET = <?= (int)($GLOBALS['config']['TIMEZONE_OFFSET'] ?? 0) ?>;
|
||||
window.CURRENT_USER = <?= json_encode([
|
||||
'id' => (int)($GLOBALS['currentUser']['user_id'] ?? 0),
|
||||
'username'=> $GLOBALS['currentUser']['username'] ?? '',
|
||||
'username' => $GLOBALS['currentUser']['username'] ?? '',
|
||||
'isAdmin' => !empty($GLOBALS['currentUser']['is_admin']),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
|
||||
</script>
|
||||
@@ -74,7 +75,7 @@ $_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
|
||||
<a href="/"
|
||||
class="lt-nav-drawer-link<?= $_lt_navActive === 'dashboard' ? ' active' : '' ?>"
|
||||
<?= $_lt_navActive === 'dashboard' ? 'aria-current="page"' : '' ?>>Dashboard</a>
|
||||
<?php if ($_lt_isAdmin): ?>
|
||||
<?php if ($_lt_isAdmin) : ?>
|
||||
<div class="lt-nav-drawer-section">Admin</div>
|
||||
<a href="/admin/templates" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-templates' ? ' active' : '' ?>">Templates</a>
|
||||
<a href="/admin/workflow" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-workflow' ? ' active' : '' ?>">Workflow</a>
|
||||
@@ -123,7 +124,7 @@ $_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
|
||||
<?= $_lt_navActive === 'dashboard' ? 'aria-current="page"' : '' ?>>
|
||||
Dashboard
|
||||
</a>
|
||||
<?php if ($_lt_isAdmin): ?>
|
||||
<?php if ($_lt_isAdmin) : ?>
|
||||
<div class="lt-nav-dropdown" data-action="toggle-nav-dropdown">
|
||||
<a href="#"
|
||||
class="lt-nav-link<?= str_starts_with($_lt_navActive, 'admin') ? ' active' : '' ?>"
|
||||
@@ -152,7 +153,7 @@ $_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
|
||||
</div><!-- /.lt-header-left -->
|
||||
|
||||
<div class="lt-header-right">
|
||||
<?php if (!empty($_lt_user)): ?>
|
||||
<?php if (!empty($_lt_user)) : ?>
|
||||
<?php
|
||||
$_lt_displayName = $_lt_user['display_name'] ?? $_lt_user['username'] ?? '';
|
||||
$_lt_words = array_filter(explode(' ', $_lt_displayName));
|
||||
@@ -160,9 +161,9 @@ $_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
|
||||
$_lt_userId = (int)($_lt_user['user_id'] ?? 0);
|
||||
$_lt_avatarColors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', ''];
|
||||
$_lt_avatarColor = $_lt_avatarColors[abs(crc32($_lt_displayName)) % count($_lt_avatarColors)];
|
||||
?>
|
||||
?>
|
||||
<div class="lt-avatar lt-avatar--sm <?= $_lt_avatarColor ?>" aria-hidden="true" title="<?= htmlspecialchars($_lt_displayName, ENT_QUOTES, 'UTF-8') ?>">
|
||||
<?php if ($_lt_userId > 0): ?>
|
||||
<?php if ($_lt_userId > 0) : ?>
|
||||
<img src="/api/user_avatar.php?user_id=<?= $_lt_userId ?>"
|
||||
alt=""
|
||||
class="lt-avatar-img">
|
||||
@@ -170,12 +171,12 @@ $_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
|
||||
<span class="lt-avatar-initials"><?= htmlspecialchars($_lt_initials) ?></span>
|
||||
</div>
|
||||
<span class="lt-header-user"><?= htmlspecialchars($_lt_displayName, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<?php if ($_lt_isAdmin): ?>
|
||||
<?php if ($_lt_isAdmin) : ?>
|
||||
<span class="lt-badge lt-badge-admin" aria-label="Administrator">ADMIN</span>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<!-- Notification Bell -->
|
||||
<?php if (!empty($_lt_user)): ?>
|
||||
<?php if (!empty($_lt_user)) : ?>
|
||||
<div class="lt-notif-dropdown-wrap" id="lt-notif-wrap">
|
||||
<button type="button"
|
||||
class="lt-btn lt-btn-ghost lt-btn-sm lt-notif-wrap"
|
||||
|
||||
Reference in New Issue
Block a user