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:
parent
4e490ea2d4
commit
51ff1d464b
@ -448,16 +448,25 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Workers</label>
|
<label class="form-label">Workers</label>
|
||||||
<select name="workers" id="adjWorkerSelect" class="form-select" multiple required style="height: auto; min-height: 42px;">
|
<div class="mb-2 d-flex gap-2 align-items-center">
|
||||||
{% for worker in all_workers %}
|
<select id="adjTeamFilter" class="form-select form-select-sm" style="max-width: 200px;">
|
||||||
<option value="{{ worker.id }}">{{ worker.name }}</option>
|
<option value="">Filter by team...</option>
|
||||||
|
{% for team in all_teams %}
|
||||||
|
<option value="{{ team.id }}">{{ team.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class="form-text">Hold Ctrl (or Cmd) to select multiple workers.</div>
|
<a href="#" id="adjSelectAll" class="small text-primary text-decoration-none text-nowrap">Select All</a>
|
||||||
<div class="mt-2">
|
<a href="#" id="adjDeselectAll" class="small text-muted text-decoration-none text-nowrap">Clear</a>
|
||||||
<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>
|
</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>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Type</label>
|
<label class="form-label">Type</label>
|
||||||
@ -597,22 +606,58 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Adjustment modal: Select All / Clear helpers
|
// Team → Worker mapping for adjustment modal
|
||||||
const adjSelect = document.getElementById('adjWorkerSelect');
|
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 adjSelectAll = document.getElementById('adjSelectAll');
|
||||||
const adjDeselectAll = document.getElementById('adjDeselectAll');
|
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) {
|
adjSelectAll.addEventListener('click', function(e) {
|
||||||
e.preventDefault();
|
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) {
|
adjDeselectAll.addEventListener('click', function(e) {
|
||||||
e.preventDefault();
|
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');
|
const addAdjForm = document.querySelector('#addAdjustmentModal form');
|
||||||
if (addAdjForm) {
|
if (addAdjForm) {
|
||||||
const advanceConfirmModalEl = document.getElementById('advanceConfirmModal');
|
const advanceConfirmModalEl = document.getElementById('advanceConfirmModal');
|
||||||
@ -621,18 +666,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
new bootstrap.Modal(document.getElementById('addAdjustmentModal'));
|
new bootstrap.Modal(document.getElementById('addAdjustmentModal'));
|
||||||
|
|
||||||
addAdjForm.addEventListener('submit', function(e) {
|
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"]');
|
const typeSelect = addAdjForm.querySelector('select[name="type"]');
|
||||||
if (typeSelect.value !== 'ADVANCE') return; // let non-ADVANCE submit normally
|
if (typeSelect.value !== 'ADVANCE') return; // let non-ADVANCE submit normally
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Gather selected workers
|
// Gather selected worker names
|
||||||
const workerSelect = document.getElementById('adjWorkerSelect');
|
|
||||||
const selectedWorkers = [];
|
const selectedWorkers = [];
|
||||||
for (const opt of workerSelect.selectedOptions) {
|
checkedWorkers.forEach(function(cb) {
|
||||||
selectedWorkers.push(opt.text);
|
const label = document.querySelector('label[for="' + cb.id + '"]');
|
||||||
}
|
selectedWorkers.push(label ? label.textContent : cb.value);
|
||||||
if (selectedWorkers.length === 0) return;
|
});
|
||||||
|
|
||||||
const amount = addAdjForm.querySelector('input[name="amount"]').value;
|
const amount = addAdjForm.querySelector('input[name="amount"]').value;
|
||||||
const description = addAdjForm.querySelector('input[name="description"]').value;
|
const description = addAdjForm.querySelector('input[name="description"]').value;
|
||||||
|
|||||||
@ -826,6 +826,12 @@ def payroll_dashboard(request):
|
|||||||
# Active Loans for dropdowns/modals
|
# Active Loans for dropdowns/modals
|
||||||
all_workers = Worker.objects.filter(is_active=True).order_by('name')
|
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)
|
# Loans data (for loans tab)
|
||||||
loan_filter = request.GET.get('loan_status', 'active')
|
loan_filter = request.GET.get('loan_status', 'active')
|
||||||
if loan_filter == 'history':
|
if loan_filter == 'history':
|
||||||
@ -905,6 +911,8 @@ def payroll_dashboard(request):
|
|||||||
'recent_payments_total': recent_payments_total,
|
'recent_payments_total': recent_payments_total,
|
||||||
'active_tab': status_filter,
|
'active_tab': status_filter,
|
||||||
'all_workers': all_workers,
|
'all_workers': all_workers,
|
||||||
|
'all_teams': all_teams,
|
||||||
|
'team_workers_map_json': json.dumps(team_workers_map),
|
||||||
'adjustment_types': PayrollAdjustment.ADJUSTMENT_TYPES,
|
'adjustment_types': PayrollAdjustment.ADJUSTMENT_TYPES,
|
||||||
'loans': loans,
|
'loans': loans,
|
||||||
'loan_filter': loan_filter,
|
'loan_filter': loan_filter,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user