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:
+76
-14
@@ -192,19 +192,23 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
<div class="lt-alert <?= $alertClass ?>" id="priorityAlertBanner"
|
||||
role="alert" aria-live="polite"
|
||||
data-alert-id="priority-banner-<?= htmlspecialchars($ticket['ticket_id']) ?>"
|
||||
data-created-at="<?= (int)strtotime($ticket['created_at']) ?>"
|
||||
data-sla-hours="<?= $slaTargetHours ?>"
|
||||
style="margin-bottom:0.75rem">
|
||||
<span class="lt-alert-icon" aria-hidden="true"><?= $alertIcon ?></span>
|
||||
<div class="lt-alert-body">
|
||||
<div class="lt-alert-title"><?= $alertLabel ?></div>
|
||||
<div class="lt-alert-msg">
|
||||
SLA target: <strong><?= $slaTargetHours ?>h</strong> —
|
||||
Elapsed: <strong><?= $elapsedHours ?>h</strong>
|
||||
<?php if ($slaBreached): ?>
|
||||
— <span class="lt-text-danger">SLA BREACHED</span>
|
||||
Elapsed: <strong id="slaElapsedTimer"><?= $elapsedHours ?>h</strong>
|
||||
<?php if (!$slaBreached): ?>
|
||||
— Remaining: <strong id="slaCountdownTimer" class="lt-text-cyan"></strong>
|
||||
<?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 ?>" style="margin-top:0.35rem"
|
||||
<div class="lt-progress lt-progress--sm <?= $progressClass ?>" id="slaProgress" style="margin-top:0.35rem"
|
||||
aria-label="SLA progress <?= $slaPct ?>%">
|
||||
<div class="lt-progress-bar" style="width:<?= $slaPct ?>%"></div>
|
||||
<div class="lt-progress-bar" id="slaProgressBar" style="width:<?= $slaPct ?>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -217,8 +221,64 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
</div>
|
||||
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
|
||||
(function(){
|
||||
var id='priority-banner-<?= htmlspecialchars($ticket['ticket_id']) ?>';
|
||||
try{ if(sessionStorage.getItem('lt_dismissed_'+id)) document.getElementById('priorityAlertBanner').classList.add('dismissed'); }catch(e){}
|
||||
var banner = document.getElementById('priorityAlertBanner');
|
||||
var id = 'priority-banner-<?= htmlspecialchars($ticket['ticket_id']) ?>';
|
||||
try { if(sessionStorage.getItem('lt_dismissed_'+id)) banner.classList.add('dismissed'); } catch(e) {}
|
||||
|
||||
// Live SLA timers — start after base.js initialises lt
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!banner || banner.classList.contains('dismissed')) return;
|
||||
var createdAt = parseInt(banner.dataset.createdAt, 10) * 1000;
|
||||
var slaMs = parseInt(banner.dataset.slaHours, 10) * 3600 * 1000;
|
||||
var deadline = new Date(createdAt + slaMs);
|
||||
var elapsedEl = document.getElementById('slaElapsedTimer');
|
||||
var countdownEl = document.getElementById('slaCountdownTimer');
|
||||
var overrunEl = document.getElementById('slaOverrunTimer');
|
||||
var progressBar = document.getElementById('slaProgressBar');
|
||||
var progressWrap = document.getElementById('slaProgress');
|
||||
|
||||
function fmtHMS(ms) {
|
||||
var s = Math.floor(Math.abs(ms) / 1000);
|
||||
var h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), ss = s % 60;
|
||||
return [h, m, ss].map(function(n){ return String(n).padStart(2,'0'); }).join(':');
|
||||
}
|
||||
|
||||
function tick() {
|
||||
var now = Date.now();
|
||||
var elapsed = now - createdAt;
|
||||
var remaining = deadline - now;
|
||||
var pct = Math.min(100, Math.round((elapsed / slaMs) * 100));
|
||||
|
||||
if (elapsedEl) elapsedEl.textContent = fmtHMS(elapsed);
|
||||
if (progressBar) progressBar.style.width = pct + '%';
|
||||
if (progressWrap) progressWrap.setAttribute('aria-label', 'SLA progress ' + pct + '%');
|
||||
|
||||
if (remaining > 0) {
|
||||
// SLA not yet breached
|
||||
if (countdownEl) {
|
||||
countdownEl.textContent = fmtHMS(remaining) + ' remaining';
|
||||
countdownEl.className = pct >= 75 ? 'lt-text-danger' : 'lt-text-cyan';
|
||||
}
|
||||
if (progressWrap && pct >= 75) {
|
||||
progressWrap.className = progressWrap.className.replace('lt-progress--green','lt-progress--red');
|
||||
}
|
||||
} else {
|
||||
// Breached
|
||||
if (countdownEl && !overrunEl) {
|
||||
countdownEl.innerHTML = 'SLA BREACHED (+' + fmtHMS(-remaining) + ')';
|
||||
countdownEl.className = 'lt-text-danger';
|
||||
} else if (overrunEl) {
|
||||
overrunEl.textContent = fmtHMS(-remaining);
|
||||
}
|
||||
if (progressWrap && !progressWrap.classList.contains('lt-progress--red')) {
|
||||
progressWrap.className = progressWrap.className.replace('lt-progress--green','').replace('lt-progress--red','') + ' lt-progress--red';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tick();
|
||||
setInterval(tick, 1000);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php endif ?>
|
||||
@@ -445,14 +505,16 @@ $progressClass = $slaBreached ? 'lt-progress--red' : ($slaPct >= 75 ? 'lt-progr
|
||||
aria-label="Add a comment"></textarea>
|
||||
</div>
|
||||
<div class="comment-controls lt-flex lt-flex-gap-sm lt-flex-align-center">
|
||||
<div class="markdown-toggles lt-flex lt-flex-gap-sm">
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox" id="markdownMaster" data-action="toggle-markdown-mode">
|
||||
Markdown
|
||||
<div class="markdown-toggles lt-flex lt-flex-gap-sm lt-flex-align-center">
|
||||
<label class="lt-toggle lt-toggle--sm" title="Enable Markdown formatting">
|
||||
<input type="checkbox" id="markdownMaster" data-action="toggle-markdown-mode">
|
||||
<span class="lt-toggle-track"><span class="lt-toggle-thumb"></span></span>
|
||||
<span class="lt-toggle-label lt-text-xs">MD</span>
|
||||
</label>
|
||||
<label class="lt-filter-option">
|
||||
<input type="checkbox" class="lt-checkbox" id="markdownToggle" data-action="toggle-preview" disabled>
|
||||
Preview
|
||||
<label class="lt-toggle lt-toggle--sm" title="Preview rendered Markdown">
|
||||
<input type="checkbox" id="markdownToggle" data-action="toggle-preview" disabled>
|
||||
<span class="lt-toggle-track"><span class="lt-toggle-thumb"></span></span>
|
||||
<span class="lt-toggle-label lt-text-xs">Preview</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" id="addCommentBtn" class="lt-btn lt-btn-primary lt-btn-sm">POST COMMENT</button>
|
||||
|
||||
Reference in New Issue
Block a user