diff --git a/core/templates/core/payroll_dashboard.html b/core/templates/core/payroll_dashboard.html index d8b5a3e..df331a1 100644 --- a/core/templates/core/payroll_dashboard.html +++ b/core/templates/core/payroll_dashboard.html @@ -556,47 +556,151 @@ {# Row actions reuse the existing Edit / Delete / Preview modals so no new JS is needed. #} {% if active_tab == 'adjustments' %} - {# --- Sticky filter bar (Choices.js enhances the multi-selects below) --- #} + {# --- Sticky filter bar (pill-popover checkbox filters for Type/Workers/Teams) --- #}
- {# --- Type multi-select (Bonus / Overtime / etc.) --- #} - {# Each label/input pair below has matching for=/id= so screen #} - {# readers announce the field name when focus moves into the #} - {# select. The .adj-multi class still drives Choices.js init — #} - {# adding id= is purely additive. #} -
- - +
+ {% for t in adj_type_choices %} + + {% endfor %} +
+
+ +
+ {# Hidden inputs — the actual form-submit state. Rewritten on OK. #} +
+ {% for v in adj_filter_values.type %} + {% endfor %} - +
- {# --- Workers multi-select (cross-filtered by Teams in Task 7) --- #} -
- - +
+ {% for w in all_workers_for_filter %} + + {% endfor %} +
+
+ + +
+ {% for v in adj_filter_values.worker %} + {% endfor %} - +
- {# --- Teams multi-select --- #} -
- - +
+ {% for t in all_teams_for_filter %} + + {% endfor %} +
+
+ + +
+ {% for v in adj_filter_values.team %} + {% endfor %} - +
{# --- Status single-select (Unpaid / Paid / All) --- #} @@ -3244,24 +3348,175 @@ document.addEventListener('DOMContentLoaded', function() { } // ================================================================= - // ADJUSTMENTS TAB — Choices.js multi-selects + direct delete button + // ADJUSTMENTS TAB — pill-popover filter module + direct delete button // Both blocks are no-ops on other tabs (the filter bar element only // exists when the Adjustments tab is active). // ================================================================= if (document.getElementById('adjustmentsFilters')) { - // --- Lazy-init Choices.js on the three multi-selects --- - // Choices.js is heavy, so we only enhance selects on this tab. - if (typeof Choices !== 'undefined') { - document.querySelectorAll('#adjFilterForm .adj-multi').forEach(function(sel) { - new Choices(sel, { - removeItemButton: true, - shouldSort: false, - placeholder: true, - placeholderValue: sel.getAttribute('data-placeholder') || '', - }); + // === Pill-popover filter module === + // Replaces the prior Choices.js chip-multiselect for Type / Workers / Teams. + // Each pill opens a popover with a checkbox list + search + action buttons. + // OK commits the pending checkbox state into the hidden block that + // the form submits; Cancel / Esc / click-outside revert to the URL state. + + // --- Display labels shown on each pill button --- + var filterLabels = { type: 'Type', worker: 'Workers', team: 'Teams' }; + + // --- Close every adj popover except (optionally) one we want to keep open --- + function closeAllAdjPopovers(except) { + document.querySelectorAll('.adj-filter-pill').forEach(function(pill) { + var id = pill.getAttribute('aria-controls'); + if (id !== except) { + var pop = document.getElementById(id); + if (pop) pop.hidden = true; + pill.setAttribute('aria-expanded', 'false'); + } }); } + // --- Rewrite the pill's label based on how many hidden inputs exist --- + // 0 selected: "Type" + // 1 selected: "Type: Bonus" (looks up checkbox label text) + // 2+ selected: "Type (2)" with a count badge + function renderPillLabel(filterName) { + var pill = document.querySelector('.adj-filter-pill[data-adj-filter="' + filterName + '"]'); + var labelEl = pill.querySelector('[data-pill-label]'); + var countEl = pill.querySelector('[data-pill-count]'); + var hiddenInputs = document.querySelectorAll( + '.adj-hidden-inputs[data-adj-filter="' + filterName + '"] input[type=hidden]' + ); + var n = hiddenInputs.length; + if (n === 0) { + labelEl.textContent = filterLabels[filterName]; + countEl.hidden = true; + } else if (n === 1) { + var v = hiddenInputs[0].value; + // Prefer the checkbox's visible label text over the raw value (IDs read poorly) + var cb = document.querySelector( + '.adj-filter-cb[data-adj-filter="' + filterName + '"][value="' + v + '"]' + ); + var text = cb ? cb.closest('label').querySelector('.adj-cb-label').textContent : v; + labelEl.textContent = filterLabels[filterName] + ': ' + text; + countEl.hidden = true; + } else { + labelEl.textContent = filterLabels[filterName]; + countEl.textContent = '(' + n + ')'; + countEl.hidden = false; + } + } + + // --- Reset checkboxes to match the current hidden-input set (Cancel / initial load) --- + function revertCheckboxes(filterName) { + var values = Array.from(document.querySelectorAll( + '.adj-hidden-inputs[data-adj-filter="' + filterName + '"] input[type=hidden]' + )).map(function(i) { return i.value; }); + document.querySelectorAll( + '.adj-filter-cb[data-adj-filter="' + filterName + '"]' + ).forEach(function(cb) { + cb.checked = values.indexOf(cb.value) !== -1; + }); + } + + // --- Commit checkbox state into the hidden-input block (OK) --- + // Clears then rebuilds the hidden inputs so the form submits exactly + // the set of values currently ticked. Uses replaceChildren() to clear + // (DOM-safe — no innerHTML) then createElement per checked box. + function commitCheckboxes(filterName) { + var container = document.querySelector( + '.adj-hidden-inputs[data-adj-filter="' + filterName + '"]' + ); + container.replaceChildren(); // wipe all previous hidden inputs + document.querySelectorAll( + '.adj-filter-cb[data-adj-filter="' + filterName + '"]:checked' + ).forEach(function(cb) { + var inp = document.createElement('input'); + inp.type = 'hidden'; + inp.name = filterName; + inp.value = cb.value; + container.appendChild(inp); + }); + renderPillLabel(filterName); + } + + // --- Initial paint: set pill labels from the URL's hidden-input state --- + ['type', 'worker', 'team'].forEach(renderPillLabel); + + // --- Pill click: toggle its popover (and close any sibling popover) --- + document.querySelectorAll('.adj-filter-pill').forEach(function(pill) { + pill.addEventListener('click', function(ev) { + ev.stopPropagation(); + var popoverId = pill.getAttribute('aria-controls'); + var pop = document.getElementById(popoverId); + var isOpen = !pop.hidden; + if (isOpen) { + pop.hidden = true; + pill.setAttribute('aria-expanded', 'false'); + } else { + closeAllAdjPopovers(popoverId); + pop.hidden = false; + pill.setAttribute('aria-expanded', 'true'); + // Focus the search input so the user can type immediately + var search = pop.querySelector('[data-popover-search]'); + if (search) setTimeout(function() { search.focus(); }, 0); + } + }); + }); + + // --- Click outside any filter-pill-wrap closes all popovers --- + document.addEventListener('click', function(ev) { + if (!ev.target.closest('.filter-pill-wrap')) closeAllAdjPopovers(); + }); + // --- Esc closes all popovers (handy when focus is in the search box) --- + document.addEventListener('keydown', function(ev) { + if (ev.key === 'Escape') closeAllAdjPopovers(); + }); + + // --- OK / Cancel buttons inside each popover --- + document.querySelectorAll('#adjustmentsFilters .filter-popover').forEach(function(pop) { + var filterName = pop.closest('.filter-pill-wrap') + .querySelector('.adj-filter-pill').getAttribute('data-adj-filter'); + var okBtn = pop.querySelector('.popover-ok'); + var cancelBtn = pop.querySelector('.popover-cancel'); + okBtn.addEventListener('click', function() { + commitCheckboxes(filterName); + closeAllAdjPopovers(); + }); + cancelBtn.addEventListener('click', function() { + revertCheckboxes(filterName); + closeAllAdjPopovers(); + }); + }); + + // --- Select All / Clear / Invert buttons --- + // Important: these operate on VISIBLE rows only — so if the user has + // typed into the search box, "All" ticks only the matching entries. + document.querySelectorAll('#adjustmentsFilters [data-popover-action]').forEach(function(btn) { + btn.addEventListener('click', function() { + var action = btn.getAttribute('data-popover-action'); + var pop = btn.closest('.filter-popover'); + var visibleRows = Array.from(pop.querySelectorAll('.adj-cb-row')) + .filter(function(r) { return r.style.display !== 'none'; }); + visibleRows.forEach(function(row) { + var cb = row.querySelector('.adj-filter-cb'); + if (action === 'select-all') cb.checked = true; + else if (action === 'clear') cb.checked = false; + else if (action === 'invert') cb.checked = !cb.checked; + }); + }); + }); + + // --- Search input: live-filter visible checkbox rows --- + document.querySelectorAll('#adjustmentsFilters [data-popover-search]').forEach(function(search) { + search.addEventListener('input', function() { + var q = search.value.toLowerCase().trim(); + var pop = search.closest('.filter-popover'); + pop.querySelectorAll('.adj-cb-row').forEach(function(row) { + var label = row.querySelector('.adj-cb-label').textContent.toLowerCase(); + row.style.display = (!q || label.indexOf(q) !== -1) ? '' : 'none'; + }); + }); + }); + // --- 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. diff --git a/static/css/custom.css b/static/css/custom.css index f4a23ea..451d37b 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -2018,3 +2018,39 @@ th.sortable .sort-arrow { } th.sortable:hover .sort-arrow, th.sortable.sorted .sort-arrow { opacity: 1; } + +/* ============================================================================= + * ADJUSTMENTS TAB — pill-popover checkbox list + * The Type / Workers / Teams filters each open a popover that reuses the + * shared .filter-popover styles (see "Inline Filters" block above). This + * section only adds the bits specific to the checkbox-list body — the rest + * of the visual vocabulary (pill button, popover chrome, sticky footer) + * is inherited. + * ============================================================================= */ + +/* --- Scrollable checkbox list inside each Adjustments popover --- */ +.adj-checkbox-list { + max-height: 280px; + overflow-y: auto; + border: 1px solid var(--border-subtle); + border-radius: 0.375rem; + padding: 0.25rem 0.5rem; +} +/* Each row is a full-width