38686-vm/docs/plans/2026-05-16-pay-salary-quick-action-plan.md
Konrad du Plessis 56c10ab938 docs: TDD plan for Pay Salary quick action (2 tasks, HARD STOP)
Task 1: tile + deep-link hook + render test (TDD on the Django-render
part; auto-click is JS/manual-checklist). Task 2: docs. Suite 207->208.
Nothing pushed until Konrad's local verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:54:30 +02:00

12 KiB
Raw Permalink Blame History

Pay Salary Quick Action — Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task (in-session, fresh subagent + 2-stage review per task). Each subagent uses superpowers:test-driven-development for the Django-render portion.

Goal: Add a "Pay Salary" tile to the home dashboard's admin Quick Actions row that deep-links to /payroll/?action=pay-salary; the payroll page auto-clicks the existing Pay Salary button (clean slate → type=Salary → managers-only scope → modal open), then strips the param.

Architecture: A plain <a> tile (admin-only branch) + a ~10-line client-side deep-link hook that triggers the existing paySalaryBtn. No duplicated behaviour, no view/model/URL change; ?action= is inert server-side.

Tech Stack: Django 5.2.7 templates + vanilla JS; SQLite local (USE_SQLITE=true); Bootstrap 5.

Design doc: docs/plans/2026-05-16-pay-salary-quick-action-design.md (commit fb19655).

Branch / baseline: ai-dev, HEAD fb19655, 207/207 tests passing. On top of the paused, un-pushed Manager/Salaried + pay-type-filter + Salary-auto-scope commits.

Test command (Git Bash, per CLAUDE.md):

USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2

TDD note. Task 1's Django-render behaviour (tile present, param inert) is unit-testable — do real TDD there (failing test first). The auto-click itself is pure browser JS with no server surface, so — per the approved precedent (Task-3 toggle, Salary auto-scope) — it is verified by Konrad's manual checklist, NOT a Django test. Do not add a JS-source-sniffing test.

HARD STOP after Task 2. Do NOT git push, do NOT deploy. Hand back to Konrad for the manual checklist. Ships bundled with the rest of the paused work in ONE push, on his explicit say-so only.


Files:

  • Modify: core/templates/core/index.html — admin Quick Actions card (the "Run Payroll" tile is currently lines 208211)
  • Modify: core/templates/core/payroll_dashboard.html — after the if (paySalaryBtn) { … } block (currently lines 21012117; insert before line 2119)
  • Test: core/tests.py — new class PaySalaryQuickActionTests, inserted immediately before class WorkHistoryTeamFilterTests

Step 1: Write the failing test

Insert this class immediately before class WorkHistoryTeamFilterTests(TestCase): in core/tests.py (reuse module-level User/TestCase/reverse already imported by neighbouring classes — do not add imports if present):

class PaySalaryQuickActionTests(TestCase):
    """Home dashboard 'Pay Salary' Quick Action: an admin sees a tile
    that deep-links /payroll/?action=pay-salary; the param is inert
    server-side (no view change). The auto-open click is client-side
    JS, verified by manual checklist (not asserted here)."""

    @classmethod
    def setUpTestData(cls):
        cls.admin = User.objects.create_user(
            username='psqa_admin', password='pw',
            is_staff=True, is_superuser=True,
        )

    def setUp(self):
        self.client.force_login(self.admin)

    def test_home_has_pay_salary_quick_action(self):
        resp = self.client.get('/')
        self.assertEqual(resp.status_code, 200)
        # The tile links to the payroll dashboard with the deep-link param.
        self.assertContains(resp, '?action=pay-salary')
        # And is labelled "Pay Salary".
        self.assertContains(resp, 'Pay Salary')

    def test_payroll_dashboard_ignores_action_param(self):
        # The param is purely a client-side signal; the view must not
        # care about it — same 200 as a plain /payroll/ load.
        resp = self.client.get('/payroll/?action=pay-salary')
        self.assertEqual(resp.status_code, 200)

Step 2: Run the tests, verify they FAIL

USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests.PaySalaryQuickActionTests -v 2

Expected: test_home_has_pay_salary_quick_action FAILS (no ?action=pay-salary in /). test_payroll_dashboard_ignores_action_param will likely already PASS (param already inert) — that's fine; it's a guard that must stay green.

Step 3: Add the home dashboard tile

In core/templates/core/index.html, the "Run Payroll" tile currently is (lines 208211):

                <a href="{% url 'payroll_dashboard' %}" class="quick-action">
                    <i class="fas fa-money-check-alt"></i>
                    <span>Run Payroll</span>
                </a>

Insert the new tile IMMEDIATELY AFTER that closing </a> (line 211) and before the "View History" tile (<a href="{% url 'work_history' %}" …>):

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

Match the file's existing indentation (16 spaces for the <a>). Each {# #} comment is a single self-contained line — do NOT let any {# span lines.

Step 4: Add the deep-link hook

In core/templates/core/payroll_dashboard.html, the paySalaryBtn wiring ends at line 2117 ( } closing if (paySalaryBtn) {), followed by a blank line 2118 and the header-button comment at line 2119. Insert this block on the blank line AFTER line 2117 and BEFORE the // When the modal is opened from the HEADER button … comment:

    // === 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 */ }

Notes: paySalaryBtn is the var declared at line 2100 — in scope here. It must run AFTER its addEventListener (line 2102) so the click has a handler — this placement (after line 2117) guarantees that. Do NOT modify the paySalaryBtn click handler or any modal-reset logic.

Step 5: Guard greps

grep -rn "^\s*{#" core/templates/core/index.html | awk -F: '$0 !~ /#}/ {print}'
grep -rn "^\s*{#" core/templates/core/payroll_dashboard.html | awk -F: '$0 !~ /#}/ {print}'
grep -c 'id="paySalaryBtn"' core/templates/core/payroll_dashboard.html
grep -c 'action=pay-salary' core/templates/core/index.html

Expected: first two → no output; third → 1 (button id still unique, untouched); fourth → 1 (one new tile).

Step 6: Run tests, verify PASS

USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests.PaySalaryQuickActionTests -v 2

Expected: 2 tests OK.

Then full suite:

USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2

Expected: 208 tests OK (207 baseline + 1 new render test; the param-inert test was already-green and stays green). If anything unrelated fails, STOP and report.

Step 7: Commit (local only, NO push, NEW commit)

git add core/templates/core/index.html core/templates/core/payroll_dashboard.html core/tests.py
git commit -m "feat: Pay Salary quick action on home dashboard (deep-link to modal)

Admin Quick Actions tile → /payroll/?action=pay-salary; the payroll
page auto-clicks the existing paySalaryBtn then strips the param.
Reuses all existing Pay-Salary machinery; param inert server-side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"

Self-review: git show HEAD — exactly 3 files; the tile is inside the admin Quick Actions card (between Run Payroll and View History); the hook is after the if (paySalaryBtn){} block (not inside it); no change to the paySalaryBtn handler or modal resets; greps clean; suite 208.


Task 2: Docs + final regression + HARD STOP

Files:

  • Modify: docs/plans/parked-work.md
  • Modify: CLAUDE.md

Step 1: Full regression (anchor)

USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2

Expected: 208 OK. If not, STOP.

Step 2: Update docs/plans/parked-work.md

Read the file; find the paused Manager/Salaried entry (under "⏸ Paused … awaiting Konrad's verification"; already carries the pay-type-filter and Salary-auto-scope notes). Append, matching the file's prose style:

Also now: a home-dashboard admin Quick Actions tile "Pay Salary" deep-links /payroll/?action=pay-salary and auto-opens the existing Pay Salary modal (param stripped after). Design docs/plans/2026-05-16-pay-salary-quick-action-design.md, plan docs/plans/2026-05-16-pay-salary-quick-action-plan.md. Same HARD STOP — bundled into the one push on Konrad's say-so.

(If no such entry exists, add a short one under "⏸ Paused" matching the others' format. Report which case.)

Step 3: Update CLAUDE.md

In the "## Manager / Salaried pay (May 2026)" section, after the existing "Salary picker safety:" line, append one line in the same terse style:

Pay Salary quick action: the home dashboard's admin Quick Actions row has a "Pay Salary" tile linking /payroll/?action=pay-salary; payroll_dashboard.html JS auto-clicks the existing paySalaryBtn on load when that param is present, then strips it via history.replaceState (no re-pop on refresh). ?action= is inert server-side — no view/URL change.

Step 4: Verify docs

grep -n "action=pay-salary\|Pay Salary quick action\|Pay Salary" docs/plans/parked-work.md CLAUDE.md

Expected: new lines present in both files; Markdown intact.

Step 5: Commit (local only, NO push, NEW commit)

git add docs/plans/parked-work.md CLAUDE.md
git commit -m "docs: note Pay Salary quick action (rides paused bundle)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"

Self-review: git show --stat HEAD — only the 2 doc files, additions only.


HARD STOP — hand back to Konrad

After Task 2's commit:

  1. git status clean; git log --oneline -3 shows the 2 new commits on ai-dev on top of fb19655.
  2. Full suite once more → 208/208 OK.
  3. Do NOT git push. Do NOT deploy. Report the commit list + test count and point Konrad at the Manual verification section of docs/plans/2026-05-16-pay-salary-quick-action-design.md (7 steps).
  4. Push/deploy only on Konrad's explicit approval, bundled with the rest of the paused Manager/Salaried + pay-type-filter + Salary-auto-scope work in ONE push (github + gitea; deploy order: pull → migrate → collectstatic → restart last).

Notes

  • DRY: the tile just triggers the existing paySalaryBtn; all real behaviour stays in one place.
  • YAGNI: no view/URL/server handling of ?action=; no generic deep-link framework; no supervisor entry.
  • Why the hook sits after if (paySalaryBtn){}: the click handler must be attached before we synthesise a click; this placement guarantees ordering and keeps paySalaryBtn in scope.
  • No migration / view / URL change; only one new Django render test by design (the auto-click is JS, manual-checklist verified — documented precedent).