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:
+66
-1
@@ -1151,7 +1151,13 @@ function populateKanbanCards() {
|
||||
card.className = 'lt-kanban-card lt-kanban-card--p' + pNum;
|
||||
card.setAttribute('role', 'button');
|
||||
card.setAttribute('tabindex', '0');
|
||||
card.onclick = () => window.location.href = '/ticket/' + encodeURIComponent(ticketId);
|
||||
card.dataset.ticketId = ticketId;
|
||||
card.dataset.status = status;
|
||||
card.addEventListener('click', (e) => {
|
||||
// Don't navigate if drag just ended (drag adds/removes is-dragging briefly)
|
||||
if (card.dataset.dragged) { delete card.dataset.dragged; return; }
|
||||
window.location.href = '/ticket/' + encodeURIComponent(ticketId);
|
||||
});
|
||||
card.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') card.click(); };
|
||||
card.innerHTML =
|
||||
'<div class="lt-kanban-card-header">' +
|
||||
@@ -1172,6 +1178,65 @@ function populateKanbanCards() {
|
||||
const s = el.dataset.status;
|
||||
el.textContent = '(' + (counts[s] || 0) + ')';
|
||||
});
|
||||
|
||||
// ── Kanban drag-and-drop via lt.sortable ──────────────────────
|
||||
if (window.lt && lt.sortable) {
|
||||
const colStatusMap = {
|
||||
'kanban-col-open': 'Open',
|
||||
'kanban-col-pending': 'Pending',
|
||||
'kanban-col-inprogress': 'In Progress',
|
||||
'kanban-col-closed': 'Closed',
|
||||
};
|
||||
|
||||
function handleKanbanSort(newItems, movedCard) {
|
||||
if (!movedCard) return;
|
||||
const newColEl = movedCard.parentNode;
|
||||
const newColId = newColEl ? newColEl.id : null;
|
||||
const newStatus = colStatusMap[newColId];
|
||||
const oldStatus = movedCard.dataset.status;
|
||||
const ticketId = movedCard.dataset.ticketId;
|
||||
|
||||
if (!newStatus || !ticketId || newStatus === oldStatus) return;
|
||||
|
||||
movedCard.dataset.status = newStatus;
|
||||
movedCard.dataset.dragged = '1';
|
||||
|
||||
// Optimistically update column counts
|
||||
const dec = document.querySelector(`.column-count[data-status="${oldStatus}"]`);
|
||||
const inc = document.querySelector(`.column-count[data-status="${newStatus}"]`);
|
||||
if (dec) dec.textContent = '(' + Math.max(0, (parseInt(dec.textContent.replace(/\D/g,''),10)||1) - 1) + ')';
|
||||
if (inc) inc.textContent = '(' + ((parseInt(inc.textContent.replace(/\D/g,''),10)||0) + 1) + ')';
|
||||
|
||||
// POST status update
|
||||
fetch('/api/update_ticket.php', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.CSRF_TOKEN || '' },
|
||||
body: JSON.stringify({ ticket_id: parseInt(ticketId, 10), status: newStatus })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
lt.toast.success('Ticket #' + ticketId + ' → ' + newStatus, 2500);
|
||||
movedCard.dataset.status = newStatus;
|
||||
} else {
|
||||
lt.toast.error('Status update failed: ' + (data.error || 'Unknown error'));
|
||||
// Revert: put card back in original column
|
||||
const origCol = document.getElementById(Object.keys(colStatusMap).find(k => colStatusMap[k] === oldStatus));
|
||||
if (origCol) origCol.appendChild(movedCard);
|
||||
movedCard.dataset.status = oldStatus;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
lt.toast.error('Network error — status not saved');
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(columns).forEach(status => {
|
||||
const col = columns[status];
|
||||
if (col) lt.sortable.init(col, { group: 'kanban', onSort: handleKanbanSort });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Restore view mode on page load — click the kanban tab button to trigger lt.tabs
|
||||
|
||||
Reference in New Issue
Block a user