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
+18 -12
View File
@@ -71,20 +71,22 @@ $visUserModel = new UserModel($conn);
$allAvailableGroups = $visUserModel->getAllGroups();
// JSON-encode ticket fields for the inline script
$json_ticket_id = json_encode($ticket['ticket_id'], JSON_HEX_TAG);
$json_title = json_encode($ticket['title'], JSON_HEX_TAG);
$json_status = json_encode($ticket['status'], JSON_HEX_TAG);
$json_priority = json_encode($ticket['priority'], JSON_HEX_TAG);
$json_category = json_encode($ticket['category'], JSON_HEX_TAG);
$json_type = json_encode($ticket['type'], JSON_HEX_TAG);
$json_ticket_id = json_encode($ticket['ticket_id'], JSON_HEX_TAG);
$json_title = json_encode($ticket['title'], JSON_HEX_TAG);
$json_status = json_encode($ticket['status'], JSON_HEX_TAG);
$json_priority = json_encode($ticket['priority'], JSON_HEX_TAG);
$json_category = json_encode($ticket['category'], JSON_HEX_TAG);
$json_type = json_encode($ticket['type'], JSON_HEX_TAG);
$json_updated_at = json_encode($ticket['updated_at'], JSON_HEX_TAG);
$pageInlineScript = <<<JS
window.ticketData = {
ticket_id: {$json_ticket_id},
title: {$json_title},
status: {$json_status},
priority: {$json_priority},
category: {$json_category},
type: {$json_type},
ticket_id: {$json_ticket_id},
title: {$json_title},
status: {$json_status},
priority: {$json_priority},
category: {$json_category},
type: {$json_type},
updated_at: {$json_updated_at},
};
window.ticketData.id = window.ticketData.ticket_id;
if (window.lt) lt.keys.initDefaults();
@@ -122,6 +124,10 @@ include __DIR__ . '/layout_header.php';
</select>
<button type="button" id="editButton" class="lt-btn lt-btn-primary lt-btn-sm">EDIT</button>
<button type="button" id="cloneButton" class="lt-btn lt-btn-sm">CLONE</button>
<a id="exportFullBtn"
href="/api/export_tickets.php?format=full&ticket_id=<?= (int)$ticket['ticket_id'] ?>"
class="lt-btn lt-btn-ghost lt-btn-sm"
title="Export this ticket with all comments and history as JSON">EXPORT</a>
<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm" data-modal-open="settingsModal">CFG</button>
</div>
</div>