40 Commits

Author SHA1 Message Date
Konrad du Plessis
6f66faf06a feat(adjustments): filter bar v2 — unify all 5 filters as pills + density pass
Konrad's feedback on the shipped Adjustments tab: "this interface
layout is very ugly. And the selection dropdown menus text is a bit
large." Plus: the 'Show as' toggle sits too close to the filter bar.

Design doc: docs/plans/2026-04-23-adjustments-filter-bar-v2-design.md

Changes:

1. All 5 filters become pill-popovers of identical shape
   - Type / Workers / Teams: unchanged (already pills)
   - Status: was <select> + <label>, now pill → popover with 3 radios
   - Date: was inline inputs + preset links + '...' toggle, now pill →
     popover with Single/Range mode toggle + picker(s) + presets + OK/Cancel
   - Pill labels update to 'Status: Unpaid' / 'Date: 24 Apr 2026' /
     'Date: 20 Apr – 26 Apr 2026' for at-a-glance state
   - Apply + Clear pushed to right end via .adj-apply-group (margin-left: auto)

2. Popover density pass
   - .adj-checkbox-list / .adj-radio-list font-size 0.8rem (~12.8px)
   - .adj-cb-row padding trimmed to 0.15rem 0.25rem
   - Checkbox visual size 0.9em
   - Popover footer buttons 0.75rem font, 0.25rem 0.6rem padding
   - Popover max-width 360px (was ~420px)
   - 7-type popover drops from ~320px tall to ~240px

3. Spacing fix above 'Show as:' toggle
   - .adj-groupby-toggle now has margin-top: 1rem + margin-bottom: 0.75rem
   - Clear visual separation from the sticky filter bar

4. Filter-bar alignment
   - align-items: center (was end, now all children are same height)
   - Gap tightened to 0.5rem

Backend contract unchanged (query params identical). No test changes
(65/65 still pass). Committed popover JS uses the same
.adj-hidden-inputs pattern as the checkbox filters — Status + Date
each have their own commit/revert logic that rewrites their hidden
inputs on OK. XSS-safe throughout (replaceChildren() + textContent,
no innerHTML with user data).

Gated the generic checkbox-popover OK/Cancel handler to
['type', 'worker', 'team'] so the new Status/Date popovers aren't
accidentally re-committed via commitCheckboxes.
2026-04-23 22:00:27 +02:00
Konrad du Plessis
672c32cfb6 ux(adjustments): drop the per-row Delete button — Edit modal handles it
Konrad's feedback: the red × Delete button on each unpaid row was
redundant — the Edit Adjustment modal already has a Delete action
inside it, so users never need a second entry point.

Removed: the .adj-delete-btn button from _adjustment_row.html and its
now-dead DOMContentLoaded handler in payroll_dashboard.html (~15 lines
of JS). Unpaid rows now show Preview + Edit only.

Bulk-delete is unaffected: the floating action bar + per-row checkboxes
remain as the fast path for deleting many rows at once. Single-row
delete flows through the Edit modal's existing delete button.

65/65 tests still pass.
2026-04-23 19:57:00 +02:00
Konrad du Plessis
3fe3e5aa01 fix(adjustments): group-by uses full filtered queryset + Apply keeps group mode
Two final-review follow-ups from the whole-feature code review:

1. Important: group-by was bucketing adj_page.object_list (the paginated
   50-row slice), making 'By Type' group headers show misleading per-page
   totals once filters returned >50 rows. Konrad's current data is under
   the threshold, but the UI promised whole-filter totals.

   Fix: group_by runs on the full filtered queryset (list(adjustments))
   BEFORE pagination. Template already branches on adj_groups, so we now
   additionally hide the pagination nav when grouped — the group headers
   act as their own navigation and their counts/sums reflect the whole
   filter not just one page.

2. Minor: Apply after picking 'By Worker' silently reset to Flat view
   because the filter form had hidden inputs for sort/order but not
   group_by. Added the missing <input type='hidden' name='group_by'>
   so the toggle round-trips across Apply.

65/65 tests still pass (no test changes — the previous tests' fixtures
are all <50 rows so neither the bug nor the fix shows up there, but
both behaviours are now correct).
2026-04-23 19:37:57 +02:00
Konrad du Plessis
9bb9ede300 polish(adjustments): empty-state card with recovery CTAs + sticky bar check
No-rows case now renders a proper card with two recovery paths:
Clear filters (href back to ?status=adjustments) and Add adjustment
(opens the existing #addAdjustmentModal — no new JS).

Sticky filter bar (Task 2 CSS) verified functional — no ancestor
clipping or overflow:hidden in the adjustments tab block.
2026-04-23 19:23:39 +02:00
Konrad du Plessis
7b71048376 feat(adjustments): sortable column headers with URL state
4 sortable columns: Date, Worker, Amount, Status. Click cycles
desc -> asc -> desc. Click a different column -> resets to desc.
Keyboard Enter / Space also works (role=button + tabindex=0).

The sort/order state lives in hidden inputs inside the adjustments
filter form, so the JS just mutates those and .submit()s — the sort
then piggy-backs on the same GET the filter bar uses, and the URL
retains it across pagination. Backend sort_map (Task 3) already
whitelists the allowed columns, so no SQL-injection surface.

Arrow icons reflect state: fa-sort (inactive), fa-sort-down (desc),
fa-sort-up (asc). Active column gets .sorted class for stronger
arrow opacity (CSS already shipped in Task 2).

No backend changes, no new tests — the existing 65 tests cover the
sort contract from the URL.
2026-04-23 19:20:49 +02:00
Konrad du Plessis
c851b49dea feat(adjustments): date picker single/range toggle + preset quick-buttons
Single by default (one <input> + '...' toggle reveals the second).
In single mode the JS mirrors From into the hidden To on every
change, so form submit sends adj_date_from=adj_date_to=X for an
exact-day filter on the backend (contract unchanged).

Four presets: Today (single), This week (Mon-Sun range), This month
(1st to last, range), Clear. Presets auto-switch mode so users see
what was populated.

On page load, range mode is inferred from the URL: if both dates
present AND differ -> range mode; else single mode. That way a
bookmarked range URL still shows both pickers.

No backend changes, no new tests — the 8 existing adjustments tests
already cover the from/to contract shape.
2026-04-23 19:17:19 +02:00
Konrad du Plessis
6905703492 feat(adjustments): Team -> Workers cross-filter in the popover JS
When Team(s) are selected via the Teams popover, the Workers popover
now only shows workers who belong to at least one of those teams.
URL-selected workers stay visible regardless (so the user can see
and untick them).

Backend adds one context key: team_worker_pairs_json — raw Python
list of {team_id, worker_id} dicts from Team.workers.through;
template renders via |json_script (safe, no double-encoding).

Frontend reads the JSON once, builds a team_id -> Set(worker_id)
index, and on every Workers-popover open (and on Teams-popover OK)
hides rows whose worker is out-of-team. display:none on the <label>
row is visually cleaner than disabling the checkbox alone.

Scope: entire roster (not date-range-scoped) — cross-filter is
about data possibility, not data in this period.

One new test locks in the pairs-context-key shape (asserts it's a
raw Python list of dicts, not a pre-serialised JSON string —
guards against the double-encoding regression from Feature 1).
65/65 tests pass.
2026-04-23 19:13:10 +02:00
Konrad du Plessis
5f2e6d8c74 fix(adjustments): rename Select-All header checkbox id to avoid collision
Code-review follow-up on Task 6:

Task 4 gave the Adjustments table's 'select all' header checkbox the
id 'adjSelectAll' — but the Add-Adjustment modal already had an
<a id='adjSelectAll'> Select-All anchor (and a matching JS click
handler on line 1823). Duplicate IDs are invalid HTML, and
getElementById returns the first occurrence in DOM order — so the
modal's Select-All handler silently started binding to the table
checkbox instead of its intended anchor. Never reported because
neither element was automated-tested.

Rename the table checkbox id to #adjTableSelectAll and update Task 6's
bulk-select JS to match. The modal's handler now correctly binds to
its own anchor again.

62/62 tests still pass — behaviour is template-driven UI, no backend
change.
2026-04-23 18:57:43 +02:00
Konrad du Plessis
03f177e7d0 feat(adjustments): bulk-delete unpaid rows + floating action bar
New POST /payroll/adjustments/bulk-delete/ endpoint takes a list of
adjustment_ids and DELETEs the ones that are still unpaid
(payroll_record__isnull=True at the DB level) — paid rows are silently
skipped, defensive against stale-UI race conditions. Admin-only;
supervisors get 403. Returns JSON {deleted, requested}.

Floating bar slides up from the bottom of the viewport when >=1 row
selected: shows count + Delete + Clear. Confirm dialog guards the
POST. On success, page reloads to reflect the new state.

CSRF via X-CSRFToken header from the csrftoken cookie (Django
middleware sets this). Two new tests lock in the 'only unpaid' +
'admin-only' contracts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:51:54 +02:00
Konrad du Plessis
0862805623 feat(adjustments): group-by type / worker + collapsible headers
Adds _group_adjustments helper that buckets a flat queryset by type or
by worker_id, with signed net_sum (+ for additive, - for deductive)
and descending-magnitude ordering so the biggest-impact bucket sits
at the top.

Template branches on adj_groups: grouped view renders one <tbody>
per group with a Bootstrap-collapse-driven header row, wrapping
include of _adjustment_row.html for the actual rows (no duplication).
Flat view is the default when group_by is empty.

By Type headers get a 4px left-border accent in the matching badge
colour so grouped rows visually echo the badges below them.
Attribute-selector based ([data-type=Bonus] etc.) so the
CSS stays self-descriptive without per-type class explosion.

Adds |money_abs template filter for signed render ('-R 100.00' in
the template becomes money_abs(-100) -> '100.00' after the caller
emits its own sign; avoids 'R -100.00' which reads wrong).

Two new tests lock in the bucket structure + net_sum signing for
both axes. Tests 58 -> 60. url_replace template tag already shipped
in the CP1 pagination fix - reused here for the toggle hrefs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:34:07 +02:00
Konrad du Plessis
4f15e4bd5f feat(adjustments): replace Choices.js chip-multiselect with popover-checkbox filters
Checkpoint-1 feedback from Konrad: the Choices.js chip pattern for
Type / Workers / Teams was visually intrusive once multiple options
were picked — the filter bar dominated the viewport.

Replacement: each filter is now a compact pill (like Feature 1's
inline-filter pills on the report page) that opens a popover with a
scrollable checkbox list, live-search, and Select All / Invert /
Clear action buttons. OK commits the pending state into hidden form
inputs; Cancel / Esc / click-outside revert. The existing Apply button
still submits the form normally.

Reuses Feature 1's .filter-pill / .filter-popover CSS vocabulary —
only new CSS is a scrollable checkbox-list rule and a pill-count
badge style. No new modals. Choices.js CDN stays loaded (other
tabs still use it).
2026-04-23 17:07:50 +02:00
Konrad du Plessis
e088192103 fix(adjustments): convert multi-line {# #} comments to {% comment %} blocks
Checkpoint-1 bug: the row partial's docstring used a multi-line {# ... #}
block. Django's single-line comment syntax doesn't match across newlines,
so the opening {# and closing #} were treated as literal text and spilled
into every rendered row — flooding the table body with the raw comment.
Worse, the browser partially parsed the literal <tr> inside the comment
text as an HTML tag, breaking the table layout entirely.

Fix: moved the multi-line docstring into a {% comment %}...{% endcomment %}
block and compressed three other multi-line {# #} blocks to single lines.

Also tripped on a second foot-gun: you can't put literal {# or #} inside
a {% comment %} block — Django's tokenizer still sees them as a nested
comment marker. Removed the meta-note about "{# ... #} is single-line
only" from inside the comment block.

All 58 tests pass. Table renders correctly with all 10 columns + type
badges + row actions visible.
2026-04-23 16:19:38 +02:00
Konrad du Plessis
06b3315641 fix(adjustments): pagination URL + filter label accessibility + teams.first N+1
Three code-review fixes:

1. Pagination links were building ?status=...&page=2&page=3 on every
   click because the template appended &page= onto an already-
   serialised querystring. Added a reusable url_replace template tag
   that replaces a single key (pre-empts Tasks 5 / 9 which also
   need it) and piped the pagination hrefs through it. Added
   rel=prev/next + aria-label on the <a> tags while we were here.

2. Filter-bar labels had no for= attribute, so screen readers
   announced the native <select> with no name. Added id= on each
   select/input and matching for= on each label. Also gave the
   Select-all checkbox an aria-label (title= alone is not an
   accessible name).

3. Row template's {% with team=adj.worker.teams.first %} issued a
   fresh ORDER BY ... LIMIT 1 query per row despite the view's
   prefetch_related('worker__teams'). Swapped to {% with
   teams=adj.worker.teams.all %} which DOES use the prefetch cache,
   bounding the Team column at 0 extra queries (was ~50 per page).
2026-04-23 15:45:41 +02:00
Konrad du Plessis
b450bd3c39 feat(adjustments): Adjustments tab — nav + filter bar + flat table + row actions
Reuses existing modals (#editAdjustmentModal, delete confirm flow) —
zero new JS for row actions. Choices.js lazy-inits only when the tab
is active. Stats row scoped to filter set. Subquery pattern on team
filter (CLAUDE.md). Group-by + bulk-delete + cross-filter come in
Tasks 5/6/7.
2026-04-23 15:34:09 +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
deef851e52 Fix dark mode contrast: table text, loan badges, modals, disabled inputs
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>
2026-04-20 18:57:44 +02:00
Konrad du Plessis
82c1906607 Redesign UI with premium orange theme, sidebar nav, and bottom tab bar
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>
2026-04-20 18:40:00 +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
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
72d40971f1 Update batch pay modal: 3-option loan filter + radio button fix
- 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>
2026-03-25 09:23:01 +02:00
Konrad du Plessis
3bb75c5615 Replace loan checkbox with 3-option dropdown on pending table
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>
2026-03-24 22:57:14 +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
00f16df8b1 Add team filter dropdown to batch pay modal
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>
2026-03-24 22:37:48 +02:00
Konrad du Plessis
9ebaae1b0c Fix batch pay radio toggle: use persistent JS reference for radio group
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>
2026-03-24 22:30:11 +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
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
44a0030c46 Show monthly total in project chart tooltip
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>
2026-03-05 17:32:14 +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
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
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
81573ba814 Fix broken Run Payroll link + redesign dashboard stat cards
- index.html: Fix Run Payroll shortcut (href="#" → payroll_dashboard URL)
- index.html: Remove max-height scroll on Outstanding by Project card
- payroll_dashboard.html: Redesign analytics cards — 7/5 split layout with
  3 stat cards stacked left, project breakdown card right (no scroll)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 16:08:32 +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
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
c8c78dd88e Add payslip feature: detail page, PDF generation, and email to Spark
- core/utils.py: render_to_pdf() wrapper for xhtml2pdf
- core/templates/core/pdf/payslip_pdf.html: A4 PDF payslip (matches V2 layout)
- core/templates/core/email/payslip_email.html: HTML email body for Spark
- core/templates/core/payslip.html: browser payslip detail page with print
- core/views.py: add payslip_detail view, wire email+PDF into process_payment
- core/urls.py: add payroll/payslip/<pk>/ route
- config/settings.py: add SPARK_RECEIPT_EMAIL setting
- payroll_dashboard.html: add "View" payslip link in Payment History tab

All templates show adjustments (bonuses, deductions, overtime, loan repayments)
as line items. Amounts always show 2 decimal places. Email failure does not
roll back payment — handled gracefully with warning message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 20:37:04 +02:00
Konrad du Plessis
efe5f08682 Add Phase 3: Payroll Dashboard with full payment processing
- PayrollAdjustmentForm with project validation for types that require it
- 7 payroll views: dashboard, process_payment, price_overtime, add/edit/delete
  adjustment, preview_payslip (all admin-only)
- Payroll dashboard template with analytics cards, Chart.js charts (monthly
  totals + per-project costs), 3 tabs (Pending/Paid/Loans), 5 modals
- XSS-safe JavaScript using createElement+textContent (zero innerHTML)
- Fix: outstanding-by-project now handles partially-paid WorkLogs per-worker
- Fix: active loan count and balance computed via aggregate in view
- Payroll navbar link wired up, 7 URL patterns added
- Zero model/migration changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:47:12 +02:00