Add sortable columns for Name, Site, IP Address

Clicking a column header sorts ascending; clicking again reverses to
descending. Active column shows ↑/↓ in blue; inactive columns show ↕
in gray. IP addresses are compared numerically (octet by octet) so
.9 sorts before .10. Sort composes with the name filter and IP filter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 16:00:00 +02:00
parent 139499839f
commit ae8c07b759

View File

@@ -72,9 +72,18 @@
<input type="checkbox" id="chk-all" <input type="checkbox" id="chk-all"
class="rounded border-gray-300 bg-white text-blue-500 focus:ring-blue-500 cursor-pointer" /> class="rounded border-gray-300 bg-white text-blue-500 focus:ring-blue-500 cursor-pointer" />
</th> </th>
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">Name</th> <th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs cursor-pointer select-none hover:text-gray-700"
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">Site</th> data-sort="name">
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">IP Address</th> <span class="flex items-center gap-1">Name <span class="sort-icon text-gray-300"></span></span>
</th>
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs cursor-pointer select-none hover:text-gray-700"
data-sort="site">
<span class="flex items-center gap-1">Site <span class="sort-icon text-gray-300"></span></span>
</th>
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs cursor-pointer select-none hover:text-gray-700"
data-sort="ip">
<span class="flex items-center gap-1">IP Address <span class="sort-icon text-gray-300"></span></span>
</th>
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">MAC Address</th> <th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">MAC Address</th>
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">Model</th> <th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">Model</th>
<th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">Status</th> <th class="px-4 py-3 text-left font-semibold text-gray-500 uppercase tracking-wide text-xs">Status</th>
@@ -256,6 +265,69 @@
filterInput.addEventListener('input', applyFilters); filterInput.addEventListener('input', applyFilters);
// ── Column sort ────────────────────────────────────────────────────────────
let sortCol = null;
let sortDir = 'asc';
const colIndex = { name: 2, site: 3, ip: 4 };
function ipToNum(ip) {
return (ip || '').split('.').reduce((acc, p) => {
const n = parseInt(p, 10);
return acc * 256 + (isNaN(n) ? 0 : n);
}, 0);
}
function getCellText(row, idx) {
return (row.querySelector(`td:nth-child(${idx})`)?.textContent || '').trim();
}
function updateSortIcons() {
document.querySelectorAll('[data-sort]').forEach(th => {
const icon = th.querySelector('.sort-icon');
if (!icon) return;
if (th.dataset.sort === sortCol) {
icon.textContent = sortDir === 'asc' ? '↑' : '↓';
icon.className = 'sort-icon text-blue-500';
} else {
icon.textContent = '↕';
icon.className = 'sort-icon text-gray-300';
}
});
}
document.querySelectorAll('[data-sort]').forEach(th => {
th.addEventListener('click', () => {
const col = th.dataset.sort;
if (sortCol === col) {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
} else {
sortCol = col;
sortDir = 'asc';
}
const idx = colIndex[col];
const tbody = document.getElementById('ap-table-body');
const rows = [...tbody.querySelectorAll('tr')];
rows.sort((a, b) => {
const av = getCellText(a, idx);
const bv = getCellText(b, idx);
let cmp;
if (col === 'ip') {
cmp = ipToNum(av) - ipToNum(bv);
} else {
cmp = av.localeCompare(bv, undefined, { sensitivity: 'base' });
}
return sortDir === 'asc' ? cmp : -cmp;
});
rows.forEach(row => tbody.appendChild(row));
updateSortIcons();
applyFilters();
});
});
// ── IP range filter button ───────────────────────────────────────────────── // ── IP range filter button ─────────────────────────────────────────────────
const btnIpFilter = document.getElementById('btn-ip-filter'); const btnIpFilter = document.getElementById('btn-ip-filter');
const ipFilterLabel = document.getElementById('ip-filter-label'); const ipFilterLabel = document.getElementById('ip-filter-label');