38686-vm/docs/plans/2026-05-16-salary-autoscope-picker-design.md
Konrad du Plessis 8f443faebc 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>
2026-05-16 22:12:03 +02:00

7.9 KiB
Raw Permalink Blame History

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 ~20892100) 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 ~21052108) 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 (~2540 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.