/** * 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 = '
Nessun ruolo: creane nell\'Organigramma per popolare la matrice.
'; return; } if (!RACI.objects.length) { wrap.innerHTML = '
Nessun oggetto collegabile (inventario, procedure, rischi, fornitori). Aggiungili nei rispettivi moduli.
'; return; } // Header: oggetti raggruppati per tipo let head = 'Ruolo \\ Oggetto'; RACI.objects.forEach(function (o) { head += '' + esc(OBJ_TYPE_LABELS[o.type] || o.type) + '' + esc(o.label) + ''; }); head += ''; let body = ''; RACI.roles.forEach(function (r) { body += '' + esc(r.role_name) + (r.is_governance_body ? ' (governance)' : '') + ''; 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 = ''; RACI_LETTERS.forEach(function (L) { opts += ''; }); const aria = 'RACI per ruolo ' + (r.role_name || '') + ' su oggetto ' + (o.label || ''); body += ''; }); body += ''; }); body += ''; wrap.innerHTML = '
' + head + body + '
'; } 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 = ''; bSel.innerHTML = ''; 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 = ''; bSel.innerHTML = ''; } await raciLoadLinks(); } function raciObjOptions(list, type) { list = Array.isArray(list) ? list : []; if (!list.length) return ''; // 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 ''; }).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 = '
Seleziona un elemento "Lato A" per vederne i collegamenti.
'; return; } wrap.innerHTML = '
Caricamento…
'; try { const links = await api.raciLinks(lt, aId); const rows = (Array.isArray(links) ? links : []); if (!rows.length) { wrap.innerHTML = '
Nessun collegamento per questo elemento.
'; 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 '' + '' + esc(cfg.bLabel) + '' + '' + esc(bLabel) + '' + '' + ''; }).join(''); wrap.innerHTML = '' + body + ''; } catch (e) { wrap.innerHTML = '
Errore: ' + esc(e.message || e) + '
'; } } 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'); } }