[FEAT] Modulo Controlli periodici (control monitoring §9.1/A.8.16) — v1.23.0
Registro controlli ricorrenti (owner/frequenza/metodo/controllo SoA-NIS2 collegato) + registro esecuzioni come EVIDENZA (data/esito/note) con avanzamento automatico della scadenza; da esito non conforme generazione NC o NC+azione correttiva (riuso non_conformities/capa_actions, nuova source 'monitoring'); report scheda+storico stampabile; scadenze nel Calendario unico. mig.057: periodic_controls + periodic_control_executions (FK CASCADE, UNIQUE (organization_id, code)). PeriodicControlController (slug periodic-controls) + controlli-periodici.html + CalendarController.srcPeriodicControls. Fix da flotta di verifica avversariale (9 finding confermati): - advance(): correzione overflow strtotime '+1 month' (31/01 -> 28/02, no salto mese) via DateTimeImmutable con clamp all'ultimo giorno valido (mensile/ trimestrale/semestrale/annuale); giornaliero/settimanale/custom invariati. - create(): retry-on-duplicate del codice CTL-NNN con UNIQUE (org_id, code). - update(): array_key_exists per azzerare owner_role_id / next_due_date. - delete(): rimossa cleanup morta su review_schedule (il calendario legge diretto). - CalendarController.srcReviewSchedule: esclude internal_audit/stakeholder_activity (gia' con sorgente diretta) -> fix doppia comparsa nel calendario. - calendario.html: TYPE_META 'periodic_control' (chip filtro + label + colore). Additivo. Cache-buster ?v= bump (sidebar) + sw cache nis2-shell-v1.23.0. 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
0738504fdd
commit
4fdeae7cee
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/** DDL idempotente Controlli periodici (mig.057). Crea le tabelle ed estende
|
||||
* l'ENUM non_conformities.source con 'monitoring' (preservando i valori).
|
||||
* Uso: docker exec nis2-app php /var/www/nis2-agile/application/cli/seed_periodic_controls.php */
|
||||
if (PHP_SAPI !== 'cli') { http_response_code(403); exit("Solo da CLI.\n"); }
|
||||
require_once __DIR__ . '/../config/env.php';
|
||||
require_once __DIR__ . '/../config/database.php';
|
||||
$pdo = Database::getInstance();
|
||||
$pdo->exec("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
|
||||
$ddl = [
|
||||
"CREATE TABLE IF NOT EXISTS periodic_controls (
|
||||
id INT NOT NULL AUTO_INCREMENT, organization_id INT NOT NULL,
|
||||
code VARCHAR(20) NULL, title VARCHAR(255) NOT NULL, description TEXT NULL, category VARCHAR(100) NULL,
|
||||
control_ref VARCHAR(32) NULL, owner_role_id INT NULL,
|
||||
frequency ENUM('giornaliero','settimanale','mensile','trimestrale','semestrale','annuale','custom') NOT NULL DEFAULT 'mensile',
|
||||
frequency_days INT NULL, method TEXT NULL, next_due_date DATE NULL, last_executed_at DATE NULL,
|
||||
status ENUM('active','suspended') NOT NULL DEFAULT 'active', created_by INT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id), UNIQUE KEY uk_pctl_code (organization_id, code),
|
||||
KEY idx_pctl_org (organization_id), KEY idx_pctl_due (organization_id, next_due_date),
|
||||
CONSTRAINT fk_pctl_org FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_pctl_role FOREIGN KEY (owner_role_id) REFERENCES org_roles (id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_pctl_user FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS periodic_control_executions (
|
||||
id INT NOT NULL AUTO_INCREMENT, control_id INT NOT NULL, executed_at DATE NOT NULL, executed_by INT NULL,
|
||||
outcome ENUM('conforme','non_conforme','parziale','non_applicabile') NOT NULL DEFAULT 'conforme',
|
||||
notes TEXT NULL, ncr_id INT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id), KEY idx_pcexec_control (control_id),
|
||||
CONSTRAINT fk_pcexec_control FOREIGN KEY (control_id) REFERENCES periodic_controls (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_pcexec_user FOREIGN KEY (executed_by) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_pcexec_ncr FOREIGN KEY (ncr_id) REFERENCES non_conformities (id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
];
|
||||
foreach ($ddl as $stmt) {
|
||||
try { $pdo->exec($stmt); }
|
||||
catch (PDOException $e) { if (!in_array($e->errorInfo[1] ?? 0, [1050, 1061], true)) { throw $e; } }
|
||||
}
|
||||
|
||||
// UNIQUE (organization_id, code) idempotente — la tabella su prod può preesistere senza
|
||||
// l'indice (creata prima del fix). Anti-doppione su CTL-NNN concorrenti (vedi retry in create()).
|
||||
try { $pdo->exec("ALTER TABLE periodic_controls ADD UNIQUE KEY uk_pctl_code (organization_id, code)"); }
|
||||
catch (PDOException $e) {
|
||||
$c = $e->errorInfo[1] ?? 0;
|
||||
if ($c === 1062) { fwrite(STDERR, "WARN uk_pctl_code: doppioni esistenti, indice NON aggiunto.\n"); }
|
||||
elseif (!in_array($c, [1061], true)) { throw $e; } // 1061 = indice già presente
|
||||
}
|
||||
|
||||
// Estende non_conformities.source con 'monitoring' preservando i valori esistenti.
|
||||
try {
|
||||
$cur = (string) $pdo->query("SELECT COLUMN_TYPE FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'non_conformities' AND COLUMN_NAME = 'source'")->fetchColumn();
|
||||
if (stripos($cur, "'monitoring'") === false) {
|
||||
$vals = [];
|
||||
if (preg_match_all("/'([^']+)'/", $cur, $m)) { $vals = $m[1]; }
|
||||
if (!in_array('monitoring', $vals, true)) { $vals[] = 'monitoring'; }
|
||||
if (!$vals) { $vals = ['assessment','audit','incident','supplier_review','management_review','external_audit','monitoring','other']; }
|
||||
$enum = implode(',', array_map(fn($v) => "'" . $v . "'", $vals));
|
||||
$pdo->exec("ALTER TABLE non_conformities MODIFY COLUMN source ENUM($enum) NOT NULL DEFAULT 'assessment'");
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
fwrite(STDERR, "WARN ALTER non_conformities.source: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
$counts = [];
|
||||
foreach (['periodic_controls', 'periodic_control_executions'] as $t) {
|
||||
$counts[$t] = (int) $pdo->query("SELECT COUNT(*) FROM $t")->fetchColumn();
|
||||
}
|
||||
$src = (string) $pdo->query("SELECT COLUMN_TYPE FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'non_conformities' AND COLUMN_NAME = 'source'")->fetchColumn();
|
||||
echo "OK seed-periodic-controls — " . json_encode($counts, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
echo "non_conformities.source has monitoring: " . (str_contains($src, 'monitoring') ? 'YES' : 'NO') . "\n";
|
||||
@@ -36,7 +36,7 @@ class CalendarController extends BaseController
|
||||
'policy_review', 'risk_treatment', 'control_review',
|
||||
'nc_target_close', 'capa_action', 'training_due',
|
||||
'stakeholder_activity', 'review_schedule',
|
||||
'internal_audit', 'management_review_decision',
|
||||
'internal_audit', 'management_review_decision', 'periodic_control',
|
||||
];
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -131,6 +131,7 @@ class CalendarController extends BaseController
|
||||
'review_schedule' => fn() => $this->srcReviewSchedule($orgId, $from, $to),
|
||||
'internal_audits' => fn() => $this->srcInternalAudits($orgId, $from, $to),
|
||||
'mgmt_reviews' => fn() => $this->srcManagementReviewDecisions($orgId, $from, $to),
|
||||
'periodic_controls'=> fn() => $this->srcPeriodicControls($orgId, $from, $to),
|
||||
];
|
||||
foreach ($sources as $rows) {
|
||||
try {
|
||||
@@ -328,10 +329,14 @@ class CalendarController extends BaseController
|
||||
/** Scadenziario revisioni periodiche (A4 4.4): review_schedule.next_review_date. */
|
||||
private function srcReviewSchedule(int $orgId, string $from, string $to): array
|
||||
{
|
||||
// Esclude i tipi che hanno già una sorgente diretta dedicata (internal_audit via
|
||||
// srcInternalAudits, stakeholder_activity via srcStkActivities): altrimenti
|
||||
// comparirebbero due volte nel calendario. Restano gli scheduler generici.
|
||||
$rows = Database::fetchAll(
|
||||
'SELECT id, title, entity_type, next_review_date, last_reviewed_at
|
||||
FROM review_schedule
|
||||
WHERE organization_id = ? AND next_review_date IS NOT NULL',
|
||||
WHERE organization_id = ? AND next_review_date IS NOT NULL
|
||||
AND entity_type NOT IN ("internal_audit", "stakeholder_activity")',
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
@@ -363,6 +368,25 @@ class CalendarController extends BaseController
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
/** Controlli periodici (mig.057, opzionale): periodic_controls.next_due_date. */
|
||||
private function srcPeriodicControls(int $orgId, string $from, string $to): array
|
||||
{
|
||||
$rows = Database::fetchAll(
|
||||
"SELECT id, code, title, next_due_date, status
|
||||
FROM periodic_controls
|
||||
WHERE organization_id = ? AND next_due_date IS NOT NULL AND status = 'active'",
|
||||
[$orgId]
|
||||
);
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$title = trim((string) $r['code'] . ' ' . (string) $r['title']);
|
||||
$out[] = $this->makeEvent('periodic_controls', 'periodic_control',
|
||||
'Controllo periodico: ' . $title, $r['next_due_date'],
|
||||
'medium', 'periodic_control', (int) $r['id'], '/controlli-periodici.html', $from, $to, false);
|
||||
}
|
||||
return array_values(array_filter($out));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decisioni del riesame di direzione (Modulo B, opzionale):
|
||||
* management_review_decisions.due_date JOIN management_reviews per filtrare per org.
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
<?php
|
||||
/**
|
||||
* NIS2 Agile - Controlli periodici (control monitoring) — mig.057
|
||||
* ----------------------------------------------------------------------------
|
||||
* Registro dei controlli RICORRENTI (owner + frequenza + metodo) e relativo
|
||||
* REGISTRO DELLE ESECUZIONI (data, esito, note, evidenze) = evidenza del
|
||||
* monitoraggio periodico (ISO 27001 §9.1 / A.8.16 — buona prassi, non obbligo).
|
||||
* Da un'esecuzione non conforme si genera una NC o un'azione correttiva (AC),
|
||||
* riusando non_conformities / capa_actions. La scadenza avanza ad ogni esecuzione
|
||||
* e alimenta il Calendario.
|
||||
*
|
||||
* Distinto da compliance_controls (registro stato), review_schedule (cadenza
|
||||
* generica) e internal_audits (audit puntuale). Vedi docs/DESIGN_CONTROLLI_PERIODICI.md.
|
||||
*
|
||||
* NOTE: Database::query/fetchAll/fetchOne/insert/update/delete; jsonError/Success exit.
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/BaseController.php';
|
||||
|
||||
class PeriodicControlController extends BaseController
|
||||
{
|
||||
private const MANAGE_ROLES = ['org_admin', 'compliance_manager', 'auditor'];
|
||||
private const FREQS = ['giornaliero', 'settimanale', 'mensile', 'trimestrale', 'semestrale', 'annuale', 'custom'];
|
||||
|
||||
// ── LETTURE ──────────────────────────────────────────────────────────────
|
||||
/** GET /api/periodic-controls/list */
|
||||
public function list(): void
|
||||
{
|
||||
$this->requireOrgAccess();
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$rows = Database::fetchAll(
|
||||
"SELECT c.id, c.code, c.title, c.category, c.control_ref, c.frequency, c.frequency_days,
|
||||
c.next_due_date, c.last_executed_at, c.status, c.owner_role_id, r.role_name AS owner_role,
|
||||
(SELECT COUNT(*) FROM periodic_control_executions e WHERE e.control_id = c.id) AS n_exec,
|
||||
(SELECT outcome FROM periodic_control_executions e WHERE e.control_id = c.id ORDER BY e.executed_at DESC, e.id DESC LIMIT 1) AS last_outcome
|
||||
FROM periodic_controls c
|
||||
LEFT JOIN org_roles r ON r.id = c.owner_role_id
|
||||
WHERE c.organization_id = ?
|
||||
ORDER BY (c.next_due_date IS NULL), c.next_due_date ASC, c.id DESC",
|
||||
[$orgId]
|
||||
);
|
||||
$today = date('Y-m-d');
|
||||
$out = array_map(function ($c) use ($today) {
|
||||
return [
|
||||
'id' => (int) $c['id'], 'code' => $c['code'], 'title' => $c['title'], 'category' => $c['category'],
|
||||
'control_ref' => $c['control_ref'], 'frequency' => $c['frequency'], 'frequency_days' => $c['frequency_days'] !== null ? (int) $c['frequency_days'] : null,
|
||||
'owner_role_id' => $c['owner_role_id'] !== null ? (int) $c['owner_role_id'] : null, 'owner_role' => $c['owner_role'],
|
||||
'next_due_date' => $c['next_due_date'], 'last_executed_at' => $c['last_executed_at'], 'status' => $c['status'],
|
||||
'n_exec' => (int) $c['n_exec'], 'last_outcome' => $c['last_outcome'],
|
||||
'due_status' => $this->dueStatus($c['next_due_date'], $today, $c['status']),
|
||||
];
|
||||
}, $rows);
|
||||
$this->jsonSuccess(['controls' => $out]);
|
||||
}
|
||||
|
||||
/** GET /api/periodic-controls/{id} */
|
||||
public function get(int $id): void
|
||||
{
|
||||
$this->requireOrgAccess();
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$c = Database::fetchOne(
|
||||
"SELECT c.*, r.role_name AS owner_role FROM periodic_controls c
|
||||
LEFT JOIN org_roles r ON r.id = c.owner_role_id
|
||||
WHERE c.id = ? AND c.organization_id = ?",
|
||||
[$id, $orgId]
|
||||
);
|
||||
if (!$c) { $this->jsonError('Controllo non trovato', 404, 'NOT_FOUND'); }
|
||||
$exec = Database::fetchAll(
|
||||
"SELECT e.id, e.executed_at, e.outcome, e.notes, e.ncr_id, e.created_at,
|
||||
u.full_name AS executed_by_name, n.ncr_code
|
||||
FROM periodic_control_executions e
|
||||
LEFT JOIN users u ON u.id = e.executed_by
|
||||
LEFT JOIN non_conformities n ON n.id = e.ncr_id
|
||||
WHERE e.control_id = ? ORDER BY e.executed_at DESC, e.id DESC",
|
||||
[$id]
|
||||
);
|
||||
$this->jsonSuccess([
|
||||
'id' => (int) $c['id'], 'code' => $c['code'], 'title' => $c['title'], 'description' => $c['description'],
|
||||
'category' => $c['category'], 'control_ref' => $c['control_ref'], 'method' => $c['method'],
|
||||
'frequency' => $c['frequency'], 'frequency_days' => $c['frequency_days'] !== null ? (int) $c['frequency_days'] : null,
|
||||
'owner_role_id' => $c['owner_role_id'] !== null ? (int) $c['owner_role_id'] : null, 'owner_role' => $c['owner_role'],
|
||||
'next_due_date' => $c['next_due_date'], 'last_executed_at' => $c['last_executed_at'], 'status' => $c['status'],
|
||||
'executions' => array_map(static fn($e) => [
|
||||
'id' => (int) $e['id'], 'executed_at' => $e['executed_at'], 'outcome' => $e['outcome'], 'notes' => $e['notes'],
|
||||
'executed_by_name' => $e['executed_by_name'], 'ncr_id' => $e['ncr_id'] !== null ? (int) $e['ncr_id'] : null, 'ncr_code' => $e['ncr_code'],
|
||||
], $exec),
|
||||
]);
|
||||
}
|
||||
|
||||
// ── SCRITTURE ────────────────────────────────────────────────────────────
|
||||
/** POST /api/periodic-controls/create */
|
||||
public function create(): void
|
||||
{
|
||||
$this->requireOrgRole(self::MANAGE_ROLES);
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$b = $this->getJsonBody();
|
||||
|
||||
$title = trim((string) ($b['title'] ?? ''));
|
||||
if ($title === '' || mb_strlen($title) > 255) { $this->jsonError('Titolo obbligatorio (max 255)', 422, 'INVALID_TITLE'); }
|
||||
$freq = in_array($b['frequency'] ?? '', self::FREQS, true) ? $b['frequency'] : 'mensile';
|
||||
$fdays = ($freq === 'custom') ? max(1, (int) ($b['frequency_days'] ?? 0)) : null;
|
||||
if ($freq === 'custom' && !$fdays) { $this->jsonError('Per frequenza custom indicare frequency_days', 422, 'INVALID_FREQ'); }
|
||||
$ownerRole = $this->validateOwnerRole($b['owner_role_id'] ?? null, $orgId);
|
||||
$next = $this->validateDate($b['next_due_date'] ?? null, 'next_due_date');
|
||||
if ($next === null) { $next = $this->advance(date('Y-m-d'), $freq, $fdays); }
|
||||
|
||||
// Retry-on-duplicate: il codice CTL-NNN è MAX+1 (race possibile su create concorrenti);
|
||||
// la UNIQUE (organization_id, code) blocca i doppioni e qui si rigenera il codice.
|
||||
$base = [
|
||||
'organization_id' => $orgId, 'title' => $title,
|
||||
'description' => $this->nullableStr($b['description'] ?? null), 'category' => $this->nullableStr($b['category'] ?? null, 100),
|
||||
'control_ref' => $this->nullableStr($b['control_ref'] ?? null, 32), 'owner_role_id' => $ownerRole,
|
||||
'frequency' => $freq, 'frequency_days' => $fdays, 'method' => $this->nullableStr($b['method'] ?? null),
|
||||
'next_due_date' => $next, 'status' => in_array($b['status'] ?? '', ['active', 'suspended'], true) ? $b['status'] : 'active',
|
||||
'created_by' => $this->getCurrentUserId(),
|
||||
];
|
||||
$code = null; $id = 0;
|
||||
for ($attempt = 0; ; $attempt++) {
|
||||
$code = $this->nextCode($orgId);
|
||||
try {
|
||||
$id = (int) Database::insert('periodic_controls', ['code' => $code] + $base);
|
||||
break;
|
||||
} catch (\PDOException $e) {
|
||||
if (($e->errorInfo[1] ?? 0) === 1062 && $attempt < 4) { continue; }
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$this->logAudit('periodic_control_created', 'periodic_control', $id, ['code' => $code, 'title' => $title]);
|
||||
$this->jsonSuccess(['id' => $id, 'code' => $code, 'next_due_date' => $next], 'Controllo periodico creato', 201);
|
||||
}
|
||||
|
||||
/** PUT /api/periodic-controls/{id} */
|
||||
public function update(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(self::MANAGE_ROLES);
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$b = $this->getJsonBody();
|
||||
$c = Database::fetchOne('SELECT id, frequency, frequency_days FROM periodic_controls WHERE id = ? AND organization_id = ?', [$id, $orgId]);
|
||||
if (!$c) { $this->jsonError('Controllo non trovato', 404, 'NOT_FOUND'); }
|
||||
|
||||
$updates = [];
|
||||
if ($this->hasParam('title')) {
|
||||
$t = trim((string) ($b['title'] ?? '')); if ($t === '' || mb_strlen($t) > 255) { $this->jsonError('Titolo non valido', 422, 'INVALID_TITLE'); }
|
||||
$updates['title'] = $t;
|
||||
}
|
||||
if ($this->hasParam('description')) { $updates['description'] = $this->nullableStr($b['description'] ?? null); }
|
||||
if ($this->hasParam('category')) { $updates['category'] = $this->nullableStr($b['category'] ?? null, 100); }
|
||||
if ($this->hasParam('control_ref')) { $updates['control_ref'] = $this->nullableStr($b['control_ref'] ?? null, 32); }
|
||||
if ($this->hasParam('method')) { $updates['method'] = $this->nullableStr($b['method'] ?? null); }
|
||||
// array_key_exists (non hasParam): distingue "chiave presente = null" (azzera) da "chiave assente" (lascia invariato).
|
||||
if (array_key_exists('owner_role_id', $b)) { $updates['owner_role_id'] = $this->validateOwnerRole($b['owner_role_id'], $orgId); }
|
||||
if ($this->hasParam('frequency') && in_array($b['frequency'], self::FREQS, true)) {
|
||||
$updates['frequency'] = $b['frequency'];
|
||||
$updates['frequency_days'] = ($b['frequency'] === 'custom') ? max(1, (int) ($b['frequency_days'] ?? $c['frequency_days'] ?? 30)) : null;
|
||||
}
|
||||
if (array_key_exists('next_due_date', $b)) { $updates['next_due_date'] = $this->validateDate($b['next_due_date'], 'next_due_date'); }
|
||||
if ($this->hasParam('status') && in_array($b['status'], ['active', 'suspended'], true)) { $updates['status'] = $b['status']; }
|
||||
|
||||
if (!empty($updates)) { Database::update('periodic_controls', $updates, 'id = ? AND organization_id = ?', [$id, $orgId]); }
|
||||
$this->logAudit('periodic_control_updated', 'periodic_control', $id, array_keys($updates));
|
||||
$this->jsonSuccess(['id' => $id, 'updated' => array_keys($updates)], 'Controllo aggiornato');
|
||||
}
|
||||
|
||||
/** DELETE /api/periodic-controls/{id} */
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(['org_admin']);
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
// Le esecuzioni si cancellano via FK ON DELETE CASCADE; il calendario legge
|
||||
// periodic_controls direttamente (CalendarController::srcPeriodicControls),
|
||||
// quindi non esiste alcuna riga review_schedule da ripulire.
|
||||
$del = Database::delete('periodic_controls', 'id = ? AND organization_id = ?', [$id, $orgId]);
|
||||
if ($del === 0) { $this->jsonError('Controllo non trovato', 404, 'NOT_FOUND'); }
|
||||
$this->logAudit('periodic_control_deleted', 'periodic_control', $id);
|
||||
$this->jsonSuccess(null, 'Controllo eliminato');
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/periodic-controls/{id}/executions
|
||||
* Body: {executed_at?, outcome, notes?, generate?: 'none'|'nc'|'ac'}
|
||||
* Registra l'esecuzione, avanza la scadenza; su esito negativo può generare NC o NC+azione correttiva.
|
||||
*/
|
||||
public function addExecution(int $id): void
|
||||
{
|
||||
$this->requireOrgRole(self::MANAGE_ROLES);
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$c = Database::fetchOne('SELECT * FROM periodic_controls WHERE id = ? AND organization_id = ?', [$id, $orgId]);
|
||||
if (!$c) { $this->jsonError('Controllo non trovato', 404, 'NOT_FOUND'); }
|
||||
$b = $this->getJsonBody();
|
||||
|
||||
$outcome = in_array($b['outcome'] ?? '', ['conforme', 'non_conforme', 'parziale', 'non_applicabile'], true) ? $b['outcome'] : null;
|
||||
if ($outcome === null) { $this->jsonError('Esito non valido', 422, 'INVALID_OUTCOME'); }
|
||||
$execDate = $this->validateDate($b['executed_at'] ?? null, 'executed_at') ?? date('Y-m-d');
|
||||
$generate = in_array($b['generate'] ?? 'none', ['none', 'nc', 'ac'], true) ? $b['generate'] : 'none';
|
||||
|
||||
$execId = (int) Database::insert('periodic_control_executions', [
|
||||
'control_id' => $id, 'executed_at' => $execDate, 'executed_by' => $this->getCurrentUserId(),
|
||||
'outcome' => $outcome, 'notes' => $this->nullableStr($b['notes'] ?? null),
|
||||
]);
|
||||
|
||||
// avanza la scadenza
|
||||
$next = $this->advance($execDate, $c['frequency'], $c['frequency_days'] !== null ? (int) $c['frequency_days'] : null);
|
||||
Database::update('periodic_controls', ['last_executed_at' => $execDate, 'next_due_date' => $next], 'id = ? AND organization_id = ?', [$id, $orgId]);
|
||||
|
||||
// generazione NC / AC su esito negativo
|
||||
$ncr = null;
|
||||
if (in_array($outcome, ['non_conforme', 'parziale'], true) && $generate !== 'none') {
|
||||
$ncr = $this->generateNc($orgId, $c, $execId, $outcome, $generate === 'ac');
|
||||
Database::update('periodic_control_executions', ['ncr_id' => $ncr['id']], 'id = ?', [$execId]);
|
||||
}
|
||||
|
||||
$this->logAudit('periodic_control_executed', 'periodic_control', $id, ['execution_id' => $execId, 'outcome' => $outcome, 'ncr' => $ncr['ncr_code'] ?? null]);
|
||||
$this->jsonSuccess(['execution_id' => $execId, 'next_due_date' => $next, 'ncr' => $ncr], 'Esecuzione registrata', 201);
|
||||
}
|
||||
|
||||
/** GET /api/periodic-controls/{id}/report */
|
||||
public function report(int $id): void
|
||||
{
|
||||
$this->requireOrgAccess();
|
||||
$orgId = $this->getCurrentOrgId();
|
||||
$c = Database::fetchOne(
|
||||
"SELECT c.*, r.role_name AS owner_role, o.name AS org_name FROM periodic_controls c
|
||||
LEFT JOIN org_roles r ON r.id = c.owner_role_id JOIN organizations o ON o.id = c.organization_id
|
||||
WHERE c.id = ? AND c.organization_id = ?",
|
||||
[$id, $orgId]
|
||||
);
|
||||
if (!$c) { $this->jsonError('Controllo non trovato', 404, 'NOT_FOUND'); }
|
||||
$exec = Database::fetchAll(
|
||||
"SELECT e.executed_at, e.outcome, e.notes, n.ncr_code, u.full_name AS by_name
|
||||
FROM periodic_control_executions e LEFT JOIN non_conformities n ON n.id = e.ncr_id
|
||||
LEFT JOIN users u ON u.id = e.executed_by WHERE e.control_id = ? ORDER BY e.executed_at DESC, e.id DESC",
|
||||
[$id]
|
||||
);
|
||||
header('Content-Type: text/html; charset=utf-8');
|
||||
echo $this->renderReport($c, $exec);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── HELPER ───────────────────────────────────────────────────────────────
|
||||
private function dueStatus(?string $next, string $today, string $status): string
|
||||
{
|
||||
if ($status === 'suspended') { return 'suspended'; }
|
||||
if ($next === null) { return 'none'; }
|
||||
if ($next < $today) { return 'overdue'; }
|
||||
$soon = date('Y-m-d', strtotime($today . ' +14 days'));
|
||||
return ($next <= $soon) ? 'due_soon' : 'upcoming';
|
||||
}
|
||||
|
||||
/**
|
||||
* Avanza una data secondo la frequenza.
|
||||
* Per le frequenze a mesi si "ancora" al primo del mese e si rimette il giorno
|
||||
* limato all'ultimo valido del mese di destinazione: evita l'overflow di
|
||||
* strtotime('+1 month') (es. 31/01 -> 03/03, febbraio saltato).
|
||||
*/
|
||||
private function advance(string $from, string $freq, ?int $days): string
|
||||
{
|
||||
$months = ['mensile' => 1, 'trimestrale' => 3, 'semestrale' => 6, 'annuale' => 12];
|
||||
$d = new DateTimeImmutable($from); // $from è sempre un Y-m-d validato (validateDate / date('Y-m-d'))
|
||||
if (isset($months[$freq])) {
|
||||
$target = $d->modify('first day of this month')->modify('+' . $months[$freq] . ' months');
|
||||
$day = min((int) $d->format('d'), (int) $target->format('t'));
|
||||
return $target->setDate((int) $target->format('Y'), (int) $target->format('m'), $day)->format('Y-m-d');
|
||||
}
|
||||
// giornaliero / settimanale / custom (e qualsiasi freq sconosciuta -> a giorni)
|
||||
$simple = ['giornaliero' => '+1 day', 'settimanale' => '+1 week'];
|
||||
$mod = $simple[$freq] ?? ('+' . max(1, (int) $days) . ' days');
|
||||
if ($freq === 'custom') { $mod = '+' . max(1, (int) $days) . ' days'; }
|
||||
return $d->modify($mod)->format('Y-m-d');
|
||||
}
|
||||
|
||||
/** Crea una NC (e opzionalmente una prima azione correttiva) da un'esecuzione fallita. */
|
||||
private function generateNc(int $orgId, array $ctl, int $execId, string $outcome, bool $withAction): array
|
||||
{
|
||||
$titleBase = ($ctl['control_ref'] ? '[' . $ctl['control_ref'] . '] ' : '') . 'Controllo periodico: ' . $ctl['title'];
|
||||
$title = mb_strlen($titleBase) > 200 ? mb_substr($titleBase, 0, 197) . '...' : $titleBase;
|
||||
$sev = $outcome === 'non_conforme' ? 'major' : 'minor';
|
||||
$ncrCode = $this->generateCode('NCR');
|
||||
$ncrId = (int) Database::insert('non_conformities', [
|
||||
'organization_id' => $orgId, 'ncr_code' => $ncrCode, 'title' => $title,
|
||||
'description' => "Rilevata dal monitoraggio del controllo periodico {$ctl['code']} ({$ctl['title']}): esito " . str_replace('_', ' ', $outcome) . '.',
|
||||
'source' => 'monitoring', 'source_entity_type' => 'periodic_control_execution', 'source_entity_id' => $execId,
|
||||
'severity' => $sev, 'status' => 'open', 'identified_by' => $this->getCurrentUserId(),
|
||||
]);
|
||||
$out = ['id' => $ncrId, 'ncr_code' => $ncrCode];
|
||||
if ($withAction) {
|
||||
$capaCode = $this->generateCode('CAPA');
|
||||
$capaId = (int) Database::insert('capa_actions', [
|
||||
'ncr_id' => $ncrId, 'organization_id' => $orgId, 'capa_code' => $capaCode, 'action_type' => 'corrective',
|
||||
'title' => 'Azione correttiva per ' . $title, 'status' => 'planned',
|
||||
]);
|
||||
$out['capa_id'] = $capaId; $out['capa_code'] = $capaCode;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
private function nextCode(int $orgId): string
|
||||
{
|
||||
$row = Database::fetchOne(
|
||||
"SELECT MAX(CAST(SUBSTRING_INDEX(code, '-', -1) AS UNSIGNED)) AS n
|
||||
FROM periodic_controls WHERE organization_id = ? AND code LIKE 'CTL-%'",
|
||||
[$orgId]
|
||||
);
|
||||
return 'CTL-' . str_pad((string) (((int) ($row['n'] ?? 0)) + 1), 3, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
private function validateOwnerRole($id, int $orgId): ?int
|
||||
{
|
||||
$id = ($id === null || $id === '') ? null : (int) $id;
|
||||
if ($id === null) { return null; }
|
||||
$r = Database::fetchOne('SELECT id FROM org_roles WHERE id = ? AND organization_id = ?', [$id, $orgId]);
|
||||
if (!$r) { $this->jsonError('Ruolo responsabile non valido', 422, 'INVALID_OWNER'); }
|
||||
return $id;
|
||||
}
|
||||
|
||||
private function validateDate($v, string $field): ?string
|
||||
{
|
||||
if ($v === null || $v === '') { return null; }
|
||||
$d = trim((string) $v); $dt = DateTime::createFromFormat('Y-m-d', $d);
|
||||
if (!$dt || $dt->format('Y-m-d') !== $d) { $this->jsonError("Data $field non valida (AAAA-MM-GG)", 422, 'INVALID_DATE'); }
|
||||
return $d;
|
||||
}
|
||||
|
||||
private function nullableStr($v, ?int $max = null): ?string
|
||||
{
|
||||
if ($v === null) { return null; }
|
||||
$s = trim((string) $v); if ($s === '') { return null; }
|
||||
if ($max !== null && mb_strlen($s) > $max) { $s = mb_substr($s, 0, $max); }
|
||||
return $s;
|
||||
}
|
||||
|
||||
private function renderReport(array $c, array $exec): string
|
||||
{
|
||||
$esc = static fn($s) => htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8');
|
||||
$rows = '';
|
||||
foreach ($exec as $e) {
|
||||
$rows .= '<tr><td>' . $esc($e['executed_at']) . '</td><td>' . $esc(str_replace('_', ' ', $e['outcome'])) . '</td>'
|
||||
. '<td>' . $esc($e['by_name'] ?? '') . '</td><td>' . $esc($e['ncr_code'] ?? '') . '</td><td>' . $esc($e['notes'] ?? '') . '</td></tr>';
|
||||
}
|
||||
if ($rows === '') { $rows = '<tr><td colspan="5" style="color:#888">Nessuna esecuzione registrata.</td></tr>'; }
|
||||
$h = '<!DOCTYPE html><html lang="it"><head><meta charset="UTF-8"><title>Controllo periodico ' . $esc($c['code']) . '</title>'
|
||||
. '<style>body{font-family:-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#1e293b;max-width:900px;margin:24px auto;padding:0 16px}'
|
||||
. 'h1{font-size:20px;color:#1e40af} .sub{color:#64748b;font-size:13px;margin-bottom:18px} table{width:100%;border-collapse:collapse;font-size:13px;margin-top:8px}'
|
||||
. 'th,td{border:1px solid #e2e8f0;padding:7px 10px;text-align:left} th{background:#f8fafc} .meta td{border:none;padding:3px 0}</style></head><body>';
|
||||
$h .= '<h1>Scheda controllo periodico ' . $esc($c['code']) . '</h1>';
|
||||
$h .= '<div class="sub">' . $esc($c['org_name']) . ' · generato il ' . date('d/m/Y H:i') . '</div>';
|
||||
$h .= '<table class="meta">'
|
||||
. '<tr><td style="width:200px"><strong>Titolo</strong></td><td>' . $esc($c['title']) . '</td></tr>'
|
||||
. '<tr><td><strong>Categoria</strong></td><td>' . $esc($c['category'] ?? '-') . '</td></tr>'
|
||||
. '<tr><td><strong>Controllo collegato</strong></td><td>' . $esc($c['control_ref'] ?? '-') . '</td></tr>'
|
||||
. '<tr><td><strong>Responsabile</strong></td><td>' . $esc($c['owner_role'] ?? '-') . '</td></tr>'
|
||||
. '<tr><td><strong>Frequenza</strong></td><td>' . $esc($c['frequency']) . ($c['frequency'] === 'custom' ? ' (' . $esc($c['frequency_days']) . ' gg)' : '') . '</td></tr>'
|
||||
. '<tr><td><strong>Prossima scadenza</strong></td><td>' . $esc($c['next_due_date'] ?? '-') . '</td></tr>'
|
||||
. '<tr><td><strong>Metodo</strong></td><td>' . $esc($c['method'] ?? '-') . '</td></tr></table>';
|
||||
$h .= '<h2 style="font-size:15px;color:#1e40af;margin-top:22px">Storico esecuzioni (evidenza)</h2>';
|
||||
$h .= '<table><thead><tr><th>Data</th><th>Esito</th><th>Eseguito da</th><th>NC</th><th>Note</th></tr></thead><tbody>' . $rows . '</tbody></table>';
|
||||
$h .= '<p style="margin-top:30px;color:#9ca3af;font-size:.78rem;">Documento generato da NIS2 Agile. Il monitoraggio periodico dei controlli e\' buona prassi (ISO 27001 §9.1/A.8.16); gli obblighi normativi in Italia derivano da NIS2 / D.Lgs. 138/2024. Strumento di supporto, non un parere legale.</p>';
|
||||
$h .= '</body></html>';
|
||||
return $h;
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,4 @@
|
||||
Nessun ticket aperto.
|
||||
|
||||
---
|
||||
_Ultimo sync: 2026-06-14 16:35:01_
|
||||
_Ultimo sync: 2026-06-17 09:20:02_
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
-- =====================================================================
|
||||
-- 057 — Controlli periodici (control monitoring con frequenza + esecuzioni + NC/AC)
|
||||
-- =====================================================================
|
||||
-- Registro dei controlli RICORRENTI con frequenza, owner e metodo; ogni
|
||||
-- ESECUZIONE è registrata (data, esito, note, evidenze su evidence_files) =
|
||||
-- EVIDENZA del monitoraggio periodico (ISO 27001 §9.1 / A.8.16; buona prassi,
|
||||
-- non obbligo). Da un esito non conforme si genera una NC o un'azione correttiva
|
||||
-- (riuso non_conformities/capa_actions). Distinto da: compliance_controls
|
||||
-- (registro stato), review_schedule (cadenza generica), internal_audits (audit
|
||||
-- puntuale). Vedi docs/DESIGN_CONTROLLI_PERIODICI.md.
|
||||
--
|
||||
-- Additivo, reversibile. Runner-safe: CREATE TABLE IF NOT EXISTS con FK inline;
|
||||
-- l'ALTER dell'ENUM non_conformities.source è applicata dal seeder idempotente
|
||||
-- (preservando i valori) — application/cli/seed_periodic_controls.php.
|
||||
-- =====================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS periodic_controls (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
organization_id INT NOT NULL,
|
||||
code VARCHAR(20) NULL, -- CTL-NNN per org
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
category VARCHAR(100) NULL,
|
||||
control_ref VARCHAR(32) NULL, -- codice SoA/NIS2 collegato (es. A.8.13)
|
||||
owner_role_id INT NULL, -- responsabile (org_roles)
|
||||
frequency ENUM('giornaliero','settimanale','mensile','trimestrale','semestrale','annuale','custom') NOT NULL DEFAULT 'mensile',
|
||||
frequency_days INT NULL, -- usato se frequency='custom'
|
||||
method TEXT NULL, -- metodologia di verifica
|
||||
next_due_date DATE NULL,
|
||||
last_executed_at DATE NULL,
|
||||
status ENUM('active','suspended') NOT NULL DEFAULT 'active',
|
||||
created_by INT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_pctl_org (organization_id),
|
||||
KEY idx_pctl_due (organization_id, next_due_date),
|
||||
CONSTRAINT fk_pctl_org FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_pctl_role FOREIGN KEY (owner_role_id) REFERENCES org_roles (id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_pctl_user FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS periodic_control_executions (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
control_id INT NOT NULL,
|
||||
executed_at DATE NOT NULL,
|
||||
executed_by INT NULL,
|
||||
outcome ENUM('conforme','non_conforme','parziale','non_applicabile') NOT NULL DEFAULT 'conforme',
|
||||
notes TEXT NULL,
|
||||
ncr_id INT NULL, -- NC eventualmente generata
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_pcexec_control (control_id),
|
||||
CONSTRAINT fk_pcexec_control FOREIGN KEY (control_id) REFERENCES periodic_controls (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_pcexec_user FOREIGN KEY (executed_by) REFERENCES users (id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_pcexec_ncr FOREIGN KEY (ncr_id) REFERENCES non_conformities (id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ALTER non_conformities.source: aggiunge 'monitoring' (eseguita dal seeder, idempotente).
|
||||
-- ALTER TABLE non_conformities MODIFY COLUMN source ENUM('assessment','audit','incident','supplier_review','management_review','external_audit','monitoring','other') NOT NULL DEFAULT 'assessment';
|
||||
|
||||
-- ROLLBACK (manuale):
|
||||
-- DROP TABLE IF EXISTS periodic_control_executions
|
||||
-- DROP TABLE IF EXISTS periodic_controls
|
||||
-- (non_conformities.source: rimuovere 'monitoring' dall'ENUM dopo aver riclassificato le NC relative)
|
||||
@@ -70,9 +70,9 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della app: api.js + common.js (helper) + common-bi.js (override BI) -->
|
||||
<script src="js/api.js?v=20260630"></script>
|
||||
<script src="js/common.js?v=20260630"></script>
|
||||
<script src="js/common-bi.js?v=20260630"></script>
|
||||
<script src="js/api.js?v=20260631"></script>
|
||||
<script src="js/common.js?v=20260631"></script>
|
||||
<script src="js/common-bi.js?v=20260631"></script>
|
||||
<script>
|
||||
// Renderizza la sidebar BI (loadSidebar è stato ridefinito da common-bi.js)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
@@ -1088,10 +1088,10 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260630"></script>
|
||||
<script src="js/common.js?v=20260630"></script>
|
||||
<script src="js/i18n.js?v=20260630"></script>
|
||||
<script src="js/help.js?v=20260630"></script>
|
||||
<script src="js/api.js?v=20260631"></script>
|
||||
<script src="js/common.js?v=20260631"></script>
|
||||
<script src="js/i18n.js?v=20260631"></script>
|
||||
<script src="js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
@@ -154,8 +154,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -165,9 +165,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
+5
-5
@@ -361,8 +361,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -372,9 +372,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Calendario scadenze - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260630">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260631">
|
||||
<style>
|
||||
.cal-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
|
||||
.cal-note { font-size:.82rem; color:#3730a3; background:#eef2ff; border:1px solid #c7d2fe; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||||
@@ -151,12 +151,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
@@ -186,7 +186,8 @@
|
||||
stakeholder_activity: { label: 'Attivita stakeholder', color: '#0d9488' },
|
||||
review_schedule: { label: 'Scadenziario revisioni', color: '#4f46e5' },
|
||||
internal_audit: { label: 'Audit interno', color: '#9333ea' },
|
||||
management_review_decision: { label: 'Decisione riesame', color: '#0369a1' }
|
||||
management_review_decision: { label: 'Decisione riesame', color: '#0369a1' },
|
||||
periodic_control: { label: 'Controllo periodico', color: '#65a30d' }
|
||||
};
|
||||
var STATUS_META = {
|
||||
overdue: { label: 'In ritardo', color: '#dc2626' },
|
||||
|
||||
@@ -349,14 +349,14 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260630"></script>
|
||||
<script src="js/common.js?v=20260630"></script>
|
||||
<script src="js/api.js?v=20260631"></script>
|
||||
<script src="js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle + override sidebar BI (common-bi.js dopo common.js: ridefinisce solo loadSidebar) -->
|
||||
<script src="vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('vendor/bootstrap-italia/dist/fonts'); }</script>
|
||||
<script src="js/common-bi.js?v=20260630"></script>
|
||||
<script src="js/i18n.js?v=20260630"></script>
|
||||
<script src="js/help.js?v=20260630"></script>
|
||||
<script src="js/common-bi.js?v=20260631"></script>
|
||||
<script src="js/i18n.js?v=20260631"></script>
|
||||
<script src="js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
|
||||
@@ -183,12 +183,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script src="/js/competenze.js?v=20260617"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Controlli periodici - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260631">
|
||||
<style>
|
||||
.pc-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
|
||||
.pc-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||||
.pc-toolbar { display:flex; justify-content:flex-end; margin-bottom:12px; }
|
||||
.pc-table { width:100%; border-collapse:collapse; font-size:.88rem; }
|
||||
.pc-table th, .pc-table td { text-align:left; padding:9px 12px; border-top:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
|
||||
.pc-table th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
|
||||
.pc-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; white-space:nowrap; }
|
||||
.pc-badge.code { background:#f1f5f9; color:#475569; font-family:monospace; }
|
||||
.d-overdue { background:#fee2e2; color:#991b1b; } .d-due_soon { background:#fef3c7; color:#92400e; }
|
||||
.d-upcoming { background:#dcfce7; color:#166534; } .d-suspended { background:#f3f4f6; color:#6b7280; } .d-none { background:#f3f4f6; color:#9ca3af; }
|
||||
.o-conforme { background:#dcfce7; color:#166534; } .o-non_conforme { background:#fee2e2; color:#991b1b; }
|
||||
.o-parziale { background:#fef3c7; color:#92400e; } .o-non_applicabile { background:#f3f4f6; color:#6b7280; }
|
||||
.pc-empty { text-align:center; padding:22px 16px; color:var(--gray-500,#6b7280); font-size:.86rem; }
|
||||
.sm-overlay { display:none; position:fixed; inset:0; background:rgba(15,23,42,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:30px 16px; }
|
||||
.sm-overlay.open { display:flex; }
|
||||
.sm-dialog { background:#fff; border-radius:12px; width:100%; max-width:600px; box-shadow:0 20px 50px rgba(0,0,0,.25); }
|
||||
.sm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||||
.sm-head h3 { margin:0; font-size:1.05rem; }
|
||||
.sm-body { padding:18px 20px; } .sm-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; }
|
||||
.sm-field { margin-bottom:14px; } .sm-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; }
|
||||
.sm-field .hint { font-weight:400; color:var(--gray-500,#6b7280); font-size:.78rem; }
|
||||
.sm-field input[type=text], .sm-field input[type=date], .sm-field input[type=number], .sm-field select, .sm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; background:#fff; }
|
||||
.sm-field textarea { min-height:60px; resize:vertical; }
|
||||
.sm-row { display:flex; gap:12px; flex-wrap:wrap; } .sm-row > .sm-field { flex:1 1 0; min-width:140px; }
|
||||
.sm-err { color:#b91c1c; font-size:.82rem; min-height:18px; }
|
||||
.sm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); }
|
||||
.hidden { display:none !important; }
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<meta name="theme-color" content="#0066CC">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="apple-mobile-web-app-title" content="NIS2 Agile">
|
||||
<meta name="application-name" content="NIS2 Agile">
|
||||
<script src="/js/pwa.js?v=20260614" defer></script>
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2 data-i18n="pctl.title">Controlli periodici</h2>
|
||||
<div class="content-header-actions">
|
||||
<button class="btn btn-primary btn-sm" onclick="pcOpen()">+ Nuovo controllo</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content-body">
|
||||
<div class="pc-intro">
|
||||
<strong>Controlli periodici (monitoraggio).</strong>
|
||||
Definisci i controlli ricorrenti (con responsabile, frequenza e metodo) e registra ogni esecuzione con esito ed evidenze:
|
||||
lo storico è la prova del monitoraggio nel tempo. Da un esito non conforme puoi generare una non conformità o un'azione correttiva.
|
||||
</div>
|
||||
<div class="pc-note">
|
||||
Ancoraggio: <strong>ISO/IEC 27001 §9.1</strong> (monitoraggio e misurazione) e <strong>A.8.16</strong> (attività di monitoraggio) — <strong>buona prassi</strong>, non un obbligo di legge. Per NIS2 supporta GV.PO-02/DE.CM. Strumento di supporto, non un parere legale.
|
||||
</div>
|
||||
<div id="pc-list" aria-live="polite"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Modale controllo (crea/modifica) -->
|
||||
<div class="sm-overlay" id="pc-modal" role="dialog" aria-modal="true" aria-labelledby="pc-modal-title">
|
||||
<div class="sm-dialog"><div class="sm-head"><h3 id="pc-modal-title">Nuovo controllo periodico</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="pcClose()">×</button></div>
|
||||
<div class="sm-body">
|
||||
<input type="hidden" id="pc-id">
|
||||
<div class="sm-field"><label for="pc-title">Titolo *</label><input type="text" id="pc-title" maxlength="255" placeholder="es. Verifica mensile dei backup"></div>
|
||||
<div class="sm-row">
|
||||
<div class="sm-field"><label for="pc-cat">Categoria</label><input type="text" id="pc-cat" maxlength="100" placeholder="es. Backup, Accessi, Rete"></div>
|
||||
<div class="sm-field"><label for="pc-ref">Controllo collegato <span class="hint">(SoA/NIS2)</span></label><input type="text" id="pc-ref" maxlength="32" placeholder="es. A.8.13"></div>
|
||||
</div>
|
||||
<div class="sm-row">
|
||||
<div class="sm-field"><label for="pc-owner">Responsabile</label><select id="pc-owner"><option value="">— Nessuno —</option></select></div>
|
||||
<div class="sm-field"><label for="pc-freq">Frequenza *</label>
|
||||
<select id="pc-freq" onchange="pcFreqChange()">
|
||||
<option value="giornaliero">Giornaliera</option><option value="settimanale">Settimanale</option>
|
||||
<option value="mensile" selected>Mensile</option><option value="trimestrale">Trimestrale</option>
|
||||
<option value="semestrale">Semestrale</option><option value="annuale">Annuale</option><option value="custom">Personalizzata (giorni)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sm-field hidden" id="pc-fdays-wrap"><label for="pc-fdays">Ogni N giorni</label><input type="number" id="pc-fdays" min="1" value="30"></div>
|
||||
</div>
|
||||
<div class="sm-row">
|
||||
<div class="sm-field"><label for="pc-next">Prossima scadenza <span class="hint">(vuoto = calcolata)</span></label><input type="date" id="pc-next"></div>
|
||||
<div class="sm-field"><label for="pc-status">Stato</label><select id="pc-status"><option value="active">Attivo</option><option value="suspended">Sospeso</option></select></div>
|
||||
</div>
|
||||
<div class="sm-field"><label for="pc-method">Metodo di verifica</label><textarea id="pc-method" maxlength="2000" placeholder="Come si esegue il controllo e quali evidenze si raccolgono"></textarea></div>
|
||||
<div class="sm-err" id="pc-err" role="alert"></div>
|
||||
</div>
|
||||
<div class="sm-foot"><button class="btn btn-outline" type="button" onclick="pcClose()">Annulla</button><button class="btn btn-primary" type="button" id="pc-save" onclick="pcSave()">Salva</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modale dettaglio + esecuzioni -->
|
||||
<div class="sm-overlay" id="det-modal" role="dialog" aria-modal="true" aria-labelledby="det-title">
|
||||
<div class="sm-dialog"><div class="sm-head"><h3 id="det-title">Dettaglio controllo</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="detClose()">×</button></div>
|
||||
<div class="sm-body" id="det-body"></div>
|
||||
<div class="sm-foot"><button class="btn btn-outline" type="button" onclick="detClose()">Chiudi</button><a class="btn btn-outline" id="det-report" href="#" target="_blank" rel="noopener">Report</a><button class="btn btn-primary" type="button" onclick="execOpen()">Registra esecuzione</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modale registra esecuzione -->
|
||||
<div class="sm-overlay" id="exec-modal" role="dialog" aria-modal="true" aria-labelledby="exec-title">
|
||||
<div class="sm-dialog" style="max-width:520px;"><div class="sm-head"><h3 id="exec-title">Registra esecuzione</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="execClose()">×</button></div>
|
||||
<div class="sm-body">
|
||||
<div class="sm-row">
|
||||
<div class="sm-field"><label for="ex-date">Data esecuzione</label><input type="date" id="ex-date"></div>
|
||||
<div class="sm-field"><label for="ex-outcome">Esito *</label>
|
||||
<select id="ex-outcome" onchange="execOutcomeChange()">
|
||||
<option value="conforme">Conforme</option><option value="parziale">Parziale</option>
|
||||
<option value="non_conforme">Non conforme</option><option value="non_applicabile">Non applicabile</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sm-field"><label for="ex-notes">Note / evidenza</label><textarea id="ex-notes" maxlength="2000"></textarea></div>
|
||||
<div class="sm-field hidden" id="ex-gen-wrap"><label for="ex-gen">Su esito negativo</label>
|
||||
<select id="ex-gen"><option value="none">Nessuna azione</option><option value="nc">Genera non conformità (NC)</option><option value="ac">Genera NC + azione correttiva (AC)</option></select>
|
||||
</div>
|
||||
<div class="sm-err" id="ex-err" role="alert"></div>
|
||||
</div>
|
||||
<div class="sm-foot"><button class="btn btn-outline" type="button" onclick="execClose()">Annulla</button><button class="btn btn-primary" type="button" id="ex-save" onclick="execSave()">Registra</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/* Controlli periodici (control monitoring §9.1/A.8.16): controlli ricorrenti + esecuzioni (evidenza) + NC/AC. */
|
||||
let PC = { list: [], roles: [], detail: null };
|
||||
const FREQ_LABEL = { giornaliero:'Giornaliera', settimanale:'Settimanale', mensile:'Mensile', trimestrale:'Trimestrale', semestrale:'Semestrale', annuale:'Annuale', custom:'Personalizzata' };
|
||||
const DUE_LABEL = { overdue:'In ritardo', due_soon:'In scadenza', upcoming:'Pianificato', suspended:'Sospeso', none:'—' };
|
||||
const OUT_LABEL = { conforme:'Conforme', non_conforme:'Non conforme', parziale:'Parziale', non_applicabile:'N.A.' };
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
||||
if (window.I18n && I18n.init) I18n.init('it');
|
||||
if (typeof loadSidebar === 'function') loadSidebar();
|
||||
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') { pcClose(); detClose(); execClose(); } });
|
||||
['pc-modal','det-modal','exec-modal'].forEach(m => { const o = el(m); if (o) o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); }); });
|
||||
try { const r = await api.orgRolesList(); PC.roles = (r && (r.roles || r)) || []; } catch (e) { PC.roles = []; }
|
||||
el('pc-owner').innerHTML = '<option value="">— Nessuno —</option>' + (Array.isArray(PC.roles) ? PC.roles : []).map(x => '<option value="' + x.id + '">' + esc(x.role_name || x.name) + '</option>').join('');
|
||||
await pcLoad();
|
||||
});
|
||||
|
||||
async function pcLoad() {
|
||||
el('pc-list').innerHTML = '<div class="pc-empty">Caricamento…</div>';
|
||||
try {
|
||||
const d = await api.pctlList(); PC.list = (d && d.controls) || [];
|
||||
pcRender();
|
||||
} catch (e) { el('pc-list').innerHTML = '<div class="pc-empty">Errore: ' + esc(e.message || e) + '</div>'; }
|
||||
}
|
||||
|
||||
function pcRender() {
|
||||
if (!PC.list.length) { el('pc-list').innerHTML = '<div class="pc-empty">Nessun controllo periodico. Creane uno con "+ Nuovo controllo".</div>'; return; }
|
||||
const rows = PC.list.map(c => {
|
||||
const due = c.due_status || 'none';
|
||||
const lo = c.last_outcome ? '<span class="pc-badge o-' + c.last_outcome + '">' + esc(OUT_LABEL[c.last_outcome] || c.last_outcome) + '</span>' : '<span style="color:#9ca3af">—</span>';
|
||||
return '<tr>'
|
||||
+ '<td><span class="pc-badge code">' + esc(c.code || '—') + '</span></td>'
|
||||
+ '<td><strong>' + esc(c.title) + '</strong>' + (c.control_ref ? ' <span class="pc-badge code">' + esc(c.control_ref) + '</span>' : '') + (c.category ? '<div style="font-size:.78rem;color:#6b7280;">' + esc(c.category) + '</div>' : '') + '</td>'
|
||||
+ '<td style="font-size:.82rem;">' + esc(c.owner_role || '—') + '</td>'
|
||||
+ '<td style="font-size:.82rem;">' + esc(FREQ_LABEL[c.frequency] || c.frequency) + (c.frequency === 'custom' && c.frequency_days ? ' (' + c.frequency_days + 'gg)' : '') + '</td>'
|
||||
+ '<td><span class="pc-badge d-' + due + '">' + esc(c.next_due_date || '—') + '</span><div style="font-size:.72rem;color:#6b7280;">' + esc(DUE_LABEL[due] || '') + '</div></td>'
|
||||
+ '<td>' + lo + '</td>'
|
||||
+ '<td style="white-space:nowrap;">'
|
||||
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="detOpen(' + c.id + ')">Dettaglio</button> '
|
||||
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="pcOpen(' + c.id + ')">Modifica</button> '
|
||||
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;color:#b91c1c;border-color:#fecaca;" onclick="pcDelete(' + c.id + ')">Elimina</button>'
|
||||
+ '</td></tr>';
|
||||
}).join('');
|
||||
el('pc-list').innerHTML = '<table class="pc-table"><thead><tr><th>Codice</th><th>Controllo</th><th>Responsabile</th><th>Frequenza</th><th>Prossima scadenza</th><th>Ultimo esito</th><th>Azioni</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
||||
}
|
||||
|
||||
function pcFreqChange() { el('pc-fdays-wrap').classList.toggle('hidden', el('pc-freq').value !== 'custom'); }
|
||||
|
||||
function pcOpen(id) {
|
||||
const c = id ? PC.list.find(x => x.id === id) : null;
|
||||
el('pc-modal-title').textContent = c ? 'Modifica controllo periodico' : 'Nuovo controllo periodico';
|
||||
el('pc-id').value = c ? c.id : '';
|
||||
el('pc-title').value = c ? (c.title || '') : '';
|
||||
el('pc-cat').value = c ? (c.category || '') : '';
|
||||
el('pc-ref').value = c ? (c.control_ref || '') : '';
|
||||
el('pc-owner').value = c && c.owner_role_id ? String(c.owner_role_id) : '';
|
||||
el('pc-freq').value = c ? (c.frequency || 'mensile') : 'mensile';
|
||||
el('pc-fdays').value = c && c.frequency_days ? c.frequency_days : 30;
|
||||
el('pc-next').value = c ? (c.next_due_date || '') : '';
|
||||
el('pc-status').value = c ? (c.status || 'active') : 'active';
|
||||
el('pc-method').value = c ? (c.method || '') : '';
|
||||
el('pc-err').textContent = ''; pcFreqChange();
|
||||
el('pc-modal').classList.add('open');
|
||||
setTimeout(() => el('pc-title').focus(), 50);
|
||||
}
|
||||
function pcClose() { el('pc-modal').classList.remove('open'); }
|
||||
|
||||
async function pcSave() {
|
||||
const id = el('pc-id').value;
|
||||
const title = el('pc-title').value.trim();
|
||||
if (!title) { el('pc-err').textContent = 'Il titolo è obbligatorio.'; return; }
|
||||
const freq = el('pc-freq').value;
|
||||
const payload = {
|
||||
title: title, category: el('pc-cat').value.trim(), control_ref: el('pc-ref').value.trim(),
|
||||
owner_role_id: el('pc-owner').value ? parseInt(el('pc-owner').value, 10) : null,
|
||||
frequency: freq, frequency_days: freq === 'custom' ? parseInt(el('pc-fdays').value, 10) || 30 : null,
|
||||
next_due_date: el('pc-next').value || null, status: el('pc-status').value, method: el('pc-method').value.trim(),
|
||||
};
|
||||
const btn = el('pc-save'); btn.disabled = true;
|
||||
try {
|
||||
if (id) await api.pctlUpdate(parseInt(id, 10), payload); else await api.pctlCreate(payload);
|
||||
showNotification(id ? 'Controllo aggiornato.' : 'Controllo creato.', 'success'); pcClose(); await pcLoad();
|
||||
} catch (e) { el('pc-err').textContent = e.message || 'Errore nel salvataggio.'; } finally { btn.disabled = false; }
|
||||
}
|
||||
|
||||
async function pcDelete(id) {
|
||||
const c = PC.list.find(x => x.id === id);
|
||||
if (!confirm('Eliminare il controllo "' + (c ? c.title : '') + '" e il suo storico?')) return;
|
||||
try { await api.pctlDelete(id); showNotification('Eliminato.', 'success'); await pcLoad(); }
|
||||
catch (e) { showNotification(e.message || 'Errore', 'error'); }
|
||||
}
|
||||
|
||||
async function detOpen(id) {
|
||||
el('det-body').innerHTML = '<div class="pc-empty">Caricamento…</div>';
|
||||
el('det-modal').classList.add('open');
|
||||
try { const c = await api.pctlGet(id); PC.detail = c; detRender(c); }
|
||||
catch (e) { el('det-body').innerHTML = '<div class="pc-empty">Errore: ' + esc(e.message) + '</div>'; }
|
||||
}
|
||||
function detClose() { el('det-modal').classList.remove('open'); PC.detail = null; }
|
||||
|
||||
function detRender(c) {
|
||||
el('det-title').textContent = 'Controllo ' + (c.code || '');
|
||||
const tok = (window.localStorage ? localStorage.getItem('nis2_access_token') : '') || '';
|
||||
const org = (window.localStorage ? localStorage.getItem('nis2_org_id') : '') || '';
|
||||
el('det-report').href = (api.baseUrl || '/api') + '/periodic-controls/' + c.id + '/report?token=' + encodeURIComponent(tok) + (org ? '&org_id=' + encodeURIComponent(org) : '');
|
||||
const ex = c.executions || [];
|
||||
let html = '<div style="font-size:.9rem;margin-bottom:8px;"><strong>' + esc(c.title) + '</strong>' + (c.control_ref ? ' <span class="pc-badge code">' + esc(c.control_ref) + '</span>' : '') + '</div>'
|
||||
+ '<div style="font-size:.82rem;color:#374151;margin-bottom:12px;">Frequenza: ' + esc(FREQ_LABEL[c.frequency] || c.frequency) + ' · Responsabile: ' + esc(c.owner_role || '—') + ' · Prossima scadenza: ' + esc(c.next_due_date || '—') + '</div>';
|
||||
if (c.method) html += '<div style="font-size:.82rem;color:#475569;margin-bottom:12px;"><em>Metodo:</em> ' + esc(c.method) + '</div>';
|
||||
html += '<h4 style="margin:8px 0 6px;font-size:.82rem;text-transform:uppercase;letter-spacing:.04em;color:#6b7280;">Storico esecuzioni (evidenza)</h4>';
|
||||
if (!ex.length) { html += '<div class="pc-empty">Nessuna esecuzione registrata.</div>'; }
|
||||
else {
|
||||
html += '<table class="pc-table"><thead><tr><th>Data</th><th>Esito</th><th>Eseguito da</th><th>NC</th><th>Note</th></tr></thead><tbody>'
|
||||
+ ex.map(e => '<tr><td>' + esc(e.executed_at) + '</td><td><span class="pc-badge o-' + e.outcome + '">' + esc(OUT_LABEL[e.outcome] || e.outcome) + '</span></td>'
|
||||
+ '<td style="font-size:.8rem;">' + esc(e.executed_by_name || '') + '</td><td style="font-size:.8rem;">' + esc(e.ncr_code || '') + '</td><td style="font-size:.82rem;">' + esc(e.notes || '') + '</td></tr>').join('')
|
||||
+ '</tbody></table>';
|
||||
}
|
||||
el('det-body').innerHTML = html;
|
||||
}
|
||||
|
||||
function execOpen() { if (!PC.detail) return; el('ex-date').value = new Date().toISOString().slice(0, 10); el('ex-outcome').value = 'conforme'; el('ex-notes').value = ''; el('ex-gen').value = 'none'; el('ex-err').textContent = ''; execOutcomeChange(); el('exec-modal').classList.add('open'); setTimeout(() => el('ex-outcome').focus(), 50); }
|
||||
function execClose() { el('exec-modal').classList.remove('open'); }
|
||||
function execOutcomeChange() { const neg = ['non_conforme', 'parziale'].indexOf(el('ex-outcome').value) !== -1; el('ex-gen-wrap').classList.toggle('hidden', !neg); }
|
||||
|
||||
async function execSave() {
|
||||
if (!PC.detail) return;
|
||||
const payload = { executed_at: el('ex-date').value || null, outcome: el('ex-outcome').value, notes: el('ex-notes').value.trim(),
|
||||
generate: (['non_conforme', 'parziale'].indexOf(el('ex-outcome').value) !== -1) ? el('ex-gen').value : 'none' };
|
||||
const btn = el('ex-save'); btn.disabled = true;
|
||||
try {
|
||||
const r = await api.pctlAddExecution(PC.detail.id, payload);
|
||||
showNotification('Esecuzione registrata' + (r && r.ncr ? ' · ' + (r.ncr.ncr_code || 'NC creata') : '') + '.', 'success');
|
||||
execClose(); const c = await api.pctlGet(PC.detail.id); PC.detail = c; detRender(c); await pcLoad();
|
||||
} catch (e) { el('ex-err').textContent = e.message || 'Errore.'; } finally { btn.disabled = false; }
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -382,8 +382,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -393,8 +393,8 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script>
|
||||
const SPRITE = '/vendor/bootstrap-italia/dist/svg/sprites.svg';
|
||||
function ico(name, cls) { return `<svg class="ico ${cls||''}" aria-hidden="true"><use href="${SPRITE}#${name}"></use></svg>`; }
|
||||
|
||||
@@ -143,8 +143,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -154,9 +154,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della forgot-password.html: ZERO modifiche backend -->
|
||||
<script src="js/api.js?v=20260630"></script>
|
||||
<script src="js/api.js?v=20260631"></script>
|
||||
<script>
|
||||
const form = document.getElementById('forgot-form');
|
||||
const err = document.getElementById('err');
|
||||
|
||||
+5
-5
@@ -1142,8 +1142,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (accordion) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
@@ -1152,9 +1152,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
// La guida è accessibile anche da non autenticati (utile per onboarding)
|
||||
// ma se sei loggato carichi sidebar + i18n normalmente. Stessa logica
|
||||
|
||||
@@ -351,8 +351,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -362,9 +362,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
// ── Auth & Init ──────────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -97,6 +97,7 @@ $controllerMap = [
|
||||
'internal-audits' => 'InternalAuditController', // Modulo A — Audit interni ISO 27001 §9.2
|
||||
'management-reviews' => 'ManagementReviewController', // Modulo B — Riesame di Direzione ISO 27001 §9.3
|
||||
'calendar' => 'CalendarController', // Modulo C — Calendario unico scadenze (aggregatore sola lettura)
|
||||
'periodic-controls' => 'PeriodicControlController', // Controlli periodici (control monitoring §9.1/A.8.16: frequenza + esecuzioni/evidenza + NC/AC)
|
||||
'assessments' => 'AssessmentController',
|
||||
'isms' => 'IsmsModelController', // Modello Organizzativo SGSI (ISO 27001/27017/27018 + SoA)
|
||||
'dashboard' => 'DashboardController',
|
||||
@@ -510,6 +511,17 @@ $actionMap = [
|
||||
'GET:summary' => 'summary',
|
||||
],
|
||||
|
||||
// ── PeriodicControlController (Controlli periodici — control monitoring §9.1/A.8.16) ──
|
||||
'periodic-controls' => [
|
||||
'GET:list' => 'list',
|
||||
'POST:create' => 'create',
|
||||
'GET:{id}' => 'get',
|
||||
'PUT:{id}' => 'update',
|
||||
'DELETE:{id}' => 'delete',
|
||||
'POST:{id}/executions'=> 'addExecution',
|
||||
'GET:{id}/report' => 'report',
|
||||
],
|
||||
|
||||
// ── AuditController ─────────────────────────────
|
||||
'audit' => [
|
||||
'GET:controls' => 'listControls',
|
||||
|
||||
@@ -910,8 +910,8 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
|
||||
|
||||
</main>
|
||||
|
||||
<script src="js/common.js?v=20260630"></script>
|
||||
<script src="js/i18n.js?v=20260630"></script>
|
||||
<script src="js/common.js?v=20260631"></script>
|
||||
<script src="js/i18n.js?v=20260631"></script>
|
||||
<script>
|
||||
function showTab(id) {
|
||||
document.querySelectorAll('.tab-btn').forEach((b, i) => {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Audit interni - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260630">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260631">
|
||||
<style>
|
||||
.ia-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
|
||||
.ia-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||||
@@ -139,12 +139,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
|
||||
+5
-5
@@ -184,8 +184,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -195,9 +195,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script src="/js/isms.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -371,6 +371,14 @@ class NIS2API {
|
||||
return this._acn(this.get('/calendar/summary' + (q ? '?' + q : '')));
|
||||
}
|
||||
|
||||
// ── Controlli periodici (control monitoring §9.1/A.8.16) ──
|
||||
pctlList() { return this._acn(this.get('/periodic-controls/list')); }
|
||||
pctlGet(id) { return this._acn(this.get(`/periodic-controls/${id}`)); }
|
||||
pctlCreate(d) { return this._acn(this.post('/periodic-controls/create', d || {})); }
|
||||
pctlUpdate(id, d) { return this._acn(this.put(`/periodic-controls/${id}`, d || {})); }
|
||||
pctlDelete(id) { return this._acn(this.del(`/periodic-controls/${id}`)); }
|
||||
pctlAddExecution(id, d) { return this._acn(this.post(`/periodic-controls/${id}/executions`, d || {})); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Dashboard
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
{ name: 'Formazione', href: 'training.html', icon: iconAcademicCap(), i18nKey: 'nav.training' },
|
||||
{ name: 'Inventario', href: 'assets.html', icon: iconServer(), i18nKey: 'nav.assets' },
|
||||
{ name: 'Audit interni', href: 'internal-audits.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm5.707 6.707a1 1 0 00-1.414-1.414L7 10.586l-.293-.293a1 1 0 00-1.414 1.414l1 1a1 1 0 001.414 0l2-2zM10 13a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.internal_audits' },
|
||||
{ name: 'Controlli periodici', href: 'controlli-periodici.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.5 2.5a1 1 0 001.414-1.414L11 9.586V6z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.periodic_controls' },
|
||||
{ name: 'Riesame di Direzione', href: 'management-review.html', icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>`, i18nKey: 'nav.management_review' },
|
||||
{ name: 'Audit & Report', href: 'reports.html', icon: iconChartBar(), i18nKey: 'nav.audit' }
|
||||
]
|
||||
|
||||
@@ -218,6 +218,7 @@ function loadSidebar() {
|
||||
{ name: 'Formazione', href: 'training.html', icon: iconAcademicCap(), i18nKey: 'nav.training' },
|
||||
{ name: 'Inventario', href: 'assets.html', icon: iconServer(), i18nKey: 'nav.assets' },
|
||||
{ name: 'Audit interni', href: 'internal-audits.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm5.707 6.707a1 1 0 00-1.414-1.414L7 10.586l-.293-.293a1 1 0 00-1.414 1.414l1 1a1 1 0 001.414 0l2-2zM10 13a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.internal_audits' },
|
||||
{ name: 'Controlli periodici', href: 'controlli-periodici.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.5 2.5a1 1 0 001.414-1.414L11 9.586V6z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.periodic_controls' },
|
||||
{ name: 'Riesame di Direzione', href: 'management-review.html', icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>`, i18nKey: 'nav.management_review' },
|
||||
{ name: 'Audit & Report',href: 'reports.html', icon: iconChartBar(), i18nKey: 'nav.audit' },
|
||||
]
|
||||
|
||||
@@ -400,6 +400,42 @@ const HelpSystem = (function () {
|
||||
'NOTA: gli obblighi normativi in Italia derivano dal D.Lgs. 138/2024; la matrice e i framework NIST CSF/ACN sono strumenti di supporto, non un parere legale.'
|
||||
]
|
||||
},
|
||||
'controlli-periodici': {
|
||||
title: 'Guida - Controlli periodici',
|
||||
intro: 'Il modulo Controlli periodici gestisce i controlli RICORRENTI di monitoraggio (ISO/IEC 27001 §9.1 e A.8.16): definisci ogni controllo con responsabile, frequenza e metodo, e registri ogni esecuzione con esito ed evidenze. Lo storico delle esecuzioni è la prova del monitoraggio nel tempo. È uno strumento di supporto organizzativo, non un parere legale.',
|
||||
sections: [
|
||||
{
|
||||
heading: 'Definire un controllo',
|
||||
items: [
|
||||
'Crea un <strong>controllo</strong> con titolo, categoria, <strong>responsabile</strong> (ruolo dell\'organigramma), <strong>frequenza</strong> (giornaliera, settimanale, mensile, trimestrale, semestrale, annuale o personalizzata) e metodo di verifica. Riceve un codice <strong>CTL-NNN</strong>.',
|
||||
'Puoi collegarlo a un <strong>controllo del SoA/NIS2</strong> (campo "Controllo collegato", es. A.8.13): così il controllo periodico diventa l\'<strong>evidenza operativa</strong> che quel controllo tecnico funziona.',
|
||||
'La <strong>prossima scadenza</strong> compare nel <strong>Calendario</strong> con stato in ritardo / in scadenza / pianificato.'
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: 'Registrare le esecuzioni (evidenza)',
|
||||
items: [
|
||||
'Dal dettaglio, <strong>Registra esecuzione</strong>: indichi data, <strong>esito</strong> (conforme, parziale, non conforme, non applicabile) e note/evidenze. La scadenza <strong>avanza automaticamente</strong> secondo la frequenza.',
|
||||
'Lo <strong>storico</strong> delle esecuzioni è la prova, all\'auditor, che il controllo è eseguito periodicamente.',
|
||||
'Il pulsante <strong>Report</strong> genera la scheda del controllo con lo storico, stampabile.'
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: 'Generazione NC / azioni correttive',
|
||||
items: [
|
||||
'Su esito <strong>non conforme o parziale</strong> puoi <strong>generare una non conformità (NC)</strong> oppure <strong>NC + azione correttiva (AC)</strong>: confluiscono nel modulo <strong>NCR/CAPA</strong>, dove gestisci root cause e azioni.',
|
||||
'Riferimenti: <strong>ISO/IEC 27001 §10.2</strong> (non conformità e azioni correttive). Le azioni correttive sono collegate alla NC di origine.'
|
||||
]
|
||||
}
|
||||
],
|
||||
references: [
|
||||
'ISO/IEC 27001:2022 §9.1 - Monitoraggio, misurazione, analisi e valutazione',
|
||||
'ISO/IEC 27001:2022 Annex A.8.16 - Attività di monitoraggio',
|
||||
'ISO/IEC 27001:2022 §10.2 - Non conformità e azioni correttive',
|
||||
'Il monitoraggio periodico dei controlli è buona prassi (continuous control monitoring); gli obblighi normativi in Italia derivano dal D.Lgs. 138/2024.',
|
||||
'NOTA: strumento di supporto organizzativo, non un parere legale.'
|
||||
]
|
||||
},
|
||||
'internal-audits': {
|
||||
title: 'Guida - Audit interni',
|
||||
intro: 'Il modulo Audit interni gestisce il ciclo degli audit del SGSI previsto dalla clausola 9.2 della ISO/IEC 27001: programmi gli audit, conduci la checklist (clausole 4-10 ISO 27001 + controlli Annex A applicabili dal tuo SoA), registri gli esiti riga per riga, apri una non conformità sui punti non conformi e generi il report stampabile. È uno strumento di supporto organizzativo, non un parere legale.',
|
||||
@@ -1403,6 +1439,8 @@ const HelpSystem = (function () {
|
||||
'management-review': 'management-review',
|
||||
'calendario.html': 'calendario',
|
||||
'calendario': 'calendario',
|
||||
'controlli-periodici.html': 'controlli-periodici',
|
||||
'controlli-periodici': 'controlli-periodici',
|
||||
'stakeholder-activities': 'stakeholder-activities',
|
||||
'risks.html': 'risks',
|
||||
'risks': 'risks',
|
||||
|
||||
@@ -82,6 +82,8 @@ const I18n = (function () {
|
||||
'iaud.title': { it: 'Audit interni', en: 'Internal audits' },
|
||||
'nav.management_review': { it: 'Riesame di Direzione', en: 'Management Review' },
|
||||
'mreview.title': { it: 'Riesame di Direzione', en: 'Management Review' },
|
||||
'nav.periodic_controls': { it: 'Controlli periodici', en: 'Periodic controls' },
|
||||
'pctl.title': { it: 'Controlli periodici', en: 'Periodic controls' },
|
||||
'nav.risks': { it: 'Rischi', en: 'Risks' },
|
||||
'nav.incidents': { it: 'Incidenti', en: 'Incidents' },
|
||||
'nav.policies': { it: 'Policy', en: 'Policies' },
|
||||
|
||||
+5
-5
@@ -151,8 +151,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). -->
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
@@ -161,9 +161,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script src="/js/kb.js"></script>
|
||||
<script>
|
||||
// Gate auth + chrome come le altre pagine -bi.
|
||||
|
||||
@@ -515,7 +515,7 @@ body { background: var(--bg-main); }
|
||||
|
||||
<div id="toast"></div>
|
||||
|
||||
<script src="js/api.js?v=20260630"></script>
|
||||
<script src="js/api.js?v=20260631"></script>
|
||||
<script>
|
||||
// ══════════════════════════════════════════════════════
|
||||
// CONFIG
|
||||
|
||||
+2
-2
@@ -94,8 +94,8 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della login attuale: ZERO modifiche backend -->
|
||||
<script src="js/api.js?v=20260630"></script>
|
||||
<script src="js/common.js?v=20260630"></script>
|
||||
<script src="js/api.js?v=20260631"></script>
|
||||
<script src="js/common.js?v=20260631"></script>
|
||||
<script>
|
||||
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Riesame di Direzione - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260630">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260631">
|
||||
<style>
|
||||
.mr-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
|
||||
.mr-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||||
@@ -126,12 +126,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>Misure e Requisiti - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260630">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260631">
|
||||
<style>
|
||||
.mr-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:14px; font-size:.92rem; line-height:1.6; }
|
||||
.mr-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||||
@@ -162,12 +162,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
|
||||
@@ -424,6 +424,6 @@ curl https://nis2.agile.software/api/services/status</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="js/common.js?v=20260630"></script>
|
||||
<script src="js/common.js?v=20260631"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -112,8 +112,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -123,9 +123,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
@@ -494,8 +494,8 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della onboarding.html: ZERO modifiche backend -->
|
||||
<script src="js/api.js?v=20260630"></script>
|
||||
<script src="js/common.js?v=20260630"></script>
|
||||
<script src="js/api.js?v=20260631"></script>
|
||||
<script src="js/common.js?v=20260631"></script>
|
||||
<script>
|
||||
// ── Auth check ──────────────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -138,12 +138,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script src="/js/organigramma.js?v=20260617"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -333,8 +333,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -344,9 +344,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
+5
-5
@@ -131,12 +131,12 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script src="/js/raci.js?v=20260619"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -268,8 +268,8 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della register.html: ZERO modifiche backend -->
|
||||
<script src="js/api.js?v=20260630"></script>
|
||||
<script src="js/common.js?v=20260630"></script>
|
||||
<script src="js/api.js?v=20260631"></script>
|
||||
<script src="js/common.js?v=20260631"></script>
|
||||
<script>
|
||||
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
||||
|
||||
|
||||
+5
-5
@@ -443,8 +443,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -454,9 +454,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -141,12 +141,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
|
||||
+5
-5
@@ -494,8 +494,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -505,9 +505,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
// ── Auth & Init ──────────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -672,8 +672,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -683,9 +683,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -165,8 +165,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260630"></script>
|
||||
<script src="js/common.js?v=20260630"></script>
|
||||
<script src="js/api.js?v=20260631"></script>
|
||||
<script src="js/common.js?v=20260631"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Attività stakeholder - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260630">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260631">
|
||||
<style>
|
||||
.sa-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
|
||||
.sa-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||||
@@ -194,12 +194,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>Stakeholder - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260630">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260631">
|
||||
<style>
|
||||
.stk-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
|
||||
.stk-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||||
@@ -191,12 +191,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
|
||||
@@ -477,8 +477,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -488,9 +488,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
* Il nome cache include la release: va BUMPATO ad ogni rilascio (vedi version.json),
|
||||
* cosi' activate elimina automaticamente la shell vecchia.
|
||||
*/
|
||||
const CACHE = 'nis2-shell-v1.22.0';
|
||||
const CACHE = 'nis2-shell-v1.23.0';
|
||||
|
||||
const PRECACHE = [
|
||||
'/offline.html',
|
||||
|
||||
@@ -292,8 +292,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -303,9 +303,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version": "1.22.0", "build": "2026-06-17-v1.22.0", "date": "2026-06-17", "changelog": "Rilascio ISO-readiness (3 moduli): Audit interni (§9.2, mig.055: internal_audits + items, checklist auto-popolata clausole 4-10 + Annex A dal SoA, esiti per riga, apertura NC->NCR/CAPA, report stampabile); Riesame di Direzione (§9.3, mig.056: management_reviews + decisions, INPUT aggregati automaticamente dai moduli (gather), verbale stampabile, approvazione con snapshot congelato); Calendario unico scadenze (aggregatore sola lettura di tutte le scadenze: incidenti/policy/rischi/NC-CAPA/formazione/stakeholder/audit/riesame/review_schedule, griglia mensile+lista+filtri). review_schedule ENUM esteso (internal_audit). Additivo."}
|
||||
{"version": "1.23.0", "build": "2026-06-17-v1.23.0", "date": "2026-06-17", "changelog": "Modulo Controlli periodici (control monitoring ISO 27001 §9.1/A.8.16): registro controlli ricorrenti (owner, frequenza, metodo, controllo SoA/NIS2 collegato) + registro esecuzioni come EVIDENZA (data/esito/note) con avanzamento automatico della scadenza; da esito non conforme generazione NC o NC+azione correttiva (riuso NCR/CAPA, nuova source 'monitoring'); report scheda+storico stampabile; scadenze nel Calendario unico. mig.057: periodic_controls + periodic_control_executions. PeriodicControlController + controlli-periodici.html. Additivo."}
|
||||
|
||||
@@ -207,8 +207,8 @@
|
||||
|
||||
<!-- Report Detail Modal handled by common.js showModal -->
|
||||
|
||||
<script src="/js/api.js?v=20260630"></script>
|
||||
<script src="/js/common.js?v=20260630"></script>
|
||||
<script src="/js/api.js?v=20260631"></script>
|
||||
<script src="/js/common.js?v=20260631"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -218,9 +218,9 @@
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260630"></script>
|
||||
<script src="/js/i18n.js?v=20260630"></script>
|
||||
<script src="/js/help.js?v=20260630"></script>
|
||||
<script src="/js/common-bi.js?v=20260631"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260631"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
@@ -319,9 +319,9 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260630"></script>
|
||||
<script src="js/common.js?v=20260630"></script>
|
||||
<script src="js/i18n.js?v=20260630"></script>
|
||||
<script src="js/api.js?v=20260631"></script>
|
||||
<script src="js/common.js?v=20260631"></script>
|
||||
<script src="js/i18n.js?v=20260631"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
Reference in New Issue
Block a user