Files
nis2-agile/application/controllers/InstitutionalDocsController.php
DevEnv nis2-agileandClaude Opus 4.8 396783cd41 [FIX] Documenti Istituzionali (#499): le 5 voci standard non comparivano mai (NO_ORG)
Root cause: InstitutionalDocsController::list() usava requireAuth()+getCurrentOrgId(),
ma currentOrgId e' popolato solo da requireOrgAccess() → getCurrentOrgId() = null →
l'endpoint rispondeva sempre NO_ORG (400) → loadDocs riceveva standards=[] → la card
mostrava solo "Aggiungi voce" (screenshot Simon). L'endpoint NON aveva mai funzionato a runtime.
- Backend: list() usa resolveOrgId() (risolve X-Organization-Id senza lanciare) e restituisce
  SEMPRE le 5 voci standard; i documenti salvati solo se un'org e' selezionata.
- Frontend (dashboard.js): render condiviso + STD_FALLBACK → le 5 voci si mostrano sempre,
  anche se l'API fallisce. Buster dashboard.js 20260825.
- Backend attivato via reload php-fpm host.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-26 18:14:07 +02:00

115 lines
5.1 KiB
PHP

<?php
/**
* NIS2 Agile - InstitutionalDocsController
*
* Documenti istituzionali per-organizzazione (card in dashboard.html).
* Ticket #499 (Simon Fattori, super_admin):
* 1. Ad ogni documento va inserita una DESCRIZIONE obbligatoria a opera dell'utente.
* 2. Oltre al tasto "aggiungi voce" ci sono 5 voci standard sempre presenti:
* Codice Etico, Mission, Vision, Statuto, Piano Sanzionatorio.
*
* Le 5 voci standard sono renderizzate sempre lato UI (STANDARDS); il backend salva
* una riga per (organization_id, doc_key) quando l'utente le compila.
*
* Endpoint:
* GET /api/institutional-docs/list lista (voci compilate + elenco standard)
* POST /api/institutional-docs/save upsert (title+description obbligatori)
* DELETE /api/institutional-docs/{id} elimina voce (org-scoped)
*/
require_once __DIR__ . '/BaseController.php';
class InstitutionalDocsController extends BaseController
{
/** Le 5 voci standard sempre presenti (richiesta punto 2). */
private const STANDARDS = [
['key' => 'codice_etico', 'label' => 'Codice Etico'],
['key' => 'mission', 'label' => 'Mission'],
['key' => 'vision', 'label' => 'Vision'],
['key' => 'statuto', 'label' => 'Statuto'],
['key' => 'piano_sanzionatorio', 'label' => 'Piano Sanzionatorio'],
];
public function list(): void
{
$this->requireAuth();
// BUGFIX (#499): usare requireAuth+getCurrentOrgId dava sempre NO_ORG (currentOrgId
// e' popolato solo da requireOrgAccess) → le 5 voci standard non comparivano MAI.
// Ora risolviamo l'org dall'header e restituiamo SEMPRE le 5 voci standard;
// i documenti salvati si aggiungono solo se un'org e' selezionata.
$orgId = $this->resolveOrgId();
$rows = $orgId ? Database::fetchAll(
"SELECT id, doc_key, title, description, file_url, is_standard, sort_order, updated_at
FROM institutional_documents
WHERE organization_id = ?
ORDER BY is_standard DESC, sort_order ASC, id ASC",
[$orgId]
) : [];
$this->jsonSuccess(['docs' => $rows, 'standards' => self::STANDARDS]);
}
public function save(): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager']);
$this->validateRequired(['title', 'description']);
$orgId = $this->getCurrentOrgId();
$userId = $this->getCurrentUserId();
$title = trim((string) $this->getParam('title'));
$desc = trim((string) $this->getParam('description'));
if ($title === '' || $desc === '') {
$this->jsonError('Titolo e descrizione sono obbligatori', 422, 'MISSING_FIELDS');
}
$fileUrl = $this->getParam('file_url');
$fileUrl = ($fileUrl !== null && trim((string) $fileUrl) !== '') ? trim((string) $fileUrl) : null;
$id = (int) ($this->getParam('id') ?? 0);
$docKey = $this->getParam('doc_key');
$stdKeys = array_column(self::STANDARDS, 'key');
$isStandard = ($docKey !== null && in_array($docKey, $stdKeys, true)) ? 1 : 0;
if (!$isStandard) { $docKey = null; } // le voci custom non hanno doc_key
// Trova riga esistente: per id, oppure per (org, doc_key) se voce standard.
$existing = null;
if ($id > 0) {
$existing = Database::fetchOne(
'SELECT id FROM institutional_documents WHERE id=? AND organization_id=?', [$id, $orgId]);
if (!$existing) { $this->jsonError('Documento non trovato', 404, 'NOT_FOUND'); }
} elseif ($isStandard) {
$existing = Database::fetchOne(
'SELECT id FROM institutional_documents WHERE organization_id=? AND doc_key=?', [$orgId, $docKey]);
}
if ($existing) {
Database::update('institutional_documents', [
'title' => $title,
'description' => $desc,
'file_url' => $fileUrl,
], 'id=? AND organization_id=?', [(int) $existing['id'], $orgId]);
$savedId = (int) $existing['id'];
} else {
$savedId = Database::insert('institutional_documents', [
'organization_id' => $orgId,
'doc_key' => $docKey,
'title' => $title,
'description' => $desc,
'file_url' => $fileUrl,
'is_standard' => $isStandard,
'created_by' => $userId,
]);
}
$this->jsonSuccess(['id' => $savedId], 'Documento salvato');
}
public function delete(int $id): void
{
$this->requireOrgRole(['org_admin', 'compliance_manager']);
$orgId = $this->getCurrentOrgId();
$row = Database::fetchOne(
'SELECT id FROM institutional_documents WHERE id=? AND organization_id=?', [$id, $orgId]);
if (!$row) { $this->jsonError('Documento non trovato', 404, 'NOT_FOUND'); }
Database::delete('institutional_documents', 'id=? AND organization_id=?', [$id, $orgId]);
$this->jsonSuccess(null, 'Documento eliminato');
}
}