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>
- Red 'Overdue' badge on workers with unpaid work from completed pay periods
- Yellow 'Loan' badge on workers with active loans/advances
- Filter bar above table: team dropdown, overdue-only toggle, exclude loans
- All three filters combine (team + overdue + loan) for flexible views
- Overdue detection uses team pay schedule cutoff from get_pay_period()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend adds has_loan flag per worker (checks active Loans).
Frontend shows checkbox only when any eligible worker has a loan.
Combined with team filter in a shared applyBatchFilters() function
that shows/hides rows based on both filters simultaneously.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Client-side filter lets admin narrow batch payment list by team.
Selecting a team hides other workers, unchecks them (so they won't
be paid), and updates the summary total. Select All respects the
filter — only toggles visible rows. Filter resets when switching
between schedule/pay-all modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The radio group was being removed from DOM then accessed via getElementById
which returned null for detached elements, silently breaking the toggle.
Now uses a persistent JS variable reference that survives DOM removal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Radio buttons in the Batch Pay modal let admin choose between:
- "Until Last Paydate" (default): splits at last completed pay period
- "Pay All": includes all unpaid work regardless of pay schedule
Preview re-fetches when mode changes. Workers without teams are
included in Pay All mode (skipped in schedule mode as before).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Batch Pay: new button on payroll dashboard lets admins pay multiple
workers at once using team pay schedules. Shows preview modal with
eligible workers, then processes all payments in one click.
Fix: "Split at Pay Date" now uses cutoff_date (end of last completed
period) instead of current period end. This includes ALL overdue work
across completed periods, not just one period.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Flatlogic's "Pull Latest" doesn't always run migrations automatically.
This endpoint lets you visit /run-migrate/ to apply pending migrations
to the production MySQL database from the browser.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enable selective payment of work logs and adjustments instead of
all-or-nothing. The preview modal now shows checkboxes on every item
(all checked by default) with dynamic net pay recalculation.
Teams can be configured with a pay frequency (weekly/fortnightly/monthly)
and anchor start date. When set, a "Split at Pay Date" button appears
that auto-unchecks items outside the current pay period.
Key changes:
- Team model: add pay_frequency and pay_start_date fields
- preview_payslip: return IDs, dates, and pay period info in JSON
- process_payment: accept optional selected_log_ids/selected_adj_ids
- Preview modal JS: checkboxes, recalcNetPay(), Split button, Pay Selected
- Backward compatible: existing Pay button still processes everything
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When hovering over a bar in the Cost by Project chart, the tooltip
now shows the total for that month across all projects at the bottom.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Same wages/additions/deductions breakdown as the home dashboard,
now also shown on the Payroll Dashboard stat card.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split the single outstanding total into unpaid wages, additions, and
deductions so the card shows where the number comes from. Rename the
'General' project bucket to 'No Project' so per-project totals now
visibly sum to the overall total.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Advances are now treated as immediate payments (not pending salary items):
- Auto-creates PayrollRecord + sends payslip email at creation time
- Auto-creates Advance Repayment adjustment for next salary cycle
- Validates worker has unpaid work logs (otherwise use New Loan)
- Requires project selection for cost tracking
- Partial repayment converts advance to regular loan
- Admin can edit auto-repayment amount before payday
- Negative net pay warning in preview modal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Redesign Advance Payments to work like loans with tracked balances:
- Add loan_type field to Loan model ('loan' or 'advance')
- Move Advance Payment from DEDUCTIVE to ADDITIVE types (worker receives money)
- Add new Advance Repayment type for deducting from future salary
- Create/edit/delete handlers mirror New Loan behavior for advances
- Loans & Advances tab with type badges and filter buttons
Enhance Payslip Preview modal into "Worker Payment Hub":
- Show outstanding loans & advances with balances in preview
- Inline repayment form per loan (amount pre-filled, note, Deduct button)
- AJAX add_repayment_ajax endpoint creates adjustment without page reload
- Modal auto-refreshes after repayment showing updated net pay
- New refreshPreview() JS function enables re-fetching after AJAX
Other changes:
- Rename History to Work History in navbar
- Advance-specific payslip layout for pure advance payments
- Fix JS noProjectTypes to hide Project field for advance types
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Work History:
- Worker names now display as rounded pill badges instead of comma-
separated text, making them easier to scan (both server-rendered
list view and JS calendar detail view)
Payroll Dashboard:
- New "By Worker" toggle on the Monthly Payroll chart card
- Dropdown to select an active worker with payment history
- Stacked bar chart shows monthly breakdown: base pay, overtime,
bonuses (positive), deductions, loan repayments, advances (negative)
- All data pre-computed server-side with 2 aggregate queries and
embedded as JSON — switching workers is instant, no AJAX needed
- Only workers with actual payment history appear in the dropdown
- Legend items auto-hide when a component has no data for that worker
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Active/Inactive/All filter buttons weren't actually hiding rows because
Bootstrap's d-flex class uses display:flex !important, which beats inline
display:none. Switched to V2's approach: a .resource-hidden CSS class with
display:none !important that properly overrides d-flex.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ported from V2: three-button filter bar (Active | Inactive | All)
that shows/hides resource rows via JS data-active attribute.
Defaults to Active so inactive workers/projects/teams are hidden.
Toggle switch updates data-active instantly and re-applies filter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Removed start_date and end_date from Project model. Flatlogic doesn't
run migrations during rebuild, so the DB columns never got created,
crashing the site. Active/inactive resource split is kept.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dashboard Manage Resources now shows only active workers/projects/teams
by default. Inactive items are hidden behind a collapsible "Show X
Inactive" button — faded at 50% opacity. Tab badges show active counts.
Also adds start_date and end_date fields to Project model (optional).
Dates display under the project name in the resource list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Admin-only CSV export with name, ID number, phone, salary, daily rate,
employment date, active status, and notes. Button on dashboard next to
Manage Resources header.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>