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:
parent
6f66faf06a
commit
503eff67a0
17
CLAUDE.md
17
CLAUDE.md
@ -5,6 +5,8 @@
|
||||
- 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
|
||||
- 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
|
||||
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
|
||||
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
|
||||
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
|
||||
templates/
|
||||
base.html — App shell (topbar + mobile menu + bottom tab bar)
|
||||
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/teams/ — 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`
|
||||
- `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`
|
||||
- `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
|
||||
- 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 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.
|
||||
- **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
|
||||
| Path | View | Purpose |
|
||||
@ -198,8 +207,8 @@ USE_SQLITE=true DJANGO_DEBUG=true python manage.py test core.tests -v 2
|
||||
|
||||
## Frontend Design Conventions
|
||||
- **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.
|
||||
A sun/moon toggle in the topbar flips a class on `<html>` and persists the choice to localStorage.
|
||||
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 the `data-theme` attribute on `<html>` and persists the choice to localStorage.
|
||||
- **CSS variables** in `static/css/custom.css` `:root` — always use `var(--name)`:
|
||||
- `--accent: #e8851a` (warm orange/amber, brand), `--accent-hover: #f59e0b`
|
||||
- `--primary-dark: #0f172a`, `--primary: #1e293b`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user