docs: design for Pay Salary dashboard quick action

Konrad-approved: a home-dashboard admin Quick Actions tile that
deep-links /payroll/?action=pay-salary and auto-clicks the existing
paySalaryBtn (then strips the param). Reuses all existing machinery;
no view/model/URL change. Rides the same paused-bundle HARD STOP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-05-16 22:52:58 +02:00
parent b397cdf46c
commit fb19655a1d

View File

@ -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". #}
<a href="{% url 'payroll_dashboard' %}?action=pay-salary" class="quick-action">
<i class="fas fa-user-tie"></i>
<span>Pay Salary</span>
</a>
```
(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.** ~2535 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.