[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
@@ -323,13 +323,16 @@ class IsmsModelController extends BaseController
|
|||||||
/** PUT /api/isms/documents/{id} */
|
/** PUT /api/isms/documents/{id} */
|
||||||
public function updateDocument(int $id): void
|
public function updateDocument(int $id): void
|
||||||
{
|
{
|
||||||
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
$this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']);
|
||||||
$model = $this->requireModel();
|
$model = $this->requireModel();
|
||||||
$body = $this->getJsonBody();
|
$body = $this->getJsonBody();
|
||||||
$row = Database::fetchOne('SELECT id FROM isms_documents WHERE id = ? AND isms_model_id = ?', [$id, $model['id']]);
|
$row = Database::fetchOne('SELECT id, status FROM isms_documents WHERE id = ? AND isms_model_id = ?', [$id, $model['id']]);
|
||||||
if (!$row) {
|
if (!$row) {
|
||||||
$this->jsonError('Documento non trovato', 404, 'NOT_FOUND');
|
$this->jsonError('Documento non trovato', 404, 'NOT_FOUND');
|
||||||
}
|
}
|
||||||
|
if (array_key_exists('body_html', $body) && !in_array($row['status'], ['draft', 'review'], true)) {
|
||||||
|
$this->jsonError('Il contenuto è modificabile solo in stato Bozza o In revisione. Crea una nuova versione per modificare un documento pubblicato.', 409, 'NOT_EDITABLE');
|
||||||
|
}
|
||||||
$fields = [];
|
$fields = [];
|
||||||
foreach (['title','body_html'] as $k) {
|
foreach (['title','body_html'] as $k) {
|
||||||
if (array_key_exists($k, $body)) $fields[$k] = (string) $body[$k];
|
if (array_key_exists($k, $body)) $fields[$k] = (string) $body[$k];
|
||||||
@@ -506,6 +509,60 @@ class IsmsModelController extends BaseController
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** GET /api/isms/documents/{id} — documento completo (per visualizzazione/editor) */
|
||||||
|
public function getDocument(int $id): void
|
||||||
|
{
|
||||||
|
$this->requireOrgAccess();
|
||||||
|
$doc = $this->requireDocument($id);
|
||||||
|
$this->jsonSuccess(['document' => $doc]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET /api/isms/documentsWordAll — scarica l'INTERO set documentale in un unico .doc (Word) */
|
||||||
|
public function exportAllDocumentsWord(): void
|
||||||
|
{
|
||||||
|
$this->requireOrgAccess();
|
||||||
|
$model = $this->requireModel();
|
||||||
|
$org = Database::fetchOne('SELECT name FROM organizations WHERE id = ?', [$this->getCurrentOrgId()]);
|
||||||
|
$orgName = htmlspecialchars((string) ($org['name'] ?? ''));
|
||||||
|
$docs = Database::fetchAll(
|
||||||
|
"SELECT title, doc_type, status, version, body_html, published_at
|
||||||
|
FROM isms_documents WHERE isms_model_id = ? AND status <> 'archived' ORDER BY id",
|
||||||
|
[$model['id']]);
|
||||||
|
$statusLabel = ['draft' => 'Bozza', 'review' => 'In revisione', 'approved' => 'Approvato', 'published' => 'Pubblicato', 'archived' => 'Archiviato'];
|
||||||
|
$pageBreak = "<br clear='all' style='mso-special-character:line-break;page-break-before:always'>";
|
||||||
|
|
||||||
|
$cover = "<h1 style='font-size:24pt;'>Sistema di Gestione per la Sicurezza delle Informazioni</h1>"
|
||||||
|
. "<h2 style='color:#555;'>$orgName</h2>"
|
||||||
|
. "<p class='meta'>Raccolta documentale — generata il " . date('d/m/Y') . " — " . count($docs) . " documenti</p>";
|
||||||
|
$toc = "<h2>Indice</h2><ol>";
|
||||||
|
foreach ($docs as $d) {
|
||||||
|
$toc .= "<li>" . htmlspecialchars((string) $d['title']) . " <span style='color:#888;'>(v" . htmlspecialchars((string) ($d['version'] ?? '1.0')) . ")</span></li>";
|
||||||
|
}
|
||||||
|
$toc .= "</ol>";
|
||||||
|
|
||||||
|
$bodyAll = '';
|
||||||
|
foreach ($docs as $d) {
|
||||||
|
$sl = $statusLabel[$d['status']] ?? $d['status'];
|
||||||
|
$m = $orgName . ' — v' . htmlspecialchars((string) ($d['version'] ?? '1.0')) . ' — ' . $sl;
|
||||||
|
if (!empty($d['published_at'])) $m .= ' — in vigore dal ' . date('d/m/Y', strtotime($d['published_at']));
|
||||||
|
$bodyAll .= $pageBreak . "<h1>" . htmlspecialchars((string) $d['title']) . "</h1><p class='meta'>$m</p>" . ($d['body_html'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = "<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'>"
|
||||||
|
. "<head><meta charset='utf-8'><title>SGSI - $orgName</title>"
|
||||||
|
. "<style>body{font-family:Calibri,Arial,sans-serif;font-size:11pt;color:#1b1b1b;line-height:1.4;}"
|
||||||
|
. "h1{font-size:18pt;color:#0066CC;}h2{font-size:14pt;color:#0066CC;}h3{font-size:12pt;color:#17324d;}"
|
||||||
|
. "table{border-collapse:collapse;width:100%;}td,th{border:1px solid #999;padding:5px;font-size:10pt;}th{background:#eef4fb;}"
|
||||||
|
. ".meta{color:#555;font-size:9pt;border-bottom:1px solid #ccc;padding-bottom:6px;margin-bottom:12px;}</style></head><body>"
|
||||||
|
. $cover . $toc . $bodyAll . "</body></html>";
|
||||||
|
|
||||||
|
$slug = trim(preg_replace('/[^A-Za-z0-9]+/', '_', (string) ($org['name'] ?? 'SGSI')), '_');
|
||||||
|
header('Content-Type: application/msword; charset=utf-8');
|
||||||
|
header('Content-Disposition: attachment; filename="SGSI_' . $slug . '_completo.doc"');
|
||||||
|
echo $html;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/isms/documents/ai-generate
|
* POST /api/isms/documents/ai-generate
|
||||||
* Body: { doc_type, title? } - genera una bozza con AI (grounding fonti certe).
|
* Body: { doc_type, title? } - genera una bozza con AI (grounding fonti certe).
|
||||||
|
|||||||
@@ -301,6 +301,8 @@ $actionMap = [
|
|||||||
'POST:documents/{subId}/newVersion' => 'newVersionDocument', // pubblicato->nuova bozza
|
'POST:documents/{subId}/newVersion' => 'newVersionDocument', // pubblicato->nuova bozza
|
||||||
'GET:documents/{subId}/versions' => 'documentVersions',
|
'GET:documents/{subId}/versions' => 'documentVersions',
|
||||||
'GET:documents/{subId}/word' => 'exportDocumentWord', // download .doc Word
|
'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:readiness' => 'readiness',
|
||||||
'GET:export' => 'export',
|
'GET:export' => 'export',
|
||||||
],
|
],
|
||||||
|
|||||||
+2
-2
@@ -197,7 +197,7 @@
|
|||||||
</script>
|
</script>
|
||||||
<script src="/js/common-bi.js?v=20260631"></script>
|
<script src="/js/common-bi.js?v=20260631"></script>
|
||||||
<script src="/js/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
<script src="/js/help.js?v=20260620"></script>
|
<script src="/js/help.js?v=20260620b"></script>
|
||||||
<script src="/js/isms.js?v=20260620"></script>
|
<script src="/js/isms.js?v=20260620b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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).',
|
'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).',
|
'<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).',
|
'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.'
|
'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 res = await api.ismsDocuments();
|
||||||
const docs = res.documents||[]; const isEn = lang()==='en';
|
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; }
|
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){
|
docs.forEach(function(d){
|
||||||
const st = DOC_STATUS[d.status] || { it:d.status, en:d.status, color:'#6b7280' };
|
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>':'';
|
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==='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==='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+')'); }
|
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(isEn?'Open':'Apri','docView('+d.id+')');
|
||||||
btns += docBtn('Word','docWord('+d.id+')');
|
btns += docBtn('Word','docWord('+d.id+')');
|
||||||
btns += docBtn(isEn?'History':'Storico','docVersions('+d.id+')');
|
btns += docBtn(isEn?'History':'Storico','docVersions('+d.id+')');
|
||||||
@@ -335,6 +336,79 @@ async function aiGenDoc(){
|
|||||||
finally { btn.disabled=false; }
|
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) ──
|
// ── Readiness (cl.9-10) ──
|
||||||
async function loadReadiness(){
|
async function loadReadiness(){
|
||||||
try {
|
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