diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css index 70660d0..fc6aac0 100644 --- a/assets/css/dashboard.css +++ b/assets/css/dashboard.css @@ -4642,6 +4642,20 @@ table td:nth-child(4) { border-top: 1px solid var(--terminal-green); } +/* ===== RELATIVE TIMESTAMP CELLS ===== */ + +/* Inline spans with data-ts show relative time; title attr has the full date */ +span.ts-cell { + cursor: help; + border-bottom: 1px dotted var(--text-muted); + text-decoration: none; +} + +/* Table cells with data-ts — no underline on full td, just cursor hint */ +td.ts-cell { + cursor: help; +} + /* ===== ENHANCED MOBILE RESPONSIVE STYLES ===== */ /* Table wrapper - horizontal scroll when table overflows container */ diff --git a/assets/css/ticket.css b/assets/css/ticket.css index c63b2d9..c86a56f 100644 --- a/assets/css/ticket.css +++ b/assets/css/ticket.css @@ -1489,6 +1489,14 @@ body.dark-mode .editable { border-color: var(--border-color) !important; } +/* ===== RELATIVE TIMESTAMP CELLS ===== */ + +span.ts-cell { + cursor: help; + border-bottom: 1px dotted var(--text-muted); + text-decoration: none; +} + /* ===== RESPONSIVE DESIGN - TERMINAL EDITION ===== */ /* Tablet and smaller */ diff --git a/assets/js/ticket.js b/assets/js/ticket.js index d8a48b3..6ce611f 100644 --- a/assets/js/ticket.js +++ b/assets/js/ticket.js @@ -924,13 +924,14 @@ function renderAttachments(attachments) { attachments.forEach(att => { const uploaderName = att.display_name || att.username || 'Unknown'; - const uploadDate = new Date(att.uploaded_at).toLocaleDateString('en-US', { + const uploadDateFormatted = new Date(att.uploaded_at).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); + const uploadDate = `${lt.time.ago(att.uploaded_at)}`; html += `
${lt.escHtml(att.icon || '[ f ]')}
@@ -941,7 +942,7 @@ function renderAttachments(attachments) {
- ${lt.escHtml(att.file_size_formatted || formatFileSize(att.file_size))} • ${lt.escHtml(uploaderName)} • ${lt.escHtml(uploadDate)} + ${lt.escHtml(att.file_size_formatted || formatFileSize(att.file_size))} • ${lt.escHtml(uploaderName)} • ${uploadDate}
@@ -1304,7 +1305,7 @@ function saveEditComment(commentId) { const newText = textarea.value.trim(); if (!newText) { - showToast('Comment cannot be empty', 'error'); + lt.toast.error('Comment cannot be empty'); return; } @@ -1364,13 +1365,13 @@ function saveEditComment(commentId) { textDiv.style.display = ''; commentDiv.classList.remove('editing'); - showToast('Comment updated successfully', 'success'); + lt.toast.success('Comment updated successfully'); } else { - showToast(data.error || 'Failed to update comment', 'error'); + lt.toast.error(data.error || 'Failed to update comment'); } }) .catch(error => { - showToast('Failed to update comment', 'error'); + lt.toast.error('Failed to update comment'); }); } @@ -1417,13 +1418,13 @@ function deleteComment(commentId) { commentDiv.style.transform = 'translateX(-20px)'; setTimeout(() => commentDiv.remove(), 300); } - showToast('Comment deleted successfully', 'success'); + lt.toast.success('Comment deleted successfully'); } else { - showToast(data.error || 'Failed to delete comment', 'error'); + lt.toast.error(data.error || 'Failed to delete comment'); } }) .catch(error => { - showToast('Failed to delete comment', 'error'); + lt.toast.error('Failed to delete comment'); }); } @@ -1490,7 +1491,7 @@ function submitReply(parentCommentId) { const ticketId = window.ticketData.id; if (!replyText || !replyText.value.trim()) { - showToast('Please enter a reply', 'warning'); + lt.toast.warning('Please enter a reply'); return; } @@ -1578,13 +1579,13 @@ function submitReply(parentCommentId) { repliesContainer.appendChild(replyDiv); } - showToast('Reply added successfully', 'success'); + lt.toast.success('Reply added successfully'); } else { - showToast(data.error || 'Failed to add reply', 'error'); + lt.toast.error(data.error || 'Failed to add reply'); } }) .catch(error => { - showToast('Failed to add reply', 'error'); + lt.toast.error('Failed to add reply'); }); } @@ -1598,6 +1599,19 @@ function toggleThreadCollapse(commentId) { } } +// ======================================== +// RELATIVE TIMESTAMPS +// ======================================== + +function initRelativeTimes() { + document.querySelectorAll('.ts-cell[data-ts]').forEach(el => { + el.textContent = lt.time.ago(el.dataset.ts); + }); +} + +document.addEventListener('DOMContentLoaded', initRelativeTimes); +setInterval(initRelativeTimes, 60000); + // Expose functions globally window.editComment = editComment; window.saveEditComment = saveEditComment; diff --git a/views/TicketView.php b/views/TicketView.php index 508852b..c0b31cf 100644 --- a/views/TicketView.php +++ b/views/TicketView.php @@ -142,13 +142,15 @@ $nonce = SecurityHeadersMiddleware::getNonce(); $creator = $ticket['creator_display_name'] ?? $ticket['creator_username'] ?? 'System'; echo "Created by: " . htmlspecialchars($creator) . ""; if (!empty($ticket['created_at'])) { - echo " on " . date('M d, Y H:i', strtotime($ticket['created_at'])); + $createdFmt = date('M d, Y H:i', strtotime($ticket['created_at'])); + echo " on " . $createdFmt . ""; } if (!empty($ticket['updater_display_name']) || !empty($ticket['updater_username'])) { $updater = $ticket['updater_display_name'] ?? $ticket['updater_username']; echo " • Last updated by: " . htmlspecialchars($updater) . ""; if (!empty($ticket['updated_at'])) { - echo " on " . date('M d, Y H:i', strtotime($ticket['updated_at'])); + $updatedFmt = date('M d, Y H:i', strtotime($ticket['updated_at'])); + echo " on " . $updatedFmt . ""; } } ?> @@ -362,7 +364,7 @@ $nonce = SecurityHeadersMiddleware::getNonce(); echo "" . htmlspecialchars($displayName) . ""; $dateStr = date('M d, Y H:i', strtotime($comment['created_at'])); $editedIndicator = !empty($comment['updated_at']) ? ' (edited)' : ''; - echo "{$dateStr}{$editedIndicator}"; + echo "" . $dateStr . "{$editedIndicator}"; // Action buttons echo "
"; @@ -498,7 +500,8 @@ $nonce = SecurityHeadersMiddleware::getNonce();
- + +