[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} */
|
||||
public function updateDocument(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
||||
$this->requireOrgRole(['org_admin', 'compliance_manager', 'board_member']);
|
||||
$model = $this->requireModel();
|
||||
$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) {
|
||||
$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 = [];
|
||||
foreach (['title','body_html'] as $k) {
|
||||
if (array_key_exists($k, $body)) $fields[$k] = (string) $body[$k];
|
||||
@@ -506,6 +509,60 @@ class IsmsModelController extends BaseController
|
||||
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
|
||||
* Body: { doc_type, title? } - genera una bozza con AI (grounding fonti certe).
|
||||
|
||||
Reference in New Issue
Block a user