{% 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' %} {# --- Cross-filter data: (team_id, worker_id) pairs for the Workers popover --- #} {{ team_worker_pairs_json|json_script:"adjTeamWorkerPairs" }} {# --- Sticky filter bar (pill-popover checkbox filters for Type/Workers/Teams) --- #}
{# === Type filter: pill-button opens popover with checkbox list === #} {# Replaced the Choices.js chip multi-select (Apr 2026): the chip #} {# pattern dominated the filter bar when multiple options were #} {# picked. Now: a compact pill shows "Type" (or "Type (N)") and #} {# clicking it opens a popover with search + checkbox list + OK. #} {# OK commits to hidden inputs below; the Apply button submits. #}
{# Hidden inputs — the actual form-submit state. Rewritten on OK. #}
{% for v in adj_filter_values.type %} {% endfor %}
{# === Workers filter: same pill + popover pattern as Type === #}
{% for v in adj_filter_values.worker %} {% endfor %}
{# === Teams filter: same pill + popover pattern as Type === #}
{% for v in adj_filter_values.team %} {% endfor %}
{# --- Status filter: pill-button opens popover with 3 radios --- #}
{# Actual submit value — rewritten by the Status popover OK handler #}
{% if adj_filter_values.adj_status %} {% endif %}
{# --- Date filter: pill-button opens popover with mode + pickers + presets --- #} {# Mode toggle (Single / Range) sits inside the popover. Presets live inside too. #} {# Backend contract unchanged: hidden inputs adj_date_from + adj_date_to. #}
{# Actual submit values — rewritten by the Date popover OK handler #}
{% if adj_filter_values.adj_date_from %} {% endif %} {% if adj_filter_values.adj_date_to %} {% endif %}
{# --- Sort state (column-header clicks set these via JS) --- #} {# --- Group-by state (keeps Flat/By Type/By Worker across Apply) --- #} {# --- Apply / Clear (pushed to the right end via .adj-apply-group) --- #}
Clear
{# --- Group-by toggle: Flat / By Type / By Worker --- #} {# Clicking a pill sets the ?group_by= querystring; url_replace preserves #} {# all other filters (type, worker, team, status, dates) so the view #} {# re-renders the SAME filtered set, just re-bucketed. #}
Show as:
{# --- 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 %}
{# === Sortable column headers — click toggles sort via filter form === #} {# Each sortable {% if adj_groups %} {# Grouped view: one per group with a clickable #} {# header row. Each group's is a Bootstrap collapse #} {# target so clicking the header hides or shows its rows. #} {# Default state: every group expanded (class="collapse show"). #} {% for group in adj_groups %} {% for adj in group.rows %} {% include 'core/_adjustment_row.html' with adj=adj additive_types=additive_types %} {% endfor %} {% endfor %} {% else %} {# Flat view (default when no group_by selected) — unchanged from Task 4. #} {% for adj in adj_page.object_list %} {% include 'core/_adjustment_row.html' with adj=adj additive_types=additive_types %} {% endfor %} {% endif %}
{# aria-label is the accessible name screen readers announce; #} {# title= is kept as the mouse-hover tooltip for sighted users. #} {# Distinct id from the Add-Adjustment modal's own #adjSelectAll #} {# anchor (line ~940) — duplicate ids are invalid HTML and caused the #} {# modal's Select-All handler to silently bind to this checkbox instead. #} reflects the current sort/order from adj_filter_values. #} {# The arrow icon is fa-sort (inactive), fa-sort-down (desc) or fa-sort-up (asc). #} {# JS click handler is wired up in the DOMContentLoaded block further down. #} Date Worker Type Amount Project Team Description Status Actions
{{ group.label }} {{ group.count }} row{{ group.count|pluralize }} · {% if group.net_sum >= 0 %}+{% else %}−{% endif %}R {{ group.net_sum|money_abs }} net
{# --- Pagination: flat view only. When grouped, group headers act as --- #} {# --- their own navigation and totals cover the whole filtered set. --- #} {% if adj_page.has_other_pages and not adj_groups %} {% endif %} {% else %} {# No rows match the current filter set. Two recovery paths: clear filters (re-fetch unfiltered) or add a new adjustment (opens the existing #addAdjustmentModal). #}
No adjustments match these filters.

Try clearing filters or adding a new adjustment.

Clear filters
{% endif %} {# Floating bulk action bar: fixed at the bottom of the viewport, centred horizontally. #} {# Shown when >= 1 unpaid row is selected. CSS .adj-bulk-bar + animation shipped in Task 2. #} {% 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