Wire optimistic locking, visibility audit log, full ticket export

Optimistic locking:
- TicketView now includes updated_at in window.ticketData
- ticket.js saveTicket() sends expected_updated_at on every save so
  the server can detect concurrent edits
- On conflict response, shows a clear toast: "ticket was modified by
  someone else while you were editing — reload to see latest version"
- On success, syncs window.ticketData.updated_at from server response
  so subsequent saves use the correct lock key
- update_ticket.php now returns updated_at in success response

Visibility audit log:
- updateVisibility() result is now checked; on success, logs a delta
  entry to the audit trail with from/to visibility and groups so the
  timeline shows who changed visibility and when

Full ticket export:
- export_tickets.php now accepts format=full with a single ticket_id
- Produces a JSON file containing ticket fields, flat comment list
  (with author, timestamps, text), and the full audit timeline
- Access-controlled: respects canUserAccessTicket() before exporting
- EXPORT button added to ticket toolbar linking directly to the endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 21:19:01 -04:00
parent 2fdd42b45b
commit cc3f667d4c
4 changed files with 132 additions and 24 deletions
+16 -5
View File
@@ -47,18 +47,29 @@ function saveTicket() {
}
}
// Include optimistic lock timestamp so the server can detect concurrent edits
if (window.ticketData && window.ticketData.updated_at) {
data.expected_updated_at = window.ticketData.updated_at;
}
// Use the correct API path
lt.api.post('/api/update_ticket.php', { ticket_id: ticketId, ...data })
.then(data => {
if (data.success) {
.then(resp => {
if (resp.success) {
const statusDisplay = document.getElementById('statusDisplay');
if (statusDisplay) {
statusDisplay.className = `status-${data.status}`;
statusDisplay.textContent = data.status;
statusDisplay.className = `status-${resp.status}`;
statusDisplay.textContent = resp.status;
}
// Keep local updated_at in sync so the next save uses the right lock key
if (resp.updated_at && window.ticketData) {
window.ticketData.updated_at = resp.updated_at;
}
lt.toast.success('Ticket updated successfully');
} else if (resp.conflict) {
lt.toast.error('This ticket was modified by someone else while you were editing. Reload to see the latest version.', 8000);
} else {
lt.toast.error('Error saving ticket: ' + (data.error || 'Unknown error'));
lt.toast.error('Error saving ticket: ' + (resp.error || 'Unknown error'));
}
})
.catch(error => {