# 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.