Per the plan at docs/plans/2026-04-23-inline-filters-plan.md Task 5, the
now-redundant configuration modal goes away:
core/templates/core/_report_config_modal.html → deleted (160 lines)
core/templates/core/index.html:
- Dashboard 'Generate Report' tile → plain link to
/report/?from_month={% now 'Y-m' %}&to_month={% now 'Y-m' %} so the
click lands on the report page with the current month pre-filled.
- Modal {% include %} at EOF removed.
core/templates/core/report.html:
- Both 'New Report' buttons (header + bottom action bar) deleted;
comments updated to say the pills ARE the new-report interface.
- {% include 'core/_report_config_modal.html' %} removed.
- Stale 'Task 5 will delete...' comment on the Choices.js CDN block
updated.
Konrad's exact ask (Checkpoint 1 feedback):
'Does it make sense to have this popup window for reports? Don't you
think clicking on generate report should just default to current month
and open the report page where users can adjust report filters?'
→ Yes. The pills do exactly that, one click in.
Verification:
grep -rn 'reportConfigModal\|_report_config_modal' core/ returns 0 hits.
47/47 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Checkpoint-1 polish (Konrad):
- 'From (optional)' — the parenthetical is now ~50% smaller (0.6rem)
so the label's primary text dominates, matching his intent of a
subordinate hint rather than a competing word.
- 'Until' — adds a small info-circle icon with a Bootstrap tooltip
reading 'Single month select'. Inline small-font text was my first
attempt but wrapped to two lines inside the narrow column; the icon
tooltip keeps the label tidy while the hint is one hover away.
Bootstrap tooltip auto-init (base.html) handles the binding —
matches CLAUDE.md's global tooltip pattern.
No functional change. 47/47 tests still green (no view code touched).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Konrad's clarification on the Checkpoint-1 UX revision: the visual order
should follow English reading — "from X until Y" reads left-to-right, so
"From" belongs on the left and "Until" on the right. Previous commit
71f8558 placed Until on the left because it's the always-filled anchor,
but that fights the natural sentence order and was confusing.
Optionality is unchanged:
- Until (right, always filled) = anchor month
- From (left, optional) = blank means single-month report
No JS change needed — input IDs (popoverFromMonth / popoverToMonth) stay
the same; only column positions in the <div class="row"> were swapped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Checkpoint 1 second-round UX feedback (Konrad, 2026-04-23):
(1) "The until option must be auto filled (and used for single month) and
the from date must be optional — this makes more sense and less clicks
if the user wants to eg check the last 3 months."
→ Inverted the month pickers. "Until" is now the always-filled anchor
(defaults to URL to_month, falling back to the current YYYY-MM when no
filter is set). "From (optional)" is the disclosure; blank = single
month (JS submits from_month = to_month). Visual order swapped so
Until sits on the left as the primary action. Matches the admin mental
model: "I want data ending now, maybe going back N months."
(2) "Is it possible to show only teams and projects that has transactions
within the selected dates — filter out teams and projects that has no
log for any of the dates chosen?"
→ The pill pickers AND the cross-filter (project_team_pairs_json) are
now scoped to the current date range. A team/project with zero logs in
the window doesn't clutter the lists. The (project_id, team_id) pair
map follows the same rule — cross-filter disables options that never
paired inside THIS window.
Guarantee: entries that are currently in the URL's ?project= / ?team=
selection are always unioned back in, so the user's own picks can
never disappear from the list even when they'd otherwise be out of
scope (e.g. picking a project, then narrowing the date range to a
period with no logs on that project).
Design-doc note at lines 108-112 of 2026-04-23-inline-filters-design.md
originally said "Scope = entire history" — Konrad's real-usage feedback
overrides that decision. Will be recorded in the Task 6 "Shipped" block.
Tests: two new ones lock in the behaviour —
- test_pickers_and_pairs_are_date_scoped: out-of-range project/team
absent from both the picker lists and the pair map
- test_url_selected_projects_survive_even_out_of_range: URL selection
unioned in regardless of date window
Plus existing 3 tests still green. 47/47 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Checkpoint 1 UX feedback (Konrad, 2026-04-23) surfaced three friction points
that all traced back to the same over-engineered "multi-stage commit" model:
1. When Choices.js opened its dropdown, it covered the popover's OK button.
User had to click in a thin strip "outside the multi-select but inside
the dropdown pane" to close Choices.js before OK became reachable.
2. Changing only a project/team didn't light up the global Apply button
(dirty-state diff bug on multi-selects), and even when it did, clicking
Apply didn't actually update the report tables. Also the Apply button
sat at the far right of the pill strip — easy to miss on desktop.
3. Single-month reports required changing BOTH From and To pickers; for a
low-frequency admin tool, that's a tax on the most common flow.
Instead of patching three bugs, collapsed the entire pending/dirty/Apply
model. Each popover's OK now:
- Rebuilds the URL from its OWN inputs only (keeping other filters intact)
- Navigates → full SSR page reload → report re-renders
The user reads the result of their change immediately; there's no "did I
remember to click Apply?" step.
Side-effect wins:
- 'dirty state', 'pending state', 'updateAllPillsDirty', 'revert...',
cross-filter auto-removal, and the toast system all become unnecessary.
Net -187 lines across template + CSS.
- The bug from (2) self-disappears because there's no dirty-diff step.
- Sticky popover footer (position: sticky; bottom: 0; z-index: 2) pins
OK to the popover edge even when Choices.js expands — solves (1).
- The To month picker is labelled "Until (optional)" with "Leave blank
for a single month" hint. Blank on submit → to_month = from_month.
Single-month URLs round-trip with a blank To input (so the form and
the data agree).
Cross-filter preserved: on popover open, the OTHER pill's URL selection
still disables invalid dropdown options. Just no runtime auto-remove —
unnecessary because the next OK submits and the server takes over.
Tested in the browser via preview MCP:
- All three pills open popovers on click
- Range URL shows both month pickers filled
- Single-month URL shows To blank
- OK with blank To → navigates to from_month=X&to_month=X
- Sticky footer keeps OK in viewport when Choices.js is open
- 45/45 tests still pass (no backend contract change)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 1 set context['project_team_pairs_json'] = json.dumps(pairs), then the
template rendered it with |json_script — which also calls json.dumps on the
value. Result was a JSON-encoded string-of-a-string in the <script
id="projectTeamPairs"> tag, so JSON.parse() returned a string (not a list)
and the pill-popover IIFE died on pairs.forEach(...). Symptom: all three
filter pills clickable but unresponsive.
Fix: pass the raw Python list; let |json_script own the serialisation (the
established pattern for team_workers_map_json and the other *_json keys on
the payroll dashboard).
Tests updated to read the raw list from resp.context. Added an end-to-end
regression test that extracts the rendered <script id="projectTeamPairs">
payload and asserts JSON.parse() would return a list (not a string) —
catches any future regression of this class even if the test suite and the
view drift apart.
Verified in the browser: all three pill popovers now open on click and
Choices.js lazy-initialises correctly for projects/teams.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Main interactive layer for the inline-filters feature. Appends two
blocks to report.html (inside {% block content %}, before the final
{% endblock %}):
1. Choices.js CDN <link> + <script> (admin-only gated, SRI-hashed) —
moved here because Task 5 will delete _report_config_modal.html,
which previously loaded the CDN. Keeping this on the report page
directly means the pills stay functional after modal retirement.
2. A scoped IIFE that wires up the three filter pills into an
interactive, state-managed UI:
- Click pill -> open popover (lazy-inits Choices.js on first open)
- Click outside / Esc / other pill -> close
- OK commits popover's local edits into pending state; dirty pills
get the orange outline + pulsing dot; Apply button slides in
- Cross-filter: picking projects auto-removes now-invalid teams
with toast notice ("Removed Team X — no logs on selected
projects"), and vice versa. Scope = entire history.
- Apply -> rebuilds querystring from pending state + navigates
(full page reload, same URL scheme as the retired modal)
- Reset -> reverts all pills to URL-current values
XSS-safe throughout: textContent and createElement; no innerHTML with
user data. Matches the pattern in base.html's work-log-payroll modal
from the Work-Log Payroll feature.
Graceful fallback: if Choices.js CDN fails to load, the module bails
early with a console warning; native <select multiple> still works
inside the popovers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the three static filter pills with clickable buttons and
inline popover shells below each one. Popovers remain hidden by
default (hidden attribute) — the JS module in Task 4 will wire up
open/close, dirty state, and Apply behaviour.
Structure per pill:
- .filter-pill-wrap (position-relative container)
- <button class="filter-pill filter-pill--editable" data-filter="...">
with chevron indicating clickability
- <a class="filter-pill__x"> (existing × clear-filter link, preserved)
- .filter-popover (the editable widget — date picker for the Date
pill, Choices.js multi-select for Projects/Teams pills)
Apply + Reset buttons sit in .apply-filters-group at the right end,
initially hidden. A <div id="filter-toast-container"> is pre-placed
for the cross-filter auto-removal notices.
Three json_script blocks embed the data the JS needs:
- projectTeamPairs: (project_id, team_id) pairs for cross-filter
- urlSelectedProjectIds / urlSelectedTeamIds: current URL state for
dirty diffing + reset
No visible behaviour change yet (no CSS, no JS). Page renders same
as before until Tasks 3-4 light it up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Serialises distinct (project_id, team_id) pairs from WorkLog as JSON on
the generate_report context. The upcoming pill-popover JS (Task 4 of the
inline-filters plan) uses this to hide teams that haven't worked on a
selected project (and vice versa) without any extra HTTP round-trips.
Scope: entire history (not the report date range) — cross-filter is about
data possibility, not data shown in this period. Filters out NULL
project or team (can't cross-filter on NULL).
2 tests cover: key is populated with correct pairs; NULL-team logs don't
leak into the pairs list.
No visible behaviour change — template doesn't consume the new key yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PDF template updated to match the new HTML structure: cover block
with static filter labels, hero KPI band (4 stacked 2x2), Chapter I
lifetime (Projects + Teams full-width, Projects now with Start /
Working Days / Avg-R-per-Working-Day columns), Chapter II selected
period (existing Total Paid Out hero + Loans/Advances pairs +
Labour Cost + Payments/Adjustments), Chapter III worker breakdown
(heading renamed), Chapter IV team x project pivot (new).
THIS YEAR section dropped per design doc section 3 (redundant with
All Time + Selected Period).
Same _build_report_context helper so HTML and PDF cannot drift in
data. All numbers identical. WeasyPrint-friendly: absolute units,
single-column body, no Font Awesome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final chapter of the executive redesign. Renders the
team_project_activity context as a pivot: rows=teams, columns=projects,
cell=COUNT(DISTINCT work-log dates). Zero cells show em-dashes in muted
grey (not '0') so non-zero cells stand out. Row totals, column totals,
and grand total on the bottom row.
Adds a tiny dictlookup template filter (format_tags.py) — Django
templates can't index a dict by a dynamic variable key, and the pivot
cell lookup is cells_by_project_id[col.id]. Defensive None + TypeError
guards so a malformed context can't 500 the page.
.table-total-row CSS: 2px top border + inset background for the footer
row so totals visually separate from the data rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the numbered 'III' chapter heading above the existing Worker
Breakdown card (the widest table in the report). Promotes the table
to .report-numeric (tabular-nums) for perfect column alignment
across dynamic adjustment columns — Inter's tabular-nums variant
keeps the rand amounts pixel-aligned.
No data or structural changes to the breakdown itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the old narrow four-card All-Time/YTD row (dropped in Task 9)
with two wider cards under a numbered 'Chapter I - Lifetime Context'
heading. Projects card gains Start, Working Days, Total Cost, and
Avg R / Working Day columns per the design. Teams card keeps name +
total.
Adds .chapter-heading and .chapter-num CSS for the orange numbered
markers (I, II, III, IV) and .report-numeric class that applies
tabular-nums across the number columns of every report table.
Renames the existing 'Selected Period' heading to Chapter II.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chapter 0 of the executive redesign: four large cards at the top
showing Paid This Period, Outstanding Now (live, stamped with the
generation time), FoxFitt Avg/Day, and FoxFitt Avg/Month.
Drops the old four-small-cards All-Time/YTD row (YTD specifically
documented as redundant per design doc section 3). All-Time detail
moves into Chapter I in the next task.
New .stat-card--hero variant uses Poppins 1.85rem for the number,
uppercase tracked labels, subtle tertiary sub-lines. tabular-nums
keeps the R-amounts pixel-aligned across cards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three pills under the header: date range, project(s), team(s). Shows
comma-joined names when multi-valued (project_name in context is
already a comma-joined string from Task 6). × buttons on the project
and team pills remove just that filter via a rebuilt querystring;
the calendar pill has no × (date range is required).
Helper context keys query_string_without_project / _without_team do
the rebuild in the view via QueryDict.setlist so multi-value keys
are properly stripped (pop() only removes the first occurrence).
Pill CSS uses existing design tokens (--bg-inset, --accent,
--text-primary, --border-default, --text-tertiary, --bg-card-hover)
so dark and light themes work without overrides.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review (on 748c7c7) flagged that Bootstrap CDN tags in base.html
use integrity=sha384-... + crossorigin=anonymous, but the Choices.js
tags added in Task 7 did not. Since both are admin-only privileged
contexts and Bootstrap sets the precedent, Choices.js should match.
Hashes computed from cdn.jsdelivr.net/npm/choices.js@10.2.0 via
curl ... | openssl dgst -sha384 -binary | openssl base64
No behavior change when the CDN is healthy; defense against a
compromised CDN serving altered bytes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the two single <select> elements in the report config modal
with <select multiple> enhanced by Choices.js (CDN 10.2.0, admin-only
gated, graceful fallback to native on CDN failure).
Removes the 'All Projects' / 'All Teams' placeholder option rows —
empty selection = all, matching Choices.js convention.
Persists selected values across submissions via two new context keys
(selected_project_ids, selected_team_ids) threaded through index() and
generate_report().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
project_id/team_id become project_ids/team_ids (list[int] or None).
Every internal filter uses the __in lookup; M2M filters use the
id__in subquery pattern documented in CLAUDE.md's Django ORM gotcha.
generate_report and generate_report_pdf switch to request.GET.getlist.
Old URL ?project=1 still works - getlist returns a single-element list.
Return dict gains six hero-KPI keys: current_outstanding, current_as_of,
company_avg_daily, company_avg_monthly, company_working_days,
team_project_activity - ready for the template restructure in Tasks 9-12.
Tests: 3 new multi-filter tests; existing inflation tests updated to the
new kwarg names. 42 total, all pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chapter I of the executive report needs per-project working-day count and
avg rand per working day. Instead of modifying the shared _get_labour_costs
helper (used by other sections with different column sets), enrich the
output INSIDE _build_report_context: wrap the raw result and add
working_days (distinct work-log dates per project) and avg_per_working_day
(total_cost / working_days, null-safe).
Also attaches start_date from the Project model (may be None if not set).
1 test added.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chapter IV pivot backend: for each (team, project) pair in the given
work-logs queryset, counts distinct work-log dates. Returns columns
(projects), rows (teams with cell dict), column totals, and grand total
ready for direct template rendering.
Logs with NULL team or NULL project are excluded (can't pivot on NULL).
Teams/projects with zero activity don't appear as rows/columns — keeps
the pivot tight.
Tests cover shape, cell counts, row+column+grand totals, and
zero-activity team omission.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two tweaks from code review on 82594fa:
1. The sort `key=lambda r: -r['amount']` placed NEGATIVE amounts
(rare but possible: a project with only a deductive adjustment)
AHEAD of larger positive exposures. Swapped to
`key=lambda r: r['amount'], reverse=True` — same runtime, clearer
intent, correct for negatives.
2. test_team_filter_scopes_total only asserted the net total. A
partial scoping regression where the adjustment leaked but netted
to zero would have silently passed. Added two assertions that
by_project has exactly the expected 2 entries and R 500 never
appears in the amount list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hero KPI card 2 needs 'Outstanding NOW' scoped to the report's selected
projects/teams. This helper wraps _compute_outstanding, reshapes the
by_project dict into a sorted list, and exposes the net total for direct
rendering.
Tests cover unfiltered total, project-scoped total, and team-scoped
total (including the worker__teams subquery path for adjustments).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Computes company-wide avg daily and monthly labour cost for the
executive report's hero KPI band (cards 3 and 4). Denominator is
working days (distinct work-log dates), not calendar days — true
cost-of-a-productive-day metric per design section 2.
Monthly = daily * 30.44 (the 365.25/12 month-length approximation,
which keeps annualised totals correct on average).
Tests cover: empty DB returns zero, known values with assertAlmostEqual
for the 30.44 multiplication, and that multiple workers on one date
count as 1 working day (not N).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure refactor: the ~45 lines of outstanding-payment math inside index()
(computing unpaid_wages + pending_adj_add - pending_adj_sub, with a
per-project breakdown) move into a standalone _compute_outstanding()
helper. index() now calls it with no arguments for unchanged behaviour.
The helper accepts optional project_ids / team_ids for Task 3.
No tests changed; 28/28 still pass. Dashboard Outstanding Payments
card shows the same value before and after.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reported: when creating a new team or project from the friendly UI
(/teams/new/ or /projects/new/), the Supervisor dropdown only lists
is_staff / is_superuser accounts. Users who should be eligible to
supervise (e.g. eendman, supervisor_smoke) are invisible in the
picker even though they are active.
Root cause:
core.forms._supervisor_user_queryset filtered to
is_active=True AND (is_staff OR is_superuser OR groups__name='Work Logger')
That was strictly more restrictive than the app's own permission
helper is_supervisor(user) in views.py, which grants supervisor
powers to ANYONE assigned to a team/project (via the team.supervisor
FK or project.supervisors M2M), regardless of group membership.
On Konrad's dev DB that excluded 2 of 6 active users from the picker
(one in a custom group, one in no group) even though both were valid
supervisor candidates by the permission model.
Fix:
Queryset now returns every active user. The act of assigning a user
to a team/project is what confers supervisor-ness downstream, so
the picker no longer needs a pre-registered allow-list. Inactive
users (is_active=False) remain excluded — the one hard guardrail.
Docstring rewritten to explain the new behavior and why. Stale comment
in TeamForm.__init__ updated to match (the old comment still described
the pre-fix Work-Logger-group requirement).
Tests: 4 new regression tests in SupervisorPickerQuerysetTests:
- regular active user is selectable (the core bug)
- user in an unrelated group is selectable
- inactive user is still excluded (guardrail)
- admin is still selectable (no regression for prior use case)
All 28 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reported: when the generate-report page is filtered by BOTH project and
team, every amount in the "Worker Breakdown" and "Payments by Date"
tables blew up by ~100x. Example: Billy Baloyi R 5,400 (correct)
became R 604,800 (wrong, 112x) after selecting Wilkot + Civils One.
Root cause:
_build_report_context chained `records.filter(work_logs__project_id=X)
.distinct().filter(work_logs__team_id=Y).distinct()`. In Django's ORM
each chained M2M filter creates a SEPARATE JOIN alias on
core_payrollrecord_work_logs, so the SQL produces the cartesian product
of (matching-logs-for-project) x (matching-logs-for-team) rows per
PayrollRecord. A downstream `.values().annotate(Sum('amount_paid'))`
then summed across those duplicated rows - inflating every total by
N * M where N and M are the log counts per record.
Why total_paid_out looked correct: `.aggregate(Sum(...))` wraps the
query in a subquery when distinct() is in play, so it dedupes before
summing. `.values().annotate(Sum(...))` uses GROUP BY on the raw
joined rows and doesn't get that help.
Fix:
Replace chained M2M filters with id__in subquery filters:
records.filter(id__in=PayrollRecord.objects.filter(
work_logs__project_id=X).values('id'))
This keeps the outer queryset JOIN-free, so values().annotate(Sum())
aggregates over distinct records. Same pattern applied to the
adjustments team-filter (worker__teams M2M) for the adjustment
summary.
Tests: 5 new regression tests in ReportContextFilterInflationTests
covering project-only, team-only, both-filters, total_paid_out
invariant, and the adjustment summary path. All 24 tests pass
(19 existing + 5 new).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Admins see cursor:pointer + data-log-id on each row. Click opens the
shared modal from base.html. Supervisors unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Admins see cursor:pointer + data-log-id on each row. Click opens the
shared modal from base.html. Supervisors unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Admin users get cursor:pointer + data-log-id on each row. Click
opens the shared modal from base.html. Supervisors unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues caught by code quality review on commit 2e60124:
1. C1 (critical): the <script> at line ~398 runs during HTML parsing,
BEFORE the modal markup at line ~627 has been parsed. getElementById
returned null, the `if (!modalEl) return;` guard silently exited the
IIFE, and the delegated click listener was never attached — so the
modal was completely dormant. Wrapped the IIFE body in a
DOMContentLoaded handler so the DOM is fully parsed before lookups.
2. I1 (a11y): added aria-labelledby on the modal root + a matching id on
the modal-title h5 so screen readers announce the title correctly
(Bootstrap 5 a11y convention).
No behavioural changes to the JS logic itself — only the wrapping and
two aria attributes on the markup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modal shell + JS click handler live in base.html so any page opts in
by adding data-log-id to a row. JS uses createElement + textContent
(matches worker_lookup_ajax pattern) to build the modal body from
JSON — no innerHTML. Supervisors never receive the markup.
Footer 'Open full page' links to /history/<id>/.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Active breadcrumb item now has aria-current="page" so screen
readers correctly announce the current page (Bootstrap 5 convention).
- Template section comments changed from {# --- #} to {# === #} to
match the CLAUDE.md Python convention used elsewhere in the project.
No logic or rendering changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
work_log_payroll_ajax serializes the helper's output to JSON with
floats (not Decimals), ISO dates, and payroll_record/worker IDs for
client-side link construction. Admin-only; supervisor = 403, anon =
302, unknown log = 404. Matches the worker_lookup_ajax pattern.
Added 4 view-level tests (total 16 passing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The helper used log.overtime (which doesn't exist on WorkLog); the
correct field is overtime_amount. Combined with a defensive
`getattr(..., None) or 0`, the bug made the flag permanently False,
which would have silently hidden the 'Price now' banner in Tasks 3
and 4. Now reads overtime_amount directly (it's non-nullable with a
0.00 default, so no defensive shim is needed).
Adds 4 regression tests:
- test_overtime_needs_pricing_flag: the bug that just got fixed
- test_query_count_is_bounded: N+1 guard (4 queries regardless of worker count)
- test_empty_log_returns_zero_totals: log with no workers attached
- test_log_without_team_has_no_pay_period: log whose team became NULL
Also removes unused `reverse` import from tests.py.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure-function helper that classifies each worker on a work log as
Paid / Priced-not-paid / Unpaid, collects log-linked adjustments,
and computes totals + pay-period context. Used by both the AJAX
endpoint and the full-page view so they can't drift.
Bootstraps core/tests.py (was empty); 8 tests cover the three
statuses, totals, log-linked adjustments, and the pay-period branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routes /history/<id>/ and /history/<id>/payroll/ajax/ to stub views.
Both admin-gated; no data yet. Sets up the surface for Tasks 2-4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
My previous commit (fb1a8a2) added a multi-line explanatory comment
using Django's {# ... #} syntax, which is single-line only. The comment
therefore rendered as literal text at the top of every page.
This is the second time this session I've made this exact mistake —
lesson for next time: always render a page on the dev server and grep
the response body for '{#' after template changes, even one-liners.
Verified locally this time: leak count = 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The static asset cache-buster in base.html was using
{{ request.timestamp|default:'1.0' }} — but `request.timestamp` is
not a Django request attribute, so the template always fell back to
the literal '1.0'. Every deploy's CSS URL resolved to the same
`custom.css?v=1.0`, so any CDN or browser cache in front of the app
held onto the pre-redesign CSS forever — even hard refreshes in
incognito couldn't bust it.
Symptom: after deploying the redesigned app, the browser continued
to receive a 1,734-byte pre-redesign custom.css while the VM's
/static/css/custom.css was the full 39,078-byte Premium Orange Theme.
.topbar-nav rules were missing, so the topbar rendered as stacked
block links.
Fix: use `deployment_timestamp` (already provided by
core.context_processors.project_context as int(time.time()) at
render time). Every restart gets a fresh URL, CDNs refetch from
origin, stale caches break.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Override Bootstrap's --bs-table-color to use theme text color so table
numbers (days, amounts, totals) are readable on dark backgrounds. Fix
Loan badge by removing text-dark class and using CSS to force black text
on bg-warning. Add dark mode overrides for disabled form controls, select
option dropdowns, btn-close filter, btn-secondary colors, and Bootstrap
text utility classes (.text-dark, .text-primary, .text-muted, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move decorative gradient glows from ::before/::after pseudo-elements on
.app-main to a separate .app-glow div. The pseudo-elements were creating
a stacking context that trapped Bootstrap modals (z-index 1055) inside
.app-main, while the backdrop (z-index 1050) was appended to <body> —
causing the backdrop to render on top of the modal content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the green accent with a warm orange/amber palette and switch to a
dark-first design. Add a fixed sidebar for desktop navigation and a bottom
tab bar for mobile, replacing the top navbar. Cards now use glass-morphism
with left accent bars, buttons use orange gradients, and decorative glow
effects add depth. All 8 page templates updated, both light and dark modes
tested across desktop and mobile viewports.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New AJAX endpoint (worker_lookup_ajax) returns a comprehensive financial
report card for any active worker. Modal shows: amount payable, outstanding
loans, paid this month/year, loans this year, recent activity, active loans
table, current project + days, PPE sizing, drivers license, and notes.
Worker names across all dashboard tabs are now clickable links that open
the modal. Header button with searchable dropdown for quick access.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New fields: shoe_size, overall_top_size, pants_size, tshirt_size,
has_drivers_license (boolean), drivers_license (file upload).
Admin organised into 3 fieldsets. CSV export updated with new columns.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each worker row now has an Adjust button (slider icon) that opens the
Add Adjustment modal with that worker pre-checked and their most recent
project pre-selected. Header Add Adjustment button resets the modal
to a clean state (no workers pre-checked).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When creating a New Loan, a "Pay Immediately" checkbox (checked by
default) processes the loan right away — creates PayrollRecord, sends
payslip to Spark, and records the loan as paid. Unchecking it keeps
the old behavior where the loan sits in Pending Payments.
Also adds loan-only payslip detection (like advance-only) across all
payslip views: email template, PDF template, and browser detail page
show a clean "Loan Payslip" layout instead of "0 days worked".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace "Exclude workers with loans" checkbox with dropdown
(All Workers / With loans only / Without loans) in batch pay modal,
matching the pending payments table filter style
- Fix radio button visual state when switching between
"Until Last Paydate" and "Pay All" modes (set checked after DOM append)
- Update CLAUDE.md with pending table filter and overdue badge docs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Loans filter now offers: All Workers / With loans only / Without loans.
Replaces the simpler exclude-only checkbox for more flexibility.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>