{% extends 'base.html' %} {% load static %} {% load format_tags %} {% block title %}Payroll Dashboard | FoxFitt{% endblock %} {% block content %}
{# === PAGE HEADER === #} {# On desktop: title left, buttons right in a row #} {# On mobile: title on top, buttons below in a 2x2 grid #}

Payroll Dashboard

{# === ANALYTICS SUMMARY BAR — compact row of key numbers === #} {# Always visible. Clicking "Show Details" expands the full stat cards and charts below. #}
{# Key numbers in a compact row #}
Outstanding
R {{ outstanding_total|floatformat:2 }}
Paid (60d)
R {{ recent_payments_total|floatformat:2 }}
Loans ({{ active_loans_count }})
R {{ active_loans_balance|floatformat:2 }}
{# Toggle button to expand/collapse full analytics #}
{# === FULL ANALYTICS (hidden by default — toggled by button above) === #} {# /analyticsDetail #} {# === TAB NAVIGATION === #} {# =============================================== #} {# === 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 #}
{# On mobile: hide Days, Day Rate, Log Amount, Adjustments, Net Adj columns #} {# Only show: Worker (with badges), Total, Adjust + Pay buttons #} {# All details are accessible by tapping the worker name (opens lookup modal) #} {% for wd in workers_data %} {% empty %} {% endfor %}
Worker Days Day Rate Log Amount Adjustments Net Adj Total Actions
{% if wd.is_overdue or wd.has_loan %}
{% if wd.is_overdue %} Overdue {% endif %} {% if wd.has_loan %} Loan {% endif %}
{% endif %}
{{ wd.unpaid_count }} R {{ wd.day_rate }} R {{ wd.unpaid_amount|floatformat:2 }} {# Show each pending adjustment as a badge #} {% for adj in wd.adjustments %} {# Badge colour logic: #} {# GREEN = earned money (Bonus, Overtime) or debt recovery (Loan/Advance Repayment) #} {# YELLOW = loan-related outflow (New Loan, Advance Payment) — matches the Loan tag #} {# RED = deductions (Deduction) #} {% if adj.type == 'Bonus' or adj.type == 'Overtime' or adj.type == 'New Loan' or adj.type == 'Advance Payment' %}+{% else %}-{% endif %}R{{ adj.amount|floatformat:2 }} {{ adj.type }} {% if adj.project %}({{ adj.project.name }}){% endif %} {% endfor %} {% if not wd.adjustments %} - {% endif %} {% if wd.adj_amount >= 0 %}+{% endif %}R {{ wd.adj_amount|floatformat:2 }} R {{ wd.total_payable|floatformat:2 }}
{% csrf_token %}
No pending payments. All workers are paid up!
{% endif %} {# =============================================== #} {# === PAYMENT HISTORY TAB === #} {# =============================================== #} {% if active_tab == 'paid' %}
{# On mobile: hide Date, Work Logs, Adjustments columns #} {# Only show: Worker, Amount Paid, View button #} {% for record in paid_records %} {% empty %} {% endfor %}
Date Worker Amount Paid Work Logs Adjustments Payslip
{{ record.date }} {{ record.worker.name }} R {{ record.amount_paid|floatformat:2 }} {{ record.work_logs.count }} day{{ record.work_logs.count|pluralize }} {% for adj in record.adjustments.all %} {{ adj.type }}: R {{ adj.amount|floatformat:2 }} {% empty %} - {% endfor %} View
No payment history yet.
{% endif %} {# =============================================== #} {# === LOANS TAB === #} {# =============================================== #} {% if active_tab == 'loans' %}
Active History
{# On mobile: hide Principal, Date, Reason, Status columns #} {# Only show: Worker, Type, Balance #} {% for loan in loans %} {% empty %} {% endfor %}
Worker Type Principal Balance Date Reason Status
{{ loan.worker.name }} {% if loan.loan_type == 'advance' %} Advance {% else %} Loan {% endif %} R {{ loan.principal_amount|floatformat:2 }} R {{ loan.remaining_balance|floatformat:2 }} {{ loan.date }} {{ loan.reason|default:"-" }} {% if loan.active %} Active {% else %} Paid Off {% endif %}
{% if loan_filter == 'active' %}No active loans or advances.{% else %}No loan/advance history.{% endif %}
{% endif %} {# =============================================== #} {# === ADJUSTMENTS TAB === #} {# =============================================== #} {# Flat, filterable table of every PayrollAdjustment in the system. #} {# Filters run server-side via GET params (type, worker, team, status, date range). #} {# Row actions reuse the existing Edit / Delete / Preview modals so no new JS is needed. #} {% if active_tab == 'adjustments' %} {# --- Sticky filter bar (Choices.js enhances the multi-selects below) --- #}
{# --- Type multi-select (Bonus / Overtime / etc.) --- #} {# Each label/input pair below has matching for=/id= so screen #} {# readers announce the field name when focus moves into the #} {# select. The .adj-multi class still drives Choices.js init — #} {# adding id= is purely additive. #}
{# --- Workers multi-select (cross-filtered by Teams in Task 7) --- #}
{# --- Teams multi-select --- #}
{# --- Status single-select (Unpaid / Paid / All) --- #}
{# --- Date range --- #}
{# --- Sort state (column-header clicks will set these via JS in Task 9) --- #} {# --- Apply / Clear buttons --- #}
Clear
{# --- Stats row (scoped to the currently-filtered set) --- #}
{{ adj_total_count }} adjustment{{ adj_total_count|pluralize }} · {{ adj_unpaid_count }} unpaid (R {{ adj_unpaid_sum|money }}) · +R {{ adj_additive_sum|money }} net additive · −R {{ adj_deductive_sum|money }} net deductive
{# --- Flat table of adjustments --- #} {% if adj_page.object_list %}
{% for adj in adj_page.object_list %} {% include 'core/_adjustment_row.html' with adj=adj additive_types=additive_types %} {% endfor %}
{# aria-label is the accessible name screen readers announce; #} {# title= is kept as the mouse-hover tooltip for sighted users. #} Date Worker Type Amount Project Team Description Status Actions
{# --- Pagination --- #} {# Hrefs go through the `url_replace` template tag (see core/templatetags/format_tags.py). It rebuilds the current querystring with ONLY the `page` key swapped out — prevents the `?page=2&page=3` stacking bug that happened when we naively appended `&page=X` to the raw `request.GET.urlencode`. #} {% if adj_page.has_other_pages %} {% endif %} {% else %} {# Simple placeholder empty state — visual polish comes in Task 10 #}

No adjustments match these filters.

{% endif %} {% endif %}
{# ================================================================== #} {# === MODALS === #} {# ================================================================== #} {# --- ADD ADJUSTMENT MODAL --- #} {# --- EDIT ADJUSTMENT MODAL --- #} {# --- DELETE CONFIRMATION MODAL --- #} {# --- PRICE OVERTIME MODAL --- #} {# --- PREVIEW PAYSLIP MODAL --- #} {# === BATCH PAY MODAL === #} {# Shows a preview of which workers will be paid (based on team pay schedules), #} {# then lets the admin confirm to process all payments at once. #} {# === WORKER LOOKUP MODAL === #} {# Shows a comprehensive financial report card for any active worker. #} {# Triggered by clicking a worker name or the "Worker Lookup" button. #} {# ================================================================== #} {# === JAVASCRIPT === #} {# ================================================================== #} {# Django's json_script filter safely outputs JSON without XSS risk #} {{ overtime_data_json|json_script:"otDataJson" }} {{ team_workers_map_json|json_script:"teamWorkersJson" }} {{ chart_labels_json|json_script:"chartLabelsJson" }} {{ chart_totals_json|json_script:"chartTotalsJson" }} {{ project_chart_json|json_script:"projectChartJson" }} {{ worker_chart_json|json_script:"workerChartJson" }} {# === CHOICES.JS CDN — loaded only when the Adjustments tab is active === #} {# Used by the Type / Workers / Teams multi-select filters. If the CDN fails #} {# the