docs: design for Salary auto-scope picker (filter + auto-untick)
Konrad-approved: when Add-Adjustment type=Salary, auto-set the pay-type filter to Managers-only, hide daily rows, and untick any selected daily worker so a Salary can never silently target a daily worker. Pure JS, hooks the toggleProjectField() chokepoint. Rides the same HARD STOP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0d77d7228d
commit
8f443faebc
163
docs/plans/2026-05-16-salary-autoscope-picker-design.md
Normal file
163
docs/plans/2026-05-16-salary-autoscope-picker-design.md
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# Salary Auto-Scope Picker — 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
|
||||||
|
+ pay-type-filter commits (HEAD `0d77d72`). Same logical feature line.
|
||||||
|
**NOT to be pushed/deployed until Konrad confirms it works locally** —
|
||||||
|
rides the same HARD STOP as the rest of the paused work; ships in one
|
||||||
|
bundled push, Konrad's call.
|
||||||
|
|
||||||
|
## Goal (one sentence)
|
||||||
|
|
||||||
|
When the Add-Adjustment modal's adjustment **type** is set to
|
||||||
|
**Salary**, automatically scope the worker picker to managers only —
|
||||||
|
set the pay-type filter to "Managers only", hide daily-worker rows,
|
||||||
|
**and untick any daily worker that was already selected** — so a
|
||||||
|
`Salary` adjustment can never silently be created for a daily worker.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Salary is the manager / fixed-pay adjustment type. CLAUDE.md is
|
||||||
|
explicit: `Salary` is **only** for `Worker(pay_type='fixed')`; a daily
|
||||||
|
worker must never receive a `Salary` adjustment, and "accuracy is
|
||||||
|
critical" for this payroll system. The pay-type filter shipped in the
|
||||||
|
previous task only *hides* rows — a daily worker ticked *before*
|
||||||
|
switching the type to Salary would stay checked-but-hidden and POST a
|
||||||
|
bad `Salary` adjustment. Auto-unticking non-managers on entry to Salary
|
||||||
|
closes that hole at the UI.
|
||||||
|
|
||||||
|
## Decision (from the brainstorm)
|
||||||
|
|
||||||
|
Konrad chose **"Filter + auto-untick"** over plain display-only filter
|
||||||
|
(B) and over a hard lock (C). Rationale: it removes the *silent*
|
||||||
|
mis-payment footgun without locking the UI; a deliberate manual
|
||||||
|
override (switch filter back to "All", re-tick a daily worker) is still
|
||||||
|
possible — a conscious act, consistent with the reversible,
|
||||||
|
display-only philosophy of the rest of the pay-type filter.
|
||||||
|
|
||||||
|
## Behaviour (single rule, both directions)
|
||||||
|
|
||||||
|
- **Type → `Salary`:**
|
||||||
|
1. `addAdjPayTypeFilter.value = 'fixed'`.
|
||||||
|
2. For every `.add-adj-worker` checkbox: read its `.form-check`
|
||||||
|
wrapper's `data-pay-type`. If it is **not** `'fixed'` → hide the
|
||||||
|
row (`style.display = 'none'`) **and** uncheck it
|
||||||
|
(`cb.checked = false`). If it **is** `'fixed'` → show the row
|
||||||
|
(`style.display = ''`); leave its checked state untouched (a
|
||||||
|
pre-ticked manager stays ticked).
|
||||||
|
3. Call the existing `updateWorkerCount()` so the "X worker(s)
|
||||||
|
selected" counter reflects any auto-untick.
|
||||||
|
- **Type → anything else** (Bonus / Deduction / New Loan / …):
|
||||||
|
1. `addAdjPayTypeFilter.value = ''`.
|
||||||
|
2. Show every row (`style.display = ''`).
|
||||||
|
3. Do **not** alter any checkbox state — daily workers that Salary
|
||||||
|
auto-unticked are **not** auto-re-ticked. The user re-selects
|
||||||
|
deliberately. (No hidden "remember what I unticked" state — keeps
|
||||||
|
the behaviour predictable.)
|
||||||
|
|
||||||
|
## Where it hooks (DRY — one chokepoint)
|
||||||
|
|
||||||
|
`toggleProjectField()` in `core/templates/core/payroll_dashboard.html`
|
||||||
|
is the single function that already runs on:
|
||||||
|
- every manual `#addAdjType` change (bound listener),
|
||||||
|
- init (called once at setup),
|
||||||
|
- the **Pay Salary** button (`paySalaryBtn` sets
|
||||||
|
`addAdjType.value='Salary'` then calls `toggleProjectField()`),
|
||||||
|
- the header-open reset (sets `addAdjType.value='Bonus'` then calls
|
||||||
|
`toggleProjectField()`).
|
||||||
|
|
||||||
|
Add the filter+untick sync logic at the **end** of
|
||||||
|
`toggleProjectField()` (or as a small helper invoked there). This makes
|
||||||
|
all four paths correct with zero new event wiring.
|
||||||
|
|
||||||
|
### One extra line for the Pay Salary path
|
||||||
|
|
||||||
|
The `show.bs.modal` reset (commit `18ec393`, lines ~2089–2100) runs
|
||||||
|
*after* the Pay-Salary button's pre-show `toggleProjectField()` call
|
||||||
|
and resets the pay-type filter to `''` + shows all rows, then
|
||||||
|
**early-returns** on `_paySalaryOpen` (lines ~2105–2108) before any
|
||||||
|
re-apply. So the managers-only scope set by the button would be wiped.
|
||||||
|
|
||||||
|
Fix: in the `_paySalaryOpen` branch, call `toggleProjectField()` once
|
||||||
|
**before** `return`. `addAdjType.value` is already `'Salary'` there, so
|
||||||
|
this idempotently re-applies the managers-only filter+untick *and*
|
||||||
|
re-affirms the project / Pay-Immediately visibility that
|
||||||
|
`toggleProjectField()` already manages. No behavioural change for that
|
||||||
|
branch beyond restoring the intended scope.
|
||||||
|
|
||||||
|
The `_quickAdjustOpen` branch is **left untouched**: quick-adjust never
|
||||||
|
selects `Salary`, and the reset block already leaves it all-visible —
|
||||||
|
adding a call there is unnecessary and risks regressing the `18ec393`
|
||||||
|
quick-adjust-visibility fix. YAGNI.
|
||||||
|
|
||||||
|
## Known, accepted boundary
|
||||||
|
|
||||||
|
While type = `Salary`, the user can still manually switch the pay-type
|
||||||
|
`<select>` back to "All": the existing `#addAdjPayTypeFilter` change
|
||||||
|
handler reveals the daily rows again (unticked, because Salary cleared
|
||||||
|
them). Manually re-ticking a daily worker and submitting a `Salary` for
|
||||||
|
them is then a **deliberate** act, not an accident. This is the
|
||||||
|
explicit consequence of choosing "filter + auto-untick" over "lock" —
|
||||||
|
the silent footgun is removed; a conscious override is not hard-blocked.
|
||||||
|
Documented here so it is a known design boundary, not a latent bug.
|
||||||
|
|
||||||
|
## Edge cases
|
||||||
|
|
||||||
|
| Scenario | Result |
|
||||||
|
|---|---|
|
||||||
|
| Open (header) → Bonus, tick a daily worker, switch to Salary | Daily rows hide, that tick clears, counter decrements, filter shows "Managers only". |
|
||||||
|
| Pre-ticked manager, switch to Salary | Manager row stays visible & ticked. |
|
||||||
|
| Salary → Bonus | All rows reappear; nothing re-ticked; filter back to "All". |
|
||||||
|
| Pay Salary button | Modal opens type=Salary, filter="Managers only", only managers visible (re-applied after the show.bs.modal reset). |
|
||||||
|
| Quick-adjust button | Unchanged — pre-checked worker visible (type is non-Salary; `18ec393` behaviour preserved). |
|
||||||
|
| Manual filter→"All" while type=Salary | Daily rows reappear **unticked** (accepted boundary above). |
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
This is **pure client-side JS** with no view / server / template-data
|
||||||
|
surface, so — exactly like the Task 3 toggle — it is verified by
|
||||||
|
Konrad's manual local checklist, not a Django unit test (the Task 3
|
||||||
|
plan established and documented this precedent). The automated
|
||||||
|
regression gate is the full suite staying **207/207 OK** (the JS
|
||||||
|
addition must not break template rendering).
|
||||||
|
|
||||||
|
Manual checklist additions (append to verification):
|
||||||
|
1. Header-open → Bonus, all workers shown. Tick a daily worker.
|
||||||
|
Switch type → **Salary**: daily rows hide, the daily tick clears,
|
||||||
|
counter updates, pay-type select reads "Managers only".
|
||||||
|
2. Tick a manager, switch **Salary → Bonus**: all rows reappear,
|
||||||
|
the manager stays ticked, pay-type select back to "All pay types".
|
||||||
|
3. Click **Pay Salary**: modal opens with type=Salary, only managers
|
||||||
|
listed, pay-type select = "Managers only".
|
||||||
|
4. While type=Salary, manually set the pay-type select to "All pay
|
||||||
|
types": daily rows reappear and are **unticked**.
|
||||||
|
|
||||||
|
## Files touched
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `core/templates/core/payroll_dashboard.html` | Extend `toggleProjectField()` with the Salary filter+untick sync (~25–40 LOC); add 1 `toggleProjectField()` call in the `_paySalaryOpen` branch before `return` |
|
||||||
|
| `docs/plans/parked-work.md` | One line: the paused entry now also covers the Salary auto-scope behaviour |
|
||||||
|
| `CLAUDE.md` | One line under "Manager / Salaried pay": type=Salary auto-applies Managers-only filter + unticks non-managers (UI guard for the Salary=`fixed` invariant) |
|
||||||
|
|
||||||
|
**No model / migration / view / URL / dependency changes.**
|
||||||
|
|
||||||
|
## Out of scope (deliberately)
|
||||||
|
|
||||||
|
- No hard lock / disabling of the pay-type `<select>` (option C — not
|
||||||
|
chosen).
|
||||||
|
- No server-side enforcement that `Salary` targets only
|
||||||
|
`pay_type='fixed'` (that is a separate, larger hardening; this design
|
||||||
|
is a UI safety improvement only — the existing server behaviour is
|
||||||
|
unchanged).
|
||||||
|
- No "remember and restore the daily workers I auto-unticked" feature
|
||||||
|
(rejected as hidden state; YAGNI).
|
||||||
|
- No change to the `_quickAdjustOpen` path.
|
||||||
|
|
||||||
|
## Branch / deploy
|
||||||
|
|
||||||
|
Build on `ai-dev` on top of `0d77d72`. **Do NOT push or deploy** until
|
||||||
|
Konrad has run the local verification and explicitly approves. Ships
|
||||||
|
bundled with the rest of the paused Manager/Salaried + pay-type-filter
|
||||||
|
work in a single push, Konrad's call.
|
||||||
Loading…
x
Reference in New Issue
Block a user