[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:
DevEnv nis2-agile
2026-06-18 19:25:16 +02:00
co-authored by Claude Opus 4.8
parent 02f3e69227
commit cb238e8380
5 changed files with 114 additions and 5 deletions
+38 -2
View File
@@ -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, '&quot;');
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>';