Files
nis2-agile/public/js/isms.js
T
DevEnv nis2-agileandClaude Opus 4.8 d29195b2c1 [FEAT] ISMS documenti: download unico set (Word) + editor in-app contenuto bozze (v1.24.1)
- exportAllDocumentsWord: GET /api/isms/documentsWordAll -> .doc unico (copertina+indice+page-break) di tutto il set non archiviato
- getDocument: GET /api/isms/documents/{id} (full doc per editor)
- updateDocument: guard NOT_EDITABLE su documenti non-bozza/revisione + ruoli estesi a board_member
- UI isms.js: pulsante 'Scarica tutto (Word)', 'Modifica' (editor contenteditable + toolbar + toggle HTML) per bozze/revisione
- help.js: editor in-app + download unico

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:11:57 +02:00

455 lines
28 KiB
JavaScript
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.
/**
* NIS2 Agile - Modello Organizzativo SGSI (ISO 27001/27017/27018)
* Wizard 6 step: Contesto → Leadership → Risk → SoA → Documenti → Monitoraggio.
* Pattern coerente con le altre pagine: client api.* che ritorna `data` e lancia su errore.
*/
'use strict';
const STEP_KEYS = ['isms.step1','isms.step2','isms.step3','isms.step4','isms.step5','isms.step6'];
const STEP_FALLBACK = ['Contesto','Leadership','Risk','SoA','Documenti','Monitoraggio'];
const THEME_LABELS = {
organizational: { it:'Organizzativi', en:'Organizational' },
people: { it:'Persone', en:'People' },
physical: { it:'Fisici', en:'Physical' },
technological: { it:'Tecnologici', en:'Technological' },
privacy: { it:'Privacy (PII)', en:'Privacy (PII)' }
};
let ISMS = { model:null, step:0, soaLoaded:false };
let soaSaveTimer = {};
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; }
function hint(id, msg){ const e=el(id); if(e){ e.textContent=msg; } }
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();
renderSteps();
await loadModel();
});
function renderSteps(){
let html='';
STEP_FALLBACK.forEach(function(lbl, i){
const t = (window.I18n && I18n.t) ? I18n.t(STEP_KEYS[i]) : lbl;
html += '<div class="isms-step" id="step-'+i+'" onclick="goStep('+i+')"><div class="num">'+(i+1)+'</div><div>'+esc(t&&t!==STEP_KEYS[i]?t:lbl)+'</div></div>';
});
el('isms-steps').innerHTML = html;
}
function goStep(i){
ISMS.step = i;
document.querySelectorAll('.isms-panel').forEach(function(p){ p.classList.remove('active'); });
const panel = el('panel-'+i); if (panel) panel.classList.add('active');
document.querySelectorAll('.isms-step').forEach(function(s,idx){ s.classList.toggle('active', idx===i); });
if (i===1) loadRoles();
if (i===3) loadSoa();
if (i===4) loadDocs();
if (i===5) loadReadiness();
window.scrollTo({top:0,behavior:'smooth'});
}
async function loadModel(){
try {
const res = await api.ismsGetModel();
ISMS.model = res.model;
if (ISMS.model) fillStep1(ISMS.model);
markDone();
} catch(e){ /* tabella non ancora migrata o nessun modello: si parte da zero */ }
goStep(0);
}
function fillStep1(m){
el('f-scope').value = m.scope_statement||'';
el('f-ctx-int').value = m.context_internal||'';
el('f-ctx-ext').value = m.context_external||'';
el('f-parties').value = (m.interested_parties||[]).join('\n');
el('f-boundaries').value = m.boundaries||'';
el('f-exclusions').value = m.exclusions||'';
el('f-uses-cloud').checked = !!m.uses_public_cloud;
el('f-cloud-provider').checked = !!m.is_cloud_provider;
el('f-pii-cloud').checked = !!m.processes_pii_in_cloud;
if (m.risk_methodology!==undefined) el('f-method').value = m.risk_methodology||'';
el('f-objectives').value = (m.isms_objectives||[]).join('\n');
}
function linesToArr(v){ return (v||'').split('\n').map(function(s){return s.trim();}).filter(Boolean); }
async function saveStep1(){
const data = {
scope_statement: el('f-scope').value,
context_internal: el('f-ctx-int').value,
context_external: el('f-ctx-ext').value,
interested_parties: linesToArr(el('f-parties').value),
boundaries: el('f-boundaries').value,
exclusions: el('f-exclusions').value,
uses_public_cloud: el('f-uses-cloud').checked,
is_cloud_provider: el('f-cloud-provider').checked,
processes_pii_in_cloud: el('f-pii-cloud').checked
};
try {
await api.ismsSaveModel(data);
hint('isms-savehint', lang()==='en'?'Saved':'Salvato');
ISMS.soaLoaded = false; // i flag cloud cambiano il perimetro SoA
await loadModel();
goStep(1);
} catch(e){ alert((e&&e.message)||'Errore'); }
}
async function saveStep3(){
try {
await api.ismsSaveModel({ risk_methodology: el('f-method').value, isms_objectives: linesToArr(el('f-objectives').value) });
hint('isms-savehint', lang()==='en'?'Saved':'Salvato');
await loadModel();
goStep(3);
} catch(e){ alert((e&&e.message)||'Errore'); }
}
// ── Ruoli (cl.5) ──
async function loadRoles(){
try {
const res = await api.ismsRoles();
let html='';
(res.roles||[]).forEach(function(r){
html += '<div class="raci-row"><span class="soa-code">'+(r.raci||'-')+'</span>'+
'<strong style="flex:1 1 140px;">'+esc(r.role_name)+'</strong>'+
'<span style="flex:2 1 200px; color:var(--gray-500,#6b7280); font-size:.85rem;">'+esc(r.responsibility||'')+'</span>'+
'<button class="btn btn-sm btn-outline" onclick="delRole('+r.id+')">×</button></div>';
});
el('roles-list').innerHTML = html || '<p class="text-muted">'+(lang()==='en'?'No roles yet.':'Nessun ruolo definito.')+'</p>';
} catch(e){ el('roles-list').innerHTML='<p class="text-muted">'+esc((e&&e.message)||'')+'</p>'; }
}
async function addRole(){
const name = el('r-name').value.trim();
if(!name){ return; }
try {
await api.ismsSaveRole({ role_name:name, responsibility:el('r-resp').value, raci:el('r-raci').value });
el('r-name').value=''; el('r-resp').value=''; el('r-raci').value='';
loadRoles(); markDone();
} catch(e){ alert((e&&e.message)||'Errore'); }
}
async function delRole(id){ try { await api.ismsDeleteRole(id); loadRoles(); } catch(e){} }
// ── SoA (cl.6.1.3) ──
async function loadSoa(){
if (ISMS.soaLoaded) return;
el('soa-groups').innerHTML = '<p class="text-muted">'+(lang()==='en'?'Loading…':'Caricamento…')+'</p>';
try {
const res = await api.ismsGetSoa();
renderSoa(res);
ISMS.soaLoaded = true;
markDone();
} catch(e){
el('soa-groups').innerHTML = '<p class="text-muted">'+esc((e&&e.message)||'')+'</p>';
}
}
async function deriveSoa(){
try { await api.ismsDeriveSoa(); ISMS.soaLoaded=false; await loadSoa(); }
catch(e){ alert((e&&e.message)||'Errore'); }
}
function renderSoa(res){
showSoaStats(res.stats);
let html='';
(res.groups||[]).forEach(function(g, gi){
// raggruppa per tema
const byTheme = {};
(g.controls||[]).forEach(function(c){ (byTheme[c.theme]=byTheme[c.theme]||[]).push(c); });
let inner='';
Object.keys(byTheme).forEach(function(th){
inner += '<div class="soa-theme">'+esc(THEME_LABELS[th]?THEME_LABELS[th][lang()]:th)+'</div>';
byTheme[th].forEach(function(c){ inner += renderCtrl(c); });
});
html += '<div class="soa-std'+(gi===0?' open':'')+'"><div class="soa-std-head" onclick="this.parentNode.classList.toggle(\'open\')">'+
'<strong>'+esc(g.label)+'</strong><span class="text-muted">'+(g.controls||[]).length+' '+(lang()==='en'?'controls':'controlli')+'</span></div>'+
'<div class="soa-std-body">'+inner+'</div></div>';
});
el('soa-groups').innerHTML = html || '<p class="text-muted">'+(lang()==='en'?'No controls.':'Nessun controllo.')+'</p>';
}
function renderCtrl(c){
const title = lang()==='en' ? (c.title_en||c.title_it) : (c.title_it||c.title_en);
const derived = (+c.derived_from_nis2) ? '<span class="soa-derived" title="'+esc(c.source_ref||'')+'">'+(lang()==='en'?'from NIS2':'da NIS2')+'</span>' : '';
const applicable = (+c.applicable)===1;
const statuses = [
['not_started', lang()==='en'?'Not started':'Da iniziare'],
['in_progress', lang()==='en'?'In progress':'In corso'],
['implemented', lang()==='en'?'Implemented':'Attuato'],
['verified', lang()==='en'?'Verified':'Verificato']
];
let opts='';
statuses.forEach(function(s){
const sel = c.implementation_status===s[0] ? ('sel-'+s[0]) : '';
opts += '<button type="button" class="soa-opt '+sel+'" onclick="setSoaStatus(this,\''+esc(c.control_code)+'\',\''+s[0]+'\')">'+s[1]+'</button>';
});
return '<div class="soa-ctrl" data-code="'+esc(c.control_code)+'">'+
'<div class="soa-ctrl-head"><div><span class="soa-code">'+esc(c.control_code)+'</span>'+esc(title)+' '+derived+'</div>'+
'<label style="font-size:.82rem; white-space:nowrap;"><input type="checkbox" '+(applicable?'checked':'')+' onchange="setSoaApplicable(\''+esc(c.control_code)+'\',this.checked)"> '+(lang()==='en'?'Applicable':'Applicabile')+'</label></div>'+
'<div class="soa-opts" style="'+(applicable?'':'opacity:.4;pointer-events:none;')+'">'+opts+'</div>'+
'<textarea class="soa-justif" placeholder="'+(lang()==='en'?'Justification (inclusion/exclusion)':'Motivazione (inclusione/esclusione)')+'" onchange="setSoaJustif(\''+esc(c.control_code)+'\',this.value,'+applicable+')">'+esc(applicable?(c.justification_inclusion||''):(c.justification_exclusion||''))+'</textarea>'+
'</div>';
}
function setSoaStatus(btn, code, status){
const wrap = btn.parentNode;
Array.prototype.forEach.call(wrap.querySelectorAll('.soa-opt'), function(b){ b.className='soa-opt'; });
btn.className='soa-opt sel-'+status;
const pct = {not_started:0,in_progress:50,implemented:100,verified:100}[status];
queueSoaSave(code, { control_code:code, implementation_status:status, implementation_pct:pct });
}
function setSoaApplicable(code, applicable){
queueSoaSave(code, { control_code:code, applicable:applicable });
ISMS.soaLoaded=false; setTimeout(loadSoa, 400);
}
function setSoaJustif(code, val, applicable){
queueSoaSave(code, applicable ? { control_code:code, justification_inclusion:val } : { control_code:code, justification_exclusion:val });
}
function queueSoaSave(code, payload){
hint('isms-savehint', lang()==='en'?'Saving…':'Salvataggio…');
if (soaSaveTimer[code]) clearTimeout(soaSaveTimer[code]);
soaSaveTimer[code] = setTimeout(async function(){
try { const r = await api.ismsUpdateSoa(payload); showSoaStats(r.stats); hint('isms-savehint', lang()==='en'?'Saved':'Salvato'); }
catch(e){ hint('isms-savehint', lang()==='en'?'Save pending':'Salvataggio in sospeso'); }
}, 600);
}
function showSoaStats(s){
if(!s){ return; }
const e = el('soa-stats');
if(e) e.textContent = (lang()==='en'?'Applicable: ':'Applicabili: ')+s.applicable+' · '+(lang()==='en'?'avg impl.: ':'impl. media: ')+s.avg_implementation_pct+'%';
}
// ── Documenti (cl.7-8) — ciclo di vita ISO 27001 cl.7.5 ──
const DOC_STATUS = {
draft: { it:'Bozza', en:'Draft', color:'#6b7280' },
review: { it:'In revisione', en:'In review', color:'#f59e0b' },
approved: { it:'Approvato', en:'Approved', color:'#3b82f6' },
published:{ it:'Pubblicato', en:'Published', color:'#16a34a' },
archived: { it:'Archiviato', en:'Archived', color:'#475569' }
};
function fmtDate(d){ if(!d) return ''; try{ return new Date((''+d).replace(' ','T')).toLocaleDateString(lang()==='en'?'en-GB':'it-IT'); }catch(e){ return esc(d); } }
function docBtn(label, onclick, bg){
return '<button type="button" onclick="'+onclick+'" style="font-size:.78rem;padding:3px 10px;border-radius:6px;border:1px solid '+(bg||'#0066CC')+';background:'+(bg||'#fff')+';color:'+(bg?'#fff':'#0066CC')+';cursor:pointer;">'+label+'</button>';
}
async function loadDocs(){
try {
const res = await api.ismsDocuments();
const docs = res.documents||[]; const isEn = lang()==='en';
if(!docs.length){ el('docs-list').innerHTML = '<p class="text-muted">'+(isEn?'No documents yet.':'Nessun documento.')+'</p>'; return; }
let html = '<div style="display:flex;justify-content:flex-end;margin-bottom:10px;">'+docBtn(isEn?'Download all (Word)':'Scarica tutto (Word)','docWordAll()','#0066CC')+'</div>';
docs.forEach(function(d){
const st = DOC_STATUS[d.status] || { it:d.status, en:d.status, color:'#6b7280' };
const ai = (+d.ai_generated)?' <span class="soa-derived">AI</span>':'';
let meta = (isEn?'Version ':'Versione ')+esc(d.version||'1.0');
if(d.approved_at) meta += ' · '+(isEn?'approved ':'approvato il ')+fmtDate(d.approved_at)+(d.approved_by_name?' ('+esc(d.approved_by_name)+')':'');
if(d.published_at) meta += ' · '+(isEn?'in force from ':'in vigore dal ')+fmtDate(d.published_at);
if(d.next_review_date) meta += ' · '+(isEn?'next review ':'prossimo riesame ')+fmtDate(d.next_review_date);
let btns='';
if(d.status==='draft'){ btns += docBtn(isEn?'Submit for review':'Invia in revisione','docAction('+d.id+',\'submit\')','#3b82f6'); }
else if(d.status==='review'){ btns += docBtn(isEn?'Approve':'Approva','docAction('+d.id+',\'approve\')','#16a34a'); btns += docBtn(isEn?'Send back':'Rimanda in bozza','docReject('+d.id+')','#ef4444'); }
else if(d.status==='approved'){ btns += docBtn(isEn?'Publish':'Pubblica','docAction('+d.id+',\'publish\')','#16a34a'); }
else if(d.status==='published'){ btns += docBtn(isEn?'New version':'Nuova versione','docNewVersion('+d.id+')'); btns += docBtn(isEn?'Archive':'Archivia','docAction('+d.id+',\'archive\')','#475569'); }
else if(d.status==='archived'){ btns += docBtn(isEn?'New version':'Nuova versione','docNewVersion('+d.id+')'); }
if(d.status==='draft'||d.status==='review') btns += docBtn(isEn?'Edit':'Modifica','docEdit('+d.id+')','#7c3aed');
btns += docBtn(isEn?'Open':'Apri','docView('+d.id+')');
btns += docBtn('Word','docWord('+d.id+')');
btns += docBtn(isEn?'History':'Storico','docVersions('+d.id+')');
const note = (d.review_note && d.status==='draft') ? ' · <span style="color:#b91c1c;">'+esc(d.review_note)+'</span>' : '';
html += '<div class="ck-item" style="display:block;padding:10px 12px;">'+
'<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;">'+
'<div><strong>'+esc(d.title)+'</strong>'+ai+' <span class="text-muted" style="font-size:.8rem;">('+esc(d.doc_type)+')</span></div>'+
'<span style="background:'+st.color+';color:#fff;padding:2px 10px;border-radius:10px;font-size:.74rem;white-space:nowrap;">'+(isEn?st.en:st.it)+'</span>'+
'</div>'+
'<div class="text-muted" style="font-size:.78rem;margin:4px 0 8px;">'+meta+note+'</div>'+
'<div style="display:flex;flex-wrap:wrap;gap:6px;">'+btns+'</div>'+
'<div id="docver-'+d.id+'" style="margin-top:8px;"></div>'+
'</div>';
});
el('docs-list').innerHTML = html;
} catch(e){ el('docs-list').innerHTML='<p class="text-muted">'+esc((e&&e.message)||'')+'</p>'; }
}
async function docAction(id, action){
const msg = { submit:(lang()==='en'?'Submit for review?':'Inviare in revisione?'),
approve:(lang()==='en'?'Approve this document?':'Approvare il documento?'),
publish:(lang()==='en'?'Publish (put in force)?':'Pubblicare il documento (metterlo in vigore)?'),
archive:(lang()==='en'?'Archive this document?':'Archiviare il documento?') }[action] || 'OK?';
if(!confirm(msg)) return;
const r = await api.post('/isms/documents/'+id+'/'+action);
if(r && r.success){ loadDocs(); } else { alert((r&&r.message)||(lang()==='en'?'Action failed':'Operazione non riuscita')); }
}
async function docReject(id){
const note = prompt(lang()==='en'?'Reason for sending back to draft:':'Motivo del rinvio in bozza:');
if(note===null) return;
const r = await api.post('/isms/documents/'+id+'/reject', { note:note });
if(r && r.success){ loadDocs(); } else { alert((r&&r.message)||'Errore'); }
}
async function docNewVersion(id){
if(!confirm(lang()==='en'?'Create a new editable draft version?':'Creare una nuova versione modificabile (bozza)?')) return;
const r = await api.post('/isms/documents/'+id+'/newVersion', { major:false });
if(r && r.success){ loadDocs(); } else { alert((r&&r.message)||'Errore'); }
}
async function docVersions(id){
const box = el('docver-'+id); if(!box) return;
if(box.innerHTML){ box.innerHTML=''; return; }
box.innerHTML = '<span class="text-muted" style="font-size:.8rem;">…</span>';
const r = await api.get('/isms/documents/'+id+'/versions');
const vs = (r && r.data && r.data.versions) || [];
if(!vs.length){ box.innerHTML = '<span class="text-muted" style="font-size:.8rem;">'+(lang()==='en'?'No version history.':'Nessuna versione storicizzata.')+'</span>'; return; }
let h = '<table style="width:100%;font-size:.78rem;border-collapse:collapse;margin-top:4px;"><tr>'+
'<th style="text-align:left;border-bottom:1px solid #ddd;padding:3px;">Vers.</th>'+
'<th style="text-align:left;border-bottom:1px solid #ddd;padding:3px;">'+(lang()==='en'?'Status':'Stato')+'</th>'+
'<th style="text-align:left;border-bottom:1px solid #ddd;padding:3px;">'+(lang()==='en'?'Note':'Nota')+'</th>'+
'<th style="text-align:left;border-bottom:1px solid #ddd;padding:3px;">'+(lang()==='en'?'By / date':'Da / data')+'</th></tr>';
vs.forEach(function(v){ h += '<tr><td style="padding:3px;">'+esc(v.version)+'</td><td style="padding:3px;">'+esc(v.status)+'</td><td style="padding:3px;">'+esc(v.change_note||'')+'</td><td style="padding:3px;">'+esc(v.by_name||'')+' · '+fmtDate(v.created_at)+'</td></tr>'; });
box.innerHTML = h + '</table>';
}
async function docWord(id){
try{
const headers = { 'Authorization':'Bearer '+api.token }; if(api.orgId) headers['X-Organization-Id']=api.orgId;
const resp = await fetch(api.baseUrl+'/isms/documents/'+id+'/word', { headers });
if(!resp.ok) throw new Error((lang()==='en'?'Download failed ':'Download non riuscito ')+'('+resp.status+')');
let fname = 'documento_'+id+'.doc';
const cd = resp.headers.get('Content-Disposition')||''; const mm = cd.match(/filename="?([^"]+)"?/); if(mm) fname = mm[1];
const blob = await resp.blob(); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href=url; a.download=fname; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
}catch(e){ alert((e&&e.message)||'Errore download'); }
}
async function docView(id){
try{
const headers = { 'Authorization':'Bearer '+api.token }; if(api.orgId) headers['X-Organization-Id']=api.orgId;
const resp = await fetch(api.baseUrl+'/isms/documents/'+id+'/word', { headers });
if(!resp.ok) throw new Error('Apertura non riuscita'); const txt = await resp.text();
const w = window.open('','_blank'); if(!w){ alert(lang()==='en'?'Enable popups to view':'Abilita i popup per visualizzare'); return; }
w.document.write(txt); w.document.close();
}catch(e){ alert((e&&e.message)||'Errore'); }
}
async function aiGenDoc(){
const btn = el('btn-aigen'); btn.disabled=true;
hint('aigen-hint', lang()==='en'?'Generating draft…':'Generazione bozza in corso…');
try {
await api.ismsAiGenerateDocument({ doc_type: el('d-type').value });
hint('aigen-hint', lang()==='en'?'Draft created (review required).':'Bozza creata (revisione obbligatoria).');
loadDocs(); markDone();
} catch(e){ hint('aigen-hint',''); alert((e&&e.message)||(lang()==='en'?'AI unavailable':'AI non disponibile')); }
finally { btn.disabled=false; }
}
// ── Download UNICO dell'intero set ──
async function docWordAll(){
try{
const headers = { 'Authorization':'Bearer '+api.token }; if(api.orgId) headers['X-Organization-Id']=api.orgId;
const resp = await fetch(api.baseUrl+'/isms/documentsWordAll', { headers });
if(!resp.ok) throw new Error((lang()==='en'?'Download failed ':'Download non riuscito ')+'('+resp.status+')');
let fname='SGSI_completo.doc';
const cd = resp.headers.get('Content-Disposition')||''; const mm=cd.match(/filename="?([^"]+)"?/); if(mm) fname=mm[1];
const blob=await resp.blob(); const url=URL.createObjectURL(blob);
const a=document.createElement('a'); a.href=url; a.download=fname; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
}catch(e){ alert((e&&e.message)||'Errore download'); }
}
// ── Editor in-app del contenuto (bozza/revisione) ──
let _docEditId = null;
function _tb(label, onclick, st){ return '<button type="button" onmousedown="event.preventDefault()" onclick="'+onclick+'" style="padding:4px 10px;border:1px solid #cbd5e1;background:#fff;border-radius:5px;cursor:pointer;font-size:.82rem;'+(st||'')+'">'+label+'</button>'; }
function ensureDocEditor(){
if(el('doc-editor-overlay')) return; const isEn=lang()==='en';
const ov=document.createElement('div'); ov.id='doc-editor-overlay';
ov.style.cssText='display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:9999;';
ov.innerHTML=
'<div style="background:#fff;max-width:920px;margin:3vh auto;max-height:94vh;display:flex;flex-direction:column;border-radius:8px;overflow:hidden;box-shadow:0 10px 40px rgba(0,0,0,.3);">'+
'<div style="display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid #e5e7eb;">'+
'<strong style="flex:0 0 auto;">'+(isEn?'Edit document':'Modifica documento')+'</strong>'+
'<input id="doc-edit-title" style="flex:1;padding:6px 10px;border:1px solid #cbd5e1;border-radius:6px;font-weight:600;">'+
'<button onclick="docEditClose()" style="border:none;background:none;font-size:1.4rem;cursor:pointer;color:#64748b;line-height:1;">&times;</button>'+
'</div>'+
'<div style="display:flex;flex-wrap:wrap;gap:4px;padding:8px 16px;border-bottom:1px solid #eee;background:#f8fafc;">'+
_tb('B','docFmt(\'bold\')','font-weight:700;')+_tb('I','docFmt(\'italic\')','font-style:italic;')+
_tb('H2','docBlock(\'h2\')','')+_tb('H3','docBlock(\'h3\')','')+_tb('¶','docBlock(\'p\')','')+
_tb('• '+(isEn?'List':'Elenco'),'docFmt(\'insertUnorderedList\')','')+_tb('1.','docFmt(\'insertOrderedList\')','')+
'<span style="flex:1;"></span>'+_tb('&lt;/&gt; HTML','docToggleHtml()','')+
'</div>'+
'<div id="doc-edit-body" contenteditable="true" style="flex:1;overflow:auto;padding:18px 22px;line-height:1.5;outline:none;min-height:240px;"></div>'+
'<textarea id="doc-edit-html" style="display:none;flex:1;overflow:auto;padding:14px;font-family:monospace;font-size:.82rem;border:none;outline:none;min-height:240px;"></textarea>'+
'<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;padding:12px 16px;border-top:1px solid #e5e7eb;">'+
'<span id="doc-edit-hint" style="font-size:.8rem;color:#64748b;"></span>'+
'<span><button onclick="docEditClose()" style="padding:7px 16px;border:1px solid #cbd5e1;background:#fff;border-radius:6px;cursor:pointer;margin-right:6px;">'+(isEn?'Cancel':'Annulla')+'</button>'+
'<button onclick="docEditSave()" style="padding:7px 18px;border:none;background:#16a34a;color:#fff;border-radius:6px;cursor:pointer;font-weight:600;">'+(isEn?'Save':'Salva')+'</button></span>'+
'</div>'+
'</div>';
document.body.appendChild(ov);
}
function docFmt(cmd){ try{ document.execCommand(cmd,false,null); }catch(e){} const b=el('doc-edit-body'); if(b) b.focus(); }
function docBlock(tag){ try{ document.execCommand('formatBlock',false,tag); }catch(e){} const b=el('doc-edit-body'); if(b) b.focus(); }
function docToggleHtml(){
const body=el('doc-edit-body'), ta=el('doc-edit-html');
if(ta.style.display==='none'){ ta.value=body.innerHTML; ta.style.display='block'; body.style.display='none'; }
else { body.innerHTML=ta.value; ta.style.display='none'; body.style.display='block'; }
}
async function docEdit(id){
ensureDocEditor();
const r=await api.get('/isms/documents/'+id); const d=r&&r.data&&r.data.document;
if(!d){ alert((r&&r.message)||'Errore'); return; }
_docEditId=id;
el('doc-edit-title').value=d.title||'';
el('doc-edit-body').innerHTML=d.body_html||'';
el('doc-edit-html').value=''; el('doc-edit-html').style.display='none'; el('doc-edit-body').style.display='block';
const st=DOC_STATUS[d.status];
el('doc-edit-hint').textContent=(lang()==='en'?'Status: ':'Stato: ')+(st?(lang()==='en'?st.en:st.it):d.status)+' · v'+(d.version||'1.0');
el('doc-editor-overlay').style.display='block';
}
function docEditClose(){ const o=el('doc-editor-overlay'); if(o) o.style.display='none'; _docEditId=null; }
async function docEditSave(){
if(!_docEditId) return;
const ta=el('doc-edit-html');
const body=(ta.style.display!=='none')?ta.value:el('doc-edit-body').innerHTML;
const title=el('doc-edit-title').value.trim();
el('doc-edit-hint').textContent=lang()==='en'?'Saving…':'Salvataggio…';
const r=await api.put('/isms/documents/'+_docEditId, { title:title, body_html:body });
if(r&&r.success){ docEditClose(); loadDocs(); } else { el('doc-edit-hint').textContent=''; alert((r&&r.message)||'Errore salvataggio'); }
}
// ── Readiness (cl.9-10) ──
async function loadReadiness(){
try {
const res = await api.ismsReadiness();
el('readiness-pct').textContent = (res.overall_pct||0)+'%';
let html='';
(res.checklist||[]).forEach(function(c){
html += '<div class="ck-item"><span><span class="soa-code">'+esc(c.clause)+'</span>'+esc(c.label)+'</span>'+
'<span class="'+(c.done?'ck-yes':'ck-no')+'">'+(c.done?'✓':'—')+'</span></div>';
});
el('readiness-checklist').innerHTML = html;
} catch(e){ el('readiness-checklist').innerHTML='<p class="text-muted">'+esc((e&&e.message)||'')+'</p>'; }
}
function markDone(){
// marca gli step "completati" in base allo stato del modello (best-effort).
const m = ISMS.model;
const done = [ m&&!!m.scope_statement, false, m&&!!m.risk_methodology, ISMS.soaLoaded, false, false ];
done.forEach(function(d,i){ const s=el('step-'+i); if(s) s.classList.toggle('done', !!d); });
}
async function ismsExportView(){
try {
const data = await api.ismsExport();
const w = window.open('', '_blank');
if(!w){ return; }
const m = data.model||{};
let soa='';
(data.soa||[]).forEach(function(c){
soa += '<tr><td>'+esc(c.control_code)+'</td><td>'+esc(c.title_it||'')+'</td><td>'+((+c.applicable)?'Sì':'No')+'</td><td>'+esc(c.implementation_status||'')+'</td><td>'+(c.implementation_pct||0)+'%</td></tr>';
});
w.document.write('<html><head><meta charset="utf-8"><title>SGSI - '+esc((data.organization&&data.organization.name)||'')+'</title>'+
'<style>body{font-family:Arial,sans-serif;padding:30px;color:#111} h1,h2{color:#1e40af} table{width:100%;border-collapse:collapse;font-size:12px} td,th{border:1px solid #ccc;padding:5px;text-align:left} .disc{background:#fffbeb;border:1px solid #fde68a;padding:10px;font-size:12px;margin:16px 0}</style></head><body>'+
'<h1>Modello Organizzativo SGSI (ISO/IEC 27001:2022)</h1>'+
'<p><strong>'+esc((data.organization&&data.organization.name)||'')+'</strong> — generato il '+esc(data.generated_at||'')+'</p>'+
'<div class="disc">'+esc(data.disclaimer||'')+'</div>'+
'<h2>Ambito</h2><p>'+esc(m.scope_statement||'—')+'</p>'+
'<h2>Metodologia di risk assessment</h2><p>'+esc(m.risk_methodology||'—')+'</p>'+
'<h2>Statement of Applicability</h2><table><tr><th>Controllo</th><th>Titolo</th><th>Applicabile</th><th>Stato</th><th>%</th></tr>'+soa+'</table>'+
'</body></html>');
w.document.close();
} catch(e){ alert((e&&e.message)||(lang()==='en'?'Start the SGSI first.':'Avvia prima il SGSI.')); }
}