docs(claude): update CLAUDE.md for session's features + newly-learnt gotchas

Audit revealed several stale / missing items:

1. Wrong CSS selector for light theme — said `:root.light`, actual is
   `[data-theme="light"]`. Task 2 of Adjustments caught this in the
   implementer's self-review; the doc didn't get updated. Now correct.

2. `_report_config_modal (partial)` removed from templates list — the
   file was deleted in commit 1d00a3a (retire modal).

3. `_adjustment_row.html` added to templates list — new partial, shared
   by flat + grouped views on the Adjustments tab.

4. `format_tags.py` now lists all 5 filters: money, money_abs, type_slug,
   url_replace, dictlookup (was just 'money').

5. New narrative paragraphs for:
   - Inline Filters on /report/ (pill popovers, cross-filter, JSON gotcha)
   - Adjustments tab (filter pills, badge palette, group-by, bulk delete)
   - _delete_adjustment_with_cascade helper (shared by single+bulk)
   - Pill-popover filter pattern (.adj-hidden-inputs + OK-rewrites-inputs)

6. Two new schema name-drifts: PayrollRecord.amount_paid (not total_amount
   / days_worked); Loan.principal_amount (not principal). Both bit an
   implementer this session when writing test fixtures.

7. Two new Coding Style rules in the top section:
   - Multi-line {# #} template comments are INVALID — use {% comment %}
     (bit us 4× in this session). With caveat that literal {# or #} can't
     appear inside a {% comment %} block either.
   - Duplicate id= attributes silently steal event handlers — grep before
     assigning (caught adjSelectAll collision between table header + modal).

Now 707 lines, 24 sections. Future sessions should have the context to
avoid the mistakes this session made.
This commit is contained in:
Konrad du Plessis 2026-04-24 00:00:07 +02:00
parent 6f66faf06a
commit 503eff67a0

View File

@ -5,6 +5,8 @@
- Add plain English comments explaining what complex logic does - Add plain English comments explaining what complex logic does
- The project owner is not a programmer — comments should be understandable by a non-technical person - The project owner is not a programmer — comments should be understandable by a non-technical person
- When creating or editing code, maintain the existing comment structure - When creating or editing code, maintain the existing comment structure
- **Django template comments `{# ... #}` are SINGLE-LINE only.** Multi-line blocks need `{% comment %}...{% endcomment %}`. A `{#` on line N with no closing `#}` on the same line renders the whole block as literal text onto the page (and silently — no error). This bit us 4× during the Adjustments feature. Also: the literal tokens `{#` and `#}` cannot appear inside a `{% comment %}` block — they'll be parsed as a nested comment marker. Rephrase meta-notes about comment syntax OUTSIDE the block.
- **Duplicate `id=""` attributes cause silent bugs.** `document.getElementById()` returns only the FIRST match in DOM order, so adding a second element with an existing id silently steals the handler from the original. Grep the template before assigning any new id (caught `adjSelectAll` collision in Task 6 — header checkbox stole the Add-Adjustment modal's Select-All handler).
## Project Overview ## Project Overview
Django payroll management system for FoxFitt Construction, a civil works contractor specializing in solar farm foundation installations. Manages field worker attendance, payroll processing, employee loans, and business expenses for solar farm projects. Django payroll management system for FoxFitt Construction, a civil works contractor specializing in solar farm foundation installations. Manages field worker attendance, payroll processing, employee loans, and business expenses for solar farm projects.
@ -29,12 +31,13 @@ core/ — Single main app: ALL business logic, models, views, forms,
views.py — All view functions (~52 functions, ~3,800 lines) — dashboard, attendance, payroll, reports, worker/team/project CRUD views.py — All view functions (~52 functions, ~3,800 lines) — dashboard, attendance, payroll, reports, worker/team/project CRUD
forms.py — All form classes + validators (WorkerForm, TeamForm, ProjectForm, AttendanceLogForm, PayrollAdjustmentForm, ExpenseReceiptForm, WorkerCertificate/WarningFormSet, 5MB file validator) forms.py — All form classes + validators (WorkerForm, TeamForm, ProjectForm, AttendanceLogForm, PayrollAdjustmentForm, ExpenseReceiptForm, WorkerCertificate/WarningFormSet, 5MB file validator)
admin.py — Django admin registrations for all core models + WorkerCertificate/Warning inlines on Worker admin.py — Django admin registrations for all core models + WorkerCertificate/Warning inlines on Worker
templatetags/ — format_tags.py (money filter for ZAR formatting) templatetags/ — format_tags.py: `money` (ZAR), `money_abs` (signed callers), `type_slug` (type→CSS class), `url_replace` (swap one query-param), `dictlookup`
management/commands/ — setup_groups, setup_test_data, import_production_data management/commands/ — setup_groups, setup_test_data, import_production_data
templates/ templates/
base.html — App shell (topbar + mobile menu + bottom tab bar) base.html — App shell (topbar + mobile menu + bottom tab bar)
core/ — Page templates: index, attendance_log, work_history, payroll_dashboard, core/ — Page templates: index, attendance_log, work_history, payroll_dashboard,
report, create_receipt, payslip, login, _report_config_modal (partial) report, create_receipt, payslip, login
Partials: _adjustment_row.html (shared row for flat + grouped Adjustments tab)
core/workers/ — 4 templates: list, detail, edit, batch_report core/workers/ — 4 templates: list, detail, edit, batch_report
core/teams/ — 4 templates: list, detail, edit, batch_report core/teams/ — 4 templates: list, detail, edit, batch_report
core/projects/— 4 templates: list, detail, edit, batch_report core/projects/— 4 templates: list, detail, edit, batch_report
@ -66,6 +69,8 @@ sessions; grep `core/models.py` before using any field you haven't used before:
- `PayrollAdjustment.description` — NOT `reason` - `PayrollAdjustment.description` — NOT `reason`
- `log.adjustments_by_work_log` (reverse accessor for PayrollAdjustment.work_log FK) — NOT `payrolladjustment_set` (the FK has `related_name` set) - `log.adjustments_by_work_log` (reverse accessor for PayrollAdjustment.work_log FK) — NOT `payrolladjustment_set` (the FK has `related_name` set)
- `log.overtime_amount` (DecimalField, default 0.00) — NOT `log.overtime` - `log.overtime_amount` (DecimalField, default 0.00) — NOT `log.overtime`
- `PayrollRecord.amount_paid` (DecimalField) + `PayrollRecord.work_logs` (M2M reverse) — NOT `total_amount` / `days_worked` (easy to guess wrong when writing test fixtures)
- `Loan.principal_amount` — NOT `principal`. `Loan.save()` auto-sets `remaining_balance = principal_amount` on create, so tests rarely need to pass both.
## Key Business Rules ## Key Business Rules
- All business logic lives in the `core/` app — do not create additional Django apps - All business logic lives in the `core/` app — do not create additional Django apps
@ -149,6 +154,10 @@ USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2
- Worker Management UI: A friendlier alternative to `/admin/core/worker/`. Reachable via the "Resources" topbar dropdown → Workers (admin-only). Pages: `/workers/` (list with search + status filter), `/workers/<id>/` (detail with Profile/Certifications/Warnings/History tabs), `/workers/<id>/edit/` or `/workers/new/` (single-page form with sections for Personal & Pay, PPE, Documents, Driver's License, plus inline formsets for certifications and warnings). Uses `WorkerForm`, `WorkerCertificateFormSet`, `WorkerWarningFormSet` from `core/forms.py`. The "+ Add Certification" / "+ Add Warning" buttons clone a `<template>` element via `content.cloneNode()` (DOM-safe, no innerHTML) and rewrite `__PREFIX__` in input names to the next formset index. File uploads validated at 5 MB max via `validate_max_5mb()` in `forms.py`. Django admin (`/admin/core/worker/`) remains fully functional as a fallback — both UIs coexist. - Worker Management UI: A friendlier alternative to `/admin/core/worker/`. Reachable via the "Resources" topbar dropdown → Workers (admin-only). Pages: `/workers/` (list with search + status filter), `/workers/<id>/` (detail with Profile/Certifications/Warnings/History tabs), `/workers/<id>/edit/` or `/workers/new/` (single-page form with sections for Personal & Pay, PPE, Documents, Driver's License, plus inline formsets for certifications and warnings). Uses `WorkerForm`, `WorkerCertificateFormSet`, `WorkerWarningFormSet` from `core/forms.py`. The "+ Add Certification" / "+ Add Warning" buttons clone a `<template>` element via `content.cloneNode()` (DOM-safe, no innerHTML) and rewrite `__PREFIX__` in input names to the next formset index. File uploads validated at 5 MB max via `validate_max_5mb()` in `forms.py`. Django admin (`/admin/core/worker/`) remains fully functional as a fallback — both UIs coexist.
- Worker Batch Report: `/workers/report/` shows every worker with aggregated lifetime history — days worked, projects worked on, teams, first/last payslip dates, total paid, cert status (active/total + expired/expiring counts), warning count. Filter by status, project, team. CSV export via `/workers/report/csv/`, PDF via `/workers/report/pdf/` (landscape A4, same amber-accent typography as the payroll report). Built on the reusable `_build_worker_report_context()` helper which uses `annotate(Min/Max/Count/Sum)` + prefetch for efficient aggregation. - Worker Batch Report: `/workers/report/` shows every worker with aggregated lifetime history — days worked, projects worked on, teams, first/last payslip dates, total paid, cert status (active/total + expired/expiring counts), warning count. Filter by status, project, team. CSV export via `/workers/report/csv/`, PDF via `/workers/report/pdf/` (landscape A4, same amber-accent typography as the payroll report). Built on the reusable `_build_worker_report_context()` helper which uses `annotate(Min/Max/Count/Sum)` + prefetch for efficient aggregation.
- Dashboard cert-expiry card: The admin dashboard shows a "Certifications Need Attention" stat card with count of expired + expiring-within-30-days certs (active workers only). Card is CONDITIONAL — renders only when count > 0, so it disappears when everything is in good standing. Clicking it goes to the worker batch report. Counts come from `index()` view adding `certs_expired_count`, `certs_expiring_count`, `certs_alert_total` to context. - Dashboard cert-expiry card: The admin dashboard shows a "Certifications Need Attention" stat card with count of expired + expiring-within-30-days certs (active workers only). Card is CONDITIONAL — renders only when count > 0, so it disappears when everything is in good standing. Clicking it goes to the worker batch report. Counts come from `index()` view adding `certs_expired_count`, `certs_expiring_count`, `certs_alert_total` to context.
- **Inline Filters on the Report page**: `/report/` has three pill-buttons (Date / Projects / Teams) in a sticky strip. Clicking a pill opens an inline popover with the editor for that filter. Popover's OK rebuilds the URL and navigates — no "dirty state", no global Apply. Date popover has a Single/Range/Custom mode toggle; Projects + Teams use Choices.js multi-select. Bidirectional project↔team cross-filter disables workers/projects that never paired in the selected date range. Context key `project_team_pairs_json` is consumed via `|json_script` (raw Python list — NEVER `json.dumps` it first; the filter does the serialisation and double-encoding silently breaks `.forEach(...)`). Deleted the old `_report_config_modal.html`; Dashboard "Generate Report" button is now a plain link with the current month pre-filled.
- **Adjustments tab** (`/payroll/?status=adjustments`): the 4th payroll-dashboard tab, next to Pending / History / Loans. Browse every `PayrollAdjustment` across all workers with 5 pill-style filters (Type / Workers / Teams / Status / Date — all popover-checkbox/radio, no native `<select>`s). Semantic colour badges per type: 7 types × 2 themes = 14 `--badge-*-bg/fg` CSS tokens, with `+15%` saturation siblings for `Loan Repayment` / `Advance Repayment` so "money coming back" reads as hotter than "money going out". Group-by toggle (Flat / By Type / By Worker) with collapsible sections; By-Type headers get a 4px left-border in the matching badge colour via `[data-type="..."]` attribute selectors. Sortable column headers (Date / Worker / Amount / Status) toggle `?sort=X&order=asc|desc`. Bulk-delete via row checkboxes + floating action bar → `POST /payroll/adjustments/bulk-delete/`. Row actions reuse existing modals (Worker Lookup, Preview Payslip, Edit adjustment) — clicking a worker name or paid-row eye icon opens a modal rather than navigating away; project name links to `/projects/<id>/#history` (a tiny hash-based tab-activation helper in `projects/detail.html` activates whichever tab matches the URL hash).
- **Adjustment cascade helper**: `_delete_adjustment_with_cascade(adj)` in `core/views.py` is the single authority for "delete this adjustment, cleaning up linked objects". Returns `(ok: bool, reason: str|None)``reason` is `'paid'` or `'has_paid_repayments'`. For `New Loan`/`Advance Payment` it deletes the linked `Loan` + any still-unpaid repayment adjustments (aborts if any are already paid). For `Overtime` it removes the worker from `work_log.priced_workers`. Both `delete_adjustment` (single-row) AND `bulk_delete_adjustments` delegate to this helper, so bulk and single-row have identical semantics. Without it, bulk-delete was silently orphaning `Loan` rows (was a critical bug caught in code review — see `test_bulk_delete_cascades_new_loan`).
- **Pill-popover filter pattern** (used by both Inline Filters on `/report/` AND the Adjustments tab's 5 filters): each filter is a `.filter-pill-wrap` containing a `<button class="filter-pill filter-pill--editable">` + a hidden `<div class="filter-popover">`. Committed state lives in hidden `<input>`s inside a `.adj-hidden-inputs[data-adj-filter="X"]` container (rewritten on popover OK via `replaceChildren()` + `createElement` — XSS-safe, no `innerHTML`). Popover OK commits + closes; Cancel/Esc/click-outside revert + close. Reusable CSS classes live in `static/css/custom.css` under `/* === Inline Filters === */`.
## URL Routes ## URL Routes
| Path | View | Purpose | | Path | View | Purpose |
@ -198,8 +207,8 @@ USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2
## Frontend Design Conventions ## Frontend Design Conventions
- **Dual-theme** (dark + light) driven by a single CSS variable set in `static/css/custom.css`. - **Dual-theme** (dark + light) driven by a single CSS variable set in `static/css/custom.css`.
The theme is dark-first; the light theme is a set of var overrides inside a `:root.light` block. The theme is dark-first; the light theme is a set of var overrides inside a `[data-theme="light"]` block.
A sun/moon toggle in the topbar flips a class on `<html>` and persists the choice to localStorage. A sun/moon toggle in the topbar flips the `data-theme` attribute on `<html>` and persists the choice to localStorage.
- **CSS variables** in `static/css/custom.css` `:root` — always use `var(--name)`: - **CSS variables** in `static/css/custom.css` `:root` — always use `var(--name)`:
- `--accent: #e8851a` (warm orange/amber, brand), `--accent-hover: #f59e0b` - `--accent: #e8851a` (warm orange/amber, brand), `--accent-hover: #f59e0b`
- `--primary-dark: #0f172a`, `--primary: #1e293b` - `--primary-dark: #0f172a`, `--primary: #1e293b`