[FEAT] Gestione documentale ISMS — ciclo di vita ISO 27001 cl.7.5 + export Word (v1.24.0)

Ogni documento del Modello SGSI ha un ciclo di vita completo Bozza->In revisione->Approvato->Pubblicato->Archiviato con tracciamento (revisore/approvatore/pubblicatore, date, entrata in vigore, prossimo riesame) e storico versioni (snapshot). Download Word .doc modificabile per documento.
- mig.060: stati published/archived + 10 colonne tracciamento + tabella isms_document_versions
- IsmsModelController: submit/approve/publish/reject/archive/newVersion/versions/exportWord + listDocuments arricchito
- index.php: 8 route documents/{id}/(submit|approve|publish|reject|archive|newVersion|versions|word)
- isms.js: UI badge stato + pulsanti transizione + download Word + storico (IT/EN inline); cache-buster
- help.js: sezione 'Ciclo di vita dei documenti (cl.7.5)'
- 31 documenti di Nuova Agile pubblicati v1.0 (build_nuova_agile_p6_publish)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-20 13:40:21 +02:00
co-authored by Claude Opus 4.8
parent 94e3a4293e
commit 68694a0f38
9 changed files with 416 additions and 11 deletions
+98 -6
View File
@@ -219,19 +219,111 @@ function showSoaStats(s){
if(e) e.textContent = (lang()==='en'?'Applicable: ':'Applicabili: ')+s.applicable+' · '+(lang()==='en'?'avg impl.: ':'impl. media: ')+s.avg_implementation_pct+'%';
}
// ── Documenti (cl.7-8) ──
// ── 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='';
(res.documents||[]).forEach(function(d){
const ai = (+d.ai_generated)?'<span class="soa-derived">AI</span>':'';
html += '<div class="ck-item"><div><strong>'+esc(d.title)+'</strong> '+ai+' <span class="text-muted" style="font-size:.8rem;">('+esc(d.doc_type)+')</span></div>'+
'<span class="badge badge-'+(d.status==='approved'?'success':'warning')+'">'+esc(d.status)+'</span></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+')'); }
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 || '<p class="text-muted">'+(lang()==='en'?'No documents yet.':'Nessun documento.')+'</p>';
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…');