[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:
co-authored by
Claude Opus 4.8
parent
7529c59a75
commit
637e1ec510
@@ -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
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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."}
|
||||
|
||||
Reference in New Issue
Block a user