61 Commits

Author SHA1 Message Date
Konrad du Plessis
10d381e2ae feat(adjustments): backend filter branch for ?status=adjustments
Type / worker / team / status / date filters, sort, stats, pagination.
Subquery pattern on the team filter avoids M2M JOIN inflation
(CLAUDE.md ORM gotcha). Group-by + bulk-delete + cross-filter
come later (Tasks 5/6/7).
2026-04-23 15:12:19 +02:00
Konrad du Plessis
f6975bfb2f feat(report): 'Last Activity' column in All Time Projects table
Konrad's Checkpoint-1 feedback:
  'Inside the all time projects table, can we have a column with the
  last transaction date for a project? It will make it easier to find
  data for projects. It is nice to have the filter, but you can still
  skip around looking for when the last transaction was.'

Added a 'last_activity' entry to each alltime_projects row in
_build_report_context — computed as max(WorkLog.date) grouped by
project name (respects the same project_ids/team_ids filters already
applied to all_time_logs). Rendered in both the on-screen report
(report.html) and the PDF (report_pdf.html) as a new 'Last Activity'
column sitting between 'Start' and 'Working Days'.

Existing ChapterOneEnrichmentTests extended with a last_activity
assertion locking in the 'most recent log date' semantics.

No other tests touched. 47/47 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:30:56 +02:00
Konrad du Plessis
71f8558ff5 feat(report): Until-primary date picker + date-scoped project/team lists
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>
2026-04-23 12:25:45 +02:00
Konrad du Plessis
5c4162d2eb fix(report): stop double-encoding project_team_pairs_json for pill cross-filter
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>
2026-04-23 10:20:58 +02:00
Konrad du Plessis
06f2e71d87 Backend: add project_team_pairs_json context for inline-filter cross-filter
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>
2026-04-23 09:35:36 +02:00
Konrad du Plessis
ea481bfbf4 Report: filter-pill strip with × to clear individual filters
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>
2026-04-22 23:14:48 +02:00
Konrad du Plessis
748c7c79d7 Modal: multi-select projects and teams via Choices.js
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>
2026-04-22 23:05:00 +02:00
Konrad du Plessis
16d192d5fc Refactor _build_report_context signature to multi-value filters
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>
2026-04-22 22:53:16 +02:00
Konrad du Plessis
ea1e4bdbcb Enrich alltime_projects context with working_days + avg_per_working_day
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>
2026-04-22 22:45:08 +02:00
Konrad du Plessis
e8ba2c6745 Add _team_project_activity helper + 4 tests
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>
2026-04-22 22:38:53 +02:00
Konrad du Plessis
ccc44a8d51 Fix _current_outstanding_in_scope sort + tighten team-filter test
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>
2026-04-22 22:35:30 +02:00
Konrad du Plessis
82594faad7 Add _current_outstanding_in_scope helper + 3 tests
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>
2026-04-22 22:29:41 +02:00
Konrad du Plessis
e74f48f050 Add _company_cost_velocity helper + 3 tests
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>
2026-04-22 22:20:14 +02:00
Konrad du Plessis
6be6a09056 Extract _compute_outstanding helper from index() (refactor)
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>
2026-04-22 22:11:58 +02:00
Konrad du Plessis
f1e246ce24 Fix: filtered payroll report inflates worker totals by N^2
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>
2026-04-22 19:51:07 +02:00
Konrad du Plessis
9276e588a0 Full-page view at /history/<id>/ for work log payroll status
Extends base.html; breadcrumb, attendance card, workers table,
adjustments card (conditional), totals. Pay-period uses
get_pay_period() and falls back to 'no schedule' + configure link.
2 view-level tests: admin 200, supervisor 403.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:34:16 +02:00
Konrad du Plessis
5720ca95ad AJAX endpoint returns JSON payload for work log payroll modal
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>
2026-04-22 14:57:16 +02:00
Konrad du Plessis
b0aa35661b Fix overtime_needs_pricing flag + add regression tests
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>
2026-04-22 14:21:34 +02:00
Konrad du Plessis
385d654082 Implement _build_work_log_payroll_context helper + 8 tests
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>
2026-04-22 14:01:14 +02:00
Konrad du Plessis
b4c3109c29 Add URL routes + stubs for work log payroll cross-link
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>
2026-04-22 13:43:13 +02:00
Konrad du Plessis
3c28387dd3 WIP: 2026-04-22 session checkpoint
Complete working state of the session. Will be split into two deploy
phases (safety scaffolding then feature release) before merging to ai-dev.

Includes:
- Security fixes (email creds / SECRET_KEY / DEBUG / CSRF)
- Backup + restore management commands and browser endpoints
- WeasyPrint migration (replaces xhtml2pdf)
- New Worker fields + WorkerCertificate + WorkerWarning models
- Worker / Team / Project friendly management UIs
- Dashboard cert-expiry card + Manage All buttons
- Bootstrap tooltips (global init + theme-aware CSS)
- Django admin template override (taller M2M pickers)
- Money filter for ZAR currency formatting
- Resources dropdown nav
- Massive CLAUDE.md expansion + deploy plan docs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 00:19:15 +02:00
Konrad du Plessis
60ee21dd61 Add Worker Lookup modal to payroll dashboard
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>
2026-04-20 14:15:04 +02:00
Konrad du Plessis
81009be0c6 Add PPE sizing and drivers license fields to Worker model
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>
2026-04-20 13:10:46 +02:00
Konrad du Plessis
cfed13c9f5 Add quick 'Adjust' button to pending payments table rows
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>
2026-03-25 10:25:10 +02:00
Konrad du Plessis
66fab12b90 Add 'Pay Immediately' option for New Loan adjustments
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>
2026-03-25 09:59:42 +02:00
Konrad du Plessis
1b6ade87af Add overdue badges and filters to pending payments table
- 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>
2026-03-24 22:53:47 +02:00
Konrad du Plessis
695b7cb3f1 Add 'Exclude workers with loans' checkbox to batch pay modal
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>
2026-03-24 22:45:49 +02:00
Konrad du Plessis
8d13c552aa Add batch pay mode toggle: Until Last Paydate / Pay All
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>
2026-03-24 22:26:33 +02:00
Konrad du Plessis
2e6881b7a4 Add batch pay feature and fix pay period cutoff logic
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>
2026-03-24 22:16:21 +02:00
Konrad du Plessis
2c8d80e4a1 Add /run-migrate/ endpoint for browser-based migration
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>
2026-03-24 21:19:55 +02:00
Konrad du Plessis
409e7bfd57 Add split payslip feature with team pay schedules
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>
2026-03-24 21:07:28 +02:00
Konrad du Plessis
ec5c4198d6 Add outstanding breakdown to payroll dashboard too
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>
2026-03-05 17:09:18 +02:00
Konrad du Plessis
d33d5943f9 Add outstanding payments breakdown on dashboard
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>
2026-03-05 15:56:40 +02:00
Konrad du Plessis
d51d06d28d Redesign advance payments: auto-process immediately with auto-repayment
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>
2026-03-05 14:23:03 +02:00
Konrad du Plessis
0257b454af Add Advance Payment system + enhanced preview modal with inline repayments
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>
2026-03-05 10:46:58 +02:00
Konrad du Plessis
19c662ec7d Fix 3 critical bugs in dashboard + attendance logging
- Fix outstanding payments: check per-worker (not per-log) to handle partially-paid WorkLogs
- Fix adjustment math: deductions now subtract from outstanding instead of adding
- Fix conflict resolution: use explicit worker ID list (QueryDict.getlist) instead of broken form.data.workers iteration
- Add missing migration 0003 for Project start_date/end_date fields
- Add CLAUDE.md project documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:28:11 +02:00
Konrad du Plessis
b7baf88cfc Add worker name pills on history page + per-worker payroll chart
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>
2026-02-24 19:22:20 +02:00
Konrad du Plessis
97866f1e74 Replace resource filter with V2's Active/Inactive/All button bar
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>
2026-02-23 01:25:29 +02:00
Konrad du Plessis
47de74bde4 Show active resources by default, collapse inactive + add project dates
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>
2026-02-23 00:49:57 +02:00
Konrad du Plessis
2aad9ac623 Add Export Workers CSV — downloads all worker data as spreadsheet
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>
2026-02-23 00:36:57 +02:00
Konrad du Plessis
b6fca98c17 Fix attendance start date, history worker filter, and add Amount column
1. Attendance form: Force start date to blank by clearing Django 5.x auto-fill
   from model default (default=timezone.now). Added self.fields['date'].initial=None
   in AttendanceLogForm.__init__().

2. History list view: When filtering by a specific worker, show only that
   worker's name in the Workers column (not all workers on the log). Uses
   filtered_worker_obj passed from the view.

3. History list view: Added Amount column (admin-only) showing daily cost.
   When filtering by worker, shows that worker's daily_rate. When unfiltered,
   shows total via new WorkLog.display_amount property (sum of all workers'
   daily_rate, uses prefetch cache for efficiency).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 00:13:46 +02:00
Konrad du Plessis
0b3ef5395f Fix work history filter — add validation, explicit form action, and visual feedback
- Add explicit action="{% url 'work_history' %}" to filter form (prevents
  potential URL mismatch on Flatlogic proxy)
- Add numeric validation for worker/project GET params (prevents 500 errors)
- Add results counter: "Showing X of Y work logs" when filters are active
- Add active filter badges showing worker name, project name, and status
- Add green left border indicator on filter card when filters are active
- Make Clear button conditional (red, only appears with active filters)
- Add SQLite dev toggle in settings.py for local testing without MariaDB

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:53:21 +02:00
Konrad du Plessis
b837932bb4 Fix Add Adjustment form silently failing — add validation + required fields
Root cause: V5 was missing required attributes that V2 had on the Add
Adjustment form. When a user submitted without selecting a project (for
types that require one), the server rejected it with messages.error()
but the error was invisible before the MESSAGE_TAGS fix. Combined with
no client-side validation for workers, the form would silently create
0 adjustments or redirect with no visible feedback.

Fixes:
- Add required attribute to Project select (toggles off for Loan types)
- Add client-side validation: blocks submit if no workers selected
- Add backend validation: returns error if no workers in POST data
- Add "Select All" / "Clear" links for worker checkboxes (matches V2)
- Add "X worker(s) selected" counter for visual feedback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:22:20 +02:00
Konrad du Plessis
0fa25e1538 Prevent duplicate payslip emails from double-click on Pay button
Problem: Supervisors on slow mobile connections sometimes double-click
the "Pay" button, causing two PayrollRecords + two payslip emails to
be sent to Spark Receipt for the same worker.

Backend fix (the critical part):
- Moved unpaid_logs and pending_adjs queries INSIDE transaction.atomic()
- Added select_for_update() on Worker row — this database-level lock
  forces the second concurrent request to WAIT until the first commits
- After the lock is acquired, the second request re-queries and finds
  no unpaid logs (already paid by first request), so it bails out

Frontend fix (defence-in-depth):
- Pay button now shows a Bootstrap spinner + "Processing..." text
- Second click is blocked with e.preventDefault() if button is
  already disabled (handles edge case where form resubmits)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:06:31 +02:00
Konrad du Plessis
f9423c0b3e Fix invisible error messages + UX improvements + calendar multi-select
1. Add MESSAGE_TAGS to settings.py — Django's messages.error() uses tag
   "error" but Bootstrap needs "danger". Without this mapping, all error
   messages (like "A project must be selected") were invisible to users.

2. Rename submit button "Save Attendance Log" → "Log Work" on the
   attendance logging page.

3. Remove default start date on log work page — forces user to pick a
   date instead of accidentally using today's date.

4. Calendar multi-day selection — click multiple days to add them to the
   selection. Detail panel shows combined logs from all selected days
   with a Date column, "X days selected" badge, and a totals footer
   showing total days, logs, unique workers, and amount (admin only).
   Click a selected day again to deselect it. Clear button resets all.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:00:04 +02:00
Konrad du Plessis
94c061fc19 Fix calendar detail showing all workers when filtering by one
When filtering by a single worker, log.workers.all() still returned
every worker on the WorkLog. Now the detail panel and cost calculation
only show the filtered worker, not the entire group.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 22:38:09 +02:00
Konrad du Plessis
19e565a088 Fix payroll dashboard JS crash + add calendar view to work history
1. Fix json_script double-encoding bug: payroll_dashboard view was
   passing json.dumps() strings to template context, then json_script
   filter serialized them AGAIN. JavaScript received strings instead
   of arrays, crashing the entire DOMContentLoaded handler and
   preventing preview, edit/delete, and other features from working.
   Fix: pass raw Python objects, let json_script handle serialization.

2. Add defense-in-depth: wrap Chart.js initialization in try-catch
   blocks and use Bootstrap getOrCreateInstance() for modals.

3. Add calendar view to work history: monthly grid with day cells
   showing work log indicators, click-to-see-details panel, month
   navigation, and responsive mobile layout. Ported from V2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 22:31:32 +02:00
Konrad du Plessis
2863f21844 Fix receipt IntegrityError: set zero defaults before first save
subtotal and total_amount have no default in the model, so the
first receipt.save() sent NULL to MariaDB which rejects it. Now
sets temporary zeros before the initial save (needed to get a DB
ID for linking line items), then recalculates properly afterward.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 22:00:50 +02:00
Konrad du Plessis
fc63d972b1 Add expense receipt feature: form, view, templates, email + PDF
Straight port from V2 adapted for V5 field names. Creates expense
receipts with dynamic line items, VAT calculation (Included/Excluded/
None at 15%), and emails HTML + PDF to Spark Receipt. Uses lazy
xhtml2pdf import to avoid crashing if not installed on server.

Files: forms.py (ExpenseReceiptForm + FormSet), views.py (create_receipt),
create_receipt.html, receipt_email.html, receipt_pdf.html, urls.py, base.html

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:38:14 +02:00
Konrad du Plessis
74cd93fede Fix 503: make xhtml2pdf import lazy to prevent app crash
If xhtml2pdf fails to install on Flatlogic's server (missing C
libraries), the top-level import crashed the entire WSGI app.
Now it imports lazily inside render_to_pdf() so the app starts
even without xhtml2pdf — only PDF generation degrades gracefully.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:07:33 +02:00