Replace worker multi-select with checkboxes and team filter

The Add Payroll Adjustment modal now uses checkboxes in a scrollable
frame instead of a multi-select dropdown, making it easier to see
who is selected. Added a team filter dropdown that auto-selects all
workers in the chosen team. Users can still check/uncheck individual
workers after selecting a team. Shows live count of selected workers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-02-19 23:41:29 +02:00
parent 4e490ea2d4
commit 51ff1d464b
2 changed files with 82 additions and 21 deletions

View File

@ -448,16 +448,25 @@
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Workers</label>
<select name="workers" id="adjWorkerSelect" class="form-select" multiple required style="height: auto; min-height: 42px;">
{% for worker in all_workers %}
<option value="{{ worker.id }}">{{ worker.name }}</option>
{% endfor %}
</select>
<div class="form-text">Hold Ctrl (or Cmd) to select multiple workers.</div>
<div class="mt-2">
<a href="#" id="adjSelectAll" class="small text-primary text-decoration-none me-3">Select All</a>
<a href="#" id="adjDeselectAll" class="small text-muted text-decoration-none">Clear</a>
<div class="mb-2 d-flex gap-2 align-items-center">
<select id="adjTeamFilter" class="form-select form-select-sm" style="max-width: 200px;">
<option value="">Filter 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 id="adjWorkerCheckboxes" class="border rounded p-2" style="max-height: 200px; overflow-y: auto;">
{% for worker in all_workers %}
<div class="form-check">
<input class="form-check-input adj-worker-cb" type="checkbox" name="workers" value="{{ worker.id }}" id="adjWorker{{ worker.id }}">
<label class="form-check-label" for="adjWorker{{ worker.id }}">{{ worker.name }}</label>
</div>
{% endfor %}
</div>
<div class="form-text mt-1"><span id="adjSelectedCount">0</span> worker(s) selected</div>
</div>
<div class="mb-3">
<label class="form-label">Type</label>
@ -597,22 +606,58 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
// Adjustment modal: Select All / Clear helpers
const adjSelect = document.getElementById('adjWorkerSelect');
// Team → Worker mapping for adjustment modal
const teamWorkersMap = {{ team_workers_map_json|safe }};
// Worker checkboxes: Select All / Clear / Team filter / Counter
const allWorkerCbs = document.querySelectorAll('.adj-worker-cb');
const adjSelectedCount = document.getElementById('adjSelectedCount');
const adjTeamFilter = document.getElementById('adjTeamFilter');
const adjSelectAll = document.getElementById('adjSelectAll');
const adjDeselectAll = document.getElementById('adjDeselectAll');
if (adjSelect && adjSelectAll) {
function updateWorkerCount() {
const count = document.querySelectorAll('.adj-worker-cb:checked').length;
adjSelectedCount.textContent = count;
}
// Update count when any checkbox changes
allWorkerCbs.forEach(function(cb) {
cb.addEventListener('change', updateWorkerCount);
});
if (adjSelectAll) {
adjSelectAll.addEventListener('click', function(e) {
e.preventDefault();
for (const opt of adjSelect.options) opt.selected = true;
allWorkerCbs.forEach(function(cb) { cb.checked = true; });
updateWorkerCount();
});
}
if (adjDeselectAll) {
adjDeselectAll.addEventListener('click', function(e) {
e.preventDefault();
for (const opt of adjSelect.options) opt.selected = false;
allWorkerCbs.forEach(function(cb) { cb.checked = false; });
if (adjTeamFilter) adjTeamFilter.value = '';
updateWorkerCount();
});
}
// Advance confirmation: intercept Add Adjustment form submit
// Team dropdown: auto-select workers in that team
if (adjTeamFilter) {
adjTeamFilter.addEventListener('change', function() {
const teamId = this.value;
if (!teamId) return; // "Filter by team..." selected — do nothing
const workerIds = teamWorkersMap[teamId] || [];
allWorkerCbs.forEach(function(cb) {
if (workerIds.includes(parseInt(cb.value))) {
cb.checked = true;
}
});
updateWorkerCount();
});
}
// Form validation: require at least one worker checked
const addAdjForm = document.querySelector('#addAdjustmentModal form');
if (addAdjForm) {
const advanceConfirmModalEl = document.getElementById('advanceConfirmModal');
@ -621,18 +666,26 @@ document.addEventListener('DOMContentLoaded', function() {
new bootstrap.Modal(document.getElementById('addAdjustmentModal'));
addAdjForm.addEventListener('submit', function(e) {
// Check at least one worker is selected
const checkedWorkers = document.querySelectorAll('.adj-worker-cb:checked');
if (checkedWorkers.length === 0) {
e.preventDefault();
alert('Please select at least one worker.');
return;
}
// Advance confirmation
const typeSelect = addAdjForm.querySelector('select[name="type"]');
if (typeSelect.value !== 'ADVANCE') return; // let non-ADVANCE submit normally
e.preventDefault();
// Gather selected workers
const workerSelect = document.getElementById('adjWorkerSelect');
// Gather selected worker names
const selectedWorkers = [];
for (const opt of workerSelect.selectedOptions) {
selectedWorkers.push(opt.text);
}
if (selectedWorkers.length === 0) return;
checkedWorkers.forEach(function(cb) {
const label = document.querySelector('label[for="' + cb.id + '"]');
selectedWorkers.push(label ? label.textContent : cb.value);
});
const amount = addAdjForm.querySelector('input[name="amount"]').value;
const description = addAdjForm.querySelector('input[name="description"]').value;

View File

@ -826,6 +826,12 @@ def payroll_dashboard(request):
# Active Loans for dropdowns/modals
all_workers = Worker.objects.filter(is_active=True).order_by('name')
# Teams and team→worker mapping for adjustment modal
all_teams = Team.objects.filter(is_active=True).order_by('name')
team_workers_map = {}
for team in all_teams:
team_workers_map[team.id] = list(team.workers.filter(is_active=True).values_list('id', flat=True))
# Loans data (for loans tab)
loan_filter = request.GET.get('loan_status', 'active')
if loan_filter == 'history':
@ -905,6 +911,8 @@ def payroll_dashboard(request):
'recent_payments_total': recent_payments_total,
'active_tab': status_filter,
'all_workers': all_workers,
'all_teams': all_teams,
'team_workers_map_json': json.dumps(team_workers_map),
'adjustment_types': PayrollAdjustment.ADJUSTMENT_TYPES,
'loans': loans,
'loan_filter': loan_filter,