Risolve #418/#420 (ARIA sembrava non rispondere / serviva riaprire il pannello): l'attesa della risposta /api/ai/ask (RAG+Claude, alcuni secondi) non mostrava alcun indicatore -> ora compare 'ARIA sta scrivendo...' con input disabilitato (anti doppio-invio). Indicatore rispetta prefers-reduced-motion (AGID/WCAG). Risolve #425: il campo di scrittura era <input> a riga singola -> ora <textarea> con Invio=invia, Shift+Invio=a capo, auto-crescita e aria-label. common.js e' statico (no PHP/USR2): bump cache-buster ?v=20260623 su 39 pagine (public + admin) + version.json 1.24.6. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
415 lines
25 KiB
HTML
415 lines
25 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="it">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||
<title>Misure e Requisiti - NIS2 Agile</title>
|
||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||
<style>
|
||
.mr-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:14px; font-size:.92rem; line-height:1.6; }
|
||
.mr-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||
.mr-ro { font-size:.8rem; color:#3730a3; background:#eef2ff; border:1px solid #c7d2fe; border-radius:8px; padding:8px 12px; margin:0 0 16px; display:flex; gap:8px; align-items:center; }
|
||
.mr-stats { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:16px; }
|
||
.mr-stat { flex:1 1 140px; min-width:120px; background:#fff; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:12px 14px; }
|
||
.mr-stat .n { font-size:1.5rem; font-weight:800; line-height:1; }
|
||
.mr-stat .l { font-size:.76rem; color:var(--gray-500,#6b7280); margin-top:4px; }
|
||
.mr-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
|
||
.mr-toolbar label { font-weight:600; font-size:.82rem; }
|
||
.mr-toolbar select, .mr-toolbar input[type=search] { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; }
|
||
.mr-toolbar .grow { flex:1 1 auto; }
|
||
.mr-group { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; }
|
||
.mr-group-head { background:#f8fafc; padding:11px 14px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||
.mr-group-head .code { font-weight:800; color:#1e3a8a; font-size:.95rem; }
|
||
.mr-group-head .area { font-size:.78rem; color:var(--gray-500,#6b7280); }
|
||
.mr-group-head .descr { font-size:.86rem; color:#334155; margin-top:3px; line-height:1.5; }
|
||
.mr-tag { display:inline-block; font-size:.66rem; font-weight:800; padding:2px 8px; border-radius:20px; margin-left:6px; vertical-align:middle; }
|
||
.mr-tag.ess { background:#fef3c7; color:#92400e; }
|
||
.mr-table { width:100%; border-collapse:collapse; font-size:.85rem; }
|
||
.mr-table th, .mr-table td { text-align:left; padding:9px 11px; border-bottom:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
|
||
.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; }
|
||
.mr-row.spento { opacity:.4; }
|
||
.mr-row.spento td { font-style:italic; }
|
||
.mr-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); }
|
||
.sv-badge { display:inline-block; font-size:.72rem; font-weight:800; padding:3px 10px; border-radius:20px; border:none; cursor:pointer; }
|
||
.sv-badge.sv-todo { background:#f1f5f9; color:#475569; }
|
||
.sv-badge.sv-na { background:#e5e7eb; color:#6b7280; }
|
||
.sv-badge.sv-ko { background:#fee2e2; color:#991b1b; }
|
||
.sv-badge.sv-mid { background:#fef3c7; color:#92400e; }
|
||
.sv-badge.sv-ok { background:#dcfce7; color:#166534; }
|
||
.sv-val { font-size:.76rem; color:#64748b; margin-top:4px; max-width:300px; }
|
||
/* Modale valutazione */
|
||
.rm-overlay { display:none; position:fixed; inset:0; background:rgba(15,23,42,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
|
||
.rm-overlay.open { display:flex; }
|
||
.rm-dialog { background:#fff; border-radius:12px; width:100%; max-width:600px; box-shadow:0 20px 50px rgba(0,0,0,.25); }
|
||
.rm-head { display:flex; justify-content:space-between; align-items:flex-start; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||
.rm-head h3 { margin:0; font-size:1.02rem; }
|
||
.rm-head .sub { font-size:.8rem; color:var(--gray-500,#6b7280); margin-top:4px; }
|
||
.rm-body { padding:18px 20px; }
|
||
.rm-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; }
|
||
.rm-field { margin-bottom:14px; }
|
||
.rm-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; }
|
||
.rm-field select, .rm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; }
|
||
.rm-field textarea { min-height:80px; resize:vertical; }
|
||
.rm-ctx { background:#f8fafc; border:1px solid var(--gray-100,#f3f4f6); border-radius:8px; padding:10px 12px; font-size:.83rem; margin-bottom:14px; line-height:1.5; }
|
||
.rm-ctx .pill { display:inline-block; font-size:.7rem; font-weight:700; color:#3730a3; background:#eef2ff; border-radius:6px; padding:1px 6px; margin-right:4px; }
|
||
.rm-err { color:#b91c1c; font-size:.82rem; min-height:18px; }
|
||
.rm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); }
|
||
</style>
|
||
<!-- PWA:start -->
|
||
<link rel="manifest" href="/manifest.webmanifest">
|
||
<meta name="theme-color" content="#0066CC">
|
||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
|
||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16.png">
|
||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||
<meta name="apple-mobile-web-app-title" content="NIS2 Agile">
|
||
<meta name="application-name" content="NIS2 Agile">
|
||
<script src="/js/pwa.js?v=20260614" defer></script>
|
||
<!-- PWA:end -->
|
||
</head>
|
||
<body>
|
||
<div class="app-layout">
|
||
<aside class="sidebar" id="sidebar"></aside>
|
||
<main class="main-content">
|
||
<header class="content-header">
|
||
<h2 data-i18n="mr.title">Misure e Requisiti</h2>
|
||
<div class="content-header-actions">
|
||
<span class="savehint" id="mr-hint" role="status" aria-live="polite"></span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="content-body">
|
||
<div class="mr-intro">
|
||
<strong>L'elenco canonico delle misure di sicurezza di base e dei relativi requisiti.</strong>
|
||
Ogni requisito riporta la <strong>policy di default</strong> che lo soddisfa e il <strong>rischio di default</strong> associato. È la base su cui si costruiscono policy, rischi e attività: le righe si "spengono" quando non sono applicabili alla classe del tuo soggetto.
|
||
</div>
|
||
<div class="mr-ro">
|
||
🔒 <span>Elenco di riferimento, <strong>sola lettura</strong>: la codifica (misure, requisiti, ordine) non è modificabile. Policy e rischi qui elencati vengono proposti come <em>default</em> nei rispettivi moduli, dove potrai personalizzarli.</span>
|
||
</div>
|
||
<div class="mr-note">
|
||
Fonte: <strong>Determinazione ACN n. 164179 del 14 aprile 2025</strong> (misure di base, Allegati 1–2). Le misure applicabili dipendono dalla classe del soggetto: <strong>importanti</strong> 37 misure / 92 requisiti, <strong>essenziali</strong> 43 / 116. Strumento di supporto, non un parere legale.
|
||
</div>
|
||
|
||
<div class="mr-stats" id="mr-stats" style="display:none;">
|
||
<div class="mr-stat"><div class="n" id="mr-stat-mis">0</div><div class="l">Misure</div></div>
|
||
<div class="mr-stat"><div class="n" id="mr-stat-req">0</div><div class="l">Requisiti</div></div>
|
||
<div class="mr-stat"><div class="n" id="mr-stat-active">0</div><div class="l">Applicabili al soggetto</div></div>
|
||
</div>
|
||
|
||
<div class="mr-toolbar">
|
||
<label for="mr-view">Vista:</label>
|
||
<select id="mr-view" onchange="mrRender()">
|
||
<option value="misura">Per misura</option>
|
||
<option value="area">Per area politica</option>
|
||
</select>
|
||
<label for="mr-class">Soggetto:</label>
|
||
<select id="mr-class" onchange="mrRender()">
|
||
<option value="auto">Automatico (dalla mia org)</option>
|
||
<option value="important">Importante</option>
|
||
<option value="essential">Essenziale</option>
|
||
<option value="all">Tutti (mostra tutto)</option>
|
||
</select>
|
||
<label style="font-weight:500;"><input type="checkbox" id="mr-hide" onchange="mrRender()"> nascondi non applicabili</label>
|
||
<span class="grow"></span>
|
||
<input type="search" id="mr-q" placeholder="Cerca codice o testo…" oninput="mrRender()" style="min-width:200px;">
|
||
</div>
|
||
|
||
<div id="mr-list" aria-live="polite"><div class="mr-empty">Caricamento…</div></div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Modale valutazione del requisito (org-scoped) -->
|
||
<div class="rm-overlay" id="mr-modal" role="dialog" aria-modal="true" aria-labelledby="mr-modal-title">
|
||
<div class="rm-dialog">
|
||
<div class="rm-head">
|
||
<div><h3 id="mr-modal-title">Valutazione del requisito</h3><div class="sub" id="mr-modal-sub"></div></div>
|
||
<button class="rm-close" type="button" onclick="mrCloseEval()" aria-label="Chiudi">×</button>
|
||
</div>
|
||
<div class="rm-body">
|
||
<input type="hidden" id="mr-eval-id">
|
||
<div class="rm-ctx" id="mr-eval-ctx"></div>
|
||
<div class="rm-field"><label for="mr-eval-stato">Stato di conformità</label>
|
||
<select id="mr-eval-stato">
|
||
<option value="da_valutare">Da valutare</option>
|
||
<option value="non_applicabile">Non applicabile</option>
|
||
<option value="non_conforme">Non conforme</option>
|
||
<option value="parziale">Parziale</option>
|
||
<option value="conforme">Conforme</option>
|
||
</select>
|
||
</div>
|
||
<div class="rm-field"><label for="mr-eval-val">Valutazione del rischio</label>
|
||
<textarea id="mr-eval-val" maxlength="4000" placeholder="Descrivi la valutazione del rischio per questo requisito (probabilità, impatto, misure in essere, gap…)."></textarea>
|
||
</div>
|
||
<div class="rm-field"><label for="mr-eval-note">Note</label>
|
||
<textarea id="mr-eval-note" maxlength="2000" placeholder="Note operative, riferimenti, evidenze…"></textarea>
|
||
</div>
|
||
<div class="rm-err" id="mr-eval-err" role="alert"></div>
|
||
</div>
|
||
<div class="rm-foot">
|
||
<button class="btn btn-outline" type="button" onclick="mrCloseEval()">Annulla</button>
|
||
<button class="btn btn-primary" type="button" id="mr-eval-save" onclick="mrSaveEval()">Salva</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/js/api.js?v=20260631"></script>
|
||
<script src="/js/common.js?v=20260623"></script>
|
||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||
<script src="/js/common-bi.js?v=20260631"></script>
|
||
<script src="/js/i18n.js?v=20260631"></script>
|
||
<script src="/js/help.js?v=20260618p2"></script>
|
||
<script>
|
||
'use strict';
|
||
/*
|
||
* NIS2 Agile - Elenco canonico Misure/Requisiti (Epic C / C1) - SOLA LETTURA.
|
||
* Dati da api.frameworkCatalog() (tabelle cfg_nis2_*). Codifica non modificabile.
|
||
* Le righe si "spengono" per la classe del soggetto (importante/essenziale).
|
||
*/
|
||
let MR = { measures: [], org_class: null, has_org: false, stati: [], byId: {} };
|
||
|
||
function el(id) { return document.getElementById(id); }
|
||
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||
// Ticket #384 p.2.1 — i codici procedura/policy si presentano con prefisso "Policy." (Proc.NN -> Policy.NN).
|
||
// 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' },
|
||
non_conforme: { label: 'Non conforme', cls: 'sv-ko' },
|
||
parziale: { label: 'Parziale', cls: 'sv-mid' },
|
||
conforme: { label: 'Conforme', cls: 'sv-ok' }
|
||
};
|
||
function statoMeta(s) { return STATO_META[s] || STATO_META.da_valutare; }
|
||
|
||
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 mrLoad();
|
||
});
|
||
|
||
async function mrLoad() {
|
||
try {
|
||
const data = await api.frameworkCatalog();
|
||
MR.measures = (data && data.measures) || [];
|
||
MR.org_class = (data && data.org_class) || null;
|
||
MR.has_org = !!(data && data.has_org);
|
||
MR.stati = (data && data.stati) || Object.keys(STATO_META);
|
||
// indicizza i requisiti per id (per la modale)
|
||
MR.byId = {};
|
||
MR.measures.forEach(function (m) { (m.requisiti || []).forEach(function (q) { MR.byId[q.id] = q; }); });
|
||
// se conosco la classe dell'org, parto in modalità "auto"; altrimenti mostro tutto
|
||
if (!MR.org_class) el('mr-class').value = 'all';
|
||
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;
|
||
if (sel === 'auto') return MR.org_class; // può essere null => tutto attivo
|
||
if (sel === 'all') return null;
|
||
return sel; // important | essential
|
||
}
|
||
// un requisito è "attivo" per la classe effettiva?
|
||
function reqActive(req, cls) {
|
||
if (!cls) return true;
|
||
return cls === 'important' ? !!req.applies_important : !!req.applies_essential;
|
||
}
|
||
function matchQuery(m, req, q) {
|
||
if (!q) return true;
|
||
q = q.toLowerCase();
|
||
return (m.misura_code + ' ' + m.misura_descr + ' ' + m.area_politica + ' ' +
|
||
req.requisito_code + ' ' + req.requisito_descr + ' ' +
|
||
(req.proc_descr || '') + ' ' + (req.risk_descr || '')).toLowerCase().indexOf(q) >= 0;
|
||
}
|
||
|
||
function mrRender() {
|
||
const cls = mrEffectiveClass();
|
||
const hide = el('mr-hide').checked;
|
||
const q = (el('mr-q').value || '').trim();
|
||
const view = el('mr-view').value;
|
||
|
||
// costruisco le righe filtrate (misura -> requisiti visibili)
|
||
let activeCount = 0;
|
||
const groupsByKey = {}; // key -> {label, descr, area, code, essOnly, rows:[]}
|
||
|
||
MR.measures.forEach(function (m) {
|
||
const essOnly = !m.applies_important;
|
||
m.requisiti.forEach(function (req) {
|
||
if (!matchQuery(m, req, q)) return;
|
||
const active = reqActive(req, cls);
|
||
if (active) activeCount++;
|
||
if (hide && !active) return;
|
||
let key, head;
|
||
if (view === 'area') {
|
||
key = m.area_politica || '—';
|
||
head = { code: m.area_politica || '—', descr: '', area: '', essOnly: false };
|
||
} else {
|
||
key = m.misura_code;
|
||
head = { code: m.misura_code + ' — ' + (m.nist_code || ''), descr: m.misura_descr, area: m.area_politica, essOnly: essOnly };
|
||
}
|
||
if (!groupsByKey[key]) groupsByKey[key] = Object.assign({ rows: [] }, head);
|
||
groupsByKey[key].rows.push({ m: m, req: req, active: active });
|
||
});
|
||
});
|
||
|
||
el('mr-stat-active').textContent = activeCount;
|
||
|
||
const keys = Object.keys(groupsByKey);
|
||
if (!keys.length) { el('mr-list').innerHTML = '<div class="mr-empty">Nessun risultato per i filtri selezionati.</div>'; return; }
|
||
|
||
let html = '';
|
||
keys.forEach(function (key) {
|
||
const g = groupsByKey[key];
|
||
html += '<div class="mr-group">';
|
||
html += '<div class="mr-group-head"><div><span class="code">' + esc(g.code) + '</span>' +
|
||
(g.essOnly ? '<span class="mr-tag ess">solo essenziali</span>' : '') + '</div>';
|
||
if (g.area) html += '<div class="area">' + esc(g.area) + '</div>';
|
||
if (g.descr) html += '<div class="descr">' + esc(g.descr) + '</div>';
|
||
html += '</div>';
|
||
html += '<table class="mr-table"><thead><tr>' +
|
||
'<th>Requisito</th><th>Descrizione</th><th>Policy (default)</th><th>Rischio (default)</th>' +
|
||
'<th style="text-align:center">Imp.</th><th style="text-align:center">Ess.</th>' +
|
||
(MR.has_org ? '<th>Stato / Valutazione</th>' : '') + '</tr></thead><tbody>';
|
||
g.rows.forEach(function (r) {
|
||
const req = r.req, m = r.m;
|
||
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>' + 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>';
|
||
if (MR.has_org) {
|
||
const sm = statoMeta(req.stato);
|
||
const _vr = req.valutazione_rischio || '';
|
||
const val = _vr ? esc(_vr.slice(0, 90)) + (_vr.length > 90 ? '…' : '') : '';
|
||
html += '<td><button type="button" class="sv-badge ' + sm.cls + '" onclick="mrEdit(' + req.id + ')" title="Modifica valutazione">' + esc(sm.label) + '</button>' +
|
||
(val ? '<div class="sv-val">' + val + '</div>' : '') + '</td>';
|
||
}
|
||
html += '</tr>';
|
||
});
|
||
html += '</tbody></table></div>';
|
||
});
|
||
el('mr-list').innerHTML = html;
|
||
}
|
||
|
||
// ── Valutazione per requisito (org-scoped) ──────────────────────────
|
||
function mrEdit(reqId) {
|
||
const q = MR.byId[reqId];
|
||
if (!q) return;
|
||
el('mr-eval-id').value = reqId;
|
||
el('mr-modal-sub').textContent = (q.requisito_code || '') + ' · ' + (policyCode(q.proc_code) || '') + ' / ' + (q.risk_code || '');
|
||
let ctx = '<div>' + esc(q.requisito_descr || '') + '</div>';
|
||
if (q.risk_code) ctx += '<div style="margin-top:6px"><span class="pill">' + esc(q.risk_code) + '</span> ' + esc(q.risk_descr || '') + '</div>';
|
||
el('mr-eval-ctx').innerHTML = ctx;
|
||
el('mr-eval-stato').value = q.stato || 'da_valutare';
|
||
el('mr-eval-val').value = q.valutazione_rischio || '';
|
||
el('mr-eval-note').value = q.note || '';
|
||
el('mr-eval-err').textContent = '';
|
||
MR._lastFocus = document.activeElement;
|
||
el('mr-modal').classList.add('open');
|
||
setTimeout(function () { const s = el('mr-eval-stato'); if (s) s.focus(); }, 0);
|
||
}
|
||
function mrCloseEval() {
|
||
el('mr-modal').classList.remove('open');
|
||
if (MR._lastFocus && typeof MR._lastFocus.focus === 'function') { try { MR._lastFocus.focus(); } catch (e) {} }
|
||
}
|
||
async function mrSaveEval() {
|
||
const reqId = parseInt(el('mr-eval-id').value, 10);
|
||
const btn = el('mr-eval-save');
|
||
el('mr-eval-err').textContent = '';
|
||
btn.disabled = true;
|
||
try {
|
||
const res = await api.frameworkSetState({
|
||
requisito_id: reqId,
|
||
stato: el('mr-eval-stato').value,
|
||
valutazione_rischio: el('mr-eval-val').value.trim(),
|
||
note: el('mr-eval-note').value.trim()
|
||
});
|
||
const q = MR.byId[reqId];
|
||
if (q && res) { q.stato = res.stato; q.valutazione_rischio = res.valutazione_rischio; q.note = res.note; }
|
||
mrCloseEval();
|
||
mrRender();
|
||
const hint = el('mr-hint'); if (hint) { hint.textContent = 'Valutazione salvata'; setTimeout(function () { hint.textContent = ''; }, 2500); }
|
||
} catch (e) {
|
||
el('mr-eval-err').textContent = (e && e.message) ? e.message : 'Errore nel salvataggio.';
|
||
} finally { btn.disabled = false; }
|
||
}
|
||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') mrCloseEval(); });
|
||
(function () {
|
||
const ov = el('mr-modal'); if (!ov) return;
|
||
ov.addEventListener('click', function (e) { if (e.target === ov) mrCloseEval(); });
|
||
// focus-trap: Tab/Shift+Tab restano dentro la modale aperta
|
||
ov.addEventListener('keydown', function (e) {
|
||
if (e.key !== 'Tab' || !ov.classList.contains('open')) return;
|
||
const nodes = ov.querySelectorAll('select, textarea, button');
|
||
const list = Array.prototype.filter.call(nodes, function (x) { return !x.disabled && x.offsetParent !== null; });
|
||
if (!list.length) return;
|
||
const first = list[0], last = list[list.length - 1];
|
||
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
||
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|