[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:
co-authored by
Claude Opus 4.8
parent
94e3a4293e
commit
68694a0f38
+98
-6
@@ -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…');
|
||||
|
||||
Reference in New Issue
Block a user