diff --git a/docs/plans/2026-05-16-salary-autoscope-picker-design.md b/docs/plans/2026-05-16-salary-autoscope-picker-design.md new file mode 100644 index 0000000..49079a7 --- /dev/null +++ b/docs/plans/2026-05-16-salary-autoscope-picker-design.md @@ -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 +`` (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.