docs(adjustments): Shipped block on design doc + CLAUDE.md URL routes

Captures the 11-task implementation, 5 deviations (biggest: the
CP1 pivot from Choices.js chip-multiselect to popover-checkbox
filter UX after Konrad flagged the chip pattern as intrusive),
14 new adjustments-tab tests, and total code churn (~+1400 lines).

CLAUDE.md URL Routes table gains two rows so future sessions
surface /payroll/?status=adjustments and the bulk-delete endpoint.

Feature ready for final whole-feature code review + batched push.
This commit is contained in:
Konrad du Plessis 2026-04-23 19:26:46 +02:00
parent 9bb9ede300
commit 269d86259a
2 changed files with 114 additions and 0 deletions

View File

@ -179,6 +179,8 @@ USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2
| `/projects/report/csv/` | `project_batch_report_csv` | Admin: project batch report as CSV download |
| `/toggle/<model>/<id>/` | `toggle_active` | Admin: AJAX toggle active status |
| `/payroll/` | `payroll_dashboard` | Admin: pending payments, loans, charts |
| `/payroll/?status=adjustments` | `payroll_dashboard` | Admin: browse ALL payroll adjustments (filter by type, worker, team, status, date; group-by type/worker; bulk-delete unpaid; row actions open existing modals) |
| `/payroll/adjustments/bulk-delete/` | `bulk_delete_adjustments` | Admin: POST-only; delete multiple unpaid adjustments in one shot via fetch() with X-CSRFToken cookie |
| `/payroll/pay/<worker_id>/` | `process_payment` | Admin: process payment (atomic) |
| `/payroll/price-overtime/` | `price_overtime` | Admin: AJAX price unpriced OT entries |
| `/payroll/adjustment/add/` | `add_adjustment` | Admin: create adjustment |

View File

@ -555,3 +555,115 @@ Hand off to `superpowers:writing-plans`. Two design docs exist today:
- `docs/plans/2026-04-23-adjustments-tab-design.md` (this doc — Feature 2)
Recommended sequence: **Feature 1 first** (smaller, ~5-6 tasks; Choices.js patterns learned here can lift into Feature 2). Ship Feature 1, validate on production, then Feature 2's plan + implementation. Both design docs stay local until their respective implementations ship; then push everything together.
---
## 19. Shipped — 2026-04-23
Implementation complete. 11 tasks + 1 hard-pause checkpoint + 1 round of
Konrad feedback fixes. 65 tests passing (up from 47 pre-feature).
### Commit map
| Task | Commits | Scope |
|------|---------|-------|
| 1 | `97d8a69` | `type_slug` template filter (+ tests) |
| 2 | `a20a025` | CSS badge palette + foundational styles |
| 3 | `10d381e`, `89f109a` | Backend filter branch + stats; strengthened subquery test |
| 4 | `b450bd3`, `06b3315` | Tab markup + filter bar + flat table; pagination / a11y / N+1 fixes |
| 4* | `e088192`, `4c1cdb6` | Two multi-line `{# #}` comment hotfixes — see Deviations #2 |
| CP1 A | `b59eb31` | Row actions → modals + project link → History tab |
| CP1 B | `4f15e4b` | **Replaced Choices.js chip-multiselect with popover-checkbox filter UX** — see Deviations #1 |
| 5 | `0862805`, `e5d06f9` | Group-by type/worker + toggle + colour-accented headers; chevron + ordering polish |
| 6 | `03f177e`, `5f2e6d8`, `4c3e90f` | Bulk-delete endpoint; id-collision fix; **cascade logic fix** — see Deviations #3 |
| 7 | `6905703` | Team → Workers cross-filter |
| 8 | `c851b49` | Date picker single/range toggle + preset buttons |
| 9 | `7b71048` | Sortable column headers with URL state |
| 10 | `9bb9ede` | Empty-state card with recovery CTAs |
### Deviations from the original design
1. **Choices.js chip-multiselect → popover-checkbox filters.** The original
design (§3) specified Choices.js for Type/Workers/Teams multi-selects —
the same pattern used in the report page's retired modal. At Checkpoint 1
Konrad flagged that the chip-style rendering was intrusive once multiple
options were selected, dominating the filter bar. We replaced the
Choices.js widgets with pill-buttons that open popovers containing a
scrollable checkbox list + search + Select All / Invert / Clear. Reuses
Feature 1's `.filter-pill` / `.filter-popover` CSS vocabulary.
Implemented in `4f15e4b`.
2. **Multi-line `{# ... #}` comment bug, twice.** Django's `{# #}` comment
syntax is single-line only — multi-line blocks need
`{% comment %}...{% endcomment %}`. We shipped the bug in the Task 4
row partial (`e088192` fixed it) and then AGAIN in the Fix-A worker
cell (`4c1cdb6` fixed it). Both shipped into production-looking
renders, not caught by automated tests. Lesson: add a repo-wide
grep guard or a Django linter for this class of template bug.
3. **Bulk-delete cascade gap.** The original Task 6 spec's reference
implementation (`PayrollAdjustment.objects.filter(...).delete()`)
silently orphaned linked `Loan` rows and `priced_workers` M2M entries
when bulk-deleting adjustments of type "New Loan", "Advance Payment",
or "Overtime". The single-row `delete_adjustment` view had 30+ lines
of cascade logic the bulk view didn't use. Code review caught it.
Fix: extracted `_delete_adjustment_with_cascade(adj)` helper and
delegated both views to it — ensuring bulk and single-row have
identical semantics. Also added a 'has_paid_repayments' skip reason
in the JSON response so the UI can indicate why some rows were kept.
Implemented in `4c3e90f`.
4. **Row actions → modals (CP1 Fix A).** The original design §4 said row
actions "match the rest of the dashboard — NO expandable rows". We
interpreted this as table-to-page navigation (Worker name → `/workers/<id>/`,
View Payslip → `/payroll/payslip/<pk>/`). At CP1 Konrad clarified he
wanted in-place MODALS matching the Pending tab: worker name opens
`#workerLookupModal`, paid-row eye icon opens `#previewPayslipModal`,
project name goes to `/projects/<id>/#history` (History tab active).
Implemented in `b59eb31`; tiny tab-activation helper in
`projects/detail.html` picks up the URL hash.
5. **id collision.** Task 4 added `id="adjSelectAll"` to the table
header checkbox, but the Add Adjustment modal already used that id
for its Select-All anchor. `document.getElementById` returns only
the first match, so the modal's handler silently bound to the table
checkbox. Renamed the table's to `#adjTableSelectAll` in `5f2e6d8`.
### Tests
Added 14 tests in `AdjustmentsTabTests`:
- `test_admin_sees_adjustments_tab` — 200 + active_tab set
- `test_supervisor_forbidden` — non-admin redirected
- `test_type_multi_filter` — union on multi-value param (uses adj_total_count)
- `test_worker_multi_filter` — worker filter
- `test_team_filter_uses_subquery_no_inflation` — proves the subquery
pattern with 2 teams × 2 workers × 3 adjustments (naive would return 6)
- `test_status_filter_unpaid` — payroll_record__isnull filter
- `test_date_range_filter` — date__gte/lte
- `test_stats_scoped_to_filtered_set` — counts + sums respect filter
- `test_group_by_type` — buckets + net_sum + descending-magnitude ordering
- `test_group_by_worker` — buckets by worker_id
- `test_bulk_delete_only_affects_unpaid` — paid row survives
- `test_bulk_delete_requires_admin` — 403 for supervisors
- `test_bulk_delete_cascades_new_loan` — Loan + unpaid repayments gone too
- `test_bulk_delete_skips_loan_with_paid_repayments` — refuses, reports reason
- `test_team_worker_pairs_json_context_key` — raw Python list shape (not double-encoded)
Also extended existing tests:
- `test_group_by_type` gained a descending-magnitude ordering assertion
- `TypeSlugFilterTests` has 3 tests for the new template filter
### Net code churn
- `core/views.py`: ~+200 lines (filter branch + 2 helpers + bulk-delete view)
- `core/templates/core/payroll_dashboard.html`: ~+450 lines (tab + filter bar + popover markup + table + JS modules)
- `core/templates/core/_adjustment_row.html`: new file, ~120 lines
- `core/templatetags/format_tags.py`: ~+35 lines (`type_slug`, `money_abs`, `url_replace`)
- `static/css/custom.css`: ~+220 lines (badge palette + layout skeleton + popover extensions + colour-accented group headers + chevron rotation)
- `core/tests.py`: ~+380 lines (14 new adjustments tests + 3 type_slug tests)
- `core/urls.py`: +1 route
- Total: ~+1,400 lines added, ~-100 replaced/removed.
(Original estimate: ~960 lines. Actual: +44% — mostly from the popover-
checkbox filter rewrite, the bulk-delete cascade, and the cross-filter JS.)