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

7.4 KiB
Raw Permalink Blame History

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:

{# === 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):

    // === 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.