diff --git a/docs/plans/2026-04-23-executive-report-v2-design.md b/docs/plans/2026-04-23-executive-report-v2-design.md new file mode 100644 index 0000000..7019d51 --- /dev/null +++ b/docs/plans/2026-04-23-executive-report-v2-design.md @@ -0,0 +1,270 @@ +# 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 `