From 7b71048376d79b410d7816b66ba510e521697b9c Mon Sep 17 00:00:00 2001 From: Konrad du Plessis Date: Thu, 23 Apr 2026 19:20:49 +0200 Subject: [PATCH] feat(adjustments): sortable column headers with URL state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/templates/core/payroll_dashboard.html | 73 ++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/core/templates/core/payroll_dashboard.html b/core/templates/core/payroll_dashboard.html index 0aa154d..e26ebf1 100644 --- a/core/templates/core/payroll_dashboard.html +++ b/core/templates/core/payroll_dashboard.html @@ -803,14 +803,34 @@ aria-label="Select all unpaid adjustments on this page" title="Select all unpaid on this page"> - Date - Worker + {# === Sortable column headers — click toggles sort via filter form === #} + {# Each sortable reflects the current sort/order from adj_filter_values. #} + {# The arrow icon is fa-sort (inactive), fa-sort-down (desc) or fa-sort-up (asc). #} + {# JS click handler is wired up in the DOMContentLoaded block further down. #} + + Date + + + + Worker + + Type - Amount + + Amount + + Project Team Description - Status + + Status + + Actions @@ -3752,6 +3772,51 @@ document.addEventListener('DOMContentLoaded', function() { }); } + // === ADJUSTMENTS TAB — Sortable column headers === + // Click a to sort by that column. Same column + // twice flips asc/desc. A different column resets to desc. Submits + // the filter form so the sort persists in the URL and survives + // pagination. Keyboard Enter/Space also works (role="button"). + // Backend whitelists allowed columns in sort_map (see views.py), + // so the data-sort values here must match: date / worker / amount / status. + var adjFilterForm = document.getElementById('adjFilterForm'); + if (adjFilterForm) { + // Scope the query to the adjustments tab. The table sits after the + // filter bar as a sibling, so we walk up to the tab container and + // collect every sortable header inside it. + var adjFilterBar = document.getElementById('adjustmentsFilters'); + var adjTabRoot = adjFilterBar ? adjFilterBar.parentElement : document; + var sortInput = adjFilterForm.querySelector('input[name="sort"]'); + var orderInput = adjFilterForm.querySelector('input[name="order"]'); + + adjTabRoot.querySelectorAll('th.sortable').forEach(function(th) { + // Clicking a sortable mutates the hidden sort/order inputs + // and submits the form so the URL carries the new state. + function triggerSort() { + if (!sortInput || !orderInput) return; + var col = th.getAttribute('data-sort'); + if (sortInput.value === col) { + // Same column clicked again — flip direction. + orderInput.value = orderInput.value === 'asc' ? 'desc' : 'asc'; + } else { + // Different column — switch to it, start at desc (default). + sortInput.value = col; + orderInput.value = 'desc'; + } + adjFilterForm.submit(); + } + th.addEventListener('click', triggerSort); + // Keyboard access: Enter or Space triggers the same sort. + // preventDefault stops Space from scrolling the page. + th.addEventListener('keydown', function(ev) { + if (ev.key === 'Enter' || ev.key === ' ') { + ev.preventDefault(); + triggerSort(); + } + }); + }); + } + // --- Direct delete buttons on each unpaid row --- // Short-circuits the edit modal's usual 2-step delete flow by opening // #deleteConfirmModal directly with the correct form action + labels.