[FEAT] Inventario: Mappa Dipendenze + eliminazione beni (#440, #439)

- Nuovo pulsante "Mappa Dipendenze" in toolbar: vista delle relazioni
  tra i beni (costruita client-side da allAssets, endpoint backend gia'
  esistente dependencyMap).
- Campo "Dipendenze" (multi-select) nel form bene: consente di compilare
  la mappa che ARIA indicava ma che non aveva UI. create/update salvano
  gia' il campo dependencies.
- Pulsante "Elimina" bene nella lista e nel dettaglio (DELETE /assets/{id}
  gia' esistente, richiede org_admin).
- Dettaglio bene: dipendenze mostrate per nome anziche' per ID grezzo.
- version.json -> 1.24.13.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-25 08:43:54 +02:00
co-authored by Claude Opus 4.8
parent 3bd2f09a45
commit 9c36604b2b
2 changed files with 79 additions and 2 deletions
+78 -1
View File
@@ -281,6 +281,7 @@
<h1 class="h2" data-i18n="assets.title">Inventario</h1> <h1 class="h2" data-i18n="assets.title">Inventario</h1>
<div class="content-header-actions"> <div class="content-header-actions">
<span class="text-muted" style="font-size:0.8rem; margin-right:8px;">Art. 21.2.i NIS2</span> <span class="text-muted" style="font-size:0.8rem; margin-right:8px;">Art. 21.2.i NIS2</span>
<button class="btn btn-secondary" onclick="showDependencyMap()" title="Mappa delle dipendenze tra i beni">Mappa Dipendenze</button>
<button class="btn btn-secondary" onclick="openImportModal()" title="Importa beni da CSV/CMDB">Importa</button> <button class="btn btn-secondary" onclick="openImportModal()" title="Importa beni da CSV/CMDB">Importa</button>
<button class="btn btn-primary" onclick="showCreateAssetModal()">+ Nuovo bene</button> <button class="btn btn-primary" onclick="showCreateAssetModal()">+ Nuovo bene</button>
</div> </div>
@@ -581,6 +582,9 @@
<button class="btn-icon" onclick="event.stopPropagation(); showEditAssetModal(${asset.id})" title="Modifica"> <button class="btn-icon" onclick="event.stopPropagation(); showEditAssetModal(${asset.id})" title="Modifica">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg> <svg viewBox="0 0 20 20" fill="currentColor"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>
</button> </button>
<button class="btn-icon" onclick="event.stopPropagation(); confirmDeleteAsset(${asset.id})" title="Elimina bene">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
</button>
</td> </td>
</tr>`; </tr>`;
}); });
@@ -732,7 +736,10 @@
try { deps = JSON.parse(deps); } catch(e) { deps = []; } try { deps = JSON.parse(deps); } catch(e) { deps = []; }
} }
if (deps && deps.length > 0) { if (deps && deps.length > 0) {
depsHtml = deps.map(d => `<span class="dep-tag">ID: ${escapeHtml(String(d))}</span>`).join(''); depsHtml = deps.map(d => {
const dep = (allAssets || []).find(x => x.id === parseInt(d));
return `<span class="dep-tag">${dep ? escapeHtml(dep.name) : ('ID: ' + escapeHtml(String(d)))}</span>`;
}).join('');
} }
} }
@@ -797,6 +804,7 @@
size: 'lg', size: 'lg',
footer: ` footer: `
<button class="btn btn-secondary" onclick="closeModal()">Chiudi</button> <button class="btn btn-secondary" onclick="closeModal()">Chiudi</button>
<button class="btn btn-danger" onclick="confirmDeleteAsset(${a.id})">Elimina</button>
<button class="btn btn-secondary" onclick="closeModal(); showScoringModal(${a.id})">Valuta Rilevanza NIS2</button> <button class="btn btn-secondary" onclick="closeModal(); showScoringModal(${a.id})">Valuta Rilevanza NIS2</button>
<button class="btn btn-primary" onclick="closeModal(); showEditAssetModal(${a.id})">Modifica</button> <button class="btn btn-primary" onclick="closeModal(); showEditAssetModal(${a.id})">Modifica</button>
` `
@@ -868,6 +876,15 @@
`<option value="${val}" ${a.status === val ? 'selected' : ''}>${label}</option>` `<option value="${val}" ${a.status === val ? 'selected' : ''}>${label}</option>`
).join(''); ).join('');
// Dipendenze (mappa delle dipendenze): opzioni = tutti gli altri beni
let curDeps = a.dependencies;
if (typeof curDeps === 'string') { try { curDeps = JSON.parse(curDeps); } catch (e) { curDeps = []; } }
curDeps = (curDeps || []).map(x => parseInt(x));
const depOptions = (allAssets || [])
.filter(x => x.id !== a.id)
.map(x => `<option value="${x.id}" ${curDeps.includes(x.id) ? 'selected' : ''}>${escapeHtml(x.name)}</option>`)
.join('') || '<option value="" disabled>Nessun altro bene disponibile in inventario</option>';
// Store the callback // Store the callback
window._assetSaveCallback = onSave; window._assetSaveCallback = onSave;
@@ -948,6 +965,11 @@
<input type="date" class="form-input" id="asset-warranty" value="${a.warranty_expiry || ''}"> <input type="date" class="form-input" id="asset-warranty" value="${a.warranty_expiry || ''}">
</div> </div>
</div> </div>
<div class="form-group">
<label for="asset-dependencies" class="form-label">Dipendenze (mappa delle dipendenze)</label>
<select class="form-select" id="asset-dependencies" multiple size="5" aria-describedby="dep-help">${depOptions}</select>
<small id="dep-help" class="form-text text-muted" style="display:block;margin-top:4px;">Seleziona i beni da cui questo bene dipende (tieni premuto Ctrl/Cmd per selezionarne piu' di uno). Alimenta la Mappa delle Dipendenze e supporta l'analisi del rischio (art. 24 D.Lgs. 138/2024).</small>
</div>
`, { `, {
size: 'lg', size: 'lg',
footer: ` footer: `
@@ -982,11 +1004,66 @@
status: document.getElementById('asset-status').value, status: document.getElementById('asset-status').value,
}; };
const depSel = document.getElementById('asset-dependencies');
data.dependencies = depSel
? Array.from(depSel.selectedOptions).map(o => parseInt(o.value)).filter(n => !isNaN(n))
: [];
if (window._assetSaveCallback) { if (window._assetSaveCallback) {
window._assetSaveCallback(data); window._assetSaveCallback(data);
} }
} }
// ── Elimina bene ────────────────────────────────────────
async function confirmDeleteAsset(id) {
const a = (allAssets || []).find(x => x.id === parseInt(id));
const nome = a ? a.name : ('#' + id);
if (!confirm(`Eliminare definitivamente il bene "${nome}"?\nL'operazione non e' reversibile.`)) return;
try {
const r = await api.deleteAsset(id);
if (r.success) {
showNotification('Bene eliminato dall\'inventario.', 'success');
closeModal();
loadAssets();
} else {
showNotification(r.message || 'Eliminazione non riuscita (serve il ruolo amministratore).', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
// ── Mappa delle Dipendenze ──────────────────────────────
function parseAssetDeps(a) {
let d = a.dependencies;
if (typeof d === 'string') { try { d = JSON.parse(d); } catch (e) { d = []; } }
return (d || []).map(x => parseInt(x)).filter(n => !isNaN(n));
}
function showDependencyMap() {
const assets = allAssets || [];
const byId = {};
assets.forEach(a => { byId[a.id] = a; });
const total = assets.reduce((s, a) => s + parseAssetDeps(a).length, 0);
let body;
if (!assets.length) {
body = `<p style="font-size:.9rem;color:var(--gray-600);">Nessun bene in inventario. Aggiungi prima i beni, poi apri <strong>Modifica</strong> su ciascuno per impostare le dipendenze.</p>`;
} else {
const rows = assets.map(a => {
const deps = parseAssetDeps(a);
const depNames = deps.length
? deps.map(id => `<span class="dep-tag">${byId[id] ? escapeHtml(byId[id].name) : ('#' + id)}</span>`).join(' ')
: '<span style="color:var(--gray-400);">— nessuna dipendenza</span>';
return `<tr><td style="font-weight:500;">${escapeHtml(a.name)}</td><td>${depNames}</td></tr>`;
}).join('');
body = `<p style="font-size:.85rem;color:var(--gray-600);margin-bottom:.8rem;">Relazioni di dipendenza tra i beni dell'inventario (${total} collegamenti totali). Per impostare o modificare le dipendenze di un bene, aprilo con <strong>Modifica</strong> e usa il campo <em>Dipendenze</em>. La mappa supporta l'analisi del rischio e la continuita' operativa (art. 24 D.Lgs. 138/2024).</p>
<table class="data-table"><thead><tr><th>Bene</th><th>Dipende da</th></tr></thead><tbody>${rows}</tbody></table>`;
}
showModal('Mappa delle Dipendenze', body, {
size: 'lg',
footer: `<button class="btn btn-secondary" onclick="closeModal()">Chiudi</button>`
});
}
// ── Initial Load ──────────────────────────────────────── // ── Initial Load ────────────────────────────────────────
loadAssets(); loadAssets();
+1 -1
View File
@@ -1 +1 @@
{"version": "1.24.12", "build": "2026-06-24-v1.24.12", "date": "2026-06-24", "changelog": "Widget Segnalazioni — Risolto l'errore \"Data too long for column 'operative_summary'\" quando la descrizione era troppo lunga: ora la casella ha un limite di 1900 caratteri con contatore live e un messaggio chiaro se si supera (al posto dell'errore tecnico). Causa radice (dimensione colonna lato ticket-ms) inoltrata ad AgileHub. Risolto ticket #430."} {"version": "1.24.13", "build": "2026-06-25-v1.24.13", "date": "2026-06-25", "changelog": "Inventario beni — Aggiunta la Mappa delle Dipendenze: nuovo pulsante 'Mappa Dipendenze' che mostra le relazioni tra i beni, e nel form di ogni bene un campo per selezionare i beni da cui dipende (supporta l'analisi del rischio, art. 24 D.Lgs. 138/2024). Aggiunto il pulsante per eliminare un bene dall'inventario (lista e dettaglio). Risolti ticket #440 e #439."}