# Executive Payroll Report v2 — Design (23 Apr 2026) ## Goal Rebuild the payroll report (`/report/`) as an **executive-grade dashboard**: multi-select project and team filters, a live "Current Outstanding" KPI card, company-wide cost-velocity metrics, a new team × project activity pivot, and a re-organised layout that leads with the numbers a business owner reads first. No model changes; no new dependencies beyond the Choices.js CDN. ## Who it's for **Admins** (`is_staff` or `is_superuser`). Supervisors keep no report access. ## Design shape at a glance ``` ┌─ Header (title + filter pills) ──────────────────────────────────────┐ ├─ HERO KPI BAND (4 big cards) ────────────────────────────────────────┤ │ Paid this period │ Outstanding NOW │ Avg R/day │ Avg R/month │ ├─ Chapter I — Lifetime Context ───────────────────────────────────────┤ │ All Time Projects (name, start, working days, total, avg/wday) │ │ All Time Teams (name, working days, total) │ ├─ Chapter II — Selected Period ───────────────────────────────────────┤ │ Summary stat cards (6: Paid, Worker-Days, Loans×2, Advances×2) │ │ Payments by Date | Adjustment Summary │ │ Labour Cost by Project | Labour Cost by Team │ ├─ Chapter III — Worker Breakdown ─────────────────────────────────────┤ │ Wide table: worker, days, total paid, dynamic adjustment columns │ ├─ Chapter IV — Team × Project Activity (NEW) ────────────────────────┤ │ Pivot: rows=team, columns=project, cell=distinct work-log dates │ └──────────────────────────────────────────────────────────────────────┘ ``` ## 1. Filters — multi-select ### UI - `` still works — degraded but functional. ### Semantics - **Empty selection** → treated as "all" - **Multiple values within one filter** → OR (`project_id IN (1, 2, 3)`) - **Across Project × Team** → AND (`project_id IN (...) AND team_id IN (...)`) ### Backend signature change ```python # Before _build_report_context(start, end, project_id=None, team_id=None) # After _build_report_context(start, end, project_ids=None, team_ids=None) # both accept list[int] | None; [] treated same as None (= "all") ``` ### URL / QS compatibility - `generate_report` and `generate_report_pdf` use `request.GET.getlist('project')` / `getlist('team')`. - Multiple values in the querystring: `?project=1&project=2&team=3`. - Old single-value URLs (`?project=1`) still resolve — `getlist` returns a one-element list. ### Filter-pill strip Directly under the page header, a horizontal row of summary pills: - `📅 Mar 2026 – Apr 2026` - `📁 Wilkot Boerdery, Solar Farm Alpha` (one pill lists all selected projects) - `👥 Civils One` (same for teams) Clickable × on each pill to remove that filter. In PDFs, pills render as static labels. ## 2. Hero KPI band (new) Four large cards in one row, sitting directly under the header / filter strip. | Card | Label | Value | Sub-line | |---|---|---|---| | 1 | PAID THIS PERIOD | `total_paid_out` (already computed) | `{start_date} – {end_date}` | | 2 | OUTSTANDING NOW | live dashboard math | `as of {now:HH:MM}` | | 3 | FOXFITT AVG / DAY | `total_lifetime_cost / total_working_days` | `lifetime avg` | | 4 | FOXFITT AVG / MONTH | daily × 30.44 | `lifetime avg` | Typography: Poppins 32pt semibold for the number; Inter 10pt uppercase tracked +0.08em for the label; Inter 9pt `--text-tertiary` for the sub-line. `--accent` orange on the left vertical bar (existing `.stat-card` style, scaled up). ### Backend — new computed values - **`current_outstanding`** — dict `{'total': Decimal, 'by_project': [{'name': str, 'amount': Decimal}, ...]}`. Reuses the dashboard math from `index()` (`unpaid_wages + pending_adj_add - pending_adj_sub`) but respects the report's project/team filters. Stamped with `current_as_of = timezone.now()`. - **`company_avg_daily`** — `Decimal`. `total_lifetime_cost / company_working_days`. `company_working_days = WorkLog.objects.values('date').distinct().count()`. - **`company_avg_monthly`** — `company_avg_daily * Decimal('30.44')`. The 30.44 is `365.25/12` — the standard month-length approximation (keeps annual totals correct on average). These come from a new helper `_company_cost_velocity()` called from `_build_report_context`. ## 3. Chapter I — Lifetime Context Two cards side-by-side, replacing the current 4 cramped cards. ### All Time — Projects (~60% width) Columns: **Project** · **Start** · **Working Days** · **Total Cost** · **Avg R / Working Day** - `Working Days` = `COUNT(DISTINCT work_log.date)` where `project_id = P` (this is your answer to 3a — working days, not calendar days, is the denominator) - `Avg R / Working Day` = `total_cost / working_days` (null-safe; shows `—` if 0 working days) - Ordered by total cost desc - Honours the filter (empty filters = all projects; otherwise only selected projects) ### All Time — Teams (~40% width) Columns: **Team** · **Working Days** · **Total Cost** - `Working Days` = `COUNT(DISTINCT work_log.date)` where `team_id = T` - Ordered by total cost desc ### Year context — removed The current "This Year — Projects / Teams" pair of cards is dropped. The lifetime + selected-period pair already covers the two timeframes that matter; the YTD cards have always been redundant with either. YAGNI. ## 4. Chapter II — Selected Period Keeps all existing content, restructured visually. ### Row A — Summary stat cards (6) Same six cards as today (Total Paid Out · Worker-Days · Loans Issued · Loans Outstanding · Advances Issued · Advances Outstanding). Restyled to match the hero band aesthetic: slightly larger Poppins numbers, thin dividers. Laid out as a single row (6 columns on desktop; 3×2 on tablet; 2×3 on mobile). ### Row B — Payments by Date | Adjustment Summary Two cards side by side, existing content. ### Row C — Labour Cost by Project | Labour Cost by Team Two cards side by side, existing content. No data changes here — just visual polish. Inter tabular-nums for all number columns (perfect right-alignment). ## 5. Chapter III — Worker Breakdown Existing wide table, restyled. No structural change: - One row per worker with `total_paid > 0` - Columns: Worker · Days · Total Paid · {dynamic columns for each non-zero adjustment type} - Ordered by total paid desc - Typography: Inter tabular-nums for right-aligned number columns ## 6. Chapter IV — Team × Project Activity (NEW) A pivot table showing how many days each team worked on each project **in the report's date range + filter scope**. ``` │ Wilkot │ Solar Alpha │ Solar Beta │ Total │ ────────────────┼────────┼─────────────┼────────────┼───────┤ Civils One │ 87 │ — │ — │ 87 │ Team Alpha │ — │ 45 │ 12 │ 57 │ Team Bravo │ 3 │ 18 │ 41 │ 62 │ ────────────────┼────────┼─────────────┼────────────┼───────┤ Total │ 90 │ 63 │ 53 │ 206 │ ``` - **Rows**: teams with ≥1 work log in the period (honours team filter) - **Columns**: projects with ≥1 work log in the period (honours project filter) - **Cell**: `COUNT(DISTINCT work_log.date)` for that (team, project) pair - **Row totals + column totals + grand total** rendered in bold - Zero cells render as em-dash in `--text-tertiary` (not `0` — makes non-zero values stand out) - Horizontally scrollable if > 6 project columns ### Backend New context key `team_project_activity`: ```python { 'columns': [{'id': 1, 'name': 'Wilkot Boerdery'}, ...], # projects in the period 'rows': [ { 'team_id': 3, 'team_name': 'Civils One', 'cells_by_project_id': {1: 87, 2: 0, 3: 0}, # count or 0 'row_total': 87, }, ... ], 'col_totals': {1: 90, 2: 63, 3: 53}, 'grand_total': 206, } ``` Computed via a dedicated `_team_project_activity(work_logs_qs)` helper. ## 7. Print / PDF layout Mirror the HTML structure in `core/templates/core/pdf/report_pdf.html`: - **Hero KPI band at the top** (cover-block style) - **Filter pills as static labels** (no × buttons) - **Single-column** for the body — PDFs look better sequential than side-by-side - Same `@page` config: `A4 portrait; margin: 1.8cm` - Same `_build_report_context` helper — HTML and PDF can't drift ## 8. Edge cases | Case | Behaviour | |---|---| | No filters | Pills show "All Projects · All Teams"; all chapters show everything | | Filter returns zero records | Zero-state banners per section; no 500 | | Choices.js fails to load (CDN blocked) | Native `