docs: design for Managers pay-type filter (Approach A, display-only)

Konrad-approved design for a display-only ?pay_type= filter on /workers/
and a "Managers only" toggle on the Add-Adjustment modal picker. No
model/migration/URL changes; rides with the paused Manager/Salaried
feature's HARD STOP (nothing pushed until local verification).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-05-16 13:13:30 +02:00
parent 4d06b83e30
commit 4aac2c1cf2

View File

@ -0,0 +1,147 @@
# Managers Pay-Type Filter — Design
**Date:** 16 May 2026
**Status:** Approved by Konrad on 16 May 2026; ready for implementation plan.
**Branch:** `ai-dev`, on top of the **paused, un-pushed** Manager/Salaried
commits (HEAD `4d06b83`). Same logical feature.
**NOT to be pushed/deployed until Konrad confirms it works locally** —
this rides with the paused Manager/Salaried feature's HARD STOP.
## Goal (one sentence)
Make `Worker(pay_type='fixed')` managers fast to find in the two places
that matter — the `/workers/` list and the Add-Adjustment modal's worker
picker — via a pure display-only `pay_type` filter that touches no model,
migration, URL, or money math.
## Why
Managers are `Worker(pay_type='fixed')` rows (Path-A — no separate model).
They deliberately stay mixed into the normal worker list and stay payable
in the Add-Adjustment modal (the documented critical invariant). But
there is currently no way to *narrow* either view to just managers, so
paying Fitz's salary means scrolling/searching the full roster. A
lightweight filter — mirroring the existing status/team filters — solves
this with the lowest possible risk.
## Decision (from the brainstorm)
Konrad chose **Approach A — `pay_type` filter** over a real "Managers
team" (B) or both. Rationale: A works regardless of whether team
membership is kept up to date, needs no new Team, and is the smallest
possible change. A Managers team can still be created manually later as
an organisational habit — nothing here precludes it.
## § 1 — `/workers/` list filter
- **`core/views.py::worker_list`** — add one GET param `?pay_type=`,
handled exactly like the existing `status`/`team` params:
- `pay_type=daily``workers = workers.filter(pay_type='daily')`
- `pay_type=fixed``workers = workers.filter(pay_type='fixed')`
- absent / anything else → no filter (existing behaviour unchanged)
- Add `pay_type_filter` to the template context.
- **`core/templates/core/workers/list.html`** — one more `<select>` in
the existing filter row, styled identically to the status/team
dropdowns: **All pay types / Daily workers / Managers (Salaried)**.
Values are the DB strings (`""` / `daily` / `fixed`) per the Path-A
convention (UI label "Managers (Salaried)", DB value `fixed`).
## § 2 — Add-Adjustment modal "Managers only" toggle
The picker (in `core/templates/core/payroll_dashboard.html`, ~line 1086)
is a scrollable list of `.form-check` rows built from the `all_workers`
context queryset, with an existing "Quick select by team…" `<select>`
and Select-All/Clear links in a filter row at ~line 1075.
- **Worker rows (~line 1087)** — add `data-pay-type="{{ w.pay_type }}"`
to each `.form-check` wrapper. `all_workers` is a `Worker` queryset so
`w.pay_type` is already available — **no view change**.
- **Filter row (~line 1075)** — add a small
`<select id="addAdjPayTypeFilter" class="form-select form-select-sm">`
next to the team quick-select: **All / Daily only / Managers only**.
- **One small JS handler** — on `change`, iterate `.add-adj-worker`
rows and toggle `display` on the wrapping `.form-check` by
`data-pay-type`. "All" clears the filter (shows every row).
### Why this is safe
- `all_workers` is **not** narrowed server-side, so managers remain
payable — the critical Manager/Salaried invariant is untouched. The
toggle only hides rows client-side and is fully reversible to "All".
- The checkbox `name="workers"` value remains the **source of truth**
for what is submitted; `data-pay-type` is presentational only (it
drives visibility, not selection state) — consistent with the
CLAUDE.md "read state from the input value, not data attributes"
gotcha (that gotcha is about *state*; visibility is fine on a
data-attr).
- Reuses the exact pattern of the existing batch-pay / pending-table
client-side team+loan filters (a `<select>` toggling row `display`
by a `data-*` attribute) — no new mechanism, no new failure modes.
## § 3 — Tests (~4, in `core/tests.py`)
1. `/workers/?pay_type=fixed` → context `workers` contains only the
manager, not the daily worker.
2. `/workers/?pay_type=daily` → context `workers` contains only the
daily worker, not the manager.
3. `/workers/` (no `pay_type` param) → both appear (regression — the
default behaviour must not change).
4. Add-Adjustment modal (payroll dashboard render) still includes a
manager in `all_workers` **and** the rendered manager row carries
`data-pay-type="fixed"` (regression on the must-stay-payable
invariant + the new attribute is present).
Confirm any existing `/workers/` list test still passes unchanged
(the no-param path is deliberately untouched).
## Files touched
| File | Change |
|---|---|
| `core/views.py::worker_list` | +`pay_type` GET param branch, +context key |
| `core/templates/core/workers/list.html` | +1 filter `<select>` |
| `core/templates/core/payroll_dashboard.html` | +`data-pay-type` on worker rows, +1 filter `<select>`, +1 small JS handler |
| `core/tests.py` | +4 tests |
| `docs/plans/parked-work.md` | Update the paused Manager/Salaried entry to note this filter rides with it |
| `CLAUDE.md` | One line under the Manager/Salaried section: `/workers/?pay_type=fixed` filter + modal toggle exist (display-only) |
**No model / migration / URL / dependency changes.** ~80120 LOC incl.
tests.
## Out of scope (deliberately)
- No "Managers team" creation (Approach B explicitly not chosen).
- No change to salary-payment logic, the `Salary` adjustment, or any
report/cost math.
- No server-side narrowing of the modal's `all_workers` (would break
the must-stay-payable invariant).
- No `pay_type` filter on `/history/`, `/absences/`, etc. — managers
can't reach those records anyway (they're excluded from attendance
and absence pickers).
## Verification (manual, local — Konrad)
1. `/workers/` → new "pay type" dropdown defaults to "All pay types";
list looks exactly as before.
2. Select **Managers (Salaried)** → only managers (e.g. Fitz) shown;
URL is `/workers/?pay_type=fixed`. Combine with a team or status
filter → both narrow together.
3. Select **Daily workers** → managers disappear; daily workers remain.
4. Payroll dashboard → **Add Adjustment** modal → new pay-type
`<select>` defaults to "All"; full list shown.
5. Choose **Managers only** → picker shows only managers; pick Fitz,
type = Salary, complete the payment as before (logic unchanged).
6. Switch back to **All** → every worker visible again; selections you
already ticked are preserved.
7. Full suite green (`USE_SQLITE=true … manage.py test core.tests`).
Then — and only then — Konrad decides whether to push to `ai-dev` +
deploy (together with the rest of the paused Manager/Salaried feature).
## Branch / deploy
Build on `ai-dev` on top of `4d06b83`. **Do NOT push to origin or
deploy** until Konrad has run the local verification above and
explicitly approves. This is part of the daily-use payroll path —
hands-on local check before production, same as the rest of the
Manager/Salaried feature it ships with.