38686-vm/docs/plans/2026-04-22-work-log-payroll-crosslink-design.md
Konrad du Plessis 6d37d1ba9b Task 10: add Task 3 full-payload test + mark design doc as shipped
Adds a consolidated regression test to WorkLogPayrollAjaxTests that
exercises: paid worker serialization shape, null team branch, OT flag
in JSON, full_page_url value, and adjustment payslip-link serialization.
Closes the 'Important' coverage gap flagged in Task 3's quality review.

Also appends a 'Shipped' block to the design doc summarising QA
status and capturing all five deferred nits (admin-gate consistency,
template branch tests, |default:0 redundancy, admin-gate expression
readability, background vs background-color) so they survive the
merge into project history.

All 19 tests pass. manage.py check clean. No migrations needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:23:24 +02:00

169 lines
8.4 KiB
Markdown
Raw 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.

# Work Log → Payroll Cross-Link — Design (22 Apr 2026)
## Goal
Let admins click a historic work log (anywhere it appears) and instantly see **which workers on that log have been paid, which haven't, and for paid ones, which payslip paid them** — with hyperlinks through to the existing Worker and Payslip detail pages.
Today the data exists (`PayrollRecord.work_logs` M2M, `PayrollAdjustment.work_log` FK) but is only reachable from the payroll dashboard by working backwards from a payment to its logs. This closes the loop: from a log you can reach its payments, not just the other way round.
## Who it's for
- **Admins** (`is_staff=True` or `is_superuser=True`) — full click-through to payroll data
- **Supervisors** (Work Logger group) — unchanged behaviour. Payroll data stays hidden per the existing permission rule.
## Entry points (three places, same modal)
| Page | Row source |
|---|---|
| `/history/` — Work History | each `WorkLog` in the paginated table |
| `/teams/<id>/` — Recent Work Logs card | same logs, team-filtered |
| `/projects/<id>/` — Recent Work Logs card | same logs, project-filtered |
On each of these, the work-log row becomes clickable for admins: `cursor: pointer`, `--bg-card-hover` on hover, a small chevron icon at the right edge. For supervisors the rows keep their current non-interactive styling.
## Interaction model — hybrid modal + full page
- **Row click** → Bootstrap modal fetches JSON and renders inline. Fast path for "did X get paid for this?". Same pattern as the existing Worker Lookup modal.
- **`[Open full page]` button** inside the modal → navigates to `/history/<log_id>/`, a dedicated bookmark-able page. Unlimited room for detail.
Both views share the same underlying data; the modal is a compact subset of the page.
## What each view shows
### Modal (compact)
1. **Header strip** — date, project (link), team (link), supervisor, worker count, OT-priced flag
2. **Workers table** — one row per `log.workers.all()`:
- Worker name → `/workers/<id>/`
- Status: `Paid` · `Priced, not paid` · `Unpaid`
- Earned from this log (net of adjustments on the same payroll record)
- Payslip reference → `/payroll/payslip/<pk>/` (or `—`)
- Paid on date (or `—`)
3. **Related adjustments** — every `PayrollAdjustment` with `adj.work_log == log`. Worker and payslip as links.
4. **Footer** — total earned, total paid, total outstanding · `[Open full page]` · `[Close]`
### Full page (`/history/<log_id>/`)
Everything the modal has, plus:
- **Breadcrumb** — `History {date} · {project} · {team}`, each segment a link
- **Attendance detail block** — OT hours per worker (from `log.overtime`), supervisor, notes
- **Pay-period context** — uses `get_pay_period(log.team)` to show which period this log falls in and expected paydate if unpaid. Graceful "no schedule configured" fallback if the team has no `pay_frequency`.
Layout matches `/workers/<id>/` — single-column card-paneled page, same typography and spacing.
## Status logic (three states)
For each worker on the log:
```
record = PayrollRecord.objects.filter(work_logs=log, worker=worker).first()
if record:
status = "Paid" # show payslip + date
elif worker in log.priced_workers.all():
status = "Priced, not paid" # OT priced, no payroll cycle yet
else:
status = "Unpaid"
```
Same per-worker checking CLAUDE.md documents for the dashboard's outstanding calculation. Handles partially-paid logs correctly.
## Edge cases
- **Brand-new log, never paid** → all workers `Unpaid`, zero totals, no error
- **Partially paid** → mixed statuses per row (the whole reason per-worker checking exists)
- **Overtime not yet priced** (`log.overtime > 0` and `log.priced_workers` empty) → amber banner at top of modal: "Overtime not yet priced · [Price now]" linking to the existing `price_overtime` flow
- **Worker deactivated since** → name struck through, grey `Inactive` pill, still shown
- **Team/project deleted** (SET_NULL) → header shows `—` for the missing reference, no crash
- **Supervisor hits URL directly** → 403 via `is_admin()`, same as every `/payroll/*` view
- **Log with many workers** → modal scrolls internally (`max-height: 70vh`); full page handles it natively
## Implementation shape (no code yet)
### New URLs
```
/history/<int:log_id>/ → work_log_payroll_detail (HTML full page)
/history/<int:log_id>/payroll/ajax/ → work_log_payroll_ajax (JSON for modal)
```
### New views in `core/views.py` (both `@login_required`, both admin-gated)
- `_build_work_log_payroll_context(log)` — private helper. Returns a context dict with workers+status, adjustments, totals, pay-period info. One function, used by both endpoints so the JSON payload and the HTML page can never drift.
- `work_log_payroll_ajax(request, log_id)` — calls the helper, returns JSON for the modal
- `work_log_payroll_detail(request, log_id)` — calls the helper, renders the full-page template
### New template
- `core/templates/core/work_log_payroll.html` — full-page view. Structure mirrors `/workers/<id>/` so it feels like a sibling.
### Small edits to existing templates
- `core/templates/core/work_history.html` — admin-gated `data-log-id` attribute on each row + JS click handler
- `core/templates/core/teams/detail.html` — same on the Recent Work Logs card rows
- `core/templates/core/projects/detail.html` — same
### New JS (inline in `base.html` or `work_log_modal.js`)
- Single click handler, attached to `[data-log-id]` elements across any page
- `fetch` → inject HTML into a shared `#work-log-payroll-modal` defined once in `base.html`
- Same pattern as the existing Worker Lookup modal
### What we DON'T need
- No model changes
- No migrations
- No new fields
- No changes to `process_payment`, `add_adjustment`, or any mutation path
## Cross-links this adds to the app's graph
Before:
```
Payroll Dashboard → Payslip → [dead end]
Work History → [dead end]
```
After:
```
Work History ↘ Payslip ↗ Worker detail
Team detail → Work Log Payroll →
Project detail ↗ Worker name ↘ Worker detail
```
Every work log becomes a first-class navigable node, reachable from three places, linking out to the two existing detail pages.
## Rough scope
- ~150 lines new view code (helper + two endpoints)
- ~120 lines new full-page template
- ~40 lines JS for the modal
- ~30 lines CSS for hover and chevron states
- ~20 lines of edits across three existing templates
No third-party dependencies added.
## Out of scope (deliberately)
- **"Pay these workers now" action** inside the modal/page — kept read-only for this pass; admins still use the payroll dashboard's Pay flow. Can add later if useful.
- **"Add adjustment pre-filled for this log" shortcut** — tempting but expands scope; revisit after the read-only version ships.
- **Supervisor-visible attendance-only view** of the same click-through — supervisors get no click affordance for now; if they later need a non-payroll drill-down, that's a separate design.
## Next step
Hand off to `superpowers:writing-plans` to produce a task-by-task implementation plan with review checkpoints.
---
## Shipped — 22 Apr 2026
**Commits:** 1c00ba2 (design) through the Task 10 "shipped" commit (this one).
**Plan:** `docs/plans/2026-04-22-work-log-payroll-crosslink-plan.md`.
**QA summary:**
- 19 tests pass (`python manage.py test core.tests`)
- `python manage.py check` — no new issues
- No model changes, no migrations, no pushed-to-prod artefacts
- Three entry points clickable for admins; supervisors unchanged
**Deferred for future passes (non-blocking):**
- Admin-gate consistency: work_history uses `{% if is_admin %}`; teams/projects detail templates use `{% if user.is_staff or user.is_superuser %}`. Both semantically identical but stylistically inconsistent.
- Task 4 template branch tests: OT banner, adjustments table, Paid badge, Inactive worker — covered end-to-end via modal tests now, not via Django test client.
- Task 4 template: redundant `|default:0` on `log.overtime_amount` (harmless, renders "0" instead of "0.00").
- Task 5 admin gate: `user.is_authenticated and user.is_staff or user.is_superuser` could be simplified to `user.is_staff or user.is_superuser`.
- `background` shorthand in Task 9 hover rule could be `background-color` for precision (pedantic).
**"Pay these workers now" modal action** and **"Add adjustment pre-filled for this log" shortcut** — explicitly out-of-scope per the design; revisit if users ask.