[FEAT] Inventario: ordinamento lista per colonna (#442)

Click su un'intestazione (Nome/Tipo/Categoria/Criticita/Rilevanza NIS2/Owner/Stato)
ordina l'inventario; secondo click inverte (freccia ▲/▼, aria-sort).
Criticita e Rilevanza ordinano per valore reale, non alfabetico.
Client-side sull'inventario gia' caricato (no paginazione su assets), si combina
coi filtri, accessibile da tastiera. Help aggiornato + cache-buster help.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-25 10:41:34 +02:00
co-authored by Claude Opus 4.8
parent 87fc5bf959
commit ab3e88299e
31 changed files with 99 additions and 41 deletions
+61 -12
View File
@@ -105,6 +105,13 @@
.data-table tr:last-child td { border-bottom: none; }
.data-table tbody tr { cursor: pointer; transition: background var(--transition-fast); }
.data-table tbody tr:hover td { background: var(--gray-50); }
/* ── Intestazioni ordinabili (click per ordinare la lista) ── */
.data-table th.sortable-th { cursor: pointer; user-select: none; white-space: nowrap; transition: background var(--transition-fast), color var(--transition-fast); }
.data-table th.sortable-th:hover { background: var(--gray-100); color: var(--gray-700); }
.data-table th.sortable-th:focus-visible { outline: 2px solid var(--primary, #0066CC); outline-offset: -2px; }
.data-table th.sortable-th.sorted { color: var(--gray-700); }
.data-table th.sortable-th .sort-ind { font-size: 0.7rem; opacity: 0.35; margin-left: 3px; }
.data-table th.sortable-th .sort-ind.active { opacity: 1; color: var(--primary, #0066CC); }
/* ── Criticality Badges ─────────────────────────────────── */
.criticality-badge {
@@ -376,7 +383,7 @@
</script>
<script src="/js/common-bi.js?v=20260624f"></script>
<script src="/js/i18n.js?v=20260631"></script>
<script src="/js/help.js?v=20260625b"></script>
<script src="/js/help.js?v=20260625c"></script>
<script>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
@@ -535,8 +542,57 @@
).join('');
}
// ── Ordinamento colonne (client-side, sull'inventario gia' caricato) ──
// L'inventario e' caricato per intero lato client (nessuna paginazione),
// quindi l'ordinamento sull'array e' completo e non parziale.
let lastView = [];
let sortState = { key: null, dir: 1 }; // dir: 1 = crescente, -1 = decrescente
const ASSET_COLUMNS = [
{ key: 'name', label: 'Nome', type: 'text', val: a => (a.name || '') },
{ key: 'type', label: 'Tipo', type: 'text', val: a => assetTipo(a) },
{ key: 'category', label: 'Categoria', type: 'text', val: a => (a.category || '') },
{ key: 'criticality', label: "Criticita'", type: 'num', val: a => ({ low: 1, medium: 2, high: 3, critical: 4 }[a.criticality || 'medium'] || 0) },
{ key: 'relevance', label: 'Rilevanza NIS2', type: 'num', val: a => (a.relevance_score == null ? -1 : Number(a.relevance_score)) },
{ key: 'owner', label: 'Owner', type: 'text', val: a => (a.owner_name || '') },
{ key: 'status', label: 'Stato', type: 'text', val: a => (a.status || '') }
];
function sortableTh(c) {
const active = sortState.key === c.key;
const ariaSort = active ? (sortState.dir === 1 ? 'ascending' : 'descending') : 'none';
const ind = active ? (sortState.dir === 1 ? '▲' : '▼') : '⇅';
const indClass = active ? 'sort-ind active' : 'sort-ind';
return `<th class="sortable-th${active ? ' sorted' : ''}" role="button" tabindex="0" aria-sort="${ariaSort}" `
+ `title="Ordina per ${escapeHtml(c.label)}" onclick="sortBy('${c.key}')" `
+ `onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();sortBy('${c.key}');}">`
+ `${escapeHtml(c.label)} <span class="${indClass}" aria-hidden="true">${ind}</span></th>`;
}
function sortAssetsView(assets) {
if (!sortState.key) return assets;
const col = ASSET_COLUMNS.find(c => c.key === sortState.key);
if (!col) return assets;
const copy = assets.slice();
copy.sort((a, b) => {
const va = col.val(a), vb = col.val(b);
let cmp;
if (col.type === 'num') cmp = (Number(va) - Number(vb));
else cmp = String(va).localeCompare(String(vb), 'it', { numeric: true, sensitivity: 'base' });
if (cmp === 0) return 0;
return cmp * sortState.dir;
});
return copy;
}
function sortBy(key) {
if (sortState.key === key) sortState.dir *= -1;
else { sortState.key = key; sortState.dir = 1; }
renderAssets(lastView);
}
function renderAssets(assets) {
const container = document.getElementById('assets-container');
lastView = assets || [];
if (!assets || assets.length === 0) {
container.innerHTML = `
@@ -548,23 +604,16 @@
return;
}
const sortedAssets = sortAssetsView(assets);
let html = `
<table class="data-table">
<thead>
<tr>
<th>Nome</th>
<th>Tipo</th>
<th>Categoria</th>
<th>Criticita'</th>
<th>Rilevanza NIS2</th>
<th>Owner</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
<tr>${ASSET_COLUMNS.map(sortableTh).join('')}<th>Azioni</th></tr>
</thead>
<tbody>`;
assets.forEach(asset => {
sortedAssets.forEach(asset => {
const crit = asset.criticality || 'medium';
const st = asset.status || 'active';
html += `