feat(adjustments): date picker single/range toggle + preset quick-buttons

Single by default (one <input> + '...' toggle reveals the second).
In single mode the JS mirrors From into the hidden To on every
change, so form submit sends adj_date_from=adj_date_to=X for an
exact-day filter on the backend (contract unchanged).

Four presets: Today (single), This week (Mon-Sun range), This month
(1st to last, range), Clear. Presets auto-switch mode so users see
what was populated.

On page load, range mode is inferred from the URL: if both dates
present AND differ -> range mode; else single mode. That way a
bookmarked range URL still shows both pickers.

No backend changes, no new tests — the 8 existing adjustments tests
already cover the from/to contract shape.
This commit is contained in:
Konrad du Plessis 2026-04-23 19:17:19 +02:00
parent 6905703492
commit c851b49dea

View File

@ -716,16 +716,32 @@
</select>
</div>
{# --- Date range --- #}
<div style="min-width: 130px;">
<label for="adjDateFrom" class="form-label small mb-1">From</label>
<input type="date" id="adjDateFrom" name="adj_date_from" class="form-control form-control-sm"
{# --- 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 }}">
</div>
<div style="min-width: 130px;">
<label for="adjDateTo" class="form-label small mb-1">To</label>
<input type="date" id="adjDateTo" name="adj_date_to" class="form-control form-control-sm"
value="{{ adj_filter_values.adj_date_to }}">
<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>
</div>
</div>
{# --- Sort state (column-header clicks will set these via JS in Task 9) --- #}
@ -3668,6 +3684,74 @@ 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');
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;
}
// 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;
});
// Preset quick-buttons
function iso(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);
} else if (preset === 'week') {
// ISO-week: Mon -> Sun
var dayOffset = (today.getDay() + 6) % 7; // 0 = Monday
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);
} 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);
} else if (preset === 'clear') {
dateFrom.value = '';
dateTo.value = '';
applyMode(false);
}
});
});
}
// --- 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.