diff --git a/api/bulk_operation.php b/api/bulk_operation.php index f25f73c..fe4f521 100644 --- a/api/bulk_operation.php +++ b/api/bulk_operation.php @@ -107,10 +107,17 @@ $result = $bulkOpsModel->processBulkOperation($operationId); if (isset($result['error'])) { $conn->close(); - echo json_encode([ + $response = [ 'success' => false, 'error' => $result['error'] - ]); + ]; + // Let the client know it should collect a comment and retry, rather than + // showing the failure as a dead end. + if (!empty($result['requires_comment'])) { + $response['requires_comment'] = true; + http_response_code(400); + } + echo json_encode($response); } else { // Invalidate stats cache so dashboard tiles reflect changes immediately require_once dirname(__DIR__) . '/models/StatsModel.php'; diff --git a/assets/js/dashboard.js b/assets/js/dashboard.js index 526ef54..f0e64c9 100644 --- a/assets/js/dashboard.js +++ b/assets/js/dashboard.js @@ -157,6 +157,12 @@ document.addEventListener('DOMContentLoaded', function() { case 'close-bulk-status-modal': closeBulkStatusModal(); break; + case 'perform-bulk-close': + performBulkCloseAction(); + break; + case 'close-bulk-close-modal': + closeBulkCloseModal(); + break; case 'perform-bulk-delete': performBulkDelete(); break; @@ -515,24 +521,59 @@ function bulkClose() { return; } - showConfirmModal( - `Close ${ticketIds.length} Ticket(s)?`, - 'Are you sure you want to close these tickets?', - 'warning', - () => performBulkCloseAction(ticketIds) - ); + // Closing needs a reason: the default workflow marks every → Closed transition + // requires_comment, so collect it here instead of failing server-side. + const modalHtml = ` +
+ `; + + document.body.insertAdjacentHTML('beforeend', modalHtml); + lt.modal.open('bulkCloseModal'); +} + +function closeBulkCloseModal() { + lt.modal.close('bulkCloseModal'); + const modal = document.getElementById('bulkCloseModal'); + if (modal) setTimeout(() => modal.remove(), 300); } function performBulkCloseAction(ticketIds) { + ticketIds = ticketIds || getSelectedTicketIds(); + const commentEl = document.getElementById('bulkCloseComment'); + const comment = commentEl ? commentEl.value.trim() : ''; lt.api.post('/api/bulk_operation.php', { operation_type: 'bulk_close', - ticket_ids: ticketIds + ticket_ids: ticketIds, + parameters: { comment: comment } }) .then(data => { + closeBulkCloseModal(); if (data.success) { if (data.failed > 0) { - lt.toast.warning(`Bulk close: ${data.processed} succeeded, ${data.failed} failed`, 5000); + lt.toast.warning(bulkResultMessage('Bulk close', data), 6000); } else { lt.toast.success(`Successfully closed ${data.processed} ticket(s)`, 4000); } @@ -542,6 +583,14 @@ function performBulkCloseAction(ticketIds) { } }) .catch(error => { + // Missing required comment — keep the modal open so it can be entered. + if (error && error.data && error.data.requires_comment) { + lt.toast.warning(error.data.error || 'A close reason is required', 6000); + const ta = document.getElementById('bulkCloseComment'); + if (ta) ta.focus(); + return; + } + closeBulkCloseModal(); lt.toast.error('Bulk close failed: ' + error.message, 5000); }); } @@ -777,6 +826,15 @@ function showBulkStatusModal() { ${(window.TICKET_STATUSES || ['Open','Pending','In Progress','Closed']).map(s => ``).join('')} + + ++ Required for transitions the Workflow Designer marks as needing a comment + (e.g. closing a ticket). Posted as a comment on every ticket changed. +