- 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>
455 lines
28 KiB
JavaScript
455 lines
28 KiB
JavaScript
/**
|
||
* 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;">×</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('</> 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.')); }
|
||
}
|