diff --git a/docs/plans/2026-05-16-pay-salary-quick-action-design.md b/docs/plans/2026-05-16-pay-salary-quick-action-design.md new file mode 100644 index 0000000..cfd8afd --- /dev/null +++ b/docs/plans/2026-05-16-pay-salary-quick-action-design.md @@ -0,0 +1,168 @@ +# Pay Salary Quick Action — 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 + Salary-auto-scope commits (HEAD `b397cdf`). Same +logical feature line. +**NOT to be pushed/deployed until Konrad confirms it works locally** — +rides the same HARD STOP; ships in the one bundled push, Konrad's call. + +## Goal (one sentence) + +Add a "Pay Salary" tile to the home dashboard's admin Quick Actions row +that deep-links to `/payroll/` and auto-opens the existing Pay Salary +modal, so paying a manager is one click from the dashboard. + +## Why + +Managers are paid via the `Salary` adjustment through the Pay Salary +button on `/payroll/`. From the home dashboard that's currently a +multi-step path (go to payroll → find the button). The Quick Actions +row already has one-click shortcuts (Log Work, Log Absence, Run Payroll, +…); a "Pay Salary" shortcut completes the set for the manager-pay +workflow and mirrors the Log-Absence shortcut pattern added earlier. + +## Decision (from the brainstorm) + +Konrad chose **"Open the Pay Salary modal directly"** over a plain link +to `/payroll/`. The whole point of a Quick Action is to jump straight +into the task (matching how Log Absence jumps to its form), so the tile +must auto-open the modal, not just land on the payroll page. + +## Approach (chosen) + +Reuse the existing `paySalaryBtn` machinery entirely — the tile is a +plain link carrying a query param; a small JS hook on the payroll page +*triggers the existing button*. No behaviour is duplicated. + +Considered + rejected: +- **Plain link to `/payroll/`** — 2-click, not a real "quick" action + (rejected by Konrad). +- **URL hash `#pay-salary`** — works but `payroll_dashboard` already + speaks query params (`?status=`), so a param is more idiomatic here + and easier to strip cleanly with `history.replaceState`. + +## § 1 — Home dashboard tile + +`core/templates/core/index.html`, inside the **admin** Quick Actions +card (the card is within the `{% if is_admin %}` branch, so the tile is +automatically admin-only — no new gating). Place it immediately AFTER +the existing "Run Payroll" tile so payroll actions stay grouped: + +```html +{# === PAY SALARY — quick path: opens the Pay-Salary modal on /payroll/ === #} +{# Same fa-user-tie icon as the payroll dashboard's Pay Salary button so #} +{# users have one mental model for "salary = fa-user-tie". #} + + + Pay Salary + +``` + +(Each `{# #}` comment is single-line — CLAUDE.md multi-line-comment +gotcha respected.) + +## § 2 — Auto-open hook + +`core/templates/core/payroll_dashboard.html`, JS, placed immediately +AFTER the existing `if (paySalaryBtn) { … }` block (so `paySalaryBtn` +is declared and its click listener attached before we trigger it): + +```javascript + // === Quick-action deep-link: /payroll/?action=pay-salary === + // The home dashboard "Pay Salary" Quick Action links here with this + // param. Auto-click the existing Pay Salary button (which does the + // clean-slate + type=Salary + managers-only scoping + modal open), + // then strip the param so a manual refresh or Back doesn't re-pop + // the modal. Best-effort — never let a deep-link quirk block the page. + try { + var _qsAction = new URLSearchParams(window.location.search).get('action'); + if (_qsAction === 'pay-salary' && paySalaryBtn) { + paySalaryBtn.click(); + var _u = new URL(window.location.href); + _u.searchParams.delete('action'); + window.history.replaceState({}, '', _u.pathname + _u.search + _u.hash); + } + } catch (e) { /* deep-link is best-effort; never block the page */ } +``` + +Why this is safe: +- `paySalaryBtn.click()` runs the **single source of truth** for Pay + Salary (clean worker slate, type=Salary, `toggleProjectField()` → + managers-only auto-scope, `modal.show()`). Zero duplicated logic; the + deep-link can never drift from the on-page button. +- `payroll_dashboard` (the view) never reads `?action=` — the param is + inert server-side. No view change, no security surface. +- `history.replaceState` strips the param so F5 / browser-Back does not + silently re-pop the modal (classic deep-link footgun). +- `try/catch` guarantees a malformed URL can never break page JS. + +## § 3 — Permissions + +No new gating. The home tile lives in the `{% if is_admin %}` branch; +`/payroll/` is admin-only server-side regardless. Supervisors get the +`{% else %}` home branch and never see the tile. + +## § 4 — Testing + +- **One Django render test** (`core/tests.py`): as an admin, GET `/` + contains the Pay Salary quick-action link with + `href="…?action=pay-salary"`; and GET `/payroll/?action=pay-salary` + returns HTTP 200 (proves the param is inert server-side). Optionally + assert a non-admin's `/` does NOT contain the tile (it's in the admin + branch) if a cheap non-admin fixture is already available. +- **The auto-click itself is client-side JS** with no server surface → + verified by Konrad's manual checklist, not a Django test (same + approved precedent as the Task-3 toggle and the Salary auto-scope). +- **Regression gate:** full suite goes 207 → **208 OK** (the one new + render test); nothing else changes. + +## Files touched + +| File | Change | +|---|---| +| `core/templates/core/index.html` | +1 `quick-action` tile (admin Quick Actions card, after "Run Payroll") | +| `core/templates/core/payroll_dashboard.html` | +~10 lines JS deep-link hook after the `paySalaryBtn` block | +| `core/tests.py` | +1 render test (tile present + param inert) | +| `docs/plans/parked-work.md` | One line: paused entry also covers the Pay Salary quick action | +| `CLAUDE.md` | One line under "Manager / Salaried pay": the `?action=pay-salary` deep-link | + +**No model / migration / view / URL / dependency change.** ~25–35 LOC +incl. test. + +## Out of scope (deliberately) + +- No new view, URL route, or server-side handling of `?action=` + (kept inert — YAGNI). +- No change to the Pay Salary button, the Salary adjustment logic, or + the manager auto-scope (all reused as-is). +- No supervisor-facing Pay Salary entry (admin-only by design). +- No generic `?action=` framework for other modals (YAGNI — one tile). + +## Manual verification (local — Konrad) + +1. From `/` as admin: the **Pay Salary** tile appears in Quick Actions + (next to Run Payroll), `fa-user-tie` icon. +2. Click it → lands on `/payroll/` with the Pay Salary modal **already + open**, type = Salary, picker auto-scoped to managers only, no + workers pre-ticked. +3. The browser URL shows a clean `/payroll/` (the `?action=pay-salary` + param has been stripped). +4. Refresh the page → the modal does **NOT** re-open (param gone). +5. The existing on-page **Pay Salary** button still works exactly as + before. +6. Log in as a non-admin supervisor → `/` does **not** show the Pay + Salary tile (admin-only branch). +7. Full suite green (`USE_SQLITE=true … manage.py test core.tests`) — + expect 208 OK. + +Then — and only then — Konrad decides whether to push (bundled with the +rest of the paused Manager/Salaried + pay-type-filter + Salary-auto-scope +work, in one push, his call). + +## Branch / deploy + +Build on `ai-dev` on top of `b397cdf`. **Do NOT push or deploy** until +Konrad has run the local verification and explicitly approves. Ships in +one bundled push with the rest of the paused work.