diff --git a/api/add_comment.php b/api/add_comment.php
index 1454ac5..8737dae 100644
--- a/api/add_comment.php
+++ b/api/add_comment.php
@@ -157,9 +157,10 @@ try {
}, $mentionedUsers);
}
- // Add user display name to result for frontend
+ // Add user info to result for frontend avatar rendering
if ($result['success']) {
$result['user_name'] = $currentUser['display_name'] ?? $currentUser['username'];
+ $result['user_id'] = $userId;
}
// Discard any unexpected output
diff --git a/api/update_ticket.php b/api/update_ticket.php
index ab82e17..ccc76bb 100644
--- a/api/update_ticket.php
+++ b/api/update_ticket.php
@@ -93,16 +93,8 @@ try {
];
}
- // Authorization: admins can edit any ticket; others only their own or assigned
- if (!$this->isAdmin
- && (int)$currentTicket['created_by'] !== (int)$this->userId
- && (int)$currentTicket['assigned_to'] !== (int)$this->userId
- ) {
- return [
- 'success' => false,
- 'error' => 'Permission denied'
- ];
- }
+ // Any authenticated team member can update tickets.
+ // Admin-only operations (delete, bulk actions) are enforced separately.
// Merge current data with updates, keeping existing values for missing fields
$updateData = [
diff --git a/assets/js/ticket.js b/assets/js/ticket.js
index 790e199..a218435 100644
--- a/assets/js/ticket.js
+++ b/assets/js/ticket.js
@@ -182,6 +182,81 @@ function toggleEditMode() {
}
}
+/**
+ * Compute avatar color class from display name (mirrors PHP crc32 % 4 logic)
+ */
+function avatarColorClass(displayName) {
+ var colors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', ''];
+ var h = 0;
+ for (var i = 0; i < displayName.length; i++) {
+ h = ((h << 5) - h + displayName.charCodeAt(i)) | 0;
+ }
+ return colors[Math.abs(h) % 4];
+}
+
+/**
+ * Build a comment/reply DOM element matching the server-rendered structure
+ */
+function buildCommentElement(opts) {
+ // opts: { commentId, userId, displayName, createdAt, commentText, isMarkdown,
+ // depth, parentId, canModify }
+ var depth = opts.depth || 0;
+ var depthClass = 'thread-depth-' + Math.min(depth, 3);
+ var threadClass = opts.parentId ? 'comment-reply' : 'comment-root';
+
+ var words = (opts.displayName || '').trim().split(/\s+/).filter(Boolean);
+ var initials = words.slice(0, 2).map(function(w) { return w[0].toUpperCase(); }).join('');
+ var color = avatarColorClass(opts.displayName || '');
+ var avatarImg = opts.userId > 0
+ ? '
'
+ : '';
+
+ var threadLine = opts.parentId ? '
' : '';
+
+ var replyBtn = depth < 3
+ ? ''
+ : '';
+ var modBtns = opts.canModify !== false
+ ? '' +
+ ''
+ : '';
+
+ var div = document.createElement('div');
+ div.className = 'comment ' + depthClass + ' ' + threadClass + ' animate-fadein';
+ div.dataset.commentId = opts.commentId;
+ div.dataset.markdownEnabled = opts.isMarkdown ? '1' : '0';
+ div.dataset.threadDepth = depth;
+ div.dataset.parentId = opts.parentId || '';
+
+ div.innerHTML =
+ threadLine +
+ '';
+
+ return div;
+}
+
function addComment() {
const newComment = document.getElementById('newComment');
if (!newComment) return;
@@ -226,32 +301,19 @@ function addComment() {
.replace(/\n/g, '
');
}
- // Add new comment to the list (using safe DOM API to prevent XSS)
+ // Add new comment to the list
const commentsList = document.querySelector('.comments-list');
-
- const commentDiv = document.createElement('div');
- commentDiv.className = 'comment';
-
- const headerDiv = document.createElement('div');
- headerDiv.className = 'comment-header';
-
- const userSpan = document.createElement('span');
- userSpan.className = 'comment-user';
- userSpan.textContent = data.user_name; // Safe - auto-escapes
-
- const dateSpan = document.createElement('span');
- dateSpan.className = 'comment-date';
- dateSpan.textContent = data.created_at; // Safe - auto-escapes
-
- const textDiv = document.createElement('div');
- textDiv.className = 'comment-text';
- textDiv.innerHTML = displayText; // displayText already sanitized above
-
- headerDiv.appendChild(userSpan);
- headerDiv.appendChild(dateSpan);
- commentDiv.appendChild(headerDiv);
- commentDiv.appendChild(textDiv);
-
+ const commentDiv = buildCommentElement({
+ commentId: data.comment_id,
+ userId: data.user_id,
+ displayName: data.user_name,
+ createdAt: data.created_at,
+ commentText: displayText,
+ rawText: commentText,
+ isMarkdown: isMarkdownEnabled,
+ depth: 0,
+ parentId: null,
+ });
commentsList.insertBefore(commentDiv, commentsList.firstChild);
} else {
lt.toast.error(data.error || 'Failed to add comment');
@@ -1550,34 +1612,17 @@ function submitReply(parentCommentId) {
}
// Create the new reply element
- const replyDiv = document.createElement('div');
- replyDiv.className = `comment thread-depth-${newDepth} comment-reply`;
- replyDiv.dataset.commentId = data.comment_id;
- replyDiv.dataset.markdownEnabled = isMarkdownEnabled ? '1' : '0';
- replyDiv.dataset.threadDepth = newDepth;
- replyDiv.dataset.parentId = parentCommentId;
-
- replyDiv.innerHTML = `
-
-
- `;
-
- // Add animation
- replyDiv.classList.add('animate-fadein');
+ const replyDiv = buildCommentElement({
+ commentId: data.comment_id,
+ userId: data.user_id,
+ displayName: data.user_name,
+ createdAt: data.created_at,
+ commentText: displayText,
+ rawText: commentText,
+ isMarkdown: isMarkdownEnabled,
+ depth: newDepth,
+ parentId: parentCommentId,
+ });
repliesContainer.appendChild(replyDiv);
}
diff --git a/models/AuditLogModel.php b/models/AuditLogModel.php
index 565041c..4f0acef 100644
--- a/models/AuditLogModel.php
+++ b/models/AuditLogModel.php
@@ -369,7 +369,7 @@ class AuditLogModel {
public function logCommentCreate($userId, $commentId, $ticketId) {
return $this->log(
$userId,
- 'create',
+ 'comment',
'comment',
(string)$commentId,
['ticket_id' => $ticketId]
diff --git a/views/TicketView.php b/views/TicketView.php
index 37589bf..1a4095e 100644
--- a/views/TicketView.php
+++ b/views/TicketView.php
@@ -37,7 +37,9 @@ function getEventIcon(string $actionType): string {
function formatAction(array $event): string {
$det = $event['details'] ?? [];
switch ($event['action_type']) {
- case 'create': return 'created this ticket';
+ case 'create':
+ if (($event['entity_type'] ?? '') === 'comment') return 'posted a comment';
+ return 'created this ticket';
case 'comment': return 'posted a comment';
case 'view': return 'viewed this ticket';
case 'attachment': return 'uploaded a file';