Fix Add Adjustment form silently failing — add validation + required fields
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 <noreply@anthropic.com>
This commit is contained in:
parent
0fa25e1538
commit
b837932bb4
@ -382,10 +382,10 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Project #}
|
||||
{# Project — required for most types, hidden for Loan types #}
|
||||
<div class="col-md-6" id="addAdjProjectGroup">
|
||||
<label class="form-label">Project</label>
|
||||
<select name="project" class="form-select" id="addAdjProject">
|
||||
<select name="project" class="form-select" id="addAdjProject" required>
|
||||
<option value="">-- Select Project --</option>
|
||||
{% for p in active_projects %}
|
||||
<option value="{{ p.id }}">{{ p.name }}</option>
|
||||
@ -408,13 +408,15 @@
|
||||
{# Workers - multi-select with team helper #}
|
||||
<div class="col-12">
|
||||
<label class="form-label">Workers</label>
|
||||
<div class="mb-2">
|
||||
<div class="mb-2 d-flex flex-wrap gap-2 align-items-center">
|
||||
<select id="addAdjTeamSelect" class="form-select form-select-sm" style="max-width: 250px;">
|
||||
<option value="">Quick select by team...</option>
|
||||
{% for team in all_teams %}
|
||||
<option value="{{ team.id }}">{{ team.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<a href="#" id="adjSelectAll" class="small text-primary text-decoration-none text-nowrap">Select All</a>
|
||||
<a href="#" id="adjDeselectAll" class="small text-muted text-decoration-none text-nowrap">Clear</a>
|
||||
</div>
|
||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 4px; padding: 8px;">
|
||||
{% for w in all_workers %}
|
||||
@ -425,6 +427,13 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{# Counter + validation message for workers #}
|
||||
<div class="form-text mt-1">
|
||||
<span id="adjSelectedCount">0</span> worker(s) selected
|
||||
</div>
|
||||
<div class="invalid-feedback" id="addAdjWorkerError" style="display: none;">
|
||||
Please select at least one worker.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user