+ {# --- 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. #}
+
@@ -750,8 +823,8 @@
{# --- Group-by state (keeps Flat/By Type/By Worker across Apply) --- #}
+ {# --- Apply / Clear (pushed to the right end via .adj-apply-group) --- #}
+
Apply
@@ -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 so clicking the text toggles the checkbox */
+/* Each row is a full-width so clicking the text toggles the input */
.adj-cb-row {
cursor: pointer;
margin: 0;
- padding: 0.2rem 0.25rem;
+ padding: 0.15rem 0.25rem;
border-radius: 0.25rem;
transition: background-color 120ms;
}
.adj-cb-row:hover { background: var(--bg-card-hover); }
.adj-cb-label { user-select: none; }
+.adj-cb-row .form-check-input { width: 0.9em; height: 0.9em; }
+
+/* Popover footer buttons (OK / Cancel / All / Invert / Clear) match the
+ same compact typography — reads as one cohesive strip. */
+.filter-popover__footer .btn { font-size: 0.75rem; padding: 0.25rem 0.6rem; }
+
+/* The Adjustments popover is also slightly narrower now (less visual weight
+ once the filter bar is all pills of the same size). */
+.adj-filter-pill + .filter-popover,
+#adjustmentsFilters .filter-popover { max-width: 360px; }
/* --- Count badge shown on the pill when 2+ options are selected --- */
/* (For 0 or 1 selected the label text carries the info; the badge stays hidden.) */