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 "";
$dateStr = date('M d, Y H:i', strtotime($comment['created_at']));
$editedIndicator = !empty($comment['updated_at']) ? ' ' : '';
- echo "";
+ echo "";
// Action buttons
echo "