Files
nis2-agile/public/acn-gap.html
T
DevEnv nis2-agileandClaude Opus 4.8 5368b0a61c [FEAT] A4 Fase 4.2 — Competenze (skill/requisiti ruolo/gap) + alert→azione NCR+CAPA
Secondo modulo del modello relazionale A4 (docs/DESIGN_A4_RELATIONAL.md).

Backend:
- Migration 042 (docs/sql/042_competences.sql) + runner aggiornato (scripts/migrate-a4.php).
  4 tabelle additive/idempotenti: skills (org-owned o globali), role_skills
  (competenze richieste dal ruolo, UNIQUE role+skill), user_skills (possedute,
  UNIQUE org+user+skill), skill_course_map. APPLICATA su prod (nis2-db v8.0.45,
  TLSv1.3).
- CompetenceController (route 'competences'): catalog, skills CRUD, roleSkills,
  userSkills, skillCourses (map/unmap), gapGrid (richiesto−posseduto per ruolo
  con titolare), openAction. Multi-tenancy + anti-IDOR + livelli 1-5; competenze
  globali in sola lettura per gli utenti org.
- alert→azione: openAction RIUSA il workflow NCR/CAPA reale (la tabella azioni è
  capa_actions figlia di non_conformities, NON 'corrective_actions' che non
  esiste): crea NCR (source='management_review', source_entity_type='competence_gap',
  source_entity_id=role_skills.id) + capa_actions figlia. Anti-duplicato sul gap.
  Zero ALTER alle tabelle esistenti. Ancoraggio PR.AT-01/02, GV.RR-04, art.24 D.Lgs.

Frontend:
- competenze.html (4 schede: Catalogo, Requisiti per ruolo, Competenze persone,
  Gap & azioni) + js/competenze.js. Bootstrap Italia V2. CTA "Assegna corso"
  (riuso /training/assign) e "Apri non conformità".
- Voce sidebar "Competenze" (common.js + common-bi.js + BI_PAGES) + nav.competences
  i18n IT/EN. api.js: metodi comp* + assignTraining.

Help/KB/PWA:
- help.js: guida contestuale 'competences' (schede, calcolo gap, PR.AT-01/02,
  GV.RR-04, art.24 D.Lgs., disclaimer no-parere-legale) + mappa pagina.
- sw.js: nome cache nis2-shell-v1.17.0 (allineamento PWA).
- Design doc corretto (rettifica capa_actions vs corrective_actions) + avanzamento 4.1/4.2.

Cache-buster: ?v=20260618 dei 5 JS condivisi su 32 HTML. version.json 1.16.0 -> 1.17.0.
Smoke E2E su prod (fpm reale): catalogo, requisito, competenza, gap=2, openAction
(NCR+CAPA), anti-dup 409, mappatura corso — tutti verdi; dati di test ripuliti.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:01:32 +02:00

452 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Gap Analysis ACN - NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
via JS (griglia misure/requisiti, scoring) -> parita' funzionale assoluta.
BI veste la chrome (sidebar, font, bottoni, icone sprite). -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="/css/style.css?v=20260613b">
<style>
/* Stili specifici della pagina ACN — identici alla live acn-gap.html
(variabili --primary/--gray-* del design system esistente) */
.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:focus-visible { outline:2px solid var(--primary,#2563eb); outline-offset:2px; }
.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; }
/* icona inline nei bottoni BI */
.btn .icon { fill:currentColor; vertical-align:-2px; margin-right:6px; }
</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">
<h1 class="h2" data-i18n="acn.title">Gap Analysis ACN (misure di base)</h1>
<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">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-star-full"></use></svg>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">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-plus"></use></svg>Nuova Gap Analysis ACN</button>
<div style="margin-top:14px;">
<button class="btn btn-outline btn-sm" onclick="showCatalog()" data-i18n="acn.catalog.btn">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-search"></use></svg>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" role="status" aria-live="polite"></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">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-chart-line"></use></svg>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">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-left"></use></svg>Torna all'inizio</button>
</div>
</div>
</div>
</main>
</div>
<script src="/js/api.js?v=20260618"></script>
<script src="/js/common.js?v=20260618"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). -->
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script>
if (window.bootstrap && bootstrap.loadFonts) {
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
}
</script>
<script src="/js/common-bi.js?v=20260618"></script>
<script src="/js/i18n.js?v=20260618"></script>
<script src="/js/help.js?v=20260618"></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">'+(lang()==='en'?'ESSENTIAL entity':'Soggetto ESSENZIALE')+'</span>'
: '<span class="acn-level-badge acn-level-important">'+(lang()==='en'?'IMPORTANT entity':'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){
try {
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);
} catch(e){
alert((e && e.message) || (lang()==='en'?'Could not open the assessment.':'Impossibile aprire l\'assessment.'));
}
}
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">'+(lang()==='en'?'ESSENTIAL':'ESSENZIALE')+'</span>'
: '<span class="acn-level-badge acn-level-important">'+(lang()==='en'?'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>