feat: SLA live timer, notification bell, lt-toggle MD, right drawer, kanban drag-drop
- TicketView: SLA banner now shows live HH:MM:SS elapsed + countdown via JS setInterval (previously showed static hours from PHP) - TicketView: Markdown toggles in comment form replaced with lt-toggle switches - layout_header: In-app notification bell (🔔) with dropdown panel for all users - layout_footer: Notification JS — polls /api/notifications.php every 60s, badge count, mark-all-read, panel open/close with Escape/outside-click - api/notifications.php (new): Returns assign/comment/status-change events from audit_log for current user's tickets and watched tickets; mark-read via user_preferences - DashboardView: Ticket preview right drawer — Ctrl+click title or ⊙ peek button opens lt-drawer-right with ticket summary extracted from table row DOM - DashboardView: lt.sortable wired on all 4 kanban columns (group='kanban') Cross-column drag = status change via POST /api/update_ticket.php with optimistic UI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -172,6 +172,84 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ── Notification Bell ─────────────────────────────────────────────
|
||||
<?php if (!empty($GLOBALS['currentUser'])): ?>
|
||||
(function() {
|
||||
var bell = document.getElementById('lt-notif-bell');
|
||||
var panel = document.getElementById('lt-notif-panel');
|
||||
var list = document.getElementById('lt-notif-list');
|
||||
var clearBtn = document.getElementById('lt-notif-clear-btn');
|
||||
var wrapEl = document.getElementById('lt-notif-wrap');
|
||||
if (!bell || !panel) return;
|
||||
|
||||
var _open = false;
|
||||
|
||||
function fmtTime(dateStr) {
|
||||
var d = new Date(dateStr);
|
||||
var diff = Math.floor((Date.now() - d) / 1000);
|
||||
if (diff < 60) return diff + 's ago';
|
||||
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||||
if (diff < 86400)return Math.floor(diff / 3600) + 'h ago';
|
||||
return Math.floor(diff / 86400) + 'd ago';
|
||||
}
|
||||
|
||||
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
function renderNotifications(data) {
|
||||
lt.notif.set(bell, data.unread_count || 0);
|
||||
if (!data.notifications || !data.notifications.length) {
|
||||
list.innerHTML = '<div style="padding:1rem;font-size:0.75rem;color:var(--text-muted);text-align:center">No recent notifications</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = data.notifications.map(function(n) {
|
||||
return '<div class="lt-notif-item' + (n.is_read ? '' : ' lt-notif-item--unread') +
|
||||
'" tabindex="0" role="link" data-url="' + esc(n.url) + '">' +
|
||||
'<div class="lt-notif-dot' + (n.is_read ? ' lt-notif-dot--read' : '') + '"></div>' +
|
||||
'<div class="lt-notif-item-body">' +
|
||||
'<div class="lt-notif-item-title">' + esc(n.title) + '</div>' +
|
||||
'<div class="lt-notif-item-time">' + fmtTime(n.created_at) + '</div>' +
|
||||
'</div></div>';
|
||||
}).join('');
|
||||
list.querySelectorAll('.lt-notif-item').forEach(function(item) {
|
||||
function go() { if (item.dataset.url) window.location.href = item.dataset.url; }
|
||||
item.addEventListener('click', go);
|
||||
item.addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); } });
|
||||
});
|
||||
}
|
||||
|
||||
function loadNotifications() {
|
||||
fetch('/api/notifications.php', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(renderNotifications)
|
||||
.catch(function() {
|
||||
list.innerHTML = '<div style="padding:0.75rem;font-size:0.75rem;color:var(--text-muted);text-align:center">Could not load</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function openPanel() { _open = true; panel.removeAttribute('aria-hidden'); bell.setAttribute('aria-expanded','true'); loadNotifications(); }
|
||||
function closePanel() { _open = false; panel.setAttribute('aria-hidden','true'); bell.setAttribute('aria-expanded','false'); }
|
||||
|
||||
bell.addEventListener('click', function(e) { e.stopPropagation(); _open ? closePanel() : openPanel(); });
|
||||
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', function() {
|
||||
fetch('/api/notifications.php', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN || '' },
|
||||
body: JSON.stringify({ action: 'mark_read' })
|
||||
}).then(loadNotifications);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', function(e) { if (_open && wrapEl && !wrapEl.contains(e.target)) closePanel(); });
|
||||
document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && _open) closePanel(); });
|
||||
|
||||
// Initial badge count + poll every 60s
|
||||
loadNotifications();
|
||||
setInterval(loadNotifications, 60000);
|
||||
})();
|
||||
<?php endif ?>
|
||||
|
||||
// Footer hint bar actions (keyboard help + settings — work on all pages)
|
||||
document.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('[data-action]');
|
||||
|
||||
Reference in New Issue
Block a user