[FEAT] Gap Analysis ACN: frontend (acn-gap.html + sidebar + api.js)
- public/acn-gap.html: catalogo requisiti consultabile, wizard per funzione FW con accordion misure/requisiti, opzioni attuato/parziale/non attuato/N.A., autosave batch debounce, risultati con punteggio per funzione + piano d'azione gap + analisi AI. Badge livello soggetto (importante/essenziale). IT/EN inline. - api.js: metodi acn* (catalog/list/create/requirements/respond/complete/report/ aiAnalyze) che spacchettano l'envelope e lanciano errore su success=false. - common.js: voce sidebar "Gap Analysis ACN". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ea2a291325
commit
91043d7391
409
public/acn-gap.html
Normal file
409
public/acn-gap.html
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Gap Analysis ACN - NIS2 Agile</title>
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<style>
|
||||||
|
.acn-intro { background:#eff6ff; border-left:4px solid var(--primary, #2563eb); padding:14px 18px; border-radius:8px; margin-bottom:20px; font-size:.92rem; line-height:1.6; }
|
||||||
|
.acn-level-badge { display:inline-block; padding:4px 12px; border-radius:999px; font-size:.8rem; font-weight:700; }
|
||||||
|
.acn-level-important { background:#fef3c7; color:#92400e; }
|
||||||
|
.acn-level-essential { background:#fee2e2; color:#991b1b; }
|
||||||
|
.acn-func { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; }
|
||||||
|
.acn-func-head { background:var(--gray-50,#f9fafb); padding:12px 16px; cursor:pointer; display:flex; justify-content:space-between; align-items:center; gap:12px; }
|
||||||
|
.acn-func-head strong { font-size:1rem; }
|
||||||
|
.acn-func-meta { font-size:.8rem; color:var(--gray-500,#6b7280); }
|
||||||
|
.acn-func-body { padding:8px 16px 16px; display:none; }
|
||||||
|
.acn-func.open .acn-func-body { display:block; }
|
||||||
|
.acn-measure { border-top:1px solid var(--gray-100,#f3f4f6); padding:14px 0; }
|
||||||
|
.acn-measure-code { display:inline-block; background:#eef2ff; color:#3730a3; font-weight:700; font-size:.72rem; padding:2px 8px; border-radius:6px; margin-right:8px; }
|
||||||
|
.acn-measure-title { font-weight:600; font-size:.92rem; }
|
||||||
|
.acn-req { padding:12px 0 12px 14px; border-left:2px solid var(--gray-100,#f3f4f6); margin:10px 0 10px 8px; }
|
||||||
|
.acn-req-text { font-size:.9rem; margin-bottom:8px; }
|
||||||
|
.acn-req-idx { color:var(--gray-400,#9ca3af); font-weight:700; margin-right:6px; }
|
||||||
|
.acn-opts { display:flex; flex-wrap:wrap; gap:6px; }
|
||||||
|
.acn-opt { min-height:38px; padding:6px 12px; border:1.5px solid var(--gray-200,#e5e7eb); border-radius:20px; background:#fff; cursor:pointer; font-size:.84rem; font-weight:600; }
|
||||||
|
.acn-opt.sel-implemented { background:#16a34a; color:#fff; border-color:#16a34a; }
|
||||||
|
.acn-opt.sel-partial { background:#f59e0b; color:#fff; border-color:#f59e0b; }
|
||||||
|
.acn-opt.sel-not_implemented { background:#dc2626; color:#fff; border-color:#dc2626; }
|
||||||
|
.acn-opt.sel-not_applicable { background:#6b7280; color:#fff; border-color:#6b7280; }
|
||||||
|
.acn-progress { position:sticky; top:0; background:#fff; z-index:5; padding:12px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||||||
|
.acn-bar { height:8px; background:var(--gray-100,#f3f4f6); border-radius:4px; overflow:hidden; margin:8px 0; }
|
||||||
|
.acn-bar-fill { height:100%; background:var(--primary,#2563eb); width:0; transition:width .3s; }
|
||||||
|
.acn-func-score { display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); font-size:.9rem; }
|
||||||
|
.acn-score-pill { font-weight:700; padding:2px 10px; border-radius:6px; }
|
||||||
|
.savehint { font-size:.78rem; color:var(--gray-400,#9ca3af); min-height:16px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<aside class="sidebar" id="sidebar"></aside>
|
||||||
|
<main class="main-content">
|
||||||
|
<header class="content-header">
|
||||||
|
<h2 data-i18n="acn.title">Gap Analysis ACN (misure di base)</h2>
|
||||||
|
<div class="content-header-actions">
|
||||||
|
<button class="btn btn-outline btn-sm" id="btn-ai-analyze" style="display:none;" onclick="acnAiAnalyze()" data-i18n="acn.ai">Analisi AI dei gap</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content-body">
|
||||||
|
<div class="acn-intro" id="acn-intro">
|
||||||
|
<strong data-i18n="acn.intro.title">Conformità puntuale alla Determinazione ACN 164179/2025.</strong>
|
||||||
|
<span data-i18n="acn.intro.body">Questa analisi va oltre le 10 misure generiche dell'Art. 21: valuta le misure e i requisiti puntuali stabiliti dall'ACN per i soggetti NIS. Il perimetro dipende dalla classificazione del soggetto (importante o essenziale).</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- START -->
|
||||||
|
<div id="acn-start" class="card mb-24">
|
||||||
|
<div class="card-body" style="padding:32px; text-align:center;">
|
||||||
|
<div id="acn-level-info" class="mb-24"></div>
|
||||||
|
<div id="acn-existing" class="mb-24" style="display:none; text-align:left;"></div>
|
||||||
|
<button class="btn btn-primary btn-lg" id="btn-new-acn" onclick="createAcn()" data-i18n="acn.new">Nuova Gap Analysis ACN</button>
|
||||||
|
<div style="margin-top:14px;">
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="showCatalog()" data-i18n="acn.catalog.btn">Consulta il catalogo dei requisiti</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CATALOG (read-only) -->
|
||||||
|
<div id="acn-catalog" class="card mb-24" style="display:none;">
|
||||||
|
<div class="card-header"><h3 data-i18n="acn.catalog.title">Catalogo requisiti ACN applicabili</h3></div>
|
||||||
|
<div class="card-body" id="acn-catalog-body"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- WIZARD -->
|
||||||
|
<div id="acn-wizard" style="display:none;">
|
||||||
|
<div class="card acn-progress">
|
||||||
|
<div class="card-body" style="padding:14px 18px;">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||||
|
<strong id="acn-wizard-title">Gap Analysis ACN</strong>
|
||||||
|
<span id="acn-wizard-level"></span>
|
||||||
|
</div>
|
||||||
|
<div class="acn-bar"><div class="acn-bar-fill" id="acn-bar-fill"></div></div>
|
||||||
|
<div style="font-size:.82rem; color:var(--gray-500,#6b7280);"><span id="acn-answered">0</span> / <span id="acn-total">0</span> <span data-i18n="acn.answered">requisiti con risposta</span></div>
|
||||||
|
<div class="savehint" id="acn-savehint"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card mb-24">
|
||||||
|
<div class="card-body" id="acn-functions"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card mb-24">
|
||||||
|
<div class="card-body" style="text-align:center;">
|
||||||
|
<div id="acn-wizard-msg"></div>
|
||||||
|
<button class="btn btn-primary btn-lg" id="btn-acn-complete" onclick="completeAcn()" data-i18n="acn.complete">Calcola punteggio e completa</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RESULTS -->
|
||||||
|
<div id="acn-results" style="display:none;">
|
||||||
|
<div class="card mb-24">
|
||||||
|
<div class="card-header"><h3 data-i18n="acn.results.title">Esito Gap Analysis ACN</h3></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div style="text-align:center; margin-bottom:20px;">
|
||||||
|
<div style="font-size:3rem; font-weight:800;" id="acn-overall">0</div>
|
||||||
|
<div class="text-muted" data-i18n="acn.results.overall">Punteggio complessivo di conformità (0-100)</div>
|
||||||
|
</div>
|
||||||
|
<h4 style="margin:16px 0 8px;" data-i18n="acn.results.byfunc">Punteggio per funzione (Framework Nazionale)</h4>
|
||||||
|
<div id="acn-func-scores"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card mb-24" id="acn-ai-card" style="display:none;">
|
||||||
|
<div class="card-header"><h3 data-i18n="acn.results.ai">Analisi AI dei gap</h3></div>
|
||||||
|
<div class="card-body" id="acn-ai-body"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card mb-24">
|
||||||
|
<div class="card-header"><h3 data-i18n="acn.results.gaps">Requisiti non conformi (piano d'azione)</h3></div>
|
||||||
|
<div class="card-body" id="acn-gaps"></div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:center; margin-bottom:24px;">
|
||||||
|
<button class="btn btn-outline" onclick="location.reload()" data-i18n="acn.backstart">Torna all'inizio</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/i18n.js"></script>
|
||||||
|
<script src="js/common.js"></script>
|
||||||
|
<script src="js/api.js"></script>
|
||||||
|
<script src="js/help.js"></script>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
const FUNC_LABELS = {
|
||||||
|
GOVERN: { it:'Governo (GV)', en:'Govern (GV)' },
|
||||||
|
IDENTIFY: { it:'Identificazione (ID)', en:'Identify (ID)' },
|
||||||
|
PROTECT: { it:'Protezione (PR)', en:'Protect (PR)' },
|
||||||
|
DETECT: { it:'Rilevamento (DE)', en:'Detect (DE)' },
|
||||||
|
RESPOND: { it:'Risposta (RS)', en:'Respond (RS)' },
|
||||||
|
RECOVER: { it:'Ripristino (RC)', en:'Recover (RC)' }
|
||||||
|
};
|
||||||
|
let ACN = { id:null, level:null, answers:{}, total:0 };
|
||||||
|
let saveTimer = null;
|
||||||
|
|
||||||
|
function lang(){ try { return I18n.getLang ? I18n.getLang() : 'it'; } catch(e){ return 'it'; } }
|
||||||
|
function el(id){ return document.getElementById(id); }
|
||||||
|
function esc(s){ const d=document.createElement('div'); d.textContent=s==null?'':String(s); return d.innerHTML; }
|
||||||
|
|
||||||
|
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 initStart();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function initStart(){
|
||||||
|
try {
|
||||||
|
const cat = await api.acnCatalog();
|
||||||
|
ACN.level = cat.entity_level;
|
||||||
|
const lvlTxt = cat.entity_level === 'essential'
|
||||||
|
? '<span class="acn-level-badge acn-level-essential">Soggetto ESSENZIALE</span>'
|
||||||
|
: '<span class="acn-level-badge acn-level-important">Soggetto IMPORTANTE</span>';
|
||||||
|
el('acn-level-info').innerHTML = lvlTxt +
|
||||||
|
'<p class="text-muted" style="margin-top:10px;">' +
|
||||||
|
(lang()==='en'?'Applicable scope: ':'Perimetro applicabile: ') +
|
||||||
|
'<strong>'+cat.totals.measures+'</strong> '+(lang()==='en'?'measures':'misure')+', <strong>'+
|
||||||
|
cat.totals.requirements+'</strong> '+(lang()==='en'?'requirements':'requisiti')+'.</p>';
|
||||||
|
window._acnCatalog = cat;
|
||||||
|
} catch(e){
|
||||||
|
if (e && e.error_code === 'ENTITY_LEVEL_REQUIRED') {
|
||||||
|
el('acn-level-info').innerHTML = '<p class="text-muted">'+(lang()==='en'
|
||||||
|
?'Classify the organization as important or essential first.'
|
||||||
|
:'Classifica prima il soggetto come importante o essenziale (Impostazioni › Classificazione NIS2).')+'</p>';
|
||||||
|
} else {
|
||||||
|
el('acn-level-info').innerHTML = '<p class="text-muted">'+(lang()==='en'?'Could not load.':'Caricamento non riuscito.')+'</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const list = await api.acnList();
|
||||||
|
if (list.assessments && list.assessments.length){
|
||||||
|
let html = '<h4 style="margin-bottom:8px;">'+(lang()==='en'?'Existing assessments':'Assessment esistenti')+'</h4>';
|
||||||
|
list.assessments.forEach(function(a){
|
||||||
|
const st = a.status==='completed'
|
||||||
|
? '<span class="badge badge-success">'+(lang()==='en'?'Completed':'Completato')+(a.overall_score!=null?(' · '+a.overall_score):'')+'</span>'
|
||||||
|
: '<span class="badge badge-warning">'+(lang()==='en'?'In progress':'In corso')+'</span>';
|
||||||
|
html += '<div style="display:flex; justify-content:space-between; align-items:center; gap:10px; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6);">'+
|
||||||
|
'<div><strong>'+esc(a.title)+'</strong> '+st+'</div>'+
|
||||||
|
'<button class="btn btn-sm btn-outline" onclick="openAcn('+a.id+',\''+a.status+'\')">'+
|
||||||
|
(a.status==='completed'?(lang()==='en'?'View':'Vedi'):(lang()==='en'?'Resume':'Riprendi'))+'</button></div>';
|
||||||
|
});
|
||||||
|
el('acn-existing').innerHTML = html;
|
||||||
|
el('acn-existing').style.display='block';
|
||||||
|
}
|
||||||
|
} catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showCatalog(){
|
||||||
|
const cat = window._acnCatalog;
|
||||||
|
if (!cat){ return; }
|
||||||
|
let html = '';
|
||||||
|
(cat.functions||[]).forEach(function(f){
|
||||||
|
html += '<div class="acn-func open"><div class="acn-func-head" onclick="this.parentNode.classList.toggle(\'open\')">'+
|
||||||
|
'<strong>'+esc(FUNC_LABELS[f.function_code]?FUNC_LABELS[f.function_code][lang()]:f.function_name)+'</strong>'+
|
||||||
|
'<span class="acn-func-meta">'+(f.measures||[]).length+' '+(lang()==='en'?'measures':'misure')+'</span></div>'+
|
||||||
|
'<div class="acn-func-body">';
|
||||||
|
(f.measures||[]).forEach(function(m){
|
||||||
|
html += '<div class="acn-measure"><span class="acn-measure-code">'+esc(m.measure_code)+'</span>'+
|
||||||
|
'<span class="acn-measure-title">'+esc(m.measure_title)+'</span>';
|
||||||
|
(m.requirements||[]).forEach(function(r){
|
||||||
|
html += '<div class="acn-req"><span class="acn-req-idx">'+r.index+'.</span>'+esc(r.text)+'</div>';
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
html += '</div></div>';
|
||||||
|
});
|
||||||
|
el('acn-catalog-body').innerHTML = html;
|
||||||
|
el('acn-catalog').style.display='block';
|
||||||
|
el('acn-catalog').scrollIntoView({behavior:'smooth'});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAcn(){
|
||||||
|
try {
|
||||||
|
const res = await api.acnCreate({});
|
||||||
|
await openAcn(res.id, 'draft');
|
||||||
|
} catch(e){
|
||||||
|
alert((e && e.message) || (lang()==='en'?'Creation failed':'Creazione non riuscita'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openAcn(id, status){
|
||||||
|
ACN.id = id;
|
||||||
|
const res = await api.acnRequirements(id);
|
||||||
|
ACN.level = res.assessment.entity_level;
|
||||||
|
if (res.assessment.status === 'completed'){
|
||||||
|
await showResults(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderWizard(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWizard(res){
|
||||||
|
el('acn-start').style.display='none';
|
||||||
|
el('acn-catalog').style.display='none';
|
||||||
|
el('acn-results').style.display='none';
|
||||||
|
el('acn-wizard').style.display='block';
|
||||||
|
el('btn-ai-analyze').style.display='none';
|
||||||
|
el('acn-wizard-level').innerHTML = ACN.level==='essential'
|
||||||
|
? '<span class="acn-level-badge acn-level-essential">ESSENZIALE</span>'
|
||||||
|
: '<span class="acn-level-badge acn-level-important">IMPORTANTE</span>';
|
||||||
|
|
||||||
|
ACN.answers = {}; ACN.total = 0;
|
||||||
|
let html = '';
|
||||||
|
(res.functions||[]).forEach(function(f, fi){
|
||||||
|
let fcount = 0;
|
||||||
|
let inner = '';
|
||||||
|
(f.categories||[]).forEach(function(c){
|
||||||
|
(c.measures||[]).forEach(function(m){
|
||||||
|
inner += '<div class="acn-measure"><span class="acn-measure-code">'+esc(m.measure_code)+'</span>'+
|
||||||
|
'<span class="acn-measure-title">'+esc(m.measure_title)+'</span>';
|
||||||
|
(m.requirements||[]).forEach(function(r){
|
||||||
|
ACN.total++;
|
||||||
|
fcount++;
|
||||||
|
if (r.response) ACN.answers[r.key] = r.response;
|
||||||
|
inner += renderReq(r);
|
||||||
|
});
|
||||||
|
inner += '</div>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
html += '<div class="acn-func'+(fi===0?' open':'')+'">'+
|
||||||
|
'<div class="acn-func-head" onclick="this.parentNode.classList.toggle(\'open\')">'+
|
||||||
|
'<strong>'+esc(FUNC_LABELS[f.function_code]?FUNC_LABELS[f.function_code][lang()]:f.function_name)+'</strong>'+
|
||||||
|
'<span class="acn-func-meta">'+fcount+' '+(lang()==='en'?'requirements':'requisiti')+'</span></div>'+
|
||||||
|
'<div class="acn-func-body">'+inner+'</div></div>';
|
||||||
|
});
|
||||||
|
el('acn-functions').innerHTML = html;
|
||||||
|
el('acn-total').textContent = ACN.total;
|
||||||
|
updateProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderReq(r){
|
||||||
|
const opts = [
|
||||||
|
['implemented', lang()==='en'?'Implemented':'Attuato'],
|
||||||
|
['partial', lang()==='en'?'Partial':'Parziale'],
|
||||||
|
['not_implemented', lang()==='en'?'Not implemented':'Non attuato'],
|
||||||
|
['not_applicable', lang()==='en'?'N/A':'Non applic.']
|
||||||
|
];
|
||||||
|
let btns = '';
|
||||||
|
opts.forEach(function(o){
|
||||||
|
const sel = r.response===o[0] ? ('sel-'+o[0]) : '';
|
||||||
|
btns += '<button type="button" class="acn-opt '+sel+'" data-key="'+esc(r.key)+'" data-val="'+o[0]+'" onclick="pick(this)">'+o[1]+'</button>';
|
||||||
|
});
|
||||||
|
return '<div class="acn-req"><div class="acn-req-text"><span class="acn-req-idx">'+r.index+'.</span>'+esc(r.text)+'</div>'+
|
||||||
|
'<div class="acn-opts">'+btns+'</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pick(btn){
|
||||||
|
const key = btn.getAttribute('data-key');
|
||||||
|
const val = btn.getAttribute('data-val');
|
||||||
|
const wrap = btn.parentNode;
|
||||||
|
Array.prototype.forEach.call(wrap.querySelectorAll('.acn-opt'), function(b){
|
||||||
|
b.className = 'acn-opt';
|
||||||
|
});
|
||||||
|
btn.className = 'acn-opt sel-'+val;
|
||||||
|
ACN.answers[key] = val;
|
||||||
|
updateProgress();
|
||||||
|
scheduleSave(key, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pendingSaves = {};
|
||||||
|
function scheduleSave(key, val){
|
||||||
|
pendingSaves[key] = val;
|
||||||
|
el('acn-savehint').textContent = lang()==='en'?'Saving…':'Salvataggio…';
|
||||||
|
if (saveTimer) clearTimeout(saveTimer);
|
||||||
|
saveTimer = setTimeout(flushSaves, 700);
|
||||||
|
}
|
||||||
|
async function flushSaves(){
|
||||||
|
const batch = Object.keys(pendingSaves).map(function(k){ return { requirement_key:k, response_value:pendingSaves[k] }; });
|
||||||
|
if (!batch.length) return;
|
||||||
|
pendingSaves = {};
|
||||||
|
try {
|
||||||
|
await api.acnRespond(ACN.id, { responses: batch });
|
||||||
|
el('acn-savehint').textContent = (lang()==='en'?'Saved ':'Salvato ')+new Date().toLocaleTimeString(lang()==='en'?'en-GB':'it-IT').slice(0,5);
|
||||||
|
} catch(e){
|
||||||
|
el('acn-savehint').textContent = lang()==='en'?'Save pending':'Salvataggio in sospeso';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress(){
|
||||||
|
const answered = Object.keys(ACN.answers).length;
|
||||||
|
el('acn-answered').textContent = answered;
|
||||||
|
el('acn-bar-fill').style.width = (ACN.total? Math.round(answered/ACN.total*100):0)+'%';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeAcn(){
|
||||||
|
await flushSaves();
|
||||||
|
const answered = Object.keys(ACN.answers).length;
|
||||||
|
if (answered < ACN.total){
|
||||||
|
el('acn-wizard-msg').innerHTML = '<div class="alert alert-warning">'+
|
||||||
|
(lang()==='en'?('Missing '+(ACN.total-answered)+' answers.'):('Mancano '+(ACN.total-answered)+' risposte.'))+'</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api.acnComplete(ACN.id);
|
||||||
|
await showResults(ACN.id);
|
||||||
|
} catch(e){
|
||||||
|
el('acn-wizard-msg').innerHTML = '<div class="alert alert-warning">'+esc((e&&e.message)||'Errore')+'</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showResults(id){
|
||||||
|
const rep = await api.acnReport(id);
|
||||||
|
const a = rep.assessment;
|
||||||
|
el('acn-start').style.display='none';
|
||||||
|
el('acn-wizard').style.display='none';
|
||||||
|
el('acn-catalog').style.display='none';
|
||||||
|
el('acn-results').style.display='block';
|
||||||
|
el('btn-ai-analyze').style.display='inline-flex';
|
||||||
|
ACN.id = id;
|
||||||
|
el('acn-overall').textContent = a.overall_score!=null ? a.overall_score : '—';
|
||||||
|
el('acn-overall').style.color = scoreColor(a.overall_score);
|
||||||
|
|
||||||
|
let fs = '';
|
||||||
|
const fscores = a.function_scores || {};
|
||||||
|
Object.keys(FUNC_LABELS).forEach(function(fc){
|
||||||
|
if (fscores[fc]==null) return;
|
||||||
|
fs += '<div class="acn-func-score"><span>'+esc(FUNC_LABELS[fc][lang()])+'</span>'+
|
||||||
|
'<span class="acn-score-pill" style="background:'+scoreBg(fscores[fc])+'; color:'+scoreColor(fscores[fc])+';">'+fscores[fc]+'</span></div>';
|
||||||
|
});
|
||||||
|
el('acn-func-scores').innerHTML = fs;
|
||||||
|
|
||||||
|
let g = '';
|
||||||
|
(rep.gaps||[]).forEach(function(gap){
|
||||||
|
const tag = gap.response_value==='not_implemented'
|
||||||
|
? '<span class="badge badge-danger">'+(lang()==='en'?'Not implemented':'Non attuato')+'</span>'
|
||||||
|
: '<span class="badge badge-warning">'+(lang()==='en'?'Partial':'Parziale')+'</span>';
|
||||||
|
g += '<div style="padding:10px 0; border-bottom:1px solid var(--gray-100,#f3f4f6);">'+
|
||||||
|
'<span class="acn-measure-code">'+esc(gap.measure_code)+'</span> '+tag+
|
||||||
|
'<div style="font-size:.88rem; margin-top:4px;">'+esc(gap.requirement_text)+'</div></div>';
|
||||||
|
});
|
||||||
|
el('acn-gaps').innerHTML = g || '<p class="text-muted">'+(lang()==='en'?'No gaps. Fully compliant.':'Nessun gap: pienamente conforme.')+'</p>';
|
||||||
|
|
||||||
|
if (a.ai_summary){
|
||||||
|
el('acn-ai-body').innerHTML = '<div style="white-space:pre-wrap; font-size:.9rem;">'+esc(a.ai_summary)+'</div>';
|
||||||
|
el('acn-ai-card').style.display='block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acnAiAnalyze(){
|
||||||
|
const btn = el('btn-ai-analyze');
|
||||||
|
btn.disabled = true;
|
||||||
|
const old = btn.textContent;
|
||||||
|
btn.textContent = lang()==='en'?'Analyzing…':'Analisi in corso…';
|
||||||
|
try {
|
||||||
|
const res = await api.acnAiAnalyze(ACN.id);
|
||||||
|
el('acn-ai-body').innerHTML = '<div style="white-space:pre-wrap; font-size:.9rem;">'+esc(res.ai_summary)+'</div>';
|
||||||
|
el('acn-ai-card').style.display='block';
|
||||||
|
el('acn-ai-card').scrollIntoView({behavior:'smooth'});
|
||||||
|
} catch(e){
|
||||||
|
alert((e&&e.message)||(lang()==='en'?'AI unavailable':'AI non disponibile'));
|
||||||
|
} finally {
|
||||||
|
btn.disabled=false; btn.textContent=old;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreColor(s){ if(s==null)return '#6b7280'; if(s>=80)return '#16a34a'; if(s>=60)return '#65a30d'; if(s>=40)return '#f59e0b'; return '#dc2626'; }
|
||||||
|
function scoreBg(s){ if(s==null)return '#f3f4f6'; if(s>=80)return '#f0fdf4'; if(s>=60)return '#f7fee7'; if(s>=40)return '#fffbeb'; return '#fef2f2'; }
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -179,6 +179,31 @@ class NIS2API {
|
|||||||
getAssessmentReport(id) { return this.get(`/assessments/${id}/report`); }
|
getAssessmentReport(id) { return this.get(`/assessments/${id}/report`); }
|
||||||
aiAnalyzeAssessment(id) { return this.post(`/assessments/${id}/ai-analyze`, {}); }
|
aiAnalyzeAssessment(id) { return this.post(`/assessments/${id}/ai-analyze`, {}); }
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Gap Analysis ACN (Determinazione 164179/2025 - misure/requisiti)
|
||||||
|
// I metodi acn* ritornano direttamente il payload `data` e lanciano
|
||||||
|
// un errore {message, error_code} se success=false (per il try/catch UI).
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
async _acn(promise) {
|
||||||
|
const r = await promise;
|
||||||
|
if (!r || !r.success) {
|
||||||
|
const err = new Error((r && r.message) || 'Errore');
|
||||||
|
err.error_code = r && (r.error_code || (r.data && r.data.error_code));
|
||||||
|
err.data = r && r.data;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return r.data;
|
||||||
|
}
|
||||||
|
acnCatalog() { return this._acn(this.get('/acn-gap/catalog')); }
|
||||||
|
acnList() { return this._acn(this.get('/acn-gap/list')); }
|
||||||
|
acnCreate(data) { return this._acn(this.post('/acn-gap/create', data || {})); }
|
||||||
|
acnGet(id) { return this._acn(this.get(`/acn-gap/${id}`)); }
|
||||||
|
acnRequirements(id) { return this._acn(this.get(`/acn-gap/${id}/requirements`)); }
|
||||||
|
acnRespond(id, data) { return this._acn(this.post(`/acn-gap/${id}/respond`, data)); }
|
||||||
|
acnComplete(id) { return this._acn(this.post(`/acn-gap/${id}/complete`, {})); }
|
||||||
|
acnReport(id) { return this._acn(this.get(`/acn-gap/${id}/report`)); }
|
||||||
|
acnAiAnalyze(id) { return this._acn(this.post(`/acn-gap/${id}/aiAnalyze`, {})); }
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
// Dashboard
|
// Dashboard
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@ -188,6 +188,7 @@ function loadSidebar() {
|
|||||||
{ name: 'Dashboard', href: 'dashboard.html', icon: iconGrid(), i18nKey: 'nav.dashboard' },
|
{ name: 'Dashboard', href: 'dashboard.html', icon: iconGrid(), i18nKey: 'nav.dashboard' },
|
||||||
{ name: 'Compliance Journey', href: 'workflow.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>` },
|
{ name: 'Compliance Journey', href: 'workflow.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>` },
|
||||||
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
|
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
|
||||||
|
{ name: 'Gap Analysis ACN', href: 'acn-gap.html', icon: iconClipboardCheck(), i18nKey: 'nav.acn_gap' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user