Files

214 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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'); }
}