[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:
co-authored by
Claude Opus 4.8
parent
68694a0f38
commit
d29195b2c1
@@ -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
@@ -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
@@ -135,7 +135,8 @@ const HelpSystem = (function () {
|
||||
'Ogni documento (Manuale, politiche, procedure, istruzioni) ha un <strong>ciclo di vita</strong>: <strong>Bozza → In revisione → Approvato → Pubblicato → 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
@@ -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;">×</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 {
|
||||
|
||||
+1
-1
@@ -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."}
|
||||
|
||||
Reference in New Issue
Block a user