diff --git a/core/templates/core/payroll_dashboard.html b/core/templates/core/payroll_dashboard.html index 1e08b54..60312f5 100644 --- a/core/templates/core/payroll_dashboard.html +++ b/core/templates/core/payroll_dashboard.html @@ -216,10 +216,33 @@ {# === PENDING PAYMENTS TAB === #} {# =============================================== #} {% if active_tab == 'pending' %} + + {# === PENDING PAYMENTS FILTER BAR === #} + {# Lets admin filter by team, show only overdue workers, or exclude workers with loans #} +
+
+ + +
+
+ + +
+
+ + +
+
+
- +
@@ -234,9 +257,17 @@ {% for wd in workers_data %} - + @@ -764,6 +795,34 @@ document.addEventListener('DOMContentLoaded', function() { return td; } + // ================================================================= + // PENDING PAYMENTS TABLE — Team / Overdue / Loan Filters + // Shows/hides rows based on filter selections. Pure client-side. + // ================================================================= + var pendingTable = document.getElementById('pendingTable'); + var pendingTeamFilter = document.getElementById('pendingTeamFilter'); + var pendingOverdueOnly = document.getElementById('pendingOverdueOnly'); + var pendingExcludeLoans = document.getElementById('pendingExcludeLoans'); + + if (pendingTable && pendingTeamFilter) { + function applyPendingFilters() { + var team = pendingTeamFilter.value; + var overdueOnly = pendingOverdueOnly ? pendingOverdueOnly.checked : false; + var excludeLoans = pendingExcludeLoans ? pendingExcludeLoans.checked : false; + var rows = pendingTable.querySelectorAll('tbody tr[data-team]'); + for (var i = 0; i < rows.length; i++) { + var row = rows[i]; + var teamMatch = !team || row.dataset.team === team; + var overdueMatch = !overdueOnly || row.dataset.overdue === 'true'; + var loanMatch = !excludeLoans || row.dataset.hasLoan !== 'true'; + row.style.display = (teamMatch && overdueMatch && loanMatch) ? '' : 'none'; + } + } + pendingTeamFilter.addEventListener('change', applyPendingFilters); + if (pendingOverdueOnly) pendingOverdueOnly.addEventListener('change', applyPendingFilters); + if (pendingExcludeLoans) pendingExcludeLoans.addEventListener('change', applyPendingFilters); + } + // ================================================================= // CHART.JS — Monthly Totals (Line Chart) // Wrapped in try-catch so a Chart.js failure doesn't prevent diff --git a/core/views.py b/core/views.py index 0f80c73..c85ce56 100644 --- a/core/views.py +++ b/core/views.py @@ -906,6 +906,21 @@ def payroll_dashboard(request): # Only include workers who have something pending if log_count > 0 or pending_adjs: + # --- Overdue detection --- + # A worker is "overdue" if they have unpaid work from a completed pay period. + # Uses their team's pay schedule to determine the cutoff date. + team = get_worker_active_team(worker) + team_name = team.name if team else '' + earliest_unpaid = min((l.date for l in unpaid_logs), default=None) if unpaid_logs else None + is_overdue = False + if earliest_unpaid and team and team.pay_frequency and team.pay_start_date: + period_start, period_end = get_pay_period(team) + if period_start: + cutoff = period_start - datetime.timedelta(days=1) + is_overdue = earliest_unpaid <= cutoff + + has_loan = Loan.objects.filter(worker=worker, active=True).exists() + workers_data.append({ 'worker': worker, 'unpaid_count': log_count, @@ -916,6 +931,10 @@ def payroll_dashboard(request): 'logs': unpaid_logs, 'ot_data': ot_data_worker, 'day_rate': float(worker.daily_rate), + 'team_name': team_name, + 'is_overdue': is_overdue, + 'has_loan': has_loan, + 'earliest_unpaid': earliest_unpaid, }) outstanding_total += max(total_payable, Decimal('0.00')) unpaid_wages_total += log_amount
Worker
{{ wd.worker.name }} + {% if wd.is_overdue %} + Overdue + {% endif %} + {% if wd.has_loan %} + Loan + {% endif %} {{ wd.unpaid_count }} R {{ wd.day_rate }}