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.
This commit is contained in:
parent
620f433d06
commit
6f66faf06a
@ -706,41 +706,114 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- Status single-select (Unpaid / Paid / All) --- #}
|
||||
<div style="min-width: 120px;">
|
||||
<label for="adjStatusSelect" class="form-label small mb-1">Status</label>
|
||||
<select id="adjStatusSelect" name="adj_status" class="form-select form-select-sm">
|
||||
<option value="" {% if not adj_filter_values.adj_status %}selected{% endif %}>All</option>
|
||||
<option value="unpaid" {% if adj_filter_values.adj_status == 'unpaid' %}selected{% endif %}>Unpaid</option>
|
||||
<option value="paid" {% if adj_filter_values.adj_status == 'paid' %}selected{% endif %}>Paid</option>
|
||||
</select>
|
||||
{# --- Status filter: pill-button opens popover with 3 radios --- #}
|
||||
<div class="filter-pill-wrap position-relative">
|
||||
<button type="button"
|
||||
class="filter-pill filter-pill--editable adj-filter-pill"
|
||||
id="adjStatusPill" data-adj-filter="adj_status"
|
||||
aria-expanded="false" aria-controls="adjStatusPopover">
|
||||
<i class="fas fa-filter me-1"></i>
|
||||
<span class="filter-pill__label" data-pill-label>Status</span>
|
||||
<i class="fas fa-chevron-down ms-2 small filter-pill__chevron"></i>
|
||||
</button>
|
||||
<div class="filter-popover" id="adjStatusPopover" role="dialog"
|
||||
aria-label="Filter by status" hidden>
|
||||
<div class="filter-popover__body">
|
||||
<div class="adj-radio-list" data-popover-list>
|
||||
<label class="d-flex align-items-center gap-2 py-1 adj-cb-row">
|
||||
<input type="radio" class="form-check-input adj-status-radio"
|
||||
name="adj_status_pending" value=""
|
||||
{% if not adj_filter_values.adj_status %}checked{% endif %}>
|
||||
<span class="adj-cb-label">All</span>
|
||||
</label>
|
||||
<label class="d-flex align-items-center gap-2 py-1 adj-cb-row">
|
||||
<input type="radio" class="form-check-input adj-status-radio"
|
||||
name="adj_status_pending" value="unpaid"
|
||||
{% if adj_filter_values.adj_status == 'unpaid' %}checked{% endif %}>
|
||||
<span class="adj-cb-label">Unpaid</span>
|
||||
</label>
|
||||
<label class="d-flex align-items-center gap-2 py-1 adj-cb-row">
|
||||
<input type="radio" class="form-check-input adj-status-radio"
|
||||
name="adj_status_pending" value="paid"
|
||||
{% if adj_filter_values.adj_status == 'paid' %}checked{% endif %}>
|
||||
<span class="adj-cb-label">Paid</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-popover__footer d-flex gap-1 justify-content-end">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary popover-cancel">Cancel</button>
|
||||
<button type="button" class="btn btn-sm btn-accent popover-ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
{# Actual submit value — rewritten by the Status popover OK handler #}
|
||||
<div class="adj-hidden-inputs" data-adj-filter="adj_status">
|
||||
{% if adj_filter_values.adj_status %}
|
||||
<input type="hidden" name="adj_status" value="{{ adj_filter_values.adj_status }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- 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. #}
|
||||
<div id="adjDateWrap" style="min-width: 180px;">
|
||||
<label class="form-label small mb-1 d-flex align-items-center gap-2">
|
||||
<span id="adjDateLabel">Date</span>
|
||||
<button type="button" id="adjDateRangeToggle" class="btn btn-link btn-sm p-0 ms-auto"
|
||||
title="Toggle range mode" aria-label="Toggle date range mode">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
</label>
|
||||
<input type="date" name="adj_date_from" id="adjDateFrom"
|
||||
class="form-control form-control-sm"
|
||||
value="{{ adj_filter_values.adj_date_from }}">
|
||||
<input type="date" name="adj_date_to" id="adjDateTo"
|
||||
class="form-control form-control-sm mt-1"
|
||||
value="{{ adj_filter_values.adj_date_to }}" hidden>
|
||||
<div class="d-flex gap-2 mt-1 small" id="adjDatePresets">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-muted" data-preset="today">Today</button>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-muted" data-preset="week">This week</button>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-muted" data-preset="month">This month</button>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-muted" data-preset="clear">Clear</button>
|
||||
{# --- 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. #}
|
||||
<div class="filter-pill-wrap position-relative">
|
||||
<button type="button"
|
||||
class="filter-pill filter-pill--editable adj-filter-pill"
|
||||
id="adjDatePill" data-adj-filter="date"
|
||||
aria-expanded="false" aria-controls="adjDatePopover">
|
||||
<i class="fas fa-calendar me-1"></i>
|
||||
<span class="filter-pill__label" data-pill-label>Date</span>
|
||||
<i class="fas fa-chevron-down ms-2 small filter-pill__chevron"></i>
|
||||
</button>
|
||||
<div class="filter-popover" id="adjDatePopover" role="dialog"
|
||||
aria-label="Filter by date" hidden>
|
||||
<div class="filter-popover__body">
|
||||
{# Mode toggle #}
|
||||
<div class="btn-group w-100 mb-2" role="group" aria-label="Date mode">
|
||||
<input type="radio" class="btn-check" name="adj_date_mode_pending"
|
||||
id="adjDateModeSingle" value="single" checked>
|
||||
<label class="btn btn-outline-secondary btn-sm" for="adjDateModeSingle">Single</label>
|
||||
<input type="radio" class="btn-check" name="adj_date_mode_pending"
|
||||
id="adjDateModeRange" value="range">
|
||||
<label class="btn btn-outline-secondary btn-sm" for="adjDateModeRange">Range</label>
|
||||
</div>
|
||||
{# Single-mode picker #}
|
||||
<div id="adjDateSingleFields">
|
||||
<label class="form-label small mb-1" for="adjDateSingle">Date</label>
|
||||
<input type="date" class="form-control form-control-sm" id="adjDateSingle">
|
||||
</div>
|
||||
{# Range-mode pickers #}
|
||||
<div id="adjDateRangeFields" class="row g-2 d-none">
|
||||
<div class="col-6">
|
||||
<label class="form-label small mb-1" for="adjDateFrom">From</label>
|
||||
<input type="date" class="form-control form-control-sm" id="adjDateFrom">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label small mb-1" for="adjDateTo">To</label>
|
||||
<input type="date" class="form-control form-control-sm" id="adjDateTo">
|
||||
</div>
|
||||
</div>
|
||||
{# Preset quick-links #}
|
||||
<div class="d-flex gap-2 mt-2 small" id="adjDatePresets">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-muted" data-preset="today">Today</button>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-muted" data-preset="week">Week</button>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-muted" data-preset="month">Month</button>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 text-muted" data-preset="clear">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-popover__footer d-flex gap-1 justify-content-end">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary popover-cancel">Cancel</button>
|
||||
<button type="button" class="btn btn-sm btn-accent popover-ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
{# Actual submit values — rewritten by the Date popover OK handler #}
|
||||
<div class="adj-hidden-inputs" data-adj-filter="date">
|
||||
{% if adj_filter_values.adj_date_from %}
|
||||
<input type="hidden" name="adj_date_from" value="{{ adj_filter_values.adj_date_from }}">
|
||||
{% endif %}
|
||||
{% if adj_filter_values.adj_date_to %}
|
||||
<input type="hidden" name="adj_date_to" value="{{ adj_filter_values.adj_date_to }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -750,8 +823,8 @@
|
||||
{# --- Group-by state (keeps Flat/By Type/By Worker across Apply) --- #}
|
||||
<input type="hidden" name="group_by" value="{{ adj_filter_values.group_by }}">
|
||||
|
||||
{# --- Apply / Clear buttons --- #}
|
||||
<div class="d-flex gap-2">
|
||||
{# --- Apply / Clear (pushed to the right end via .adj-apply-group) --- #}
|
||||
<div class="adj-apply-group">
|
||||
<button type="submit" class="btn btn-sm btn-accent">
|
||||
<i class="fas fa-filter me-1"></i>Apply
|
||||
</button>
|
||||
@ -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 <input> 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 ===
|
||||
|
||||
@ -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 <label> so clicking the text toggles the checkbox */
|
||||
/* Each row is a full-width <label> 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.) */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user