[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:
co-authored by
Claude Opus 4.8
parent
cb238e8380
commit
99bd293745
@@ -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(),
|
||||||
|
|||||||
@@ -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 === '' ? '—' : 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
@@ -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."}
|
||||||
|
|||||||
Reference in New Issue
Block a user