From 72d40971f19fab11179c462e016a7f173fd51b18 Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Wed, 25 Mar 2026 09:23:01 +0200 Subject: [PATCH] Update batch pay modal: 3-option loan filter + radio button fix - Replace "Exclude workers with loans" checkbox with dropdown (All Workers / With loans only / Without loans) in batch pay modal, matching the pending payments table filter style - Fix radio button visual state when switching between "Until Last Paydate" and "Pay All" modes (set checked after DOM append) - Update CLAUDE.md with pending table filter and overdue badge docs Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 5 ++- core/templates/core/payroll_dashboard.html | 47 +++++++++++++--------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0b52857..43256ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,9 +103,10 @@ python manage.py check # System check - Advance Payment auto-processing: `add_adjustment` immediately creates PayrollRecord + sends payslip when an advance is created. Also auto-creates an "Advance Repayment" adjustment for the next salary cycle. Uses `_send_payslip_email()` helper (shared with `process_payment`) - Advance-to-loan conversion: When an Advance Repayment is only partially paid, `process_payment` changes the Loan's `loan_type` from 'advance' to 'loan' so the remainder is tracked as a regular loan - Split Payslip: Preview modal has checkboxes on work logs and adjustments (all checked by default). `process_payment()` accepts optional `selected_log_ids` / `selected_adj_ids` POST params to pay only selected items. Falls back to "pay all" if no IDs provided (backward compatible with the quick Pay button). -- Team Pay Schedules: Teams have optional `pay_frequency` + `pay_start_date` fields. `get_pay_period(team)` calculates current period boundaries by stepping forward from the anchor date. The preview modal shows a "Split at Pay Date" button that auto-unchecks items outside the current pay period. `get_worker_active_team(worker)` returns the worker's first active team. +- Team Pay Schedules: Teams have optional `pay_frequency` + `pay_start_date` fields. `get_pay_period(team)` calculates current period boundaries by stepping forward from the anchor date. The preview modal shows a "Split at Pay Date" button that auto-unchecks items after the `cutoff_date` (end of last completed period — includes ALL overdue work, not just one period). `get_worker_active_team(worker)` returns the worker's first active team. - Pay period calculation: `pay_start_date` is an anchor (never needs updating). Weekly=7 days, Fortnightly=14 days, Monthly=calendar month stepping. Uses `calendar.monthrange()` for month-length edge cases (no `dateutil` dependency). -- Batch Pay: "Batch Pay" button on payroll dashboard opens a modal with two radio modes — **"Until Last Paydate"** (default, splits at last completed pay period per team schedule) and **"Pay All"** (includes all unpaid items regardless of date). Preview fetches from `batch_pay_preview` with `?mode=schedule|all`. Workers without team pay schedules are skipped in schedule mode but included in Pay All mode. `batch_pay` POST endpoint processes each worker in independent atomic transactions; emails are sent after all payments complete. Uses `_process_single_payment()` shared helper (same logic as individual `process_payment`). +- Batch Pay: "Batch Pay" button on payroll dashboard opens a modal with two radio modes — **"Until Last Paydate"** (default, splits at last completed pay period per team schedule) and **"Pay All"** (includes all unpaid items regardless of date). Preview fetches from `batch_pay_preview` with `?mode=schedule|all`. Workers without team pay schedules are skipped in schedule mode but included in Pay All mode. `batch_pay` POST endpoint processes each worker in independent atomic transactions; emails are sent after all payments complete. Uses `_process_single_payment()` shared helper (same logic as individual `process_payment`). Modal includes team filter dropdown and 3-option loan filter (All / With loans only / Without loans). +- Pending Payments Table: Shows overdue badges (red) for workers with unpaid work from completed pay periods, and loan badges (yellow) for workers with active loans/advances. Filter bar has: team dropdown, "Overdue only" checkbox, and loan dropdown (All Workers / With loans only / Without loans). Overdue detection uses `get_pay_period()` cutoff logic. ## URL Routes | Path | View | Purpose | diff --git a/core/templates/core/payroll_dashboard.html b/core/templates/core/payroll_dashboard.html index 0cc8987..2994b8a 100644 --- a/core/templates/core/payroll_dashboard.html +++ b/core/templates/core/payroll_dashboard.html @@ -2254,12 +2254,12 @@ document.addEventListener('DOMContentLoaded', function() { // Re-fetch preview when radio changes radioSchedule.addEventListener('change', function() { loadBatchPreview('schedule'); }); radioAll.addEventListener('change', function() { loadBatchPreview('all'); }); - } else { - // Update checked state using the saved reference (not getElementById — element is detached) - _batchRadioGroup.querySelector('#batchModeSchedule').checked = (mode === 'schedule'); - _batchRadioGroup.querySelector('#batchModeAll').checked = (mode === 'all'); } + // Re-append radio group to DOM first, THEN set checked state. + // Radio buttons only properly toggle siblings when attached to the DOM. body.appendChild(_batchRadioGroup); + _batchRadioGroup.querySelector('#batchModeSchedule').checked = (mode === 'schedule'); + _batchRadioGroup.querySelector('#batchModeAll').checked = (mode === 'all'); // Show loading spinner below radio buttons var loadDiv = document.createElement('div'); @@ -2339,22 +2339,29 @@ document.addEventListener('DOMContentLoaded', function() { }); filterRow.appendChild(filterSelect); - // Only show the "Exclude loans" checkbox if any worker has a loan + // Loan filter dropdown (All / With loans only / Without loans) + var batchLoanFilter = null; var anyHasLoan = data.eligible.some(function(w) { return w.has_loan; }); - var excludeLoansCheck = null; if (anyHasLoan) { var loanDiv = document.createElement('div'); - loanDiv.className = 'form-check ms-3 mb-0'; - excludeLoansCheck = document.createElement('input'); - excludeLoansCheck.type = 'checkbox'; - excludeLoansCheck.className = 'form-check-input'; - excludeLoansCheck.id = 'batchExcludeLoans'; - loanDiv.appendChild(excludeLoansCheck); + loanDiv.className = 'd-flex align-items-center gap-2 ms-3'; var loanLabel = document.createElement('label'); - loanLabel.className = 'form-check-label text-muted small'; - loanLabel.setAttribute('for', 'batchExcludeLoans'); - loanLabel.textContent = 'Exclude workers with loans'; + loanLabel.className = 'text-muted small mb-0'; + loanLabel.textContent = 'Loans:'; + loanLabel.setAttribute('for', 'batchLoanFilter'); loanDiv.appendChild(loanLabel); + batchLoanFilter = document.createElement('select'); + batchLoanFilter.id = 'batchLoanFilter'; + batchLoanFilter.className = 'form-select form-select-sm'; + batchLoanFilter.style.width = 'auto'; + var opts = [['', 'All Workers'], ['with', 'With loans only'], ['without', 'Without loans']]; + opts.forEach(function(o) { + var opt = document.createElement('option'); + opt.value = o[0]; + opt.textContent = o[1]; + batchLoanFilter.appendChild(opt); + }); + loanDiv.appendChild(batchLoanFilter); filterRow.appendChild(loanDiv); } @@ -2447,12 +2454,14 @@ document.addEventListener('DOMContentLoaded', function() { // --- Shared filter function (team + loan filters combined) --- function applyBatchFilters() { var selectedTeam = filterSelect.value; - var excludeLoans = excludeLoansCheck ? excludeLoansCheck.checked : false; + var loanMode = batchLoanFilter ? batchLoanFilter.value : ''; var rows = tbody.querySelectorAll('tr'); for (var i = 0; i < rows.length; i++) { var row = rows[i]; var teamMatch = !selectedTeam || row.dataset.team === selectedTeam; - var loanMatch = !excludeLoans || row.dataset.hasLoan !== 'true'; + var loanMatch = !loanMode + || (loanMode === 'with' && row.dataset.hasLoan === 'true') + || (loanMode === 'without' && row.dataset.hasLoan !== 'true'); if (teamMatch && loanMatch) { row.style.display = ''; row.querySelector('.batch-worker-cb').checked = true; @@ -2466,8 +2475,8 @@ document.addEventListener('DOMContentLoaded', function() { } filterSelect.addEventListener('change', applyBatchFilters); - if (excludeLoansCheck) { - excludeLoansCheck.addEventListener('change', applyBatchFilters); + if (batchLoanFilter) { + batchLoanFilter.addEventListener('change', applyBatchFilters); } // --- Skipped workers (collapsible) ---