[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>
This commit is contained in:
DevEnv nis2-agile
2026-06-20 14:11:57 +02:00
co-authored by Claude Opus 4.8
parent 68694a0f38
commit d29195b2c1
6 changed files with 141 additions and 7 deletions
+2
View File
@@ -301,6 +301,8 @@ $actionMap = [
'POST:documents/{subId}/newVersion' => 'newVersionDocument', // pubblicato->nuova bozza
'GET:documents/{subId}/versions' => 'documentVersions',
'GET:documents/{subId}/word' => 'exportDocumentWord', // download .doc Word
'GET:documentsWordAll' => 'exportAllDocumentsWord', // download UNICO di tutto il set
'GET:documents/{subId}' => 'getDocument', // documento completo (editor) — {subId} numerico
'GET:readiness' => 'readiness',
'GET:export' => 'export',
],
+2 -2
View File
@@ -197,7 +197,7 @@
</script>
<script src="/js/common-bi.js?v=20260631"></script>
<script src="/js/i18n.js?v=20260631"></script>
<script src="/js/help.js?v=20260620"></script>
<script src="/js/isms.js?v=20260620"></script>
<script src="/js/help.js?v=20260620b"></script>
<script src="/js/isms.js?v=20260620b"></script>
</body>
</html>
+2 -1
View File
@@ -135,7 +135,8 @@ const HelpSystem = (function () {
'Ogni documento (Manuale, politiche, procedure, istruzioni) ha un <strong>ciclo di vita</strong>: <strong>Bozza &rarr; In revisione &rarr; Approvato &rarr; Pubblicato &rarr; Archiviato</strong>, mostrato con un badge colorato nel passo 5 (Documenti).',
'<strong>Transizioni</strong>: "Invia in revisione", "Approva" / "Rimanda in bozza", "Pubblica" (mette il documento <strong>in vigore</strong>), "Archivia" (lo ritira), "Nuova versione" (riapre una bozza con numero di versione incrementato).',
'Ad ogni pubblicazione viene salvato uno <strong>snapshot di versione</strong>: il pulsante "Storico" mostra tutte le versioni con autore, data e nota. Alla pubblicazione vengono impostate la data di entrata in vigore e la <strong>prossima data di riesame</strong> (a 1 anno).',
'<strong>Scarica Word</strong>: il pulsante "Word" esporta il documento in formato .doc <strong>modificabile</strong> (mantiene titoli, elenchi e tabelle); "Apri" lo mostra in una nuova scheda.',
'<strong>Scarica Word</strong>: il pulsante "Word" esporta il singolo documento in formato .doc <strong>modificabile</strong> (mantiene titoli, elenchi e tabelle); "Apri" lo mostra in una nuova scheda. Il pulsante <strong>"Scarica tutto (Word)"</strong> esporta l\'intero set documentale in un unico file (copertina + indice).',
'<strong>Modifica in-app</strong>: per le <strong>bozze</strong> e i documenti <strong>in revisione</strong> il pulsante "Modifica" apre un editor con barra strumenti (grassetto, titoli, elenchi) e modalita HTML. I documenti gia pubblicati non si modificano direttamente: usa prima "Nuova versione" per riaprirli in bozza (cosi la versione in vigore resta tracciata).',
'E\' la gestione delle <strong>informazioni documentate</strong> richiesta dalla ISO/IEC 27001 <strong>cl. 7.5</strong>: creazione, approvazione, controllo delle versioni e della distribuzione.'
]
},
+75 -1
View File
@@ -237,7 +237,7 @@ async function loadDocs(){
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='';
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>':'';
@@ -251,6 +251,7 @@ async function loadDocs(){
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+')');
@@ -335,6 +336,79 @@ async function aiGenDoc(){
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 {
+1 -1
View File
@@ -1 +1 @@
{"version": "1.24.0", "build": "2026-06-20-v1.24.0", "date": "2026-06-20", "changelog": "Gestione documentale ISMS / ciclo di vita ISO 27001 cl.7.5. Ogni documento del Modello SGSI ha ora un ciclo di vita completo: Bozza -> In revisione -> Approvato -> Pubblicato -> Archiviato, con tracciamento di revisore/approvatore/pubblicatore e date, data di entrata in vigore e prossima data di riesame. Storico versioni (snapshot ad ogni approvazione/pubblicazione, consultabile). Download Word (.doc) modificabile per ogni documento. Migrazione 060 (estensione stati + colonne tracciamento + tabella isms_document_versions). 8 nuovi endpoint /api/isms/documents/{id}/(submit|approve|publish|reject|archive|newVersion|versions|word). UI nello step Documenti con badge di stato, pulsanti di transizione, download e storico."}
{"version": "1.24.1", "build": "2026-06-20-v1.24.1", "date": "2026-06-20", "changelog": "Gestione documentale ISMS — completata con: (1) download UNICO dell'intero set documentale in un solo file Word (.doc) con copertina e indice (endpoint /api/isms/documentsWordAll); (2) editor in-app del contenuto per le bozze e i documenti in revisione (barra strumenti grassetto/titoli/elenchi + modalita HTML), con guard che impedisce la modifica diretta dei documenti pubblicati (creare prima una nuova versione). Endpoint GET /api/isms/documents/{id} per il caricamento del documento nell'editor. Permessi di modifica estesi a board_member."}