diff --git a/core/templates/core/payroll_dashboard.html b/core/templates/core/payroll_dashboard.html index 552c7bf..ca5f79c 100644 --- a/core/templates/core/payroll_dashboard.html +++ b/core/templates/core/payroll_dashboard.html @@ -706,41 +706,114 @@ - {# --- Status single-select (Unpaid / Paid / All) --- #} -
- - + {# --- Status filter: pill-button opens popover with 3 radios --- #} +
+ + + {# Actual submit value — rewritten by the Status popover OK handler #} +
+ {% if adj_filter_values.adj_status %} + + {% endif %} +
- {# --- Date picker: single by default, "..." toggles range mode --- #} - {# - Single mode (default): pick ONE date; JS mirrors it into the hidden #} - {# "To" field on submit so the backend's adj_date_from/adj_date_to #} - {# contract stays the same. #} - {# - Range mode: both pickers visible, JS leaves the "To" alone. #} - {# - Presets below: Today / This week / This month / Clear. #} -
- - - -
- - - - + {# --- Date filter: pill-button opens popover with mode + pickers + presets --- #} + {# Mode toggle (Single / Range) sits inside the popover. Presets live inside too. #} + {# Backend contract unchanged: hidden inputs adj_date_from + adj_date_to. #} +
+ + + {# Actual submit values — rewritten by the Date popover OK handler #} +
+ {% if adj_filter_values.adj_date_from %} + + {% endif %} + {% if adj_filter_values.adj_date_to %} + + {% endif %}
@@ -750,8 +823,8 @@ {# --- Group-by state (keeps Flat/By Type/By Worker across Apply) --- #} - {# --- Apply / Clear buttons --- #} -
+ {# --- Apply / Clear (pushed to the right end via .adj-apply-group) --- #} +
@@ -3585,10 +3658,15 @@ document.addEventListener('DOMContentLoaded', function() { if (ev.key === 'Escape') closeAllAdjPopovers(); }); - // --- OK / Cancel buttons inside each popover --- + // --- OK / Cancel buttons inside each CHECKBOX-style popover --- + // The Status and Date popovers below use radios / date inputs instead + // of checkboxes, so they register their own OK/Cancel handlers and are + // skipped here by gating on data-adj-filter in ['type', 'worker', 'team']. + var checkboxFilters = ['type', 'worker', 'team']; document.querySelectorAll('#adjustmentsFilters .filter-popover').forEach(function(pop) { var filterName = pop.closest('.filter-pill-wrap') .querySelector('.adj-filter-pill').getAttribute('data-adj-filter'); + if (checkboxFilters.indexOf(filterName) === -1) return; var okBtn = pop.querySelector('.popover-ok'); var cancelBtn = pop.querySelector('.popover-cancel'); okBtn.addEventListener('click', function() { @@ -3719,72 +3797,210 @@ document.addEventListener('DOMContentLoaded', function() { applyWorkerCrossFilter(); } - // === ADJUSTMENTS TAB — Date picker single/range toggle + presets === - // Single mode (default): one visible. On form submit, JS mirrors - // the From value into the hidden To so backend gets from==to (exact-day - // filter). Range mode: both inputs visible, user picks independently. - var dateWrap = document.getElementById('adjDateWrap'); - if (dateWrap) { - var dateFrom = document.getElementById('adjDateFrom'); - var dateTo = document.getElementById('adjDateTo'); - var toggleBtn = document.getElementById('adjDateRangeToggle'); - var dateLabel = document.getElementById('adjDateLabel'); + // === ADJUSTMENTS TAB — Status pill popover === + // Single hidden input adj_status, rewritten by the popover OK handler. + // The 3 radios inside the popover hold PENDING state; committed state + // lives in the .adj-hidden-inputs[data-adj-filter="adj_status"] div. + var statusPill = document.getElementById('adjStatusPill'); + if (statusPill) { + var statusPopover = document.getElementById('adjStatusPopover'); + var statusLabel = statusPill.querySelector('[data-pill-label]'); + var statusHiddenContainer = document.querySelector( + '.adj-hidden-inputs[data-adj-filter="adj_status"]' + ); - function applyMode(rangeMode) { - dateTo.hidden = !rangeMode; - dateLabel.textContent = rangeMode ? 'Date range' : 'Date'; - // In single mode mirror dateFrom into dateTo so form submit - // sends equal values for an exact-day match. - if (!rangeMode) dateTo.value = dateFrom.value; + function currentStatusValue() { + var hidden = statusHiddenContainer.querySelector('input[type="hidden"]'); + return hidden ? hidden.value : ''; + } + function statusValueToLabel(v) { + if (v === 'unpaid') return 'Status: Unpaid'; + if (v === 'paid') return 'Status: Paid'; + return 'Status'; + } + function renderStatusPillLabel() { + statusLabel.textContent = statusValueToLabel(currentStatusValue()); + } + function revertStatusRadios() { + // Match radio state to committed hidden input + var v = currentStatusValue(); + document.querySelectorAll('input[name="adj_status_pending"]').forEach(function(r) { + r.checked = (r.value === v); + }); + } + function commitStatus() { + // Rewrite hidden input from whichever radio is checked + var checked = document.querySelector('input[name="adj_status_pending"]:checked'); + var newVal = checked ? checked.value : ''; + statusHiddenContainer.replaceChildren(); // XSS-safe clear + if (newVal) { + var inp = document.createElement('input'); + inp.type = 'hidden'; + inp.name = 'adj_status'; + inp.value = newVal; + statusHiddenContainer.appendChild(inp); + } + renderStatusPillLabel(); } - // Start in range mode if URL already has different From and To - var urlFrom = dateFrom.value || ''; - var urlTo = dateTo.value || ''; - var initialRange = !!urlFrom && !!urlTo && urlFrom !== urlTo; - applyMode(initialRange); - toggleBtn.addEventListener('click', function() { - // Toggle: if hidden was the 'To' then we're switching to range - applyMode(dateTo.hidden); - }); - // Keep the mirror in sync while in single mode - dateFrom.addEventListener('change', function() { - if (dateTo.hidden) dateTo.value = dateFrom.value; - }); + // Initial pill label reflects committed state + renderStatusPillLabel(); - // Preset quick-buttons - function iso(d) { return d.toISOString().slice(0, 10); } + // OK + Cancel wiring + statusPopover.querySelector('.popover-ok').addEventListener('click', function() { + commitStatus(); + closeAllAdjPopovers(); + }); + statusPopover.querySelector('.popover-cancel').addEventListener('click', function() { + revertStatusRadios(); + closeAllAdjPopovers(); + }); + } + + // === ADJUSTMENTS TAB — Date pill popover (mode toggle + pickers + presets) === + // Three inputs inside the popover hold PENDING state: + // - adjDateSingle (single-mode date) + // - adjDateFrom / adjDateTo (range-mode dates) + // On OK the selected mode decides which pair of values gets written + // into the hidden inputs adj_date_from + adj_date_to. Cancel reverts. + var datePill = document.getElementById('adjDatePill'); + if (datePill) { + var datePopover = document.getElementById('adjDatePopover'); + var datePillLabel = datePill.querySelector('[data-pill-label]'); + var dateHiddenContainer = document.querySelector( + '.adj-hidden-inputs[data-adj-filter="date"]' + ); + var adjDateSingle = document.getElementById('adjDateSingle'); + var adjDateFrom = document.getElementById('adjDateFrom'); + var adjDateTo = document.getElementById('adjDateTo'); + var modeSingleRadio = document.getElementById('adjDateModeSingle'); + var modeRangeRadio = document.getElementById('adjDateModeRange'); + var singleFields = document.getElementById('adjDateSingleFields'); + var rangeFields = document.getElementById('adjDateRangeFields'); + + function committedDateFrom() { + var f = dateHiddenContainer.querySelector('input[name="adj_date_from"]'); + return f ? f.value : ''; + } + function committedDateTo() { + var t = dateHiddenContainer.querySelector('input[name="adj_date_to"]'); + return t ? t.value : ''; + } + function humanDate(ymd) { + // "2026-04-24" -> "24 Apr 2026" + if (!ymd) return ''; + var parts = ymd.split('-'); + var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + return parseInt(parts[2], 10) + ' ' + months[parseInt(parts[1], 10) - 1] + ' ' + parts[0]; + } + function renderDatePillLabel() { + var f = committedDateFrom(); + var t = committedDateTo(); + if (!f && !t) { datePillLabel.textContent = 'Date'; return; } + if (f && t && f === t) { + datePillLabel.textContent = 'Date: ' + humanDate(f); + } else if (f && t) { + datePillLabel.textContent = 'Date: ' + humanDate(f) + ' – ' + humanDate(t); + } else { + datePillLabel.textContent = 'Date: ' + humanDate(f || t); + } + } + function applyDateMode(rangeMode) { + modeRangeRadio.checked = rangeMode; + modeSingleRadio.checked = !rangeMode; + singleFields.classList.toggle('d-none', rangeMode); + rangeFields.classList.toggle('d-none', !rangeMode); + } + function populatePendingFromCommitted() { + // Seed the popover's input values from the committed hidden inputs + var f = committedDateFrom(); + var t = committedDateTo(); + if (f && t && f !== t) { + // Range + adjDateFrom.value = f; + adjDateTo.value = t; + adjDateSingle.value = ''; + applyDateMode(true); + } else { + // Single (or empty) + adjDateSingle.value = f || t || ''; + adjDateFrom.value = ''; + adjDateTo.value = ''; + applyDateMode(false); + } + } + function commitDate() { + var newFrom = '', newTo = ''; + if (modeRangeRadio.checked) { + newFrom = adjDateFrom.value || ''; + newTo = adjDateTo.value || ''; + } else { + newFrom = newTo = adjDateSingle.value || ''; + } + dateHiddenContainer.replaceChildren(); + if (newFrom) { + var f = document.createElement('input'); + f.type = 'hidden'; f.name = 'adj_date_from'; f.value = newFrom; + dateHiddenContainer.appendChild(f); + } + if (newTo) { + var t = document.createElement('input'); + t.type = 'hidden'; t.name = 'adj_date_to'; t.value = newTo; + dateHiddenContainer.appendChild(t); + } + renderDatePillLabel(); + } + + // Initial state + populatePendingFromCommitted(); + renderDatePillLabel(); + + // Mode toggle syncs the visible section + modeSingleRadio.addEventListener('change', function() { applyDateMode(false); }); + modeRangeRadio.addEventListener('change', function() { applyDateMode(true); }); + + // Preset buttons populate the pending fields (do NOT commit) + function isoDate(d) { return d.toISOString().slice(0, 10); } document.querySelectorAll('#adjDatePresets [data-preset]').forEach(function(btn) { btn.addEventListener('click', function() { var preset = btn.getAttribute('data-preset'); var today = new Date(); if (preset === 'today') { - dateFrom.value = iso(today); - dateTo.value = iso(today); - applyMode(false); + adjDateSingle.value = isoDate(today); + applyDateMode(false); } else if (preset === 'week') { - // ISO-week: Mon -> Sun - var dayOffset = (today.getDay() + 6) % 7; // 0 = Monday + var dayOffset = (today.getDay() + 6) % 7; var weekStart = new Date(today); weekStart.setDate(today.getDate() - dayOffset); var weekEnd = new Date(weekStart); weekEnd.setDate(weekStart.getDate() + 6); - dateFrom.value = iso(weekStart); - dateTo.value = iso(weekEnd); - applyMode(true); + adjDateFrom.value = isoDate(weekStart); + adjDateTo.value = isoDate(weekEnd); + applyDateMode(true); } else if (preset === 'month') { var monthStart = new Date(today.getFullYear(), today.getMonth(), 1); var monthEnd = new Date(today.getFullYear(), today.getMonth() + 1, 0); - dateFrom.value = iso(monthStart); - dateTo.value = iso(monthEnd); - applyMode(true); + adjDateFrom.value = isoDate(monthStart); + adjDateTo.value = isoDate(monthEnd); + applyDateMode(true); } else if (preset === 'clear') { - dateFrom.value = ''; - dateTo.value = ''; - applyMode(false); + adjDateSingle.value = ''; + adjDateFrom.value = ''; + adjDateTo.value = ''; + applyDateMode(false); } }); }); + + // OK + Cancel wiring + datePopover.querySelector('.popover-ok').addEventListener('click', function() { + commitDate(); + closeAllAdjPopovers(); + }); + datePopover.querySelector('.popover-cancel').addEventListener('click', function() { + populatePendingFromCommitted(); // revert + closeAllAdjPopovers(); + }); } // === ADJUSTMENTS TAB — Sortable column headers === diff --git a/static/css/custom.css b/static/css/custom.css index dc7518e..9651820 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1946,13 +1946,26 @@ body, .card, .modal-content, .form-control, .form-select, top: 0; z-index: 10; background: var(--bg-card); - padding: 0.75rem 1rem; + padding: 0.6rem 1rem; border-bottom: 1px solid var(--border-default); border-radius: 0.5rem 0.5rem 0 0; display: flex; flex-wrap: wrap; - gap: 0.75rem; - align-items: end; + gap: 0.5rem; + /* All children are now same-height pills — center-align them */ + align-items: center; +} +/* Apply / Clear push to the right end of the bar */ +.adjustments-filter-bar > form { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; +} +.adjustments-filter-bar .adj-apply-group { + margin-left: auto; + display: flex; + gap: 0.35rem; } /* --- Group header (collapsible section divider for group-by mode) --- */ @@ -2025,6 +2038,12 @@ body, .card, .modal-content, .form-control, .form-select, .adj-empty-state .adj-empty-icon { font-size: 2.5rem; opacity: 0.35; margin-bottom: 1rem; } /* --- Group-by toggle pill buttons (Flat / By Type / By Worker) --- */ +/* margin-top: 1rem gives breathing room between the sticky filter bar and + this row — they used to read as one cramped block. */ +.adj-groupby-toggle { + margin-top: 1rem; + margin-bottom: 0.75rem; +} .adj-groupby-toggle .btn { font-size: 0.8rem; padding: 0.3rem 0.75rem; } /* --- Sort header arrows --- */ @@ -2047,24 +2066,40 @@ th.sortable.sorted .sort-arrow { opacity: 1; } * is inherited. * ============================================================================= */ -/* --- Scrollable checkbox list inside each Adjustments popover --- */ -.adj-checkbox-list { - max-height: 280px; +/* --- Adjustments popover — tighter density for the checkbox/radio list --- + Font-size drop (~14px → 12.8px) + tighter row padding cuts the + 7-type Type popover from ~320px tall to ~240px without losing + readability. Radios in the Status popover and date fields in the + Date popover inherit the same size via .adj-radio-list. */ +.adj-checkbox-list, +.adj-radio-list { + max-height: 260px; overflow-y: auto; border: 1px solid var(--border-subtle); border-radius: 0.375rem; padding: 0.25rem 0.5rem; + font-size: 0.8rem; } -/* Each row is a full-width