From 1c00ba2628c6df393c0a12fb919a79f8479cc05d Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Wed, 22 Apr 2026 13:23:33 +0200 Subject: [PATCH] Design: work log -> payroll cross-link (modal + /history// page) Brainstorm output for the next UI refinement. Adds a click-through from any historic work log (Work History, team detail Recent Work Logs, project detail Recent Work Logs) to a compact modal showing paid/unpaid status per worker, with links out to /workers// and /payroll/payslip//. The modal has a "Open full page" button that navigates to a new /history// route for bookmark-able detail + pay-period context (via get_pay_period). Admin-only; supervisors unchanged. Read-only pass; no model changes, no migrations. Uses existing data: PayrollRecord.work_logs (M2M) and PayrollAdjustment.work_log (FK). Also fixes local dev: run_dev.bat now sets DJANGO_DEBUG=true so runserver auto-serves /static/ (prior behaviour: CSS 404 on localhost because Django's dev server only serves static files when DEBUG=True; production keeps DEBUG=false and is served by Apache, so unaffected). Design doc: docs/plans/2026-04-22-work-log-payroll-crosslink-design.md Co-Authored-By: Claude Opus 4.7 (1M context) --- ...04-22-work-log-payroll-crosslink-design.md | 146 ++++++++++++++++++ run_dev.bat | 6 + 2 files changed, 152 insertions(+) create mode 100644 docs/plans/2026-04-22-work-log-payroll-crosslink-design.md diff --git a/docs/plans/2026-04-22-work-log-payroll-crosslink-design.md b/docs/plans/2026-04-22-work-log-payroll-crosslink-design.md new file mode 100644 index 0000000..5b626af --- /dev/null +++ b/docs/plans/2026-04-22-work-log-payroll-crosslink-design.md @@ -0,0 +1,146 @@ +# 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//` — Recent Work Logs card | same logs, team-filtered | +| `/projects//` — 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//`, 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//` + - Status: `Paid` · `Priced, not paid` · `Unpaid` + - Earned from this log (net of adjustments on the same payroll record) + - Payslip reference → `/payroll/payslip//` (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//`) +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//` — 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// → work_log_payroll_detail (HTML full page) +/history//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//` 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. diff --git a/run_dev.bat b/run_dev.bat index b5ec3a7..8b984df 100644 --- a/run_dev.bat +++ b/run_dev.bat @@ -1,3 +1,9 @@ @echo off +REM === Local dev launcher === +REM USE_SQLITE=true -> use SQLite, skip MySQL requirement, relax prod-only checks +REM DJANGO_DEBUG=true -> make Django's dev server auto-serve /static/ files so +REM CSS/JS/images load without needing collectstatic (prod +REM on Flatlogic keeps DEBUG=false; this only affects local) set USE_SQLITE=true +set DJANGO_DEBUG=true python manage.py runserver 0.0.0.0:8000