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

261 lines
12 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 — 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):**
```bash
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.
---
### Task 1: Tile + deep-link hook + render test
**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):
```python
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**
```bash
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):
```html
<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' %}" …>`):
```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>
```
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:
```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 */ }
```
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**
```bash
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**
```bash
USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests.PaySalaryQuickActionTests -v 2
```
Expected: 2 tests OK.
Then full suite:
```bash
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)**
```bash
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)**
```bash
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**
```bash
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)**
```bash
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).