[FEAT] Inventario: rinomina/rimuovi sottoclassi org dalla modifica bene (ticket #426)

Aggiunti endpoint PUT/DELETE /api/assets/subclasses/{id} (org-scoped, le
sottoclassi di sistema restano protette; FK ON DELETE SET NULL scollega i
beni senza cancellarli) + link UI «rinomina»/«rimuovi» accanto a «aggiungi».
Cache-buster api.js + bump version 1.24.8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-23 19:42:02 +02:00
co-authored by Claude Opus 4.8
parent 7529c59a75
commit 637e1ec510
5 changed files with 91 additions and 3 deletions
@@ -340,6 +340,59 @@ class AssetController extends BaseController
$this->jsonSuccess(['id' => $id, 'voce_code' => $voce, 'label' => $label], 'Sottoclasse aggiunta', 201);
}
/**
* PUT /api/assets/subclasses/{id} { label* }
* Rinomina una sottoclasse ORGANIZZATIVA dell'org corrente. Le sottoclassi di
* sistema (organization_id NULL) NON sono modificabili dall'utente.
*/
public function updateSubclass(int $id): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager']);
$body = $this->getJsonBody();
$label = trim((string) ($body['label'] ?? ''));
if ($label === '' || mb_strlen($label) > 120) {
$this->jsonError('Etichetta sottoclasse obbligatoria (max 120 caratteri)', 422, 'INVALID_LABEL');
}
$orgId = $this->getCurrentOrgId();
$row = Database::fetchOne(
'SELECT id, voce_code FROM inventory_subclassi WHERE id = ? AND organization_id = ?',
[$id, $orgId]
);
if (!$row) {
$this->jsonError('Sottoclasse non trovata o non modificabile (le voci di sistema non si modificano)', 404, 'SUBCLASS_NOT_FOUND');
}
$dup = Database::fetchOne(
'SELECT id FROM inventory_subclassi WHERE voce_code = ? AND label = ? AND (organization_id = ? OR organization_id IS NULL) AND id <> ?',
[$row['voce_code'], $label, $orgId, $id]
);
if ($dup) { $this->jsonError('Sottoclasse già esistente', 409, 'DUPLICATE'); }
Database::update('inventory_subclassi', ['label' => $label], 'id = ? AND organization_id = ?', [$id, $orgId]);
$this->jsonSuccess(['id' => $id, 'label' => $label], 'Sottoclasse aggiornata');
}
/**
* DELETE /api/assets/subclasses/{id}
* Rimuove una sottoclasse ORGANIZZATIVA dell'org corrente. Gli asset che la
* usano restano senza sottoclasse (FK ON DELETE SET NULL su assets.subclass_id).
* Le sottoclassi di sistema (organization_id NULL) NON sono eliminabili.
*/
public function deleteSubclass(int $id): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager']);
$orgId = $this->getCurrentOrgId();
$row = Database::fetchOne(
'SELECT id FROM inventory_subclassi WHERE id = ? AND organization_id = ?',
[$id, $orgId]
);
if (!$row) {
$this->jsonError('Sottoclasse non trovata o non eliminabile (le voci di sistema non si rimuovono)', 404, 'SUBCLASS_NOT_FOUND');
}
$used = Database::fetchOne('SELECT COUNT(*) AS c FROM assets WHERE subclass_id = ?', [$id]);
$usedCount = (int) ($used['c'] ?? 0);
Database::delete('inventory_subclassi', 'id = ? AND organization_id = ?', [$id, $orgId]);
$this->jsonSuccess(['id' => $id, 'unlinked_assets' => $usedCount], 'Sottoclasse rimossa');
}
public function delete(int $id): void
{
$this->requireOrgRole(['org_admin']);
+33 -2
View File
@@ -361,7 +361,7 @@
</main>
</div>
<script src="/js/api.js?v=20260631"></script>
<script src="/js/api.js?v=20260623g"></script>
<script src="/js/common.js?v=20260623"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
@@ -442,6 +442,33 @@
else showNotification((r && r.message) || 'Errore.', 'warning');
} catch (e) { showNotification('Errore aggiunta sottoclasse.', 'warning'); }
}
async function renameSubclassPrompt() {
const sel = document.getElementById('asset-subclass');
const id = sel.value;
if (!id) { showNotification('Seleziona prima una sottoclasse da rinominare.', 'info'); return; }
const cur = sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].text : '';
const label = (prompt('Nuovo nome per la sottoclasse:', cur) || '').trim();
if (!label || label === cur) return;
const voce = document.getElementById('asset-voce').value;
try {
const r = await api.updateAssetSubclass(id, { label: label });
if (r && r.success) { await loadSubclasses(); document.getElementById('asset-subclass').innerHTML = subclassOptionsHtml(voce, id); showNotification('Sottoclasse rinominata.', 'success'); }
else showNotification((r && r.message) || 'Operazione non consentita (solo sottoclassi della tua organizzazione).', 'warning');
} catch (e) { showNotification('Errore rinomina sottoclasse.', 'warning'); }
}
async function removeSubclassPrompt() {
const sel = document.getElementById('asset-subclass');
const id = sel.value;
if (!id) { showNotification('Seleziona prima una sottoclasse da rimuovere.', 'info'); return; }
const cur = sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].text : '';
if (!confirm('Rimuovere la sottoclasse «' + cur + '»? Gli asset che la usano resteranno senza sottoclasse.')) return;
const voce = document.getElementById('asset-voce').value;
try {
const r = await api.deleteAssetSubclass(id);
if (r && r.success) { await loadSubclasses(); document.getElementById('asset-subclass').innerHTML = subclassOptionsHtml(voce, ''); showNotification('Sottoclasse rimossa.', 'success'); }
else showNotification((r && r.message) || 'Operazione non consentita (solo sottoclassi della tua organizzazione).', 'warning');
} catch (e) { showNotification('Errore rimozione sottoclasse.', 'warning'); }
}
// ── State ───────────────────────────────────────────────
let allAssets = [];
@@ -835,7 +862,11 @@
<select class="form-select" id="asset-voce" onchange="onVoceChange()">${voceOpts}</select>
</div>
<div class="form-group">
<label for="asset-subclass" class="form-label">Sottoclasse <a href="#" onclick="addSubclassPrompt();return false;" style="font-weight:400;font-size:.78rem;margin-left:6px;">+ aggiungi</a></label>
<label for="asset-subclass" class="form-label">Sottoclasse
<a href="#" onclick="addSubclassPrompt();return false;" style="font-weight:400;font-size:.78rem;margin-left:6px;">+ aggiungi</a>
<a href="#" onclick="renameSubclassPrompt();return false;" style="font-weight:400;font-size:.78rem;margin-left:8px;">✎ rinomina</a>
<a href="#" onclick="removeSubclassPrompt();return false;" style="font-weight:400;font-size:.78rem;margin-left:8px;">🗑 rimuovi</a>
</label>
<select class="form-select" id="asset-subclass">${subOpts}</select>
</div>
</div>
+2
View File
@@ -442,6 +442,8 @@ $actionMap = [
'GET:dependencyMap' => 'dependencyMap',
'GET:subclasses' => 'subclasses', // Epic C / C4 — voci + sottoclassi
'POST:subclasses' => 'addSubclass', // Epic C / C4 — aggiunta sottoclasse org
'PUT:subclasses/{subId}' => 'updateSubclass', // C4 — rinomina sottoclasse org (#426)
'DELETE:subclasses/{subId}' => 'deleteSubclass', // C4 — rimuove sottoclasse org (#426)
'GET:{id}' => 'get',
'PUT:{id}' => 'update',
'DELETE:{id}' => 'delete',
+2
View File
@@ -470,6 +470,8 @@ class NIS2API {
importAssets(data) { return this.post('/assets/import', data); } // P2 import CMDB/CSV
listAssetSubclasses() { return this.get('/assets/subclasses'); } // C4 — voci + sottoclassi
addAssetSubclass(data) { return this.post('/assets/subclasses', data); } // C4 — nuova sottoclasse org
updateAssetSubclass(id, data) { return this.put('/assets/subclasses/' + id, data); } // C4 — rinomina sottoclasse org (#426)
deleteAssetSubclass(id) { return this.del('/assets/subclasses/' + id); } // C4 — rimuove sottoclasse org (#426)
getControlsMonitoring() { return this.get('/audit/controlsMonitoring'); }
getAcnRequirements() { return this.get('/audit/acnRequirements'); } // requisiti ACN per org
updateAcnRequirement(id, status, note) { return this.put(`/audit/acnRequirements/${id}`, { status, evidence_note: note }); }
+1 -1
View File
@@ -1 +1 @@
{"version": "1.24.7", "build": "2026-06-23-v1.24.7", "date": "2026-06-23", "changelog": "Inventario beni — Valutazione Rilevanza NIS2 (GV.OC-04): risolto il bug per cui, riaprendo un bene gia' valutato, le tende a discesa (combo) dei 6 criteri risultavano vuote invece di mostrare le scelte salvate. Ora la finestra di valutazione ripropone correttamente i valori precedenti, cosi' si puo' rivedere/aggiornare il punteggio senza ricompilare da zero. Risolto ticket #427. Nota: i beni di esempio precaricati possono ancora apparire vuoti perche' salvati con un vecchio schema di criteri; basta rivalutarli una volta."}
{"version": "1.24.8", "build": "2026-06-23-v1.24.8", "date": "2026-06-23", "changelog": "Inventario beni — Sottoclassi: ora dalla finestra di modifica del bene si possono anche RINOMINARE e RIMUOVERE le sottoclassi create dalla propria organizzazione (prima si potevano solo aggiungere). I due nuovi link «✎ rinomina» e «🗑 rimuovi» stanno accanto a «+ aggiungi». Le sottoclassi di sistema (predefinite) restano protette e non si modificano/eliminano. Rimuovendo una sottoclasse, i beni che la usavano restano semplicemente senza sottoclasse. Risolto ticket #426."}