fix: Batch Pay modal — filters no longer silently re-tick unticked workers; surface server errors

Two payment-safety fixes in the Batch Pay modal JS:

1. Changing the team/loan filter force-checked every visible row,
   silently re-selecting workers the admin had deliberately unticked
   (untick a disputed worker -> change filter -> Confirm & Pay All pays
   them anyway). Filters now only EXCLUDE (hidden rows untick); visible
   rows keep the admin's manual choice, and Select All reflects the
   real state instead of being forced on.

2. The batch-pay fetch() redirected to the dashboard on ANY HTTP
   response — fetch only rejects on network failure, so a 500 (batch
   died partway; each worker pays in its own transaction) looked like
   success. Now checks resp.ok and tells the admin to verify the
   History tab before retrying.

JS-only change; needs manual verification in the browser (no JS test
harness in this project).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-06-12 17:42:39 +02:00
parent cfc78b72ad
commit 7ce3bfb232

View File

@ -3266,10 +3266,16 @@ document.addEventListener('DOMContentLoaded', function() {
});
// --- Shared filter function (team + loan filters combined) ---
// SAFETY RULE: a filter change may EXCLUDE workers from the
// batch (hidden rows are unticked) but must NEVER silently
// re-tick a worker the admin deliberately unticked — that
// would pay someone the admin chose to skip. Use Select All
// to re-include everyone after changing filters.
function applyBatchFilters() {
var selectedTeam = filterSelect.value;
var loanMode = batchLoanFilter ? batchLoanFilter.value : '';
var rows = tbody.querySelectorAll('tr');
var allChecked = true;
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var teamMatch = !selectedTeam || row.dataset.team === selectedTeam;
@ -3278,13 +3284,17 @@ document.addEventListener('DOMContentLoaded', function() {
|| (loanMode === 'without' && row.dataset.hasLoan !== 'true');
if (teamMatch && loanMatch) {
row.style.display = '';
row.querySelector('.batch-worker-cb').checked = true;
// keep the admin's manual tick/untick choice
if (!row.querySelector('.batch-worker-cb').checked) {
allChecked = false;
}
} else {
row.style.display = 'none';
row.querySelector('.batch-worker-cb').checked = false;
}
}
selectAllCb.checked = true;
// Reflect the real state of the visible rows (not forced on)
selectAllCb.checked = allChecked;
updateBatchSummary(data, summary);
}
@ -3374,17 +3384,27 @@ document.addEventListener('DOMContentLoaded', function() {
'X-CSRFToken': '{{ csrf_token }}',
},
body: JSON.stringify({ workers: workers }),
}).then(function() {
}).then(function(resp) {
// fetch() does NOT reject on HTTP errors (4xx/5xx) — only on
// network failure. A 500 here can mean the batch died partway
// (each worker pays in its own transaction), so surface it
// instead of redirecting as if everything succeeded.
if (!resp.ok) {
throw new Error('server returned ' + resp.status);
}
// Redirect to refresh page and show Django success messages
window.location.href = '/payroll/';
}).catch(function() {
}).catch(function(err) {
btn.disabled = false;
while (btn.firstChild) btn.removeChild(btn.firstChild);
var retryIcon = document.createElement('i');
retryIcon.className = 'fas fa-money-bill-wave me-1';
btn.appendChild(retryIcon);
btn.appendChild(document.createTextNode('Confirm & Pay All'));
alert('Batch payment failed. Please try again.');
alert('Batch payment may have partially failed ('
+ (err && err.message ? err.message : 'network error')
+ '). Check the History tab to see which workers were paid '
+ 'before retrying — do not blindly re-pay everyone.');
});
});
}