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:
@@ -1029,4 +1029,125 @@ var advForm = document.getElementById('advancedSearchForm');
|
||||
if (advForm) advForm.addEventListener('submit', performAdvancedSearch);
|
||||
</script>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
TICKET PREVIEW RIGHT DRAWER
|
||||
Opens when a ticket title link is ctrl/cmd+clicked or via
|
||||
the preview icon — shows summary without full navigation.
|
||||
═══════════════════════════════════════════════════════════ -->
|
||||
<aside class="lt-drawer-right" id="ticketPreviewDrawer" aria-hidden="true"
|
||||
aria-label="Ticket preview" data-overlay="ticketPreviewDrawerOverlay">
|
||||
<div class="lt-drawer-right-header">
|
||||
<span class="lt-drawer-right-title" id="drawerTicketId">Ticket Preview</span>
|
||||
<button type="button" class="lt-drawer-right-close" data-drawer-close aria-label="Close preview">✕</button>
|
||||
</div>
|
||||
<div class="lt-drawer-right-body" id="drawerBody">
|
||||
<!-- Content injected by JS -->
|
||||
</div>
|
||||
<div class="lt-drawer-right-footer">
|
||||
<a id="drawerOpenLink" href="#" class="lt-btn lt-btn-primary lt-btn-sm">Open Full Ticket</a>
|
||||
<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm" data-drawer-close>Close</button>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="lt-drawer-right-overlay" id="ticketPreviewDrawerOverlay"></div>
|
||||
|
||||
<script>
|
||||
// ── Ticket Preview Drawer ──────────────────────────────────────────
|
||||
(function() {
|
||||
var drawer = document.getElementById('ticketPreviewDrawer');
|
||||
var body = document.getElementById('drawerBody');
|
||||
var idLabel = document.getElementById('drawerTicketId');
|
||||
var openLink = document.getElementById('drawerOpenLink');
|
||||
if (!drawer || !body) return;
|
||||
|
||||
var pLabels = { '1':'P1 — Critical', '2':'P2 — High', '3':'P3 — Medium', '4':'P4 — Low', '5':'P5 — Minimal' };
|
||||
var dotClass = { 'Open':'lt-dot-up', 'In Progress':'lt-dot-warn', 'Pending':'lt-dot--orange', 'Closed':'lt-dot-idle' };
|
||||
|
||||
function esc(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
function fmtAge(dateStr) {
|
||||
var d = new Date(dateStr);
|
||||
if (isNaN(d)) return dateStr;
|
||||
var diff = Math.floor((Date.now() - d) / 1000);
|
||||
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 openDrawerFromRow(link) {
|
||||
var href = link.getAttribute('href') || '';
|
||||
var m = href.match(/\/ticket\/(\d+)/);
|
||||
if (!m) return;
|
||||
var ticketId = m[1];
|
||||
if (openLink) openLink.href = href;
|
||||
|
||||
// Extract data from the table row (already rendered in DOM — no extra fetch needed)
|
||||
var row = link.closest('tr');
|
||||
var cells = row ? row.querySelectorAll('td') : [];
|
||||
var hasCheckbox = row && row.querySelector('input[type="checkbox"]') !== null;
|
||||
var o = hasCheckbox ? 1 : 0; // column offset for checkbox col
|
||||
|
||||
var priority = cells[1 + o] ? cells[1 + o].textContent.trim() : '';
|
||||
var title = cells[2 + o] ? cells[2 + o].querySelector('.ticket-link')?.textContent.trim() || '' : '';
|
||||
var category = cells[3 + o] ? cells[3 + o].textContent.trim() : '';
|
||||
var typeVal = cells[4 + o] ? cells[4 + o].textContent.trim() : '';
|
||||
var status = cells[5 + o] ? cells[5 + o].textContent.trim().replace(/^\s*●\s*/, '') : '';
|
||||
var createdBy = cells[6 + o] ? cells[6 + o].textContent.trim() : '';
|
||||
var assignedTo= cells[7 + o] ? cells[7 + o].textContent.trim() : '';
|
||||
var age = cells[8 + o] ? cells[8 + o].textContent.trim() : '';
|
||||
|
||||
if (idLabel) idLabel.textContent = '[ #' + esc(ticketId) + ' ]';
|
||||
var dc = dotClass[status] || 'lt-dot-idle';
|
||||
var pNum = priority.replace(/[^1-5]/g, '') || '?';
|
||||
var pLabel = pLabels[pNum] || ('P' + pNum);
|
||||
|
||||
body.innerHTML =
|
||||
'<div class="lt-frame" style="margin-bottom:0.75rem;padding:0.6rem 0.75rem">' +
|
||||
'<div style="font-weight:700;margin-bottom:0.5rem;font-size:0.9rem;line-height:1.3">' + esc(title) + '</div>' +
|
||||
'<div class="lt-kv-grid" style="margin-bottom:0">' +
|
||||
'<div class="lt-kv-row"><span class="lt-kv-label">Status</span><span class="lt-kv-value"><span class="lt-dot ' + dc + '" style="display:inline-block;vertical-align:middle;margin-right:0.35rem"></span>' + esc(status) + '</span></div>' +
|
||||
'<div class="lt-kv-row"><span class="lt-kv-label">Priority</span><span class="lt-kv-value">' + esc(pLabel) + '</span></div>' +
|
||||
'<div class="lt-kv-row"><span class="lt-kv-label">Category</span><span class="lt-kv-value">' + esc(category||'—') + '</span></div>' +
|
||||
'<div class="lt-kv-row"><span class="lt-kv-label">Type</span><span class="lt-kv-value">' + esc(typeVal||'—') + '</span></div>' +
|
||||
'<div class="lt-kv-row"><span class="lt-kv-label">Assigned</span><span class="lt-kv-value">' + esc(assignedTo||'Unassigned') + '</span></div>' +
|
||||
(createdBy ? '<div class="lt-kv-row"><span class="lt-kv-label">Created by</span><span class="lt-kv-value">' + esc(createdBy) + '</span></div>' : '') +
|
||||
(age ? '<div class="lt-kv-row"><span class="lt-kv-label">Age</span><span class="lt-kv-value">' + esc(age) + '</span></div>' : '') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<p class="lt-text-muted lt-text-xs" style="text-align:center">Click "Open Full Ticket" for description, comments & attachments.</p>';
|
||||
|
||||
lt.rightDrawer.open('ticketPreviewDrawer');
|
||||
}
|
||||
|
||||
// Intercept clicks on .ticket-link with Ctrl/Cmd held → open drawer
|
||||
// Normal left-click still navigates to full ticket page
|
||||
document.addEventListener('click', function(e) {
|
||||
var link = e.target.closest('.ticket-link');
|
||||
if (!link) return;
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
e.preventDefault();
|
||||
openDrawerFromRow(link);
|
||||
}
|
||||
});
|
||||
|
||||
// Add a small [⊙] peek icon after each title link for easy drawer access
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.ticket-link').forEach(function(link) {
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.title = 'Quick preview (Ctrl+click)';
|
||||
btn.setAttribute('aria-label', 'Quick preview');
|
||||
btn.innerHTML = '⧉';
|
||||
btn.style.cssText = 'font-size:0.7rem;margin-left:0.3rem;opacity:0;border:none;background:none;cursor:pointer;color:var(--accent-cyan);vertical-align:middle;padding:0 0.1rem;line-height:1;transition:opacity 0.15s';
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
openDrawerFromRow(link);
|
||||
});
|
||||
link.addEventListener('mouseenter', function() { btn.style.opacity = '0.5'; });
|
||||
link.addEventListener('mouseleave', function() { btn.style.opacity = '0'; });
|
||||
link.parentNode.insertBefore(btn, link.nextSibling);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php include __DIR__ . '/layout_footer.php'; ?>
|
||||
|
||||
Reference in New Issue
Block a user