Add overdue badges and filters to pending payments table
- Red 'Overdue' badge on workers with unpaid work from completed pay periods - Yellow 'Loan' badge on workers with active loans/advances - Filter bar above table: team dropdown, overdue-only toggle, exclude loans - All three filters combine (team + overdue + loan) for flexible views - Overdue detection uses team pay schedule cutoff from get_pay_period() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
695b7cb3f1
commit
1b6ade87af
@ -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 #}
|
||||
<div class="d-flex align-items-center gap-3 mb-3 flex-wrap" id="pendingFilters">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<label class="text-muted small mb-0" for="pendingTeamFilter">Team:</label>
|
||||
<select id="pendingTeamFilter" class="form-select form-select-sm" style="width: auto;">
|
||||
<option value="">All Teams</option>
|
||||
{% for team in all_teams %}
|
||||
<option value="{{ team.name }}">{{ team.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-check mb-0">
|
||||
<input type="checkbox" class="form-check-input" id="pendingOverdueOnly">
|
||||
<label class="form-check-label text-muted small" for="pendingOverdueOnly">Overdue only</label>
|
||||
</div>
|
||||
<div class="form-check mb-0">
|
||||
<input type="checkbox" class="form-check-input" id="pendingExcludeLoans">
|
||||
<label class="form-check-label text-muted small" for="pendingExcludeLoans">Exclude workers with loans</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<table class="table table-hover mb-0" id="pendingTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col" class="ps-4">Worker</th>
|
||||
@ -234,9 +257,17 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for wd in workers_data %}
|
||||
<tr>
|
||||
<tr data-team="{{ wd.team_name }}"
|
||||
data-overdue="{{ wd.is_overdue|yesno:'true,false' }}"
|
||||
data-has-loan="{{ wd.has_loan|yesno:'true,false' }}">
|
||||
<td class="ps-4 align-middle">
|
||||
<strong>{{ wd.worker.name }}</strong>
|
||||
{% if wd.is_overdue %}
|
||||
<span class="badge bg-danger ms-1" title="Has unpaid work from a completed pay period (since {{ wd.earliest_unpaid|date:'d M Y' }})">Overdue</span>
|
||||
{% endif %}
|
||||
{% if wd.has_loan %}
|
||||
<span class="badge bg-warning text-dark ms-1" title="Has active loan or advance">Loan</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle">{{ wd.unpaid_count }}</td>
|
||||
<td class="align-middle">R {{ wd.day_rate }}</td>
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user