Add client-side filtering to dashboard
Filter bar with Status, Brand, Size, Storage Location dropdowns and text search. Select-all and bulk toolbar respect visible rows so users can filter to a group (e.g. a storage location) and bulk-action them in one step.
This commit is contained in:
+93
-19
@@ -43,6 +43,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="filter-bar" style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;margin-bottom:0.75rem;">
|
||||
<select id="filter-status" onchange="applyFilters()" style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="available">Available</option>
|
||||
<option value="installed">Installed</option>
|
||||
<option value="retired">Retired</option>
|
||||
</select>
|
||||
<select id="filter-brand" onchange="applyFilters()" style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;">
|
||||
<option value="">All Brands</option>
|
||||
{% for b in batteries|map(attribute='brand')|unique|sort %}
|
||||
<option value="{{ b }}">{{ b }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="filter-size" onchange="applyFilters()" style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;">
|
||||
<option value="">All Sizes</option>
|
||||
{% for s in batteries|map(attribute='size')|select|unique|sort %}
|
||||
<option value="{{ s }}">{{ s }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="filter-storage" onchange="applyFilters()" style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;">
|
||||
<option value="">All Locations</option>
|
||||
{% for loc in storage_locations %}
|
||||
<option value="{{ loc }}">{{ loc }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="text" id="filter-text" oninput="applyFilters()" placeholder="Search…"
|
||||
style="padding:0.25rem 0.5rem;font-size:0.85rem;border:1px solid #cbd5e1;border-radius:4px;width:140px;">
|
||||
<button type="button" onclick="resetFilters()" class="btn btn-sm btn-secondary" id="filter-reset" style="display:none;">✕ Reset</button>
|
||||
<span id="filter-count" style="font-size:0.8rem;color:#64748b;"></span>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ url_for('battery_bulk_action') }}" id="bulk-form">
|
||||
|
||||
<div id="bulk-toolbar" style="display:none;margin-bottom:0.75rem;padding:0.6rem 0.75rem;background:#f1f5f9;border-radius:6px;display:none;align-items:center;gap:0.5rem;flex-wrap:wrap;">
|
||||
@@ -104,7 +135,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in batteries %}
|
||||
<tr>
|
||||
<tr data-brand="{{ b.brand }}" data-size="{{ b.size or '' }}" data-status="{{ b.status }}" data-storage="{{ b.storage_location or '' }}">
|
||||
<td><input type="checkbox" name="battery_ids" value="{{ b.id }}" class="row-cb"></td>
|
||||
<td><a href="{{ url_for('battery_detail', battery_id=b.id) }}"><strong>{{ b.label }}</strong></a></td>
|
||||
<td>{{ b.brand }}</td>
|
||||
@@ -161,27 +192,70 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var cbs = document.querySelectorAll('.row-cb');
|
||||
var selectAll = document.getElementById('select-all');
|
||||
var toolbar = document.getElementById('bulk-toolbar');
|
||||
var countEl = document.getElementById('selected-count');
|
||||
var selectAll = document.getElementById('select-all');
|
||||
var toolbar = document.getElementById('bulk-toolbar');
|
||||
var countEl = document.getElementById('selected-count');
|
||||
|
||||
function updateToolbar() {
|
||||
var checked = document.querySelectorAll('.row-cb:checked');
|
||||
var n = checked.length;
|
||||
toolbar.style.display = n > 0 ? 'flex' : 'none';
|
||||
countEl.textContent = n + ' selected';
|
||||
selectAll.indeterminate = n > 0 && n < cbs.length;
|
||||
selectAll.checked = cbs.length > 0 && n === cbs.length;
|
||||
}
|
||||
function visibleCbs() {
|
||||
return Array.prototype.filter.call(
|
||||
document.querySelectorAll('.row-cb'),
|
||||
function(cb) { return cb.closest('tr').style.display !== 'none'; }
|
||||
);
|
||||
}
|
||||
|
||||
cbs.forEach(function (cb) { cb.addEventListener('change', updateToolbar); });
|
||||
selectAll.addEventListener('change', function () {
|
||||
cbs.forEach(function (cb) { cb.checked = selectAll.checked; });
|
||||
updateToolbar();
|
||||
function updateToolbar() {
|
||||
var checked = document.querySelectorAll('.row-cb:checked');
|
||||
var vis = visibleCbs();
|
||||
var n = checked.length;
|
||||
toolbar.style.display = n > 0 ? 'flex' : 'none';
|
||||
countEl.textContent = n + ' selected';
|
||||
var visChecked = vis.filter(function(cb) { return cb.checked; });
|
||||
selectAll.indeterminate = visChecked.length > 0 && visChecked.length < vis.length;
|
||||
selectAll.checked = vis.length > 0 && visChecked.length === vis.length;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.row-cb').forEach(function(cb) {
|
||||
cb.addEventListener('change', updateToolbar);
|
||||
});
|
||||
selectAll.addEventListener('change', function() {
|
||||
visibleCbs().forEach(function(cb) { cb.checked = selectAll.checked; });
|
||||
updateToolbar();
|
||||
});
|
||||
|
||||
function applyFilters() {
|
||||
var status = document.getElementById('filter-status').value;
|
||||
var brand = document.getElementById('filter-brand').value;
|
||||
var size = document.getElementById('filter-size').value;
|
||||
var storage = document.getElementById('filter-storage').value;
|
||||
var text = document.getElementById('filter-text').value.trim().toLowerCase();
|
||||
var anyActive = status || brand || size || storage || text;
|
||||
document.getElementById('filter-reset').style.display = anyActive ? '' : 'none';
|
||||
|
||||
var rows = document.querySelectorAll('tbody tr[data-brand]');
|
||||
var visible = 0;
|
||||
rows.forEach(function(row) {
|
||||
var show = true;
|
||||
if (status && row.dataset.status !== status) show = false;
|
||||
if (brand && row.dataset.brand !== brand) show = false;
|
||||
if (size && row.dataset.size !== size) show = false;
|
||||
if (storage && row.dataset.storage !== storage) show = false;
|
||||
if (text && row.textContent.toLowerCase().indexOf(text) === -1) show = false;
|
||||
row.style.display = show ? '' : 'none';
|
||||
if (show) visible++;
|
||||
});
|
||||
}());
|
||||
|
||||
var fc = document.getElementById('filter-count');
|
||||
fc.textContent = anyActive ? (visible + ' of ' + rows.length + ' shown') : '';
|
||||
updateToolbar();
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
['filter-status','filter-brand','filter-size','filter-storage'].forEach(function(id) {
|
||||
document.getElementById(id).value = '';
|
||||
});
|
||||
document.getElementById('filter-text').value = '';
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function updateBulkField(sel) {
|
||||
var field = sel.value;
|
||||
|
||||
Reference in New Issue
Block a user