[FEAT] #384 p.6 — Matrice RACI sulle policy + R/A obbligatorie in approvazione

Sezione 'Matrice RACI' nel dettaglio policy: assegnazione ruoli organigramma
(org_roles) alle responsabilita R/A/C/I via infrastruttura RACI esistente
(raci_assignments, object_type='procedure'). In bozza libera; PolicyController::approve
ora richiede almeno un ruolo R e uno A (422 RACI_REQUIRED). Nessuna migrazione.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-18 19:37:51 +02:00
co-authored by Claude Opus 4.8
parent cb238e8380
commit 99bd293745
3 changed files with 146 additions and 1 deletions
@@ -237,6 +237,25 @@ class PolicyController extends BaseController
{ {
$this->requireOrgRole(['org_admin']); $this->requireOrgRole(['org_admin']);
// Ticket #384 punto 6 — gate RACI: in approvazione R e A sono obbligatorie
// (in bozza l'assegnazione resta libera). Usa l'infrastruttura RACI esistente
// (raci_assignments, object_type='procedure' -> policy_id). Decisione di design
// del richiedente super_admin: NON e' un vincolo normativo, e' una regola di prodotto.
$assignedRaci = Database::fetchAll(
"SELECT DISTINCT raci FROM raci_assignments
WHERE organization_id = ? AND object_type = 'procedure' AND object_id = ?",
[$this->getCurrentOrgId(), $id]
);
$present = array_column($assignedRaci, 'raci');
$missing = array_values(array_diff(['R', 'A'], $present));
if (!empty($missing)) {
$this->jsonError(
'Per approvare la policy assegna nella Matrice RACI almeno un ruolo ' . implode(' e uno ', $missing) . '.',
422,
'RACI_REQUIRED'
);
}
Database::update('policies', [ Database::update('policies', [
'status' => 'approved', 'status' => 'approved',
'approved_by' => $this->getCurrentUserId(), 'approved_by' => $this->getCurrentUserId(),
+126
View File
@@ -661,8 +661,26 @@
<p id="policy-att-hint-${policy.id}" style="font-size:0.78rem;color:var(--gray-500);margin:8px 0 0;">Formati ammessi: PDF, immagini, documenti Office, txt, csv, zip. Dimensione massima 10 MB.</p> <p id="policy-att-hint-${policy.id}" style="font-size:0.78rem;color:var(--gray-500);margin:8px 0 0;">Formati ammessi: PDF, immagini, documenti Office, txt, csv, zip. Dimensione massima 10 MB.</p>
</div> </div>
</div> </div>
<div class="card" style="margin-top:20px;">
<div class="card-header">
<h3>Matrice RACI</h3>
</div>
<div class="card-body">
<p style="font-size:0.85rem;color:var(--gray-600);margin:0 0 14px;">
Assegna le responsabilita' ai ruoli dell'<a class="text-link" href="organigramma.html">organigramma</a>:
<strong>R</strong> Responsible (esegue), <strong>A</strong> Accountable (approva/risponde),
<strong>C</strong> Consulted (consultato), <strong>I</strong> Informed (informato).
In bozza l'assegnazione e' facoltativa; <strong>per approvare la policy sono obbligatori almeno un ruolo R e uno A.</strong>
</p>
<div id="policy-raci-${policy.id}" aria-live="polite">
<p style="color:var(--gray-500);font-size:0.85rem;margin:0;">Caricamento matrice RACI...</p>
</div>
</div>
</div>
`; `;
loadPolicyAttachments(policy.id); loadPolicyAttachments(policy.id);
loadPolicyRaci(policy.id);
} }
function backToList() { function backToList() {
@@ -750,6 +768,114 @@
} }
} }
// ── Matrice RACI Policy (ticket #384 punto 6) ───────────────
// Riusa l'infrastruttura RACI esistente (org_roles + raci_assignments,
// object_type='procedure' -> policy_id). Nessuna migrazione.
const RACI_COLS = ['', 'R', 'A', 'C', 'I'];
const RACI_TITLES = {
'': 'Nessuna responsabilita',
'R': 'Responsible — esegue il lavoro',
'A': 'Accountable — approva e ne risponde',
'C': 'Consulted — viene consultato',
'I': 'Informed — viene informato'
};
window._policyRaciMap = window._policyRaciMap || {};
function canManageRaci() {
const role = (localStorage.getItem('nis2_user_role') || '').toLowerCase();
return ['super_admin', 'org_admin', 'compliance_manager'].includes(role);
}
async function loadPolicyRaci(policyId) {
const box = document.getElementById('policy-raci-' + policyId);
if (!box) return;
try {
const result = await api.raciMatrix();
if (!result || !result.success || !result.data) {
box.innerHTML = '<p style="color:var(--danger,#b91c1c);font-size:0.85rem;margin:0;">Impossibile caricare la matrice RACI.</p>';
return;
}
const roles = result.data.roles || [];
const map = {};
(result.data.assignments || []).forEach(a => {
if (a.object_type === 'procedure' && Number(a.object_id) === Number(policyId)) {
map[a.role_id] = a.raci;
}
});
window._policyRaciMap[policyId] = map;
if (!roles.length) {
box.innerHTML = '<p style="color:var(--gray-500);font-size:0.85rem;margin:0;">Nessun ruolo definito. Aggiungi i ruoli nell\'<a class="text-link" href="organigramma.html">organigramma</a> per assegnare la matrice RACI.</p>';
return;
}
const editable = canManageRaci();
const vals = Object.values(map);
const hasR = vals.includes('R');
const hasA = vals.includes('A');
const missing = [!hasR ? 'R' : null, !hasA ? 'A' : null].filter(Boolean);
const banner = (hasR && hasA)
? '<span class="badge badge-success">Requisiti di approvazione soddisfatti (R e A assegnate)</span>'
: '<span class="badge badge-warning">Per l\'approvazione manca: ' + missing.join(' e ') + '</span>';
let html = '<div style="margin-bottom:12px;">' + banner + '</div>';
html += '<div style="overflow-x:auto;"><table class="data-table" style="width:100%;border-collapse:collapse;">';
html += '<thead><tr><th style="text-align:left;">Ruolo (organigramma)</th>';
RACI_COLS.forEach(L => {
html += '<th style="text-align:center;min-width:48px;" title="' + RACI_TITLES[L] + '">' + (L === '' ? '&mdash;' : L) + '</th>';
});
html += '</tr></thead><tbody>';
roles.forEach(r => {
const cur = map[r.role_id] || '';
const rn = escapeHtml(r.role_name || ('Ruolo #' + r.role_id));
html += '<tr><td>' + rn + (r.is_governance_body ? ' <span class="badge badge-info" style="font-size:0.65rem;">organo di governo</span>' : '') + '</td>';
RACI_COLS.forEach(L => {
const checked = (cur === L) ? 'checked' : '';
const dis = editable ? '' : ' disabled';
html += '<td style="text-align:center;">'
+ '<input type="radio" name="raci-' + policyId + '-' + r.role_id + '" ' + checked + dis
+ ' onclick="setPolicyRaci(' + policyId + ',' + r.role_id + ',\'' + L + '\')"'
+ ' title="' + RACI_TITLES[L] + '"'
+ ' aria-label="' + RACI_TITLES[L] + ' per ' + rn + '"></td>';
});
html += '</tr>';
});
html += '</tbody></table></div>';
if (!editable) {
html += '<p style="font-size:0.78rem;color:var(--gray-500);margin:10px 0 0;">Solo Amministratore o Compliance Manager possono modificare la matrice RACI.</p>';
}
box.innerHTML = html;
} catch (e) {
box.innerHTML = '<p style="color:var(--danger,#b91c1c);font-size:0.85rem;margin:0;">Errore nel caricamento della matrice RACI.</p>';
}
}
async function setPolicyRaci(policyId, roleId, raci) {
if (!canManageRaci()) {
showNotification('Non hai i permessi per modificare la matrice RACI (richiede Amministratore o Compliance Manager).', 'warning');
loadPolicyRaci(policyId);
return;
}
const curMap = window._policyRaciMap[policyId] || {};
const cur = curMap[roleId] || '';
if (cur === raci) return; // no-op: stato gia' allineato
try {
let result;
if (raci === '') {
result = await api.raciUnassign({ role_id: roleId, object_type: 'procedure', object_id: policyId });
} else {
result = await api.raciAssign({ role_id: roleId, object_type: 'procedure', object_id: policyId, raci: raci });
}
if (result && result.success) {
showNotification('Matrice RACI aggiornata.', 'success');
} else {
showNotification((result && result.message) || 'Errore nell\'aggiornamento della matrice RACI.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
loadPolicyRaci(policyId);
}
// ── Create / Edit Modal ───────────────────────────────── // ── Create / Edit Modal ─────────────────────────────────
function openCreateModal() { function openCreateModal() {
showPolicyFormModal(null); showPolicyFormModal(null);
+1 -1
View File
@@ -1 +1 @@
{"version": "1.23.7", "build": "2026-06-18-v1.23.7", "date": "2026-06-18", "changelog": "Ticket #384 punti 2.2 e 3 (UI multi-valore + link attivi bidirezionali). In MISURE E REQUISITI la colonna 'Policy (default)' ora mostra TUTTE le policy collegate al requisito (non piu' una sola) come codici 'Policy.NN' cliccabili: il clic porta direttamente alla policy nel menu POLICY. Nel menu POLICY la colonna 'Requisito NIS2' ora mostra TUTTI i requisiti collegati con la codifica MISURA+REQUISITO (es. 'DE.CM-01 comma 1.') come link cliccabili: il clic porta al requisito in MISURE E REQUISITI, evidenziandolo. Navigazione nei due sensi (Policy <-> Misure e Requisiti). Nessuna modifica ai dati: poggia sul modello relazionale gia' introdotto (tabella requisito_policy, mig.059). Resta a fasi: matrice RACI assegnabile per policy con R e A obbligatorie in approvazione (punto 6)."} {"version": "1.23.8", "build": "2026-06-18-v1.23.8", "date": "2026-06-18", "changelog": "Ticket #384 punto 6 - Matrice RACI sulle policy. Nel dettaglio di ogni policy compare la sezione 'Matrice RACI': si assegnano i ruoli dell'organigramma alle responsabilita R (esegue), A (approva/risponde), C (consultato), I (informato). In bozza l'assegnazione e' libera; in fase di APPROVAZIONE sono ora obbligatori almeno un ruolo R e uno A (altrimenti l'approvazione viene bloccata con un messaggio che invita a completare la matrice). La A resta coerente con lo stato di approvazione gia' presente. Poggia sull'infrastruttura RACI esistente (org_roles + raci_assignments, object_type='procedure'), nessuna migrazione. Resta da rifinire help contestuale/traduzioni del nuovo pannello RACI."}