/**
* 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 = '';
}
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 = '| Tipo | Elemento collegato | |
' + 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'); }
}