From b837932bb4118879e1bf7f95fc2330d9f8aa700c Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Sun, 22 Feb 2026 23:22:20 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20Add=20Adjustment=20form=20silently=20fail?= =?UTF-8?q?ing=20=E2=80=94=20add=20validation=20+=20required=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: V5 was missing required attributes that V2 had on the Add Adjustment form. When a user submitted without selecting a project (for types that require one), the server rejected it with messages.error() but the error was invisible before the MESSAGE_TAGS fix. Combined with no client-side validation for workers, the form would silently create 0 adjustments or redirect with no visible feedback. Fixes: - Add required attribute to Project select (toggles off for Loan types) - Add client-side validation: blocks submit if no workers selected - Add backend validation: returns error if no workers in POST data - Add "Select All" / "Clear" links for worker checkboxes (matches V2) - Add "X worker(s) selected" counter for visual feedback Co-Authored-By: Claude Opus 4.6 --- core/templates/core/payroll_dashboard.html | 102 +++++++++++++++++++-- core/views.py | 7 ++ 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/core/templates/core/payroll_dashboard.html b/core/templates/core/payroll_dashboard.html index 5b3f009..505bc51 100644 --- a/core/templates/core/payroll_dashboard.html +++ b/core/templates/core/payroll_dashboard.html @@ -382,10 +382,10 @@ - {# Project #} + {# Project — required for most types, hidden for Loan types #}
- {% for p in active_projects %} @@ -408,13 +408,15 @@ {# Workers - multi-select with team helper #}
-
+
+ Select All + Clear
{% for w in all_workers %} @@ -425,6 +427,13 @@
{% endfor %}
+ {# Counter + validation message for workers #} +
+ 0 worker(s) selected +
+
{# Description #} @@ -906,35 +915,108 @@ document.addEventListener('DOMContentLoaded', function() { } // ================================================================= - // ADD ADJUSTMENT — Team quick-select + project visibility + // ADD ADJUSTMENT — Team quick-select, project visibility, + // worker count display, and form validation // ================================================================= const addAdjType = document.getElementById('addAdjType'); const addAdjProjectGroup = document.getElementById('addAdjProjectGroup'); + const addAdjProject = document.getElementById('addAdjProject'); const addAdjTeamSelect = document.getElementById('addAdjTeamSelect'); + const addAdjWorkerCheckboxes = document.querySelectorAll('.add-adj-worker'); + const adjSelectedCount = document.getElementById('adjSelectedCount'); + const addAdjWorkerError = document.getElementById('addAdjWorkerError'); - // Show/hide project field based on adjustment type + // Show/hide project field based on adjustment type. + // Also toggles the HTML "required" attribute so browser validation + // only enforces the project when the type actually needs one. function toggleProjectField() { if (!addAdjType || !addAdjProjectGroup) return; // Loan and Loan Repayment don't need a project - const noProjectTypes = ['New Loan', 'Loan Repayment']; - addAdjProjectGroup.style.display = noProjectTypes.indexOf(addAdjType.value) !== -1 ? 'none' : ''; + var noProjectTypes = ['New Loan', 'Loan Repayment']; + var needsProject = noProjectTypes.indexOf(addAdjType.value) === -1; + addAdjProjectGroup.style.display = needsProject ? '' : 'none'; + if (addAdjProject) { + if (needsProject) { + addAdjProject.setAttribute('required', ''); + } else { + addAdjProject.removeAttribute('required'); + addAdjProject.value = ''; // Clear selection when hidden + } + } } if (addAdjType) { addAdjType.addEventListener('change', toggleProjectField); toggleProjectField(); } + // Update the "X worker(s) selected" counter + function updateWorkerCount() { + var count = 0; + addAdjWorkerCheckboxes.forEach(function(cb) { + if (cb.checked) count++; + }); + if (adjSelectedCount) adjSelectedCount.textContent = count; + // Hide the error message when workers are selected + if (addAdjWorkerError && count > 0) { + addAdjWorkerError.style.display = 'none'; + } + } + addAdjWorkerCheckboxes.forEach(function(cb) { + cb.addEventListener('change', updateWorkerCount); + }); + + // Select All / Deselect All links + var adjSelectAllLink = document.getElementById('adjSelectAll'); + var adjDeselectAllLink = document.getElementById('adjDeselectAll'); + if (adjSelectAllLink) { + adjSelectAllLink.addEventListener('click', function(e) { + e.preventDefault(); + addAdjWorkerCheckboxes.forEach(function(cb) { cb.checked = true; }); + updateWorkerCount(); + }); + } + if (adjDeselectAllLink) { + adjDeselectAllLink.addEventListener('click', function(e) { + e.preventDefault(); + addAdjWorkerCheckboxes.forEach(function(cb) { cb.checked = false; }); + updateWorkerCount(); + }); + } + // Team quick-select: check workers in that team if (addAdjTeamSelect) { addAdjTeamSelect.addEventListener('change', function() { - const teamId = this.value; + var teamId = this.value; if (!teamId) return; - const workerIds = teamWorkersMap[teamId] || []; - document.querySelectorAll('.add-adj-worker').forEach(function(cb) { + var workerIds = teamWorkersMap[teamId] || []; + addAdjWorkerCheckboxes.forEach(function(cb) { if (workerIds.indexOf(parseInt(cb.value)) !== -1) { cb.checked = true; } }); + updateWorkerCount(); + }); + } + + // Form validation: ensure at least one worker is selected before submit. + // Without this, the form would submit and silently create 0 adjustments. + var addAdjForm = document.querySelector('#addAdjustmentModal form'); + if (addAdjForm) { + addAdjForm.addEventListener('submit', function(e) { + var anyChecked = false; + addAdjWorkerCheckboxes.forEach(function(cb) { + if (cb.checked) anyChecked = true; + }); + if (!anyChecked) { + e.preventDefault(); + if (addAdjWorkerError) { + addAdjWorkerError.style.display = 'block'; + } + // Scroll to the workers section so the error is visible + var workersLabel = addAdjForm.querySelector('.col-12'); + if (workersLabel) workersLabel.scrollIntoView({behavior: 'smooth', block: 'center'}); + return false; + } }); } diff --git a/core/views.py b/core/views.py index 6d5050d..73e5420 100644 --- a/core/views.py +++ b/core/views.py @@ -1100,6 +1100,13 @@ def add_adjustment(request): date_str = request.POST.get('date', '') project_id = request.POST.get('project', '') + # Validate workers — at least one must be selected. + # The frontend also checks this, but this is a safety net in case + # the user has JavaScript disabled or submits via other means. + if not worker_ids: + messages.error(request, 'Please select at least one worker.') + return redirect('payroll_dashboard') + # Validate amount try: amount = Decimal(amount_str)