diff --git a/application/cli/build_nuova_agile_p6_publish.php b/application/cli/build_nuova_agile_p6_publish.php new file mode 100644 index 0000000..0a49208 --- /dev/null +++ b/application/cli/build_nuova_agile_p6_publish.php @@ -0,0 +1,36 @@ + pubblicato (in vigore) con traccia approvazione/pubblicazione + snapshot versione. IDEMPOTENTE. + */ +if (PHP_SAPI !== 'cli') { http_response_code(403); exit("CLI only\n"); } +require_once __DIR__ . '/../config/env.php'; +require_once __DIR__ . '/../config/database.php'; +$pdo = Database::getInstance(); +function ex(PDO $p,string $s,array $a=[]):void{$st=$p->prepare($s);$st->execute($a);} +function one(PDO $p,string $s,array $a=[]){$st=$p->prepare($s);$st->execute($a);return $st->fetch(PDO::FETCH_ASSOC);} +function all(PDO $p,string $s,array $a=[]){$st=$p->prepare($s);$st->execute($a);return $st->fetchAll(PDO::FETCH_ASSOC);} +$NOW='2026-06-20 13:30:00'; $TODAY='2026-06-20'; $NEXT='2027-06-20'; $ORGID=996003; $RSGSI=326; $PRES=103; +$m=one($pdo,"SELECT id FROM isms_models WHERE organization_id=?",[$ORGID]); $modelId=(int)$m['id']; + +$docs=all($pdo,"SELECT id,version,status,body_html FROM isms_documents WHERE isms_model_id=? AND status='approved'",[$modelId]); +$pub=0; +foreach($docs as $d){ + $v=$d['version']?:'1.0'; + // snapshot pubblicazione (evita doppioni) + $exsnap=one($pdo,"SELECT id FROM isms_document_versions WHERE document_id=? AND version=? AND status='published'",[$d['id'],$v]); + if(!$exsnap){ + ex($pdo,"INSERT INTO isms_document_versions (document_id,isms_model_id,organization_id,version,status,body_html,change_note,created_by,created_at) + VALUES (?,?,?,?, 'published', ?, ?, ?, ?)", + [$d['id'],$modelId,$ORGID,$v,$d['body_html'],'Pubblicazione versione '.$v.' (set documentale iniziale)',$PRES,$NOW]); + } + ex($pdo,"UPDATE isms_documents SET status='published', + approved_by=?, approved_at=?, reviewed_by=?, reviewed_at=?, + published_by=?, published_at=?, effective_date=?, next_review_date=?, updated_at=? + WHERE id=?", + [$RSGSI,$NOW,$RSGSI,$NOW,$PRES,$NOW,$TODAY,$NEXT,$NOW,$d['id']]); + $pub++; +} +$tot=one($pdo,"SELECT COUNT(*) c FROM isms_documents WHERE isms_model_id=? AND status='published'",[$modelId])['c']; +echo "Pubblicati ora: $pub. Totale documenti PUBBLICATI: $tot / ".one($pdo,"SELECT COUNT(*) c FROM isms_documents WHERE isms_model_id=?",[$modelId])['c']."\n"; +echo "FASE 6 OK\n"; diff --git a/application/cli/migrate_060_isms_doc_lifecycle.php b/application/cli/migrate_060_isms_doc_lifecycle.php new file mode 100644 index 0000000..cd2cf2a --- /dev/null +++ b/application/cli/migrate_060_isms_doc_lifecycle.php @@ -0,0 +1,56 @@ +query('SELECT DATABASE()')->fetchColumn(); +function col_exists(PDO $p,string $db,string $t,string $c):bool{ + $s=$p->prepare("SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=? AND TABLE_NAME=? AND COLUMN_NAME=?"); + $s->execute([$db,$t,$c]); return (bool)$s->fetchColumn(); +} +function add_col(PDO $p,string $db,string $t,string $c,string $ddl):void{ + if(col_exists($p,$db,$t,$c)){ echo " = $t.$c\n"; return; } + $p->exec("ALTER TABLE `$t` ADD COLUMN $ddl"); echo " + $t.$c\n"; +} + +echo "Migrazione 060 — ciclo di vita documenti ISMS (db=$db)\n"; + +/* 1) Estensione enum stato (idempotente: MODIFY) */ +$pdo->exec("ALTER TABLE isms_documents MODIFY COLUMN status ENUM('draft','review','approved','published','archived') NOT NULL DEFAULT 'draft'"); +echo " enum status -> draft/review/approved/published/archived\n"; + +/* 2) Colonne di tracciamento del ciclo di vita */ +add_col($pdo,$db,'isms_documents','reviewed_by', "reviewed_by INT NULL"); +add_col($pdo,$db,'isms_documents','reviewed_at', "reviewed_at DATETIME NULL"); +add_col($pdo,$db,'isms_documents','approved_by', "approved_by INT NULL"); +add_col($pdo,$db,'isms_documents','approved_at', "approved_at DATETIME NULL"); +add_col($pdo,$db,'isms_documents','published_by', "published_by INT NULL"); +add_col($pdo,$db,'isms_documents','published_at', "published_at DATETIME NULL"); +add_col($pdo,$db,'isms_documents','archived_at', "archived_at DATETIME NULL"); +add_col($pdo,$db,'isms_documents','review_note', "review_note VARCHAR(500) NULL"); +add_col($pdo,$db,'isms_documents','effective_date',"effective_date DATE NULL"); +add_col($pdo,$db,'isms_documents','next_review_date',"next_review_date DATE NULL"); + +/* 3) Tabella storico versioni (snapshot ad ogni approvazione/pubblicazione) */ +$pdo->exec("CREATE TABLE IF NOT EXISTS isms_document_versions ( + id INT AUTO_INCREMENT PRIMARY KEY, + document_id INT NOT NULL, + isms_model_id INT NOT NULL, + organization_id INT NOT NULL, + version VARCHAR(20) NOT NULL, + status VARCHAR(20) NOT NULL, + body_html LONGTEXT NULL, + change_note VARCHAR(255) NULL, + created_by INT NULL, + created_at DATETIME NULL, + INDEX idx_docver_doc (document_id), + INDEX idx_docver_org (organization_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); +echo " tabella isms_document_versions OK\n"; + +echo "FATTO. Prossima mig=061.\n"; diff --git a/application/controllers/IsmsModelController.php b/application/controllers/IsmsModelController.php index b6a03bf..ebed7f3 100644 --- a/application/controllers/IsmsModelController.php +++ b/application/controllers/IsmsModelController.php @@ -287,8 +287,13 @@ class IsmsModelController extends BaseController $this->requireOrgAccess(); $model = $this->requireModel(); $rows = Database::fetchAll( - "SELECT id, doc_type, title, status, ai_generated, version, updated_at - FROM isms_documents WHERE isms_model_id = ? ORDER BY updated_at DESC", + "SELECT d.id, d.doc_type, d.title, d.status, d.ai_generated, d.version, d.updated_at, + d.approved_at, d.published_at, d.next_review_date, d.effective_date, d.review_note, + ua.full_name AS approved_by_name, up.full_name AS published_by_name + FROM isms_documents d + LEFT JOIN users ua ON ua.id = d.approved_by + LEFT JOIN users up ON up.id = d.published_by + WHERE d.isms_model_id = ? ORDER BY d.id", [$model['id']] ); $this->jsonSuccess(['documents' => $rows]); @@ -338,6 +343,169 @@ class IsmsModelController extends BaseController $this->jsonSuccess(['id' => $id], 'Documento aggiornato'); } + /* ───────── Ciclo di vita documentale (ISO 27001 cl.7.5) ───────── */ + + private function requireDocument(int $id): array + { + $model = $this->requireModel(); + $doc = Database::fetchOne('SELECT * FROM isms_documents WHERE id = ? AND isms_model_id = ?', [$id, $model['id']]); + if (!$doc) { $this->jsonError('Documento non trovato', 404, 'NOT_FOUND'); } + return $doc; + } + + private function bumpVersion(string $v, bool $major = false): string + { + $parts = explode('.', preg_replace('/[^0-9.]/', '', $v ?: '1.0')); + $maj = (int) ($parts[0] ?? 1); $min = (int) ($parts[1] ?? 0); + if ($major) { $maj++; $min = 0; } else { $min++; } + return $maj . '.' . $min; + } + + private function snapshotDocument(array $doc, string $changeNote): void + { + Database::insert('isms_document_versions', [ + 'document_id' => $doc['id'], + 'isms_model_id' => $doc['isms_model_id'], + 'organization_id' => $doc['organization_id'], + 'version' => $doc['version'] ?? '1.0', + 'status' => $doc['status'], + 'body_html' => $doc['body_html'] ?? null, + 'change_note' => $changeNote, + 'created_by' => $this->getCurrentUserId(), + 'created_at' => date('Y-m-d H:i:s'), + ]); + } + + /** POST /api/isms/documents/{id}/submit bozza -> in revisione */ + public function submitDocument(int $id): void + { + $this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']); + $doc = $this->requireDocument($id); + if ($doc['status'] !== 'draft') { $this->jsonError('Solo una bozza può essere inviata in revisione', 409, 'BAD_STATE'); } + Database::update('isms_documents', ['status' => 'review', 'review_note' => null, 'updated_at' => date('Y-m-d H:i:s')], 'id = ?', [$id]); + $this->logAudit('isms_document_submitted', 'isms_document', $id, []); + $this->jsonSuccess(['id' => $id, 'status' => 'review'], 'Documento inviato in revisione'); + } + + /** POST /api/isms/documents/{id}/approve in revisione -> approvato */ + public function approveDocument(int $id): void + { + $this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']); + $doc = $this->requireDocument($id); + if (!in_array($doc['status'], ['review', 'draft'], true)) { $this->jsonError('Stato non valido per l\'approvazione', 409, 'BAD_STATE'); } + $now = date('Y-m-d H:i:s'); + Database::update('isms_documents', [ + 'status' => 'approved', 'approved_by' => $this->getCurrentUserId(), 'approved_at' => $now, + 'reviewed_by' => $this->getCurrentUserId(), 'reviewed_at' => $now, 'review_note' => null, 'updated_at' => $now, + ], 'id = ?', [$id]); + $this->logAudit('isms_document_approved', 'isms_document', $id, ['version' => $doc['version']]); + $this->jsonSuccess(['id' => $id, 'status' => 'approved'], 'Documento approvato'); + } + + /** POST /api/isms/documents/{id}/publish approvato -> pubblicato (in vigore) */ + public function publishDocument(int $id): void + { + $this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']); + $doc = $this->requireDocument($id); + if ($doc['status'] !== 'approved') { $this->jsonError('Solo un documento approvato può essere pubblicato', 409, 'BAD_STATE'); } + $now = date('Y-m-d H:i:s'); $today = date('Y-m-d'); $nextReview = date('Y-m-d', strtotime('+1 year')); + $this->snapshotDocument(array_merge($doc, ['status' => 'published']), 'Pubblicazione versione ' . ($doc['version'] ?? '1.0')); + Database::update('isms_documents', [ + 'status' => 'published', 'published_by' => $this->getCurrentUserId(), 'published_at' => $now, + 'effective_date' => $today, 'next_review_date' => $nextReview, 'updated_at' => $now, + ], 'id = ?', [$id]); + $this->logAudit('isms_document_published', 'isms_document', $id, ['version' => $doc['version']]); + $this->jsonSuccess(['id' => $id, 'status' => 'published'], 'Documento pubblicato (in vigore)'); + } + + /** POST /api/isms/documents/{id}/reject in revisione -> bozza (con nota) */ + public function rejectDocument(int $id): void + { + $this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']); + $doc = $this->requireDocument($id); + if ($doc['status'] !== 'review') { $this->jsonError('Solo un documento in revisione può essere rimandato in bozza', 409, 'BAD_STATE'); } + $note = trim((string) ($this->getJsonBody()['note'] ?? '')); + Database::update('isms_documents', ['status' => 'draft', 'review_note' => ($note !== '' ? $note : 'Rimandato in bozza dal revisore'), 'updated_at' => date('Y-m-d H:i:s')], 'id = ?', [$id]); + $this->logAudit('isms_document_rejected', 'isms_document', $id, ['note' => $note]); + $this->jsonSuccess(['id' => $id, 'status' => 'draft'], 'Documento rimandato in bozza'); + } + + /** POST /api/isms/documents/{id}/archive pubblicato -> archiviato */ + public function archiveDocument(int $id): void + { + $this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']); + $doc = $this->requireDocument($id); + if ($doc['status'] !== 'published') { $this->jsonError('Solo un documento pubblicato può essere archiviato', 409, 'BAD_STATE'); } + $now = date('Y-m-d H:i:s'); + Database::update('isms_documents', ['status' => 'archived', 'archived_at' => $now, 'updated_at' => $now], 'id = ?', [$id]); + $this->logAudit('isms_document_archived', 'isms_document', $id, []); + $this->jsonSuccess(['id' => $id, 'status' => 'archived'], 'Documento archiviato'); + } + + /** POST /api/isms/documents/{id}/newVersion pubblicato/archiviato -> nuova bozza */ + public function newVersionDocument(int $id): void + { + $this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']); + $doc = $this->requireDocument($id); + if (!in_array($doc['status'], ['published', 'archived', 'approved'], true)) { $this->jsonError('Nuova versione possibile solo da documento approvato/pubblicato/archiviato', 409, 'BAD_STATE'); } + $major = !empty($this->getJsonBody()['major']); + $newV = $this->bumpVersion($doc['version'] ?? '1.0', $major); + $this->snapshotDocument($doc, 'Apertura nuova versione ' . $newV . ' (dalla ' . ($doc['version'] ?? '1.0') . ')'); + Database::update('isms_documents', [ + 'status' => 'draft', 'version' => $newV, + 'approved_by' => null, 'approved_at' => null, 'reviewed_by' => null, 'reviewed_at' => null, + 'published_by' => null, 'published_at' => null, 'archived_at' => null, 'review_note' => null, 'updated_at' => date('Y-m-d H:i:s'), + ], 'id = ?', [$id]); + $this->logAudit('isms_document_new_version', 'isms_document', $id, ['version' => $newV]); + $this->jsonSuccess(['id' => $id, 'status' => 'draft', 'version' => $newV], 'Nuova versione ' . $newV . ' in bozza'); + } + + /** GET /api/isms/documents/{id}/versions */ + public function documentVersions(int $id): void + { + $this->requireOrgAccess(); + $this->requireDocument($id); + $rows = Database::fetchAll( + 'SELECT v.id, v.version, v.status, v.change_note, v.created_at, u.full_name AS by_name + FROM isms_document_versions v LEFT JOIN users u ON u.id = v.created_by + WHERE v.document_id = ? ORDER BY v.id DESC', [$id]); + $this->jsonSuccess(['versions' => $rows]); + } + + /** GET /api/isms/documents/{id}/word — scarica .doc (Word, modificabile) */ + public function exportDocumentWord(int $id): void + { + $this->requireOrgAccess(); + $doc = $this->requireDocument($id); + $org = Database::fetchOne('SELECT name FROM organizations WHERE id = ?', [$doc['organization_id']]); + $orgName = htmlspecialchars((string) ($org['name'] ?? '')); + $statusLabel = [ + 'draft' => 'Bozza', 'review' => 'In revisione', 'approved' => 'Approvato', + 'published' => 'Pubblicato', 'archived' => 'Archiviato', + ][$doc['status']] ?? $doc['status']; + $meta = $orgName . ' — Versione ' . htmlspecialchars((string) ($doc['version'] ?? '1.0')) . ' — Stato: ' . $statusLabel; + if (!empty($doc['approved_at'])) $meta .= ' — Approvato il ' . date('d/m/Y', strtotime($doc['approved_at'])); + if (!empty($doc['published_at'])) $meta .= ' — In vigore dal ' . date('d/m/Y', strtotime($doc['published_at'])); + if (!empty($doc['next_review_date'])) $meta .= ' — Prossimo riesame: ' . date('d/m/Y', strtotime($doc['next_review_date'])); + + $title = htmlspecialchars((string) $doc['title']); + $body = $doc['body_html'] ?? ''; + $html = "" + . "$title" + . "" + . "

$title

$meta

$body"; + + $slug = trim(preg_replace('/[^A-Za-z0-9]+/', '_', (string) $doc['title']), '_'); + $fname = $slug . '_v' . ($doc['version'] ?? '1.0') . '.doc'; + header('Content-Type: application/msword; charset=utf-8'); + header('Content-Disposition: attachment; filename="' . $fname . '"'); + echo $html; + exit; + } + /** * POST /api/isms/documents/ai-generate * Body: { doc_type, title? } - genera una bozza con AI (grounding fonti certe). diff --git a/docs/sql/060_isms_document_lifecycle.sql b/docs/sql/060_isms_document_lifecycle.sql new file mode 100644 index 0000000..a7c8d8f --- /dev/null +++ b/docs/sql/060_isms_document_lifecycle.sql @@ -0,0 +1,35 @@ +-- 060_isms_document_lifecycle.sql — Ciclo di vita documentale ISMS (ISO/IEC 27001 cl. 7.5) +-- Record DDL. Apply autoritativo idempotente: application/cli/migrate_060_isms_doc_lifecycle.php +-- (ALTER guardati da information_schema; CREATE TABLE IF NOT EXISTS). + +-- Stati del ciclo di vita: aggiunti 'published' e 'archived' +ALTER TABLE isms_documents + MODIFY COLUMN status ENUM('draft','review','approved','published','archived') NOT NULL DEFAULT 'draft'; + +-- Tracciamento del ciclo di vita +ALTER TABLE isms_documents ADD COLUMN reviewed_by INT NULL; +ALTER TABLE isms_documents ADD COLUMN reviewed_at DATETIME NULL; +ALTER TABLE isms_documents ADD COLUMN approved_by INT NULL; +ALTER TABLE isms_documents ADD COLUMN approved_at DATETIME NULL; +ALTER TABLE isms_documents ADD COLUMN published_by INT NULL; +ALTER TABLE isms_documents ADD COLUMN published_at DATETIME NULL; +ALTER TABLE isms_documents ADD COLUMN archived_at DATETIME NULL; +ALTER TABLE isms_documents ADD COLUMN review_note VARCHAR(500) NULL; +ALTER TABLE isms_documents ADD COLUMN effective_date DATE NULL; +ALTER TABLE isms_documents ADD COLUMN next_review_date DATE NULL; + +-- Storico versioni (snapshot ad ogni approvazione/pubblicazione/nuova versione) +CREATE TABLE IF NOT EXISTS isms_document_versions ( + id INT AUTO_INCREMENT PRIMARY KEY, + document_id INT NOT NULL, + isms_model_id INT NOT NULL, + organization_id INT NOT NULL, + version VARCHAR(20) NOT NULL, + status VARCHAR(20) NOT NULL, + body_html LONGTEXT NULL, + change_note VARCHAR(255) NULL, + created_by INT NULL, + created_at DATETIME NULL, + INDEX idx_docver_doc (document_id), + INDEX idx_docver_org (organization_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/public/index.php b/public/index.php index dec08a7..0f89e82 100644 --- a/public/index.php +++ b/public/index.php @@ -293,6 +293,14 @@ $actionMap = [ 'POST:documents' => 'createDocument', 'POST:documents/aiGenerate' => 'aiGenerateDocument', 'PUT:documents/{subId}' => 'updateDocument', + 'POST:documents/{subId}/submit' => 'submitDocument', // ciclo di vita: bozza->revisione + 'POST:documents/{subId}/approve' => 'approveDocument', // revisione->approvato + 'POST:documents/{subId}/publish' => 'publishDocument', // approvato->pubblicato + 'POST:documents/{subId}/reject' => 'rejectDocument', // revisione->bozza (con nota) + 'POST:documents/{subId}/archive' => 'archiveDocument', // pubblicato->archiviato + 'POST:documents/{subId}/newVersion' => 'newVersionDocument', // pubblicato->nuova bozza + 'GET:documents/{subId}/versions' => 'documentVersions', + 'GET:documents/{subId}/word' => 'exportDocumentWord', // download .doc Word 'GET:readiness' => 'readiness', 'GET:export' => 'export', ], diff --git a/public/isms.html b/public/isms.html index a0615a1..2ab39c6 100644 --- a/public/isms.html +++ b/public/isms.html @@ -197,7 +197,7 @@ - - + + diff --git a/public/js/help.js b/public/js/help.js index a887a20..db5d920 100644 --- a/public/js/help.js +++ b/public/js/help.js @@ -129,6 +129,16 @@ const HelpSystem = (function () { '6. Monitoraggio e Miglioramento (cl. 9-10): audit interni e non conformita si gestiscono nei moduli Audit & Report e NCR/CAPA. Una checklist mostra il completamento del SGSI.' ] }, + { + heading: 'Ciclo di vita dei documenti (cl. 7.5)', + items: [ + 'Ogni documento (Manuale, politiche, procedure, istruzioni) ha un ciclo di vita: Bozza → In revisione → Approvato → Pubblicato → Archiviato, mostrato con un badge colorato nel passo 5 (Documenti).', + 'Transizioni: "Invia in revisione", "Approva" / "Rimanda in bozza", "Pubblica" (mette il documento in vigore), "Archivia" (lo ritira), "Nuova versione" (riapre una bozza con numero di versione incrementato).', + 'Ad ogni pubblicazione viene salvato uno snapshot di versione: il pulsante "Storico" mostra tutte le versioni con autore, data e nota. Alla pubblicazione vengono impostate la data di entrata in vigore e la prossima data di riesame (a 1 anno).', + 'Scarica Word: il pulsante "Word" esporta il documento in formato .doc modificabile (mantiene titoli, elenchi e tabelle); "Apri" lo mostra in una nuova scheda.', + 'E\' la gestione delle informazioni documentate richiesta dalla ISO/IEC 27001 cl. 7.5: creazione, approvazione, controllo delle versioni e della distribuzione.' + ] + }, { heading: 'SoA pre-popolato dal NIS2', items: [ diff --git a/public/js/isms.js b/public/js/isms.js index 3d25160..9363ca4 100644 --- a/public/js/isms.js +++ b/public/js/isms.js @@ -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 ''; +} + async function loadDocs(){ try { const res = await api.ismsDocuments(); + const docs = res.documents||[]; const isEn = lang()==='en'; + if(!docs.length){ el('docs-list').innerHTML = '

'+(isEn?'No documents yet.':'Nessun documento.')+'

'; return; } let html=''; - (res.documents||[]).forEach(function(d){ - const ai = (+d.ai_generated)?'AI':''; - html += '
'+esc(d.title)+' '+ai+' ('+esc(d.doc_type)+')
'+ - ''+esc(d.status)+'
'; + docs.forEach(function(d){ + const st = DOC_STATUS[d.status] || { it:d.status, en:d.status, color:'#6b7280' }; + const ai = (+d.ai_generated)?' AI':''; + 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') ? ' · '+esc(d.review_note)+'' : ''; + html += '
'+ + '
'+ + '
'+esc(d.title)+''+ai+' ('+esc(d.doc_type)+')
'+ + ''+(isEn?st.en:st.it)+''+ + '
'+ + '
'+meta+note+'
'+ + '
'+btns+'
'+ + '
'+ + '
'; }); - el('docs-list').innerHTML = html || '

'+(lang()==='en'?'No documents yet.':'Nessun documento.')+'

'; + el('docs-list').innerHTML = html; } catch(e){ el('docs-list').innerHTML='

'+esc((e&&e.message)||'')+'

'; } } + +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 = '…'; + const r = await api.get('/isms/documents/'+id+'/versions'); + const vs = (r && r.data && r.data.versions) || []; + if(!vs.length){ box.innerHTML = ''+(lang()==='en'?'No version history.':'Nessuna versione storicizzata.')+''; return; } + let h = ''+ + ''+ + ''+ + ''+ + ''; + vs.forEach(function(v){ h += ''; }); + box.innerHTML = h + '
Vers.'+(lang()==='en'?'Status':'Stato')+''+(lang()==='en'?'Note':'Nota')+''+(lang()==='en'?'By / date':'Da / data')+'
'+esc(v.version)+''+esc(v.status)+''+esc(v.change_note||'')+''+esc(v.by_name||'')+' · '+fmtDate(v.created_at)+'
'; +} +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…'); diff --git a/public/version.json b/public/version.json index 2fd6b41..03c9cbf 100644 --- a/public/version.json +++ b/public/version.json @@ -1 +1 @@ -{"version": "1.23.10", "build": "2026-06-19-v1.23.10", "date": "2026-06-19", "changelog": "UI V2 (Bootstrap Italia / AGID) completata su tutte le pagine: migrate architecture.html e workflow.html (CSS Bootstrap Italia + bundle JS + common-bi.js per la sidebar V2 coerente con il resto dell'app) e i documenti integrazioniext.html e mktg-api-doc.html (CSS Bootstrap Italia, design system style.css resta autorevole). Rimossa la pagina morta setup-org.html (sostituita da onboarding.html). Il funnel login/register/onboarding era gia' su Bootstrap Italia. Nessuna modifica di funzionalita', nessuna migrazione DB."} +{"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."}