Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b42597c927 | |||
| e721b33911 | |||
| d7775e62ec |
+2
-2
@@ -71,8 +71,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
|||||||
// Normal JSON response for filtered logs
|
// Normal JSON response for filtered logs
|
||||||
try {
|
try {
|
||||||
// Get pagination parameters
|
// Get pagination parameters
|
||||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||||
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 50;
|
$limit = min(500, max(1, (int)($_GET['limit'] ?? 50)));
|
||||||
$offset = ($page - 1) * $limit;
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
// Build filters
|
// Build filters
|
||||||
|
|||||||
+32
-2
@@ -85,6 +85,8 @@
|
|||||||
--lt-cyan: var(--accent-cyan);
|
--lt-cyan: var(--accent-cyan);
|
||||||
--lt-success: var(--accent-green);
|
--lt-success: var(--accent-green);
|
||||||
--lt-text-primary: var(--accent-green);
|
--lt-text-primary: var(--accent-green);
|
||||||
|
--lt-border: var(--border-color);
|
||||||
|
--lt-surface: var(--bg-card);
|
||||||
|
|
||||||
/* Legacy aliases — keeps all existing component HTML working */
|
/* Legacy aliases — keeps all existing component HTML working */
|
||||||
--terminal-green: var(--accent-green);
|
--terminal-green: var(--accent-green);
|
||||||
@@ -359,6 +361,10 @@ hr {
|
|||||||
.lt-main {
|
.lt-main {
|
||||||
padding-top: calc(var(--header-height) + var(--space-lg));
|
padding-top: calc(var(--header-height) + var(--space-lg));
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
/* When body is a flex column, margin:0 auto from .lt-container would prevent
|
||||||
|
stretch. Force full width so max-width+auto-margin centering still works. */
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0; /* prevent flex overflow on very small viewports */
|
||||||
}
|
}
|
||||||
|
|
||||||
.lt-layout {
|
.lt-layout {
|
||||||
@@ -962,11 +968,26 @@ select option:checked {
|
|||||||
clip-path: none;
|
clip-path: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Compact size variants for inline filter bars and tight layouts */
|
||||||
|
.lt-select-sm {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.25rem 1.6rem 0.25rem 0.5rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.lt-input-sm {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.lt-form-hint {
|
.lt-form-hint {
|
||||||
font-size: 0.63rem;
|
font-size: 0.63rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
.lt-form-hint--warn { color: var(--accent-amber); }
|
||||||
|
|
||||||
|
.lt-font-mono { font-family: var(--font-mono); }
|
||||||
|
|
||||||
/* Search */
|
/* Search */
|
||||||
.lt-search { position: relative; }
|
.lt-search { position: relative; }
|
||||||
@@ -1211,6 +1232,7 @@ select option:checked {
|
|||||||
.lt-badge-green { color: var(--accent-green); }
|
.lt-badge-green { color: var(--accent-green); }
|
||||||
.lt-badge-amber { color: var(--accent-amber); }
|
.lt-badge-amber { color: var(--accent-amber); }
|
||||||
.lt-badge-red { color: var(--accent-red); }
|
.lt-badge-red { color: var(--accent-red); }
|
||||||
|
.lt-badge-sm { font-size: 0.52rem; padding: 0.05rem 0.3rem; letter-spacing: 0.08em; }
|
||||||
|
|
||||||
/* Status + priority badge variants (dark-mode base) */
|
/* Status + priority badge variants (dark-mode base) */
|
||||||
.lt-badge-open { color: var(--accent-green); background: rgba(0,255,136,0.08); border-color: rgba(0,255,136,0.35); text-shadow: var(--glow-green); }
|
.lt-badge-open { color: var(--accent-green); background: rgba(0,255,136,0.08); border-color: rgba(0,255,136,0.35); text-shadow: var(--glow-green); }
|
||||||
@@ -1581,12 +1603,14 @@ select option:checked {
|
|||||||
}
|
}
|
||||||
.lt-msg::before { flex-shrink: 0; font-weight: 700; }
|
.lt-msg::before { flex-shrink: 0; font-weight: 700; }
|
||||||
|
|
||||||
.lt-msg-error { color: var(--accent-red); background: var(--accent-red-dim); border-left-color: var(--accent-red); }
|
.lt-msg-error,
|
||||||
|
.lt-msg-danger { color: var(--accent-red); background: var(--accent-red-dim); border-left-color: var(--accent-red); }
|
||||||
.lt-msg-success { color: var(--accent-green); background: var(--accent-green-dim); border-left-color: var(--accent-green); }
|
.lt-msg-success { color: var(--accent-green); background: var(--accent-green-dim); border-left-color: var(--accent-green); }
|
||||||
.lt-msg-warning { color: var(--accent-amber); background: var(--accent-amber-dim); border-left-color: var(--accent-amber); }
|
.lt-msg-warning { color: var(--accent-amber); background: var(--accent-amber-dim); border-left-color: var(--accent-amber); }
|
||||||
.lt-msg-info { color: var(--accent-cyan); background: var(--accent-cyan-dim); border-left-color: var(--accent-cyan); }
|
.lt-msg-info { color: var(--accent-cyan); background: var(--accent-cyan-dim); border-left-color: var(--accent-cyan); }
|
||||||
|
|
||||||
.lt-msg-error::before { content: '✗'; }
|
.lt-msg-error::before,
|
||||||
|
.lt-msg-danger::before { content: '✗'; }
|
||||||
.lt-msg-success::before { content: '✓'; }
|
.lt-msg-success::before { content: '✓'; }
|
||||||
.lt-msg-warning::before { content: '!'; }
|
.lt-msg-warning::before { content: '!'; }
|
||||||
.lt-msg-info::before { content: 'i'; }
|
.lt-msg-info::before { content: 'i'; }
|
||||||
@@ -3192,6 +3216,11 @@ input[type="range"].lt-range::-moz-range-thumb {
|
|||||||
.lt-kv-val--green { color: var(--accent-green); }
|
.lt-kv-val--green { color: var(--accent-green); }
|
||||||
.lt-kv-val--red { color: var(--accent-red); }
|
.lt-kv-val--red { color: var(--accent-red); }
|
||||||
|
|
||||||
|
/* v1.2 aliases: lt-kv-row wraps label+value as a transparent grid wrapper */
|
||||||
|
.lt-kv-row { display: contents; }
|
||||||
|
.lt-kv-label { padding: var(--space-xs) var(--space-md) var(--space-xs) 0; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.7rem; white-space: nowrap; border-right: 1px solid var(--border-dim); }
|
||||||
|
.lt-kv-value { padding: var(--space-xs) 0 var(--space-xs) var(--space-md); color: var(--text-primary); }
|
||||||
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------
|
/* ----------------------------------------------------------------
|
||||||
43. HERO / BANNER SECTION
|
43. HERO / BANNER SECTION
|
||||||
@@ -4456,6 +4485,7 @@ body.lt-is-offline .lt-main { margin-top: 2rem; transition: margin-top 0.25s eas
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.lt-timeline-actor { color: var(--accent-cyan); }
|
.lt-timeline-actor { color: var(--accent-cyan); }
|
||||||
|
.lt-timeline-action { color: var(--text-secondary); font-style: italic; }
|
||||||
.lt-timeline-time { margin-left: auto; white-space: nowrap; }
|
.lt-timeline-time { margin-left: auto; white-space: nowrap; }
|
||||||
.lt-timeline-body { font-size: 0.78rem; color: var(--text-secondary); line-height: 1.5; }
|
.lt-timeline-body { font-size: 0.78rem; color: var(--text-secondary); line-height: 1.5; }
|
||||||
.lt-timeline-body code { font-size: 0.72rem; color: var(--accent-green); }
|
.lt-timeline-body code { font-size: 0.72rem; color: var(--accent-green); }
|
||||||
|
|||||||
+13
-12
@@ -143,7 +143,7 @@ include __DIR__ . '/layout_header.php';
|
|||||||
<!-- ═══════════════════════════════════════════════════════════
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
VIEW TABS (Table / Kanban) — dual-purpose: lt.tabs + dashboard.js set-view-mode
|
VIEW TABS (Table / Kanban) — dual-purpose: lt.tabs + dashboard.js set-view-mode
|
||||||
═══════════════════════════════════════════════════════════ -->
|
═══════════════════════════════════════════════════════════ -->
|
||||||
<div class="lt-tabs" role="tablist" aria-label="Ticket view mode">
|
<div class="lt-tab-bar" role="tablist" aria-label="Ticket view mode">
|
||||||
<button type="button" id="tableViewBtn"
|
<button type="button" id="tableViewBtn"
|
||||||
class="lt-tab active"
|
class="lt-tab active"
|
||||||
role="tab" aria-selected="true" aria-controls="tab-table"
|
role="tab" aria-selected="true" aria-controls="tab-table"
|
||||||
@@ -158,7 +158,7 @@ include __DIR__ . '/layout_header.php';
|
|||||||
data-action="set-view-mode" data-mode="card">
|
data-action="set-view-mode" data-mode="card">
|
||||||
⊕ Kanban
|
⊕ Kanban
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div><!-- /.lt-tab-bar -->
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
LAYOUT WRAPPER: sidebar + main content
|
LAYOUT WRAPPER: sidebar + main content
|
||||||
@@ -387,14 +387,15 @@ include __DIR__ . '/layout_header.php';
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php if (empty($tickets)): ?>
|
<?php if (empty($tickets)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="<?= $colCount ?>" class="lt-empty" style="text-align:center;padding:2rem">
|
<td colspan="<?= $colCount ?>" class="lt-empty">
|
||||||
<pre class="lt-text-muted lt-text-xs" style="display:inline-block;text-align:left">
|
<div class="lt-empty-state">
|
||||||
╔══════════════════════════════════╗
|
<div class="lt-empty-state-icon">📭</div>
|
||||||
║ ║
|
<div class="lt-empty-state-title">No Tickets Found</div>
|
||||||
║ NO TICKETS FOUND ║
|
<div class="lt-empty-state-body">No tickets match your current filters.</div>
|
||||||
║ [ ] Empty queue — all clear ║
|
<?php if (!empty($activeFilters) || !empty($_GET['search'])): ?>
|
||||||
║ ║
|
<a href="?" class="lt-btn lt-btn-sm lt-btn-ghost">Clear Filters</a>
|
||||||
╚══════════════════════════════════╝</pre>
|
<?php endif ?>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
@@ -422,8 +423,8 @@ include __DIR__ . '/layout_header.php';
|
|||||||
class="ticket-link"><?= htmlspecialchars($row['ticket_id']) ?></a>
|
class="ticket-link"><?= htmlspecialchars($row['ticket_id']) ?></a>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Priority">
|
<td data-label="Priority">
|
||||||
<?php $chipClass = match($pNum) { 1 => 'lt-chip-critical', 2 => 'lt-chip-warn', 3 => 'lt-chip-info', default => 'lt-chip-ok' }; ?>
|
<?php $badgeClass = match($pNum) { 1 => 'lt-badge-p1', 2 => 'lt-badge-p2', 3 => 'lt-badge-p3', default => 'lt-badge-p4' }; ?>
|
||||||
<span class="lt-chip <?= $chipClass ?>">P<?= $pNum ?></span>
|
<span class="lt-badge <?= $badgeClass ?>">P<?= $pNum ?></span>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Title"><?= htmlspecialchars($row['title']) ?></td>
|
<td data-label="Title"><?= htmlspecialchars($row['title']) ?></td>
|
||||||
<td data-label="Category" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['category']) ?></td>
|
<td data-label="Category" class="lt-text-muted lt-text-xs"><?= htmlspecialchars($row['category']) ?></td>
|
||||||
|
|||||||
@@ -71,12 +71,12 @@ $visUserModel = new UserModel($conn);
|
|||||||
$allAvailableGroups = $visUserModel->getAllGroups();
|
$allAvailableGroups = $visUserModel->getAllGroups();
|
||||||
|
|
||||||
// JSON-encode ticket fields for the inline script
|
// JSON-encode ticket fields for the inline script
|
||||||
$json_ticket_id = json_encode($ticket['ticket_id']);
|
$json_ticket_id = json_encode($ticket['ticket_id'], JSON_HEX_TAG);
|
||||||
$json_title = json_encode($ticket['title']);
|
$json_title = json_encode($ticket['title'], JSON_HEX_TAG);
|
||||||
$json_status = json_encode($ticket['status']);
|
$json_status = json_encode($ticket['status'], JSON_HEX_TAG);
|
||||||
$json_priority = json_encode($ticket['priority']);
|
$json_priority = json_encode($ticket['priority'], JSON_HEX_TAG);
|
||||||
$json_category = json_encode($ticket['category']);
|
$json_category = json_encode($ticket['category'], JSON_HEX_TAG);
|
||||||
$json_type = json_encode($ticket['type']);
|
$json_type = json_encode($ticket['type'], JSON_HEX_TAG);
|
||||||
$pageInlineScript = <<<JS
|
$pageInlineScript = <<<JS
|
||||||
window.ticketData = {
|
window.ticketData = {
|
||||||
ticket_id: {$json_ticket_id},
|
ticket_id: {$json_ticket_id},
|
||||||
@@ -266,7 +266,7 @@ include __DIR__ . '/layout_header.php';
|
|||||||
<!-- ═══════════════════════════════════════════════════════════
|
<!-- ═══════════════════════════════════════════════════════════
|
||||||
TAB NAVIGATION
|
TAB NAVIGATION
|
||||||
═══════════════════════════════════════════════════════════ -->
|
═══════════════════════════════════════════════════════════ -->
|
||||||
<div class="lt-tabs" role="tablist" aria-label="Ticket content sections">
|
<div class="lt-tab-bar" role="tablist" aria-label="Ticket content sections">
|
||||||
<button type="button" class="lt-tab active" id="description-tab-btn"
|
<button type="button" class="lt-tab active" id="description-tab-btn"
|
||||||
role="tab" data-tab="description-panel" aria-selected="true" aria-controls="description-panel">
|
role="tab" data-tab="description-panel" aria-selected="true" aria-controls="description-panel">
|
||||||
Description
|
Description
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
<tr><td colspan="8" class="lt-empty">No API keys found. Generate one above.</td></tr>
|
<tr><td colspan="8" class="lt-empty">No API keys found. Generate one above.</td></tr>
|
||||||
<?php else: foreach ($apiKeys as $key): ?>
|
<?php else: foreach ($apiKeys as $key): ?>
|
||||||
<?php $expired = $key['expires_at'] && strtotime($key['expires_at']) < time(); ?>
|
<?php $expired = $key['expires_at'] && strtotime($key['expires_at']) < time(); ?>
|
||||||
<tr id="key-row-<?= $key['api_key_id'] ?>">
|
<tr id="key-row-<?= (int)$key['api_key_id'] ?>">
|
||||||
<td data-label="Name"><strong><?= htmlspecialchars($key['key_name']) ?></strong></td>
|
<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="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 By" class="lt-text-xs"><?= htmlspecialchars($key['display_name'] ?? $key['username'] ?? 'Unknown') ?></td>
|
||||||
@@ -96,7 +96,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
<td data-label="Actions">
|
<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"
|
<button type="button" class="lt-btn lt-btn-sm lt-btn-danger"
|
||||||
data-action="revoke-key" data-id="<?= $key['api_key_id'] ?>">REVOKE</button>
|
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>
|
<span class="lt-text-muted lt-text-xs">—</span>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
@@ -173,10 +173,12 @@ document.getElementById('generateKeyForm').addEventListener('submit', function (
|
|||||||
});
|
});
|
||||||
|
|
||||||
function copyApiKey() {
|
function copyApiKey() {
|
||||||
var input = document.getElementById('newKeyValue');
|
var val = document.getElementById('newKeyValue').value;
|
||||||
input.select();
|
lt.copy(val).then(function () {
|
||||||
document.execCommand('copy');
|
|
||||||
lt.toast.success('Copied to clipboard!');
|
lt.toast.success('Copied to clipboard!');
|
||||||
|
}).catch(function () {
|
||||||
|
lt.toast.error('Copy failed — select the key manually');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function revokeKey(keyId) {
|
function revokeKey(keyId) {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
<select name="user_id" id="user_id" class="lt-select lt-select-sm">
|
<select name="user_id" id="user_id" class="lt-select lt-select-sm">
|
||||||
<option value="">All Users</option>
|
<option value="">All Users</option>
|
||||||
<?php if (isset($users)): foreach ($users as $u): ?>
|
<?php if (isset($users)): foreach ($users as $u): ?>
|
||||||
<option value="<?= $u['user_id'] ?>" <?= ($filters['user_id'] ?? '') == $u['user_id'] ? 'selected' : '' ?>>
|
<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>
|
</option>
|
||||||
<?php endforeach; endif ?>
|
<?php endforeach; endif ?>
|
||||||
@@ -112,13 +112,35 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
<div class="lt-pagination" role="navigation">
|
<div class="lt-pagination" role="navigation">
|
||||||
<?php
|
<?php
|
||||||
$params = $_GET;
|
$params = $_GET;
|
||||||
for ($i = 1; $i <= min($totalPages, 10); $i++) {
|
$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;
|
$params['page'] = $i;
|
||||||
$url = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
$url = htmlspecialchars('?' . http_build_query($params), ENT_QUOTES, 'UTF-8');
|
||||||
$class = ($i == $page) ? ' lt-btn-primary' : '';
|
$class = ($i == $page) ? ' lt-btn-primary' : '';
|
||||||
echo "<a href='$url' class='lt-btn lt-btn-sm$class'>$i</a> ";
|
$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>';
|
||||||
}
|
}
|
||||||
if ($totalPages > 10) echo '<span class="lt-text-muted lt-text-xs">…</span>';
|
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif ?>
|
<?php endif ?>
|
||||||
|
|||||||
@@ -202,8 +202,10 @@ function editField(id) {
|
|||||||
}
|
}
|
||||||
document.getElementById('cfModalTitle').textContent = 'Edit Custom Field';
|
document.getElementById('cfModalTitle').textContent = 'Edit Custom Field';
|
||||||
lt.modal.open('fieldModal');
|
lt.modal.open('fieldModal');
|
||||||
|
} else {
|
||||||
|
lt.toast.error(data.error || 'Failed to load field');
|
||||||
}
|
}
|
||||||
});
|
}).catch(function () { lt.toast.error('Failed to load field'); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteField(id) {
|
function deleteField(id) {
|
||||||
|
|||||||
@@ -240,8 +240,10 @@ function editRecurring(id) {
|
|||||||
document.getElementById('assigned_to').value = rt.assigned_to || '';
|
document.getElementById('assigned_to').value = rt.assigned_to || '';
|
||||||
document.getElementById('recModalTitle').textContent = 'Edit Recurring Ticket';
|
document.getElementById('recModalTitle').textContent = 'Edit Recurring Ticket';
|
||||||
lt.modal.open('recurringModal');
|
lt.modal.open('recurringModal');
|
||||||
|
} else {
|
||||||
|
lt.toast.error(data.error || 'Failed to load schedule');
|
||||||
}
|
}
|
||||||
});
|
}).catch(function () { lt.toast.error('Failed to load schedule'); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleRecurring(id) {
|
function toggleRecurring(id) {
|
||||||
@@ -287,7 +289,7 @@ function loadUsers() {
|
|||||||
select.appendChild(opt);
|
select.appendChild(opt);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}).catch(function () { /* non-critical: assigned_to stays as manual input */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScheduleOptions();
|
updateScheduleOptions();
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
<td data-label="Name"><strong><?= htmlspecialchars($tpl['template_name']) ?></strong></td>
|
<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="Category" class="lt-text-xs"><?= htmlspecialchars($tpl['category'] ?? 'Any') ?></td>
|
||||||
<td data-label="Type" class="lt-text-xs"><?= htmlspecialchars($tpl['type'] ?? 'Any') ?></td>
|
<td data-label="Type" class="lt-text-xs"><?= htmlspecialchars($tpl['type'] ?? 'Any') ?></td>
|
||||||
<?php $tp = (int)($tpl['default_priority'] ?? 4); $tChip = match($tp) { 1 => 'lt-chip-critical', 2 => 'lt-chip-warn', 3 => 'lt-chip-info', default => 'lt-chip-ok' }; ?>
|
<?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-chip <?= $tChip ?>">P<?= $tp ?></span></td>
|
<td data-label="Priority"><span class="lt-badge <?= $tBadge ?>">P<?= $tp ?></span></td>
|
||||||
<td data-label="Status">
|
<td data-label="Status">
|
||||||
<span class="lt-status <?= ($tpl['is_active'] ?? 1) ? 'lt-status-open' : 'lt-status-closed' ?>">
|
<span class="lt-status <?= ($tpl['is_active'] ?? 1) ? 'lt-status-open' : 'lt-status-closed' ?>">
|
||||||
<?= ($tpl['is_active'] ?? 1) ? 'Active' : 'Inactive' ?>
|
<?= ($tpl['is_active'] ?? 1) ? 'Active' : 'Inactive' ?>
|
||||||
@@ -137,7 +137,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="<?= $nonce ?>">
|
<script nonce="<?= $nonce ?>">
|
||||||
var templates = <?= json_encode($templates ?? []) ?>;
|
var templates = <?= json_encode($templates ?? [], JSON_HEX_TAG) ?>;
|
||||||
|
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
var target = e.target.closest('[data-action]');
|
var target = e.target.closest('[data-action]');
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script nonce="<?= $nonce ?>">
|
<script nonce="<?= $nonce ?>">
|
||||||
var workflows = <?= json_encode($workflows ?? []) ?>;
|
var workflows = <?= json_encode($workflows ?? [], JSON_HEX_TAG) ?>;
|
||||||
|
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
var target = e.target.closest('[data-action]');
|
var target = e.target.closest('[data-action]');
|
||||||
|
|||||||
@@ -55,6 +55,63 @@
|
|||||||
<span aria-label="Application version">TINKER TICKETS — TDS v1.2</span>
|
<span aria-label="Application version">TINKER TICKETS — TDS v1.2</span>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- ================================================================
|
||||||
|
KEYBOARD SHORTCUTS HELP MODAL — opened by ? key or footer [?] hint
|
||||||
|
================================================================ -->
|
||||||
|
<div id="lt-keys-help" class="lt-modal-overlay" aria-hidden="true">
|
||||||
|
<div class="lt-modal" role="dialog" aria-modal="true" aria-labelledby="keys-help-title">
|
||||||
|
<div class="lt-modal-header">
|
||||||
|
<span class="lt-modal-title" id="keys-help-title">Keyboard Shortcuts</span>
|
||||||
|
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="lt-modal-body">
|
||||||
|
<table class="lt-data-table" style="width:100%">
|
||||||
|
<thead>
|
||||||
|
<tr><th scope="col">Shortcut</th><th scope="col">Action</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Ctrl / ⌘ + K</td><td>Focus search box</td></tr>
|
||||||
|
<tr><td>Ctrl / ⌘ + E</td><td>Toggle edit mode (ticket page)</td></tr>
|
||||||
|
<tr><td>Ctrl / ⌘ + S</td><td>Save changes (ticket page)</td></tr>
|
||||||
|
<tr><td>j / ↓</td><td>Select next row</td></tr>
|
||||||
|
<tr><td>k / ↑</td><td>Select previous row</td></tr>
|
||||||
|
<tr><td>Enter</td><td>Open selected ticket</td></tr>
|
||||||
|
<tr><td>n</td><td>New ticket</td></tr>
|
||||||
|
<tr><td>1–4</td><td>Change ticket status (ticket page)</td></tr>
|
||||||
|
<tr><td>c</td><td>Jump to comment box (ticket page)</td></tr>
|
||||||
|
<tr><td>?</td><td>Show this help</td></tr>
|
||||||
|
<tr><td>ESC</td><td>Close modal / cancel</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="lt-modal-footer">
|
||||||
|
<button type="button" class="lt-btn" data-modal-close>Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================================================================
|
||||||
|
COMMAND PALETTE — Ctrl+K opens when no search input focused
|
||||||
|
================================================================ -->
|
||||||
|
<div id="lt-cmd-overlay" class="lt-cmd-overlay" role="dialog" aria-modal="true" aria-label="Command palette" aria-hidden="true">
|
||||||
|
<div class="lt-cmd-palette" id="lt-cmd-palette">
|
||||||
|
<div class="lt-cmd-input-wrap">
|
||||||
|
<span class="lt-cmd-prompt">></span>
|
||||||
|
<input id="lt-cmd-input" class="lt-cmd-input" type="text"
|
||||||
|
placeholder="Search commands…" autocomplete="off"
|
||||||
|
spellcheck="false" aria-label="Search commands">
|
||||||
|
</div>
|
||||||
|
<div class="lt-cmd-results" id="lt-cmd-results">
|
||||||
|
<div class="lt-cmd-empty">Start typing to search…</div>
|
||||||
|
</div>
|
||||||
|
<div class="lt-cmd-footer">
|
||||||
|
<span><kbd>↑</kbd><kbd>↓</kbd> Navigate</span>
|
||||||
|
<span><kbd>Enter</kbd> Select</span>
|
||||||
|
<span><kbd>Esc</kbd> Close</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- base.js + utils.js + globals already loaded in <head> via layout_header.php -->
|
<!-- base.js + utils.js + globals already loaded in <head> via layout_header.php -->
|
||||||
|
|
||||||
<?php if (!empty($pageScripts)): ?>
|
<?php if (!empty($pageScripts)): ?>
|
||||||
@@ -75,6 +132,27 @@
|
|||||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
|
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
if (window.lt) {
|
if (window.lt) {
|
||||||
lt.init({ bootName: 'TINKER TICKETS' });
|
lt.init({ bootName: 'TINKER TICKETS' });
|
||||||
|
|
||||||
|
// Theme toggle button
|
||||||
|
var themeBtn = document.getElementById('lt-theme-btn');
|
||||||
|
if (themeBtn) themeBtn.addEventListener('click', function() { lt.theme.toggle(); });
|
||||||
|
|
||||||
|
// Command palette — global navigation commands available on all pages
|
||||||
|
lt.cmdPalette.init([
|
||||||
|
{
|
||||||
|
group: 'Navigation',
|
||||||
|
items: [
|
||||||
|
{ icon: '~', label: 'Dashboard', kbd: 'G D', action: function() { window.location.href = '/'; } },
|
||||||
|
{ icon: '+', label: 'New Ticket', kbd: 'N', action: function() { window.location.href = '/ticket/create'; } },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'Help',
|
||||||
|
items: [
|
||||||
|
{ icon: '?', label: 'Keyboard Shortcuts', kbd: '?', action: function() { lt.modal.open('lt-keys-help'); } },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Footer hint bar actions (keyboard help + settings — work on all pages)
|
// Footer hint bar actions (keyboard help + settings — work on all pages)
|
||||||
|
|||||||
+23
-32
@@ -41,15 +41,15 @@ $_lt_navActive = $activeNav ?? 'dashboard';
|
|||||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="/assets/js/utils.js"></script>
|
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="/assets/js/utils.js"></script>
|
||||||
<!-- Inline JS globals (CSRF, timezone, user) available immediately -->
|
<!-- Inline JS globals (CSRF, timezone, user) available immediately -->
|
||||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
|
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
|
||||||
window.CSRF_TOKEN = <?= json_encode(CsrfMiddleware::getToken(), JSON_UNESCAPED_UNICODE) ?>;
|
window.CSRF_TOKEN = <?= json_encode(CsrfMiddleware::getToken(), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
|
||||||
window.APP_TIMEZONE = <?= json_encode($GLOBALS['config']['TIMEZONE'] ?? 'UTC', JSON_UNESCAPED_UNICODE) ?>;
|
window.APP_TIMEZONE = <?= json_encode($GLOBALS['config']['TIMEZONE'] ?? 'UTC', JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
|
||||||
window.APP_TIMEZONE_ABBREV = <?= json_encode($GLOBALS['config']['TIMEZONE_ABBREV'] ?? 'UTC', JSON_UNESCAPED_UNICODE) ?>;
|
window.APP_TIMEZONE_ABBREV = <?= json_encode($GLOBALS['config']['TIMEZONE_ABBREV'] ?? 'UTC', JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
|
||||||
window.APP_TIMEZONE_OFFSET = <?= (int)($GLOBALS['config']['TIMEZONE_OFFSET'] ?? 0) ?>;
|
window.APP_TIMEZONE_OFFSET = <?= (int)($GLOBALS['config']['TIMEZONE_OFFSET'] ?? 0) ?>;
|
||||||
window.CURRENT_USER = <?= json_encode([
|
window.CURRENT_USER = <?= json_encode([
|
||||||
'id' => (int)($GLOBALS['currentUser']['user_id'] ?? 0),
|
'id' => (int)($GLOBALS['currentUser']['user_id'] ?? 0),
|
||||||
'username'=> $GLOBALS['currentUser']['username'] ?? '',
|
'username'=> $GLOBALS['currentUser']['username'] ?? '',
|
||||||
'isAdmin' => !empty($GLOBALS['currentUser']['is_admin']),
|
'isAdmin' => !empty($GLOBALS['currentUser']['is_admin']),
|
||||||
], JSON_UNESCAPED_UNICODE) ?>;
|
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -62,39 +62,30 @@ $_lt_navActive = $activeNav ?? 'dashboard';
|
|||||||
<pre id="lt-boot-text" class="lt-boot-text"></pre>
|
<pre id="lt-boot-text" class="lt-boot-text"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- MOBILE NAV DRAWER -->
|
<!-- MOBILE NAV DRAWER — matches web_template structure exactly -->
|
||||||
<div class="lt-nav-drawer" id="lt-nav-drawer" role="dialog" aria-modal="true" aria-label="Mobile navigation">
|
<div id="lt-nav-drawer" class="lt-nav-drawer" aria-hidden="true" role="dialog" aria-modal="true" aria-label="Navigation menu">
|
||||||
<div class="lt-nav-drawer-backdrop" id="lt-nav-drawer-backdrop" data-action="close-nav-drawer" aria-hidden="true"></div>
|
|
||||||
<nav class="lt-nav-drawer-panel" aria-label="Mobile main navigation">
|
|
||||||
<div class="lt-nav-drawer-header">
|
<div class="lt-nav-drawer-header">
|
||||||
<span class="lt-nav-drawer-title">TINKER TICKETS</span>
|
<span class="lt-brand-title">TINKER TICKETS</span>
|
||||||
<button type="button"
|
<button type="button" class="lt-nav-drawer-close" id="lt-nav-drawer-close" aria-label="Close navigation">✕</button>
|
||||||
class="lt-nav-drawer-close"
|
|
||||||
id="lt-nav-drawer-close"
|
|
||||||
data-action="close-nav-drawer"
|
|
||||||
aria-label="Close navigation">✕</button>
|
|
||||||
</div>
|
</div>
|
||||||
<ul class="lt-nav-drawer-list" role="list">
|
<nav class="lt-nav-drawer-links" aria-label="Mobile navigation">
|
||||||
<li>
|
|
||||||
<a href="/"
|
<a href="/"
|
||||||
class="lt-nav-drawer-link<?= $_lt_navActive === 'dashboard' ? ' active' : '' ?>"
|
class="lt-nav-drawer-link<?= $_lt_navActive === 'dashboard' ? ' active' : '' ?>"
|
||||||
<?= $_lt_navActive === 'dashboard' ? 'aria-current="page"' : '' ?>>
|
<?= $_lt_navActive === 'dashboard' ? 'aria-current="page"' : '' ?>>Dashboard</a>
|
||||||
Dashboard
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<?php if ($_lt_isAdmin): ?>
|
<?php if ($_lt_isAdmin): ?>
|
||||||
<li class="lt-nav-drawer-section-label" aria-hidden="true">Admin</li>
|
<div class="lt-nav-drawer-section">Admin</div>
|
||||||
<li><a href="/admin/templates" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-templates' ? ' active' : '' ?>">Templates</a></li>
|
<a href="/admin/templates" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-templates' ? ' active' : '' ?>">Templates</a>
|
||||||
<li><a href="/admin/workflow" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-workflow' ? ' active' : '' ?>">Workflow</a></li>
|
<a href="/admin/workflow" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-workflow' ? ' active' : '' ?>">Workflow</a>
|
||||||
<li><a href="/admin/recurring-tickets" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-recurring' ? ' active' : '' ?>">Recurring</a></li>
|
<a href="/admin/recurring-tickets" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-recurring' ? ' active' : '' ?>">Recurring</a>
|
||||||
<li><a href="/admin/custom-fields" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-custom-fields' ? ' active' : '' ?>">Custom Fields</a></li>
|
<a href="/admin/custom-fields" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-custom-fields' ? ' active' : '' ?>">Custom Fields</a>
|
||||||
<li><a href="/admin/user-activity" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-user-activity' ? ' active' : '' ?>">User Activity</a></li>
|
<a href="/admin/user-activity" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-user-activity' ? ' active' : '' ?>">User Activity</a>
|
||||||
<li><a href="/admin/audit-log" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-audit-log' ? ' active' : '' ?>">Audit Log</a></li>
|
<a href="/admin/audit-log" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-audit-log' ? ' active' : '' ?>">Audit Log</a>
|
||||||
<li><a href="/admin/api-keys" class="lt-nav-drawer-link<?= $_lt_navActive === 'admin-api-keys' ? ' active' : '' ?>">API Keys</a></li>
|
<a href="/admin/api-keys" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-api-keys' ? ' active' : '' ?>">API Keys</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</ul>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div><!-- /.lt-nav-drawer -->
|
</div><!-- /.lt-nav-drawer -->
|
||||||
|
<!-- Overlay: outside drawer, full-screen; JS toggles .open class -->
|
||||||
|
<div id="lt-nav-overlay" class="lt-nav-drawer-overlay"></div>
|
||||||
|
|
||||||
<!-- PRIMARY HEADER -->
|
<!-- PRIMARY HEADER -->
|
||||||
<header class="lt-header" role="banner">
|
<header class="lt-header" role="banner">
|
||||||
@@ -160,13 +151,13 @@ $_lt_navActive = $activeNav ?? 'dashboard';
|
|||||||
|
|
||||||
<div class="lt-header-right">
|
<div class="lt-header-right">
|
||||||
<?php if (!empty($_lt_user)): ?>
|
<?php if (!empty($_lt_user)): ?>
|
||||||
<span class="lt-header-username">
|
<span class="lt-header-user"><?= htmlspecialchars($_lt_user['display_name'] ?? $_lt_user['username'] ?? '', ENT_QUOTES, 'UTF-8') ?></span>
|
||||||
<?= htmlspecialchars($_lt_user['display_name'] ?? $_lt_user['username'] ?? '', 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>
|
<span class="lt-badge lt-badge-admin" aria-label="Administrator">ADMIN</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<button type="button" class="lt-theme-btn" id="lt-theme-btn"
|
||||||
|
aria-label="Switch to light mode" title="Switch to light mode">☀</button>
|
||||||
</div><!-- /.lt-header-right -->
|
</div><!-- /.lt-header-right -->
|
||||||
|
|
||||||
</header><!-- /.lt-header -->
|
</header><!-- /.lt-header -->
|
||||||
|
|||||||
Reference in New Issue
Block a user