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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Project #}
|
{# Project — required for most types, hidden for Loan types #}
|
||||||
<div class="col-md-6" id="addAdjProjectGroup">
|
<div class="col-md-6" id="addAdjProjectGroup">
|
||||||
<label class="form-label">Project</label>
|
<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>
|
<option value="">-- Select Project --</option>
|
||||||
{% for p in active_projects %}
|
{% for p in active_projects %}
|
||||||
<option value="{{ p.id }}">{{ p.name }}</option>
|
<option value="{{ p.id }}">{{ p.name }}</option>
|
||||||
@ -408,13 +408,15 @@
|
|||||||
{# Workers - multi-select with team helper #}
|
{# Workers - multi-select with team helper #}
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label">Workers</label>
|
<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;">
|
<select id="addAdjTeamSelect" class="form-select form-select-sm" style="max-width: 250px;">
|
||||||
<option value="">Quick select by team...</option>
|
<option value="">Quick select by team...</option>
|
||||||
{% for team in all_teams %}
|
{% for team in all_teams %}
|
||||||
<option value="{{ team.id }}">{{ team.name }}</option>
|
<option value="{{ team.id }}">{{ team.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</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>
|
||||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 4px; padding: 8px;">
|
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 4px; padding: 8px;">
|
||||||
{% for w in all_workers %}
|
{% for w in all_workers %}
|
||||||
@ -425,6 +427,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{# Description #}
|
{# 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 addAdjType = document.getElementById('addAdjType');
|
||||||
const addAdjProjectGroup = document.getElementById('addAdjProjectGroup');
|
const addAdjProjectGroup = document.getElementById('addAdjProjectGroup');
|
||||||
|
const addAdjProject = document.getElementById('addAdjProject');
|
||||||
const addAdjTeamSelect = document.getElementById('addAdjTeamSelect');
|
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() {
|
function toggleProjectField() {
|
||||||
if (!addAdjType || !addAdjProjectGroup) return;
|
if (!addAdjType || !addAdjProjectGroup) return;
|
||||||
// Loan and Loan Repayment don't need a project
|
// Loan and Loan Repayment don't need a project
|
||||||
const noProjectTypes = ['New Loan', 'Loan Repayment'];
|
var noProjectTypes = ['New Loan', 'Loan Repayment'];
|
||||||
addAdjProjectGroup.style.display = noProjectTypes.indexOf(addAdjType.value) !== -1 ? 'none' : '';
|
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) {
|
if (addAdjType) {
|
||||||
addAdjType.addEventListener('change', toggleProjectField);
|
addAdjType.addEventListener('change', toggleProjectField);
|
||||||
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
|
// Team quick-select: check workers in that team
|
||||||
if (addAdjTeamSelect) {
|
if (addAdjTeamSelect) {
|
||||||
addAdjTeamSelect.addEventListener('change', function() {
|
addAdjTeamSelect.addEventListener('change', function() {
|
||||||
const teamId = this.value;
|
var teamId = this.value;
|
||||||
if (!teamId) return;
|
if (!teamId) return;
|
||||||
const workerIds = teamWorkersMap[teamId] || [];
|
var workerIds = teamWorkersMap[teamId] || [];
|
||||||
document.querySelectorAll('.add-adj-worker').forEach(function(cb) {
|
addAdjWorkerCheckboxes.forEach(function(cb) {
|
||||||
if (workerIds.indexOf(parseInt(cb.value)) !== -1) {
|
if (workerIds.indexOf(parseInt(cb.value)) !== -1) {
|
||||||
cb.checked = true;
|
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', '')
|
date_str = request.POST.get('date', '')
|
||||||
project_id = request.POST.get('project', '')
|
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
|
# Validate amount
|
||||||
try:
|
try:
|
||||||
amount = Decimal(amount_str)
|
amount = Decimal(amount_str)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user