Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
214 lines
10 KiB
JavaScript
214 lines
10 KiB
JavaScript
/**
|
||
* NIS2 Agile - Matrice RACI (A4 Fase 4.3)
|
||
* 2 tab: Matrice RACI (ruoli × oggetti con celle R/A/C/I) · Collegamenti (link m2m).
|
||
* Client api.raci*: ritorna `data`, lancia su success=false (api.js _acn).
|
||
* I DELETE non hanno body → i parametri compositi viaggiano in query string (vedi api.js).
|
||
* Ancoraggio: GV.RR-02 (ruoli/responsabilità), ID.AM (inventario), GV.PO (procedure),
|
||
* art.24 D.Lgs. 138/2024 + ID.RA (rischi). Strumento di supporto, non un parere legale.
|
||
*/
|
||
'use strict';
|
||
|
||
let RACI = { roles: [], objects: [], assignments: {}, tab: 'matrix' };
|
||
|
||
function el(id) { return document.getElementById(id); }
|
||
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||
|
||
const OBJ_TYPE_LABELS = { inventory: 'Inventario', procedure: 'Procedura', risk: 'Rischio', supplier: 'Fornitore' };
|
||
const RACI_LETTERS = ['R', 'A', 'C', 'I'];
|
||
|
||
// link_type → { label, aType, bType } (bType 'measure' = catalogo misure ACN).
|
||
const LINK_TYPES = {
|
||
procedure_inventory: { label: 'Procedura ↔ Inventario', aType: 'procedure', bType: 'inventory', aLabel: 'Procedura', bLabel: 'Inventario (asset)' },
|
||
procedure_risk: { label: 'Procedura ↔ Rischio', aType: 'procedure', bType: 'risk', aLabel: 'Procedura', bLabel: 'Rischio' },
|
||
inventory_risk: { label: 'Inventario ↔ Rischio', aType: 'inventory', bType: 'risk', aLabel: 'Inventario (asset)', bLabel: 'Rischio' },
|
||
risk_measure: { label: 'Rischio ↔ Misura', aType: 'risk', bType: 'measure', aLabel: 'Rischio', bLabel: 'Misura ACN' }
|
||
};
|
||
|
||
document.addEventListener('DOMContentLoaded', async function () {
|
||
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
||
if (window.I18n && I18n.init) I18n.init('it');
|
||
if (typeof loadSidebar === 'function') loadSidebar();
|
||
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
|
||
|
||
await raciLoadMatrix();
|
||
raciRenderMatrix();
|
||
});
|
||
|
||
function raciKey(roleId, type, objId) { return roleId + '|' + type + '|' + objId; }
|
||
|
||
function raciTab(name) {
|
||
RACI.tab = name;
|
||
document.querySelectorAll('.cmp-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
|
||
document.querySelectorAll('.cmp-panel').forEach(p => p.classList.remove('active'));
|
||
const panel = el('panel-' + name);
|
||
if (panel) panel.classList.add('active');
|
||
if (name === 'links' && !RACI._linksInit) { RACI._linksInit = true; raciLinkTypeChanged(); }
|
||
}
|
||
|
||
/* ════════════════ TAB 1 — Matrice RACI ════════════════ */
|
||
async function raciLoadMatrix() {
|
||
try {
|
||
const m = await api.raciMatrix();
|
||
RACI.roles = (m && m.roles) || [];
|
||
RACI.objects = (m && m.objects) || [];
|
||
RACI.assignments = {};
|
||
((m && m.assignments) || []).forEach(function (a) {
|
||
RACI.assignments[raciKey(a.role_id, a.object_type, a.object_id)] = a.raci;
|
||
});
|
||
} catch (e) {
|
||
showNotification('Errore nel caricamento della matrice: ' + (e.message || e), 'error');
|
||
RACI.roles = []; RACI.objects = []; RACI.assignments = {};
|
||
}
|
||
}
|
||
|
||
function raciRenderMatrix() {
|
||
const wrap = el('raci-matrix');
|
||
el('raci-matrix-summary').textContent = RACI.roles.length + ' ruoli · ' + RACI.objects.length + ' oggetti collegabili';
|
||
if (!RACI.roles.length) {
|
||
wrap.innerHTML = '<div class="cmp-empty">Nessun ruolo: creane nell\'Organigramma per popolare la matrice.</div>';
|
||
return;
|
||
}
|
||
if (!RACI.objects.length) {
|
||
wrap.innerHTML = '<div class="cmp-empty">Nessun oggetto collegabile (inventario, procedure, rischi, fornitori). Aggiungili nei rispettivi moduli.</div>';
|
||
return;
|
||
}
|
||
|
||
// Header: oggetti raggruppati per tipo
|
||
let head = '<thead><tr><th class="raci-corner">Ruolo \\ Oggetto</th>';
|
||
RACI.objects.forEach(function (o) {
|
||
head += '<th class="raci-objhead"><span class="raci-objtype">' + esc(OBJ_TYPE_LABELS[o.type] || o.type) + '</span>' + esc(o.label) + '</th>';
|
||
});
|
||
head += '</tr></thead>';
|
||
|
||
let body = '<tbody>';
|
||
RACI.roles.forEach(function (r) {
|
||
body += '<tr><th class="raci-rolehead">' + esc(r.role_name) + (r.is_governance_body ? ' <span class="gap-role-sub">(governance)</span>' : '') + '</th>';
|
||
RACI.objects.forEach(function (o) {
|
||
const cur = RACI.assignments[raciKey(r.role_id, o.type, o.id)] || '';
|
||
const cls = cur ? ' has-' + cur.toLowerCase() : '';
|
||
let opts = '<option value="">—</option>';
|
||
RACI_LETTERS.forEach(function (L) { opts += '<option value="' + L + '"' + (cur === L ? ' selected' : '') + '>' + L + '</option>'; });
|
||
const aria = 'RACI per ruolo ' + (r.role_name || '') + ' su oggetto ' + (o.label || '');
|
||
body += '<td><select class="raci-cell-sel' + cls + '" aria-label="' + esc(aria) + '" '
|
||
+ 'onchange="raciSetCell(' + r.role_id + ',\'' + o.type + '\',' + o.id + ',this.value,this)">' + opts + '</select></td>';
|
||
});
|
||
body += '</tr>';
|
||
});
|
||
body += '</tbody>';
|
||
|
||
wrap.innerHTML = '<div class="raci-matrix-wrap"><table class="raci-matrix">' + head + body + '</table></div>';
|
||
}
|
||
|
||
async function raciSetCell(roleId, type, objId, value, selEl) {
|
||
const prev = RACI.assignments[raciKey(roleId, type, objId)] || '';
|
||
try {
|
||
if (value) {
|
||
await api.raciAssign({ role_id: roleId, object_type: type, object_id: objId, raci: value });
|
||
RACI.assignments[raciKey(roleId, type, objId)] = value;
|
||
showNotification('Responsabilità ' + value + ' assegnata.', 'success');
|
||
} else {
|
||
await api.raciUnassign({ role_id: roleId, object_type: type, object_id: objId });
|
||
delete RACI.assignments[raciKey(roleId, type, objId)];
|
||
showNotification('Responsabilità rimossa.', 'success');
|
||
}
|
||
if (selEl) {
|
||
selEl.classList.remove('has-r', 'has-a', 'has-c', 'has-i');
|
||
if (value) selEl.classList.add('has-' + value.toLowerCase());
|
||
}
|
||
} catch (e) {
|
||
// ripristina il valore precedente in caso di errore
|
||
if (selEl) {
|
||
selEl.value = prev;
|
||
selEl.classList.remove('has-r', 'has-a', 'has-c', 'has-i');
|
||
if (prev) selEl.classList.add('has-' + prev.toLowerCase());
|
||
}
|
||
showNotification(e.message || 'Errore.', 'error');
|
||
}
|
||
}
|
||
|
||
/* ════════════════ TAB 2 — Collegamenti (link m2m) ════════════════ */
|
||
function currentLinkType() { return el('raci-linktype').value; }
|
||
|
||
async function raciLinkTypeChanged() {
|
||
const lt = currentLinkType();
|
||
const cfg = LINK_TYPES[lt];
|
||
if (!cfg) return;
|
||
el('raci-alabel').textContent = cfg.aLabel;
|
||
el('raci-blabel').textContent = cfg.bLabel;
|
||
const aSel = el('raci-aside');
|
||
const bSel = el('raci-bside');
|
||
aSel.innerHTML = '<option value="">— Caricamento… —</option>';
|
||
bSel.innerHTML = '<option value="">— Caricamento… —</option>';
|
||
try {
|
||
const [aList, bList] = await Promise.all([api.raciObjects(cfg.aType), api.raciObjects(cfg.bType)]);
|
||
aSel.innerHTML = raciObjOptions(aList, cfg.aType);
|
||
bSel.innerHTML = raciObjOptions(bList, cfg.bType);
|
||
} catch (e) {
|
||
showNotification('Errore nel caricamento degli oggetti: ' + (e.message || e), 'error');
|
||
aSel.innerHTML = '<option value="">— Errore —</option>';
|
||
bSel.innerHTML = '<option value="">— Errore —</option>';
|
||
}
|
||
await raciLoadLinks();
|
||
}
|
||
|
||
function raciObjOptions(list, type) {
|
||
list = Array.isArray(list) ? list : [];
|
||
if (!list.length) return '<option value="">— Nessun elemento —</option>';
|
||
// misure: value = code (string); altri: value = id (int)
|
||
return list.map(function (o) {
|
||
const v = (type === 'measure') ? o.code : o.id;
|
||
const label = (type === 'measure') ? o.label : o.label;
|
||
return '<option value="' + esc(v) + '">' + esc(label) + '</option>';
|
||
}).join('');
|
||
}
|
||
|
||
async function raciLoadLinks() {
|
||
const lt = currentLinkType();
|
||
const cfg = LINK_TYPES[lt];
|
||
const aId = el('raci-aside').value;
|
||
const wrap = el('raci-links');
|
||
if (!aId) { wrap.innerHTML = '<div class="cmp-empty">Seleziona un elemento "Lato A" per vederne i collegamenti.</div>'; return; }
|
||
wrap.innerHTML = '<div class="cmp-empty">Caricamento…</div>';
|
||
try {
|
||
const links = await api.raciLinks(lt, aId);
|
||
const rows = (Array.isArray(links) ? links : []);
|
||
if (!rows.length) { wrap.innerHTML = '<div class="cmp-empty">Nessun collegamento per questo elemento.</div>'; return; }
|
||
const body = rows.map(function (lk) {
|
||
// risk_measure: { link_id, measure_code, measure_label }; altri: { link_id, b_id, b_label }
|
||
const bId = (lt === 'risk_measure') ? lk.measure_code : lk.b_id;
|
||
const bLabel = (lt === 'risk_measure') ? lk.measure_label : lk.b_label;
|
||
return '<tr>'
|
||
+ '<td>' + esc(cfg.bLabel) + '</td>'
|
||
+ '<td><strong>' + esc(bLabel) + '</strong></td>'
|
||
+ '<td style="text-align:right;"><button class="btn btn-outline btn-sm" onclick="raciRemoveLink(\'' + esc(String(bId)) + '\')">Rimuovi</button></td>'
|
||
+ '</tr>';
|
||
}).join('');
|
||
wrap.innerHTML = '<table class="raci-linklist"><thead><tr><th>Tipo</th><th>Elemento collegato</th><th></th></tr></thead><tbody>' + body + '</tbody></table>';
|
||
} catch (e) {
|
||
wrap.innerHTML = '<div class="cmp-empty">Errore: ' + esc(e.message || e) + '</div>';
|
||
}
|
||
}
|
||
|
||
async function raciAddLink() {
|
||
const lt = currentLinkType();
|
||
const aId = el('raci-aside').value;
|
||
const bId = el('raci-bside').value;
|
||
if (!aId || !bId) { showNotification('Seleziona entrambi gli elementi.', 'error'); return; }
|
||
try {
|
||
await api.raciLink({ link_type: lt, a_id: aId, b_id: bId });
|
||
showNotification('Collegamento creato.', 'success');
|
||
await raciLoadLinks();
|
||
} catch (e) { showNotification(e.message || 'Errore.', 'error'); }
|
||
}
|
||
|
||
async function raciRemoveLink(bId) {
|
||
const lt = currentLinkType();
|
||
const aId = el('raci-aside').value;
|
||
if (!confirm('Rimuovere questo collegamento?')) return;
|
||
try {
|
||
await api.raciUnlink({ link_type: lt, a_id: aId, b_id: bId });
|
||
showNotification('Collegamento rimosso.', 'success');
|
||
await raciLoadLinks();
|
||
} catch (e) { showNotification(e.message || 'Errore.', 'error'); }
|
||
}
|