38686-vm/docs/plans/2026-05-16-pay-salary-quick-action-design.md
Konrad du Plessis fb19655a1d 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>
2026-05-16 22:52:58 +02:00

169 lines
7.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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