[FEAT] #384 p.2.2/3 — colonne multi-valore + link attivi bidirezionali Policy<->Misure e Requisiti
Misure e Requisiti: colonna Policy (default) mostra tutte le policy collegate al requisito come codici Policy.NN cliccabili -> aprono la policy (policies.html?policy=ID). Policy: colonna Requisito NIS2 mostra tutti i requisiti collegati (codifica MISURA+ REQUISITO, es. 'DE.CM-01 comma 1.') come link -> evidenziano il requisito (misure-requisiti.html?requisito=ID). Backend: FrameworkController::catalog espone policies[] per requisito e PolicyController::list espone requisiti[] per policy (org-scoped, tabella requisito_policy mig.059, try/catch con degrado al dato di default). Solo lettura/visualizzazione: nessuna modifica dati, 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
02f3e69227
commit
cb238e8380
@@ -79,6 +79,32 @@ class FrameworkController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
// Ticket #384 p.2.2/p.3 — policy d'organizzazione collegate a ciascun requisito
|
||||
// (M:N org-scoped, tabella requisito_policy mig.059). try/catch: l'eventuale assenza
|
||||
// della tabella NON deve rompere il catalogo (degrada al solo proc_code di default).
|
||||
$policiesByReq = [];
|
||||
if ($orgId) {
|
||||
try {
|
||||
foreach (Database::fetchAll(
|
||||
'SELECT rp.requisito_id, p.id AS policy_id, p.title, p.proc_code, p.status
|
||||
FROM requisito_policy rp
|
||||
JOIN policies p ON p.id = rp.policy_id
|
||||
WHERE rp.organization_id = ?
|
||||
ORDER BY (p.proc_code IS NULL), p.proc_code, p.title',
|
||||
[$orgId]
|
||||
) as $rp) {
|
||||
$policiesByReq[(int) $rp['requisito_id']][] = [
|
||||
'id' => (int) $rp['policy_id'],
|
||||
'proc_code' => $rp['proc_code'],
|
||||
'title' => $rp['title'],
|
||||
'status' => $rp['status'],
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[FrameworkController] requisito_policy lookup (org ' . $orgId . '): ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$ambiti = Database::fetchAll('SELECT nist_code, nist_descr FROM cfg_nis2_ambiti ORDER BY nist_code ASC');
|
||||
|
||||
$misure = Database::fetchAll(
|
||||
@@ -108,6 +134,7 @@ class FrameworkController extends BaseController
|
||||
'requisito_descr' => $q['requisito_descr'],
|
||||
'proc_code' => $q['proc_code'],
|
||||
'proc_descr' => $q['proc_descr'],
|
||||
'policies' => $policiesByReq[$rid] ?? [], // #384 p.2.2/p.3 (multi-valore)
|
||||
'risk_code' => $q['risk_code'],
|
||||
'risk_descr' => $q['risk_descr'],
|
||||
'applies_important' => (int) $q['applies_important'],
|
||||
|
||||
@@ -52,6 +52,35 @@ class PolicyController extends BaseController
|
||||
$params
|
||||
);
|
||||
|
||||
// Ticket #384 p.2.2/p.3 — requisiti collegati a ciascuna policy (M:N org-scoped,
|
||||
// tabella requisito_policy mig.059). Il codice mostrato e' la concatenazione
|
||||
// MISURA + REQUISITO (spec 2.2). try/catch: degrada al solo nis2_article se assente.
|
||||
try {
|
||||
$reqRows = Database::fetchAll(
|
||||
'SELECT rp.policy_id, q.id AS requisito_id, q.misura_code, q.requisito_code
|
||||
FROM requisito_policy rp
|
||||
JOIN cfg_nis2_requisiti q ON q.id = rp.requisito_id
|
||||
WHERE rp.organization_id = ?
|
||||
ORDER BY q.misura_code, q.n, q.id',
|
||||
[$this->getCurrentOrgId()]
|
||||
);
|
||||
$reqByPolicy = [];
|
||||
foreach ($reqRows as $r) {
|
||||
$reqByPolicy[(int) $r['policy_id']][] = [
|
||||
'id' => (int) $r['requisito_id'],
|
||||
'misura_code' => $r['misura_code'],
|
||||
'requisito_code' => $r['requisito_code'],
|
||||
'code' => trim(($r['misura_code'] ?? '') . ' ' . ($r['requisito_code'] ?? '')),
|
||||
];
|
||||
}
|
||||
foreach ($policies as &$pol) {
|
||||
$pol['requisiti'] = $reqByPolicy[(int) $pol['id']] ?? [];
|
||||
}
|
||||
unset($pol);
|
||||
} catch (\Throwable $e) {
|
||||
error_log('[PolicyController] requisiti per policy (org ' . $this->getCurrentOrgId() . '): ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$this->jsonSuccess($policies);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
.mr-table th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); background:#fff; position:sticky; top:0; }
|
||||
.mr-req-code { white-space:nowrap; font-weight:700; color:#475569; }
|
||||
.mr-code-pill { display:inline-block; font-size:.7rem; font-weight:700; color:#3730a3; background:#eef2ff; border-radius:6px; padding:1px 6px; }
|
||||
/* Ticket #384 p.3 — codici Policy come link attivi verso il menu POLICY */
|
||||
a.mr-code-link { text-decoration:none; cursor:pointer; margin-right:3px; }
|
||||
a.mr-code-link:hover { background:#e0e7ff; text-decoration:underline; }
|
||||
a.mr-code-link:focus-visible { outline:2px solid #3730a3; outline-offset:1px; }
|
||||
.mr-row-focus td { background:#fef9c3 !important; transition:background .4s ease; }
|
||||
.mr-yn { text-align:center; font-weight:800; }
|
||||
.mr-yn.y { color:#166534; }
|
||||
.mr-yn.n { color:#cbd5e1; }
|
||||
@@ -183,6 +188,24 @@
|
||||
// Il codice canonico nel catalogo resta proc_code (chiave di join requisiti<->procedure): qui si rietichetta solo per la UI.
|
||||
function policyCode(code) { return code ? String(code).replace(/^Proc\./i, 'Policy.') : code; }
|
||||
|
||||
// Ticket #384 p.2.2/p.3 — colonna "Policy (default)" multi-valore con link attivi al menu POLICY.
|
||||
// Usa le policy d'organizzazione collegate (req.policies dal backend); in assenza degrada
|
||||
// al codice di default del catalogo (proc_code), com'era prima.
|
||||
function renderPolicyLinks(req) {
|
||||
const pols = req.policies || [];
|
||||
if (pols.length) {
|
||||
return pols.map(function (p) {
|
||||
const code = p.proc_code ? policyCode(p.proc_code) : ('Policy.' + p.id);
|
||||
const title = (p.title || '').replace(/"/g, '"');
|
||||
return '<a class="mr-code-pill mr-code-link" href="policies.html?policy=' + p.id +
|
||||
'" title="' + esc(title) + '">' + esc(code) + '</a>';
|
||||
}).join(' ');
|
||||
}
|
||||
return req.proc_code
|
||||
? '<span class="mr-code-pill">' + esc(policyCode(req.proc_code)) + '</span> ' + esc(req.proc_descr || '')
|
||||
: '—';
|
||||
}
|
||||
|
||||
const STATO_META = {
|
||||
da_valutare: { label: 'Da valutare', cls: 'sv-todo' },
|
||||
non_applicabile: { label: 'Non applicabile', cls: 'sv-na' },
|
||||
@@ -215,12 +238,25 @@
|
||||
el('mr-stat-mis').textContent = (data && data.totals && data.totals.misure) || MR.measures.length;
|
||||
el('mr-stat-req').textContent = (data && data.totals && data.totals.requisiti) || 0;
|
||||
el('mr-stats').style.display = 'flex';
|
||||
// Ticket #384 p.3 — deep-link dal menu POLICY: ?requisito=ID → mostra ed evidenzia il requisito.
|
||||
const focusReq = parseInt(new URLSearchParams(location.search).get('requisito') || '', 10);
|
||||
if (focusReq) el('mr-class').value = 'all'; // evita che la riga sia filtrata via
|
||||
mrRender();
|
||||
if (focusReq) mrFocusRequisito(focusReq);
|
||||
} catch (e) {
|
||||
el('mr-list').innerHTML = '<div class="mr-empty">Impossibile caricare l\'elenco. Riprova più tardi.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Ticket #384 p.3 — scorre alla riga del requisito e la evidenzia (arrivo da link Policy).
|
||||
function mrFocusRequisito(reqId) {
|
||||
const row = document.getElementById('req-' + reqId);
|
||||
if (!row) return;
|
||||
row.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
row.classList.add('mr-row-focus');
|
||||
setTimeout(function () { row.classList.remove('mr-row-focus'); }, 4000);
|
||||
}
|
||||
|
||||
// classe effettiva selezionata (risolve "auto" sulla classe dell'org)
|
||||
function mrEffectiveClass() {
|
||||
const sel = el('mr-class').value;
|
||||
@@ -291,14 +327,14 @@
|
||||
(MR.has_org ? '<th>Stato / Valutazione</th>' : '') + '</tr></thead><tbody>';
|
||||
g.rows.forEach(function (r) {
|
||||
const req = r.req, m = r.m;
|
||||
html += '<tr class="mr-row' + (r.active ? '' : ' spento') + '">';
|
||||
html += '<tr id="req-' + req.id + '" class="mr-row' + (r.active ? '' : ' spento') + '">';
|
||||
if (view === 'area') {
|
||||
html += '<td class="mr-req-code"><span class="mr-code-pill">' + esc(m.misura_code) + '</span> ' + esc(req.requisito_code) + '</td>';
|
||||
} else {
|
||||
html += '<td class="mr-req-code">' + esc(req.requisito_code) + '</td>';
|
||||
}
|
||||
html += '<td>' + esc(req.requisito_descr) + '</td>';
|
||||
html += '<td>' + (req.proc_code ? '<span class="mr-code-pill">' + esc(policyCode(req.proc_code)) + '</span> ' + esc(req.proc_descr || '') : '—') + '</td>';
|
||||
html += '<td>' + renderPolicyLinks(req) + '</td>';
|
||||
html += '<td>' + (req.risk_code ? '<span class="mr-code-pill">' + esc(req.risk_code) + '</span> ' + esc(req.risk_descr || '') : '—') + '</td>';
|
||||
html += '<td class="mr-yn ' + (req.applies_important ? 'y' : 'n') + '">' + (req.applies_important ? '✓' : '—') + '</td>';
|
||||
html += '<td class="mr-yn ' + (req.applies_essential ? 'y' : 'n') + '">' + (req.applies_essential ? '✓' : '—') + '</td>';
|
||||
|
||||
+19
-2
@@ -410,6 +410,19 @@
|
||||
currentView = 'list';
|
||||
}
|
||||
|
||||
// Ticket #384 p.2.2/p.3 — colonna "Requisito NIS2" multi-valore con link attivi a MISURE E REQUISITI.
|
||||
// Il codice mostrato e' MISURA + REQUISITO (dal backend, tabella requisito_policy). In assenza di
|
||||
// associazioni degrada al campo nis2_article, com'era prima.
|
||||
function renderReqLinks(p) {
|
||||
const reqs = (p && p.requisiti) || [];
|
||||
if (reqs.length) {
|
||||
return reqs.map(r =>
|
||||
`<a class="text-link" href="misure-requisiti.html?requisito=${r.id}" title="Vai al requisito in Misure e Requisiti">${escapeHtml(r.code || r.misura_code || '')}</a>`
|
||||
).join(', ');
|
||||
}
|
||||
return escapeHtml(p.nis2_article || '-');
|
||||
}
|
||||
|
||||
// ── Load Policies ───────────────────────────────────────
|
||||
async function loadPolicies() {
|
||||
const params = {};
|
||||
@@ -475,7 +488,7 @@
|
||||
<span class="text-link" onclick="viewPolicy(${p.id})">${escapeHtml(p.title)}</span>
|
||||
</td>
|
||||
<td>${escapeHtml(catLabel)}</td>
|
||||
<td>${escapeHtml(p.nis2_article || '-')}</td>
|
||||
<td>${renderReqLinks(p)}</td>
|
||||
<td>${escapeHtml(p.version || '1.0')}</td>
|
||||
<td><span class="badge ${badgeClass}">${escapeHtml(statusLabel)}</span></td>
|
||||
<td>${formatDate(p.next_review_date)}</td>
|
||||
@@ -1075,7 +1088,11 @@
|
||||
}
|
||||
|
||||
// ── Init ────────────────────────────────────────────────
|
||||
loadPolicies();
|
||||
loadPolicies().then(function () {
|
||||
// Ticket #384 p.3 — deep-link da MISURE E REQUISITI: ?policy=ID apre il dettaglio della policy.
|
||||
const pid = parseInt(new URLSearchParams(location.search).get('policy') || '', 10);
|
||||
if (pid) viewPolicy(pid);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version": "1.23.6", "build": "2026-06-18-v1.23.6", "date": "2026-06-18", "changelog": "Ticket #384 punto 2.2/3 (fondazione dati): introdotto il modello relazionale REQUISITO<->POLICY (migrazione 059, additiva/reversibile). Ripristinata sulla riga policy la chiave 'proc_code' (persa quando le procedure di default venivano copiate nell'elenco Policy) e creata la tabella requisito_policy per il legame molti-a-molti org-scoped (un requisito puo' avere piu' policy e una policy piu' requisiti). Backfill sicuro: valorizzato proc_code su 42 policy e popolate 116 associazioni di default esistenti. Nessun impatto visibile per l'utente in questo passo: e' la base dati su cui poggeranno l'unificazione multi-valore delle colonne (2.2) e i link attivi bidirezionali tra i due menu (3). Restano in lavorazione a fasi: UI multi-valore + link bidirezionali (2.2/3) e matrice RACI assegnabile per policy con R e A obbligatorie in approvazione (punto 6)."}
|
||||
{"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)."}
|
||||
|
||||
Reference in New Issue
Block a user