[FEAT] ISO-readiness: Audit interni (§9.2) + Riesame Direzione (§9.3) + Calendario scadenze (mig.055-056, v1.22.0)

Rilascio unico (3 moduli) costruito in flotta parallela + verifica avversariale.

MODULO A — Audit interni (ISO 27001 §9.2, mig.055): internal_audits + internal_audit_items;
codice AUD-NNN; checklist pre-popolata (clausole 4-10 + Annex A applicabili dal SoA);
esiti per riga; apertura non conformità collegata a NCR/CAPA (source polimorfico);
report HTML stampabile. InternalAuditController + internal-audits.html.

MODULO B — Riesame di Direzione (ISO 27001 §9.3, mig.056): management_reviews +
management_review_decisions; codice RD-AAAA-NN; INPUT aggregati automaticamente dai
moduli (gather: NC/CAPA, audit interni, rischi+trattamenti, KPI/score, obiettivi, formazione,
stakeholder, normative, scadenze), congelati nello snapshot all'approvazione; decisioni;
verbale stampabile. ManagementReviewController + management-review.html.

MODULO C — Calendario unico scadenze: aggregatore SOLA LETTURA (nessuna migrazione) di tutte
le scadenze (incidenti/policy/rischi/NC-CAPA/formazione/stakeholder/audit/riesame/review_schedule),
griglia mensile + lista + filtri + deep-link. CalendarController + calendario.html.

Integrazione: router (3 controller+actionMap), api.js, sidebar V2+legacy, help.js (3 sezioni),
i18n (IT+EN), review_schedule ENUM += internal_audit. v1.22.0 + sw cache + cache-buster 20260630.

Verifica flotta (28 agenti, 4 dim + avversariale): 9 finding confermati, TUTTI corretti —
MAJOR gatherRisks (risk_treatments.organization_id inesistente → JOIN risks); nextCode numerico
(no dup >99/anno); footer report audit con etichetta "ISO buona prassi, non obbligo"; help 24→25
clausole; ARIA tab/calendario; focus modali; tasti su celle calendario; rimossi helper api morti.

Smoke prod OK (calendario 18 eventi, audit AUD-001 25 item + report + audit→NCR + audit→calendario,
riesame gather/decisione/approve/report, gatherRisks ora available); org 151 ripulita. Additivo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-17 07:48:49 +02:00
co-authored by Claude Opus 4.8
parent 18a4a0b509
commit 79ca72fba9
54 changed files with 3774 additions and 148 deletions
+3 -3
View File
@@ -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=20260629"></script>
<script src="js/common.js?v=20260629"></script>
<script src="js/common-bi.js?v=20260629"></script>
<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>
// Renderizza la sidebar BI (loadSidebar è stato ridefinito da common-bi.js)
document.addEventListener('DOMContentLoaded', function () {
+4 -4
View File
@@ -1088,10 +1088,10 @@
</main>
</div>
<script src="js/api.js?v=20260629"></script>
<script src="js/common.js?v=20260629"></script>
<script src="js/i18n.js?v=20260629"></script>
<script src="js/help.js?v=20260629"></script>
<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>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
+5 -5
View File
@@ -154,8 +154,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+5 -5
View File
@@ -361,8 +361,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+403
View File
@@ -0,0 +1,403 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<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">
<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; }
.cal-bar { display:flex; flex-wrap:wrap; gap:12px; align-items:center; justify-content:space-between; margin-bottom:14px; }
.cal-nav { display:flex; align-items:center; gap:8px; }
.cal-nav h3 { margin:0; font-size:1.05rem; min-width:170px; text-align:center; text-transform:capitalize; }
.cal-views { display:flex; gap:6px; }
.cal-view-btn { padding:7px 14px; font-weight:600; font-size:.85rem; cursor:pointer; border:1px solid var(--gray-200,#e5e7eb); background:#fff; color:var(--gray-600,#4b5563); border-radius:8px; }
.cal-view-btn.active { background:var(--primary,#2563eb); color:#fff; border-color:var(--primary,#2563eb); }
.cal-summary { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:14px; }
.cal-kpi { flex:1 1 0; min-width:120px; background:#fff; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:10px 14px; }
.cal-kpi .n { font-size:1.5rem; font-weight:800; line-height:1; }
.cal-kpi .l { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); margin-top:4px; }
.cal-kpi.k-overdue .n { color:#b91c1c; } .cal-kpi.k-due_soon .n { color:#b45309; }
.cal-kpi.k-upcoming .n { color:#1d4ed8; } .cal-kpi.k-done .n { color:#15803d; }
.cal-filters { background:#f8fafc; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:12px 14px; margin-bottom:14px; }
.cal-filters h4 { margin:0 0 8px; font-size:.78rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
.cal-chips { display:flex; flex-wrap:wrap; gap:8px; }
.cal-chip { display:inline-flex; align-items:center; gap:6px; font-size:.82rem; padding:5px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:20px; background:#fff; cursor:pointer; user-select:none; }
.cal-chip input { margin:0; cursor:pointer; }
.cal-chip .dot { width:10px; height:10px; border-radius:50%; display:inline-block; }
.cal-filter-row { margin-top:10px; }
table.cal-grid { width:100%; border-collapse:collapse; table-layout:fixed; }
table.cal-grid th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); padding:6px 4px; text-align:center; }
table.cal-grid td { border:1px solid var(--gray-100,#f3f4f6); vertical-align:top; height:96px; padding:4px; background:#fff; }
table.cal-grid td.cal-out { background:#fafafa; }
table.cal-grid td.cal-today { background:#fffbeb; outline:2px solid #fbbf24; outline-offset:-2px; }
.cal-daynum { font-size:.78rem; font-weight:600; color:var(--gray-600,#4b5563); }
.cal-day-dots { display:flex; flex-wrap:wrap; gap:3px; margin-top:4px; }
.cal-dot { width:9px; height:9px; border-radius:50%; cursor:pointer; }
.cal-more { font-size:.7rem; color:var(--gray-500,#6b7280); cursor:pointer; margin-top:3px; }
.st-overdue { background:#dc2626; } .st-due_soon { background:#d97706; }
.st-upcoming { background:#2563eb; } .st-done { background:#16a34a; }
.cal-list { width:100%; border-collapse:collapse; font-size:.88rem; }
.cal-list th, .cal-list td { text-align:left; padding:9px 12px; border-top:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
.cal-list th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
.cal-list tr.row-overdue td { background:#fef2f2; }
.cal-list a.cal-link { color:var(--primary,#2563eb); text-decoration:none; font-weight:600; }
.cal-list a.cal-link:hover { text-decoration:underline; }
.st-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; white-space:nowrap; color:#fff; }
.st-badge.st-overdue { background:#dc2626; } .st-badge.st-due_soon { background:#d97706; }
.st-badge.st-upcoming { background:#2563eb; } .st-badge.st-done { background:#16a34a; }
.cal-empty { text-align:center; padding:22px 16px; color:var(--gray-500,#6b7280); font-size:.86rem; }
.hidden { display:none !important; }
.type-tag { display:inline-block; font-size:.7rem; padding:2px 7px; border-radius:6px; background:#eef2ff; color:#3730a3; }
/* popover giorno */
.cal-pop { position:fixed; inset:0; background:rgba(15,23,42,.4); z-index:1050; display:none; align-items:flex-start; justify-content:center; padding:40px 16px; overflow:auto; }
.cal-pop.open { display:flex; }
.cal-pop-card { background:#fff; border-radius:12px; width:100%; max-width:520px; box-shadow:0 20px 50px rgba(0,0,0,.25); }
.cal-pop-head { display:flex; justify-content:space-between; align-items:center; padding:14px 18px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
.cal-pop-head h3 { margin:0; font-size:1rem; }
.cal-pop-body { padding:14px 18px; }
.cal-pop-item { display:flex; gap:8px; align-items:flex-start; padding:8px 0; border-top:1px solid var(--gray-100,#f3f4f6); }
.cal-pop-item:first-child { border-top:none; }
.cal-pop-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); }
</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="cal.title">Calendario scadenze</h2>
</header>
<div class="content-body">
<div class="cal-intro">
<strong>Tutte le scadenze in un solo posto.</strong>
Il calendario aggrega <em>in tempo reale</em> ogni scadenza del sistema — incidenti, revisioni policy e controlli,
trattamenti di rischio, non conformita e azioni correttive, formazione, attivita stakeholder, scadenziario revisioni,
audit interni e decisioni del riesame — con stato e link diretto all'elemento.
</div>
<div class="cal-note">
Ancoraggio: buona prassi di monitoraggio scadenze e revisioni periodiche (<strong>ISO 9.1/9.3</strong>,
NIS2 <strong>GV.PO-02 / GV.SC-07 / DE.CM</strong>). Strumento di supporto operativo, non un parere legale.
</div>
<!-- KPI sintetici -->
<div class="cal-summary" id="cal-summary" aria-live="polite"></div>
<!-- Barra: navigazione mese + selettore vista -->
<div class="cal-bar">
<div class="cal-nav">
<button class="btn btn-outline-primary btn-sm" type="button" onclick="calPrevMonth()" aria-label="Mese precedente">&lsaquo;</button>
<h3 id="cal-month-label">—</h3>
<button class="btn btn-outline-primary btn-sm" type="button" onclick="calNextMonth()" aria-label="Mese successivo">&rsaquo;</button>
<button class="btn btn-outline-secondary btn-sm" type="button" onclick="calToday()">Oggi</button>
</div>
<div class="cal-views" role="tablist" aria-label="Vista calendario">
<button class="cal-view-btn active" id="view-grid" role="tab" aria-selected="true" aria-controls="pane-grid" onclick="calSetView('grid')">Griglia</button>
<button class="cal-view-btn" id="view-list" role="tab" aria-selected="false" aria-controls="pane-list" onclick="calSetView('list')">Lista</button>
</div>
</div>
<!-- Filtri -->
<div class="cal-filters">
<h4>Filtra per tipo</h4>
<div class="cal-chips" id="cal-type-chips"></div>
<div class="cal-filter-row">
<h4>Filtra per stato</h4>
<div class="cal-chips" id="cal-status-chips"></div>
</div>
</div>
<!-- Vista griglia mensile -->
<section id="pane-grid" role="tabpanel" aria-labelledby="view-grid" aria-live="polite">
<table class="cal-grid">
<thead>
<tr id="cal-weekhead"></tr>
</thead>
<tbody id="cal-grid-body"></tbody>
</table>
</section>
<!-- Vista lista -->
<section id="pane-list" class="hidden" role="tabpanel" aria-labelledby="view-list" aria-live="polite">
<div id="cal-list-wrap"></div>
</section>
</div>
</main>
</div>
<!-- Popover eventi di un giorno -->
<div class="cal-pop" id="cal-pop" role="dialog" aria-modal="true" aria-labelledby="cal-pop-title">
<div class="cal-pop-card">
<div class="cal-pop-head">
<h3 id="cal-pop-title">Eventi</h3>
<button class="cal-pop-close" type="button" aria-label="Chiudi" onclick="calPopClose()">&times;</button>
</div>
<div class="cal-pop-body" id="cal-pop-body"></div>
</div>
</div>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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>
'use strict';
/*
* NIS2 Agile - Calendario unico delle scadenze (Modulo C)
* Aggregatore READ-ONLY: griglia mensile + lista + filtri (tipo/stato) + deep-link.
* Dependency-free: nessuna libreria calendario, render in JS puro.
*/
var CAL = {
view: 'grid',
cursor: new Date(), // mese visualizzato (giorno 1)
events: [], // tutti gli eventi della finestra caricata
rangeFrom: null, rangeTo: null,
activeTypes: null, // null = tutti
activeStatuses: null, // null = tutti
};
var TYPE_META = {
incident_early_warning: { label: 'Incidente - Early Warning', color: '#dc2626' },
incident_notification: { label: 'Incidente - Notifica CSIRT', color: '#ea580c' },
incident_final_report: { label: 'Incidente - Report finale', color: '#f59e0b' },
policy_review: { label: 'Revisione policy', color: '#2563eb' },
risk_treatment: { label: 'Trattamento rischio', color: '#7c3aed' },
control_review: { label: 'Revisione controllo', color: '#0891b2' },
nc_target_close: { label: 'Chiusura non conformita', color: '#be123c' },
capa_action: { label: 'Azione correttiva', color: '#c026d3' },
training_due: { label: 'Formazione', color: '#16a34a' },
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' }
};
var STATUS_META = {
overdue: { label: 'In ritardo', color: '#dc2626' },
due_soon: { label: 'In scadenza', color: '#d97706' },
upcoming: { label: 'Futura', color: '#2563eb' },
done: { label: 'Assolta', color: '#16a34a' }
};
var WEEKDAYS = ['Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom'];
var MONTHS = ['gennaio','febbraio','marzo','aprile','maggio','giugno','luglio','agosto','settembre','ottobre','novembre','dicembre'];
function el(id) { return document.getElementById(id); }
function esc(s) { var d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
function escAttr(s) { return esc(s).replace(/"/g, '&quot;'); }
function ymd(d) {
var m = String(d.getMonth() + 1).padStart(2, '0');
var day = String(d.getDate()).padStart(2, '0');
return d.getFullYear() + '-' + m + '-' + day;
}
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', function (e) { if (e.key === 'Escape') calPopClose(); });
var pop = el('cal-pop'); if (pop) pop.addEventListener('click', function (e) { if (e.target === pop) calPopClose(); });
CAL.cursor = new Date(); CAL.cursor.setDate(1);
renderFilters();
await calLoad();
});
// ── Caricamento dati (finestra ampia attorno al cursore) ──
async function calLoad() {
var from = new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() - 2, 1);
var to = new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() + 3, 0);
CAL.rangeFrom = ymd(from); CAL.rangeTo = ymd(to);
el('cal-grid-body').innerHTML = '<tr><td colspan="7" class="cal-empty">Caricamento…</td></tr>';
try {
var data = await api.calendarEvents({ from: CAL.rangeFrom, to: CAL.rangeTo });
CAL.events = (data && data.events) ? data.events : [];
} catch (e) {
CAL.events = [];
el('cal-grid-body').innerHTML = '<tr><td colspan="7" class="cal-empty">Errore nel caricamento: ' + esc(e.message || '') + '</td></tr>';
}
await loadSummary();
render();
}
async function loadSummary() {
try {
var s = await api.calendarSummary();
var bs = (s && s.by_status) || {};
var defs = [['overdue', 'In ritardo'], ['due_soon', 'In scadenza'], ['upcoming', 'Future'], ['done', 'Assolte']];
el('cal-summary').innerHTML = defs.map(function (d) {
return '<div class="cal-kpi k-' + d[0] + '"><div class="n">' + (bs[d[0]] || 0) + '</div><div class="l">' + d[1] + '</div></div>';
}).join('');
} catch (e) { el('cal-summary').innerHTML = ''; }
}
// ── Filtri (chip checkbox per tipo e per stato) ──
function renderFilters() {
el('cal-type-chips').innerHTML = Object.keys(TYPE_META).map(function (t) {
var m = TYPE_META[t];
return '<label class="cal-chip"><input type="checkbox" class="f-type" value="' + escAttr(t) + '" checked onchange="onTypeFilter()">' +
'<span class="dot" style="background:' + m.color + '"></span>' + esc(m.label) + '</label>';
}).join('');
el('cal-status-chips').innerHTML = Object.keys(STATUS_META).map(function (s) {
var m = STATUS_META[s];
return '<label class="cal-chip"><input type="checkbox" class="f-status" value="' + escAttr(s) + '" checked onchange="onStatusFilter()">' +
'<span class="dot" style="background:' + m.color + '"></span>' + esc(m.label) + '</label>';
}).join('');
}
function onTypeFilter() {
var all = Array.prototype.slice.call(document.querySelectorAll('.f-type'));
var checked = all.filter(function (c) { return c.checked; }).map(function (c) { return c.value; });
CAL.activeTypes = (checked.length === all.length) ? null : checked;
render();
}
function onStatusFilter() {
var all = Array.prototype.slice.call(document.querySelectorAll('.f-status'));
var checked = all.filter(function (c) { return c.checked; }).map(function (c) { return c.value; });
CAL.activeStatuses = (checked.length === all.length) ? null : checked;
render();
}
function visibleEvents() {
return CAL.events.filter(function (e) {
if (CAL.activeTypes && CAL.activeTypes.indexOf(e.type) < 0) return false;
if (CAL.activeStatuses && CAL.activeStatuses.indexOf(e.status) < 0) return false;
return true;
});
}
// ── Navigazione mese / vista ──
function calPrevMonth() { CAL.cursor.setMonth(CAL.cursor.getMonth() - 1); maybeReload(); }
function calNextMonth() { CAL.cursor.setMonth(CAL.cursor.getMonth() + 1); maybeReload(); }
function calToday() { CAL.cursor = new Date(); CAL.cursor.setDate(1); maybeReload(); }
function maybeReload() {
// Ricarica dal server solo se il mese esce dalla finestra gia' caricata.
var first = ymd(new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth(), 1));
var last = ymd(new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() + 1, 0));
if (!CAL.rangeFrom || first < CAL.rangeFrom || last > CAL.rangeTo) { calLoad(); }
else { render(); }
}
function calSetView(v) {
CAL.view = v;
el('view-grid').classList.toggle('active', v === 'grid');
el('view-list').classList.toggle('active', v === 'list');
el('view-grid').setAttribute('aria-selected', v === 'grid' ? 'true' : 'false');
el('view-list').setAttribute('aria-selected', v === 'list' ? 'true' : 'false');
el('pane-grid').classList.toggle('hidden', v !== 'grid');
el('pane-list').classList.toggle('hidden', v !== 'list');
render();
}
function render() {
el('cal-month-label').textContent = MONTHS[CAL.cursor.getMonth()] + ' ' + CAL.cursor.getFullYear();
if (CAL.view === 'grid') renderGrid(); else renderList();
}
// ── Vista griglia mensile ──
function renderGrid() {
el('cal-weekhead').innerHTML = WEEKDAYS.map(function (w) { return '<th>' + w + '</th>'; }).join('');
var year = CAL.cursor.getFullYear(), month = CAL.cursor.getMonth();
var firstOfMonth = new Date(year, month, 1);
// settimana inizia lunedi (getDay: 0=dom)
var startOffset = (firstOfMonth.getDay() + 6) % 7;
var gridStart = new Date(year, month, 1 - startOffset);
var todayStr = ymd(new Date());
// raggruppa eventi visibili per giorno
var byDay = {};
visibleEvents().forEach(function (e) { (byDay[e.date] = byDay[e.date] || []).push(e); });
var html = '';
for (var w = 0; w < 6; w++) {
html += '<tr>';
for (var d = 0; d < 7; d++) {
var cur = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + (w * 7 + d));
var key = ymd(cur);
var inMonth = (cur.getMonth() === month);
var cls = inMonth ? '' : ' cal-out';
if (key === todayStr) cls += ' cal-today';
var evs = byDay[key] || [];
var dots = '';
var shown = evs.slice(0, 6);
shown.forEach(function (e) {
var c = (TYPE_META[e.type] || {}).color || '#64748b';
var tip = escAttr((TYPE_META[e.type] || { label: e.type }).label + ' — ' + e.title);
dots += '<span class="cal-dot st-' + esc(e.status) + '" style="background:' + c + '" title="' + tip + '" onclick="calPopDay(\'' + key + '\')"></span>';
});
var more = evs.length > shown.length ? '<div class="cal-more" onclick="calPopDay(\'' + key + '\')">+' + (evs.length - shown.length) + ' altri</div>' : '';
var clickDay = evs.length ? ' style="cursor:pointer" role="button" tabindex="0" aria-label="' + escAttr(cur.getDate() + ': ' + evs.length + ' scadenze') + '" onclick="calPopDay(\'' + key + '\')" onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();calPopDay(\'' + key + '\');}"' : '';
html += '<td class="' + cls + '">' +
'<div class="cal-daynum"' + clickDay + '>' + cur.getDate() + '</div>' +
(evs.length ? '<div class="cal-day-dots">' + dots + '</div>' + more : '') +
'</td>';
}
html += '</tr>';
}
el('cal-grid-body').innerHTML = html;
}
// ── Vista lista (overdue in cima, poi per data) ──
function renderList() {
var evs = visibleEvents().slice().sort(function (a, b) {
var oa = a.status === 'overdue' ? 0 : 1, ob = b.status === 'overdue' ? 0 : 1;
if (oa !== ob) return oa - ob;
return a.date < b.date ? -1 : (a.date > b.date ? 1 : 0);
});
if (!evs.length) { el('cal-list-wrap').innerHTML = '<div class="cal-empty">Nessuna scadenza per i filtri selezionati.</div>'; return; }
var rows = evs.map(function (e) {
var m = TYPE_META[e.type] || { label: e.type, color: '#64748b' };
var sm = STATUS_META[e.status] || { label: e.status };
return '<tr class="row-' + esc(e.status) + '">' +
'<td>' + esc(formatDate(e.date)) + '</td>' +
'<td><span class="type-tag" style="background:' + m.color + '22;color:' + m.color + '">' + esc(m.label) + '</span></td>' +
'<td><a class="cal-link" href="' + escAttr(e.link || '#') + '">' + esc(e.title) + '</a></td>' +
'<td><span class="st-badge st-' + esc(e.status) + '">' + esc(sm.label) + '</span></td>' +
'</tr>';
}).join('');
el('cal-list-wrap').innerHTML =
'<table class="cal-list"><thead><tr><th>Data</th><th>Tipo</th><th>Elemento</th><th>Stato</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
function formatDate(s) {
var p = String(s).split('-');
if (p.length !== 3) return s;
return p[2] + '/' + p[1] + '/' + p[0];
}
// ── Popover eventi di un giorno ──
function calPopDay(dateKey) {
var evs = visibleEvents().filter(function (e) { return e.date === dateKey; });
if (!evs.length) return;
el('cal-pop-title').textContent = 'Scadenze del ' + formatDate(dateKey);
el('cal-pop-body').innerHTML = evs.map(function (e) {
var m = TYPE_META[e.type] || { label: e.type, color: '#64748b' };
var sm = STATUS_META[e.status] || { label: e.status };
return '<div class="cal-pop-item">' +
'<span class="cal-dot st-' + esc(e.status) + '" style="background:' + m.color + ';margin-top:4px;flex:0 0 auto"></span>' +
'<div style="flex:1 1 auto">' +
'<div><a class="cal-link" href="' + escAttr(e.link || '#') + '">' + esc(e.title) + '</a></div>' +
'<div style="font-size:.78rem;color:#6b7280;margin-top:2px">' + esc(m.label) +
' · <span class="st-badge st-' + esc(e.status) + '">' + esc(sm.label) + '</span></div>' +
'</div></div>';
}).join('');
el('cal-pop').classList.add('open');
}
function calPopClose() { el('cal-pop').classList.remove('open'); }
</script>
</body>
</html>
+5 -5
View File
@@ -349,14 +349,14 @@
</main>
</div>
<script src="js/api.js?v=20260629"></script>
<script src="js/common.js?v=20260629"></script>
<script src="js/api.js?v=20260630"></script>
<script src="js/common.js?v=20260630"></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=20260629"></script>
<script src="js/i18n.js?v=20260629"></script>
<script src="js/help.js?v=20260629"></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>
if (!checkAuth()) throw new Error('Not authenticated');
+5 -5
View File
@@ -183,12 +183,12 @@
</div>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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/competenze.js?v=20260617"></script>
</body>
</html>
+4 -4
View File
@@ -382,8 +382,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/common-bi.js?v=20260630"></script>
<script src="/js/i18n.js?v=20260630"></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>`; }
+5 -5
View File
@@ -143,8 +143,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+1 -1
View File
@@ -73,7 +73,7 @@
</script>
<!-- Stessa logica della forgot-password.html: ZERO modifiche backend -->
<script src="js/api.js?v=20260629"></script>
<script src="js/api.js?v=20260630"></script>
<script>
const form = document.getElementById('forgot-form');
const err = document.getElementById('err');
+5 -5
View File
@@ -1142,8 +1142,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
// La guida è accessibile anche da non autenticati (utile per onboarding)
// ma se sei loggato carichi sidebar + i18n normalmente. Stessa logica
+5 -5
View File
@@ -351,8 +351,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
// ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+35
View File
@@ -94,6 +94,9 @@ $controllerMap = [
'stakeholders' => 'StakeholderController', // Epic C / C5 — Registro stakeholder + matrice di Mendelow
'stakeholder-activities' => 'StakeholderActivityController', // Epic C / C5.2 — Attività stakeholder (questionari/azioni + calendario)
'stakeholder-portal' => 'StakeholderPortalController', // Epic C / C5.2b — Portale esterno stakeholder (token magic-link, NO JWT)
'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)
'assessments' => 'AssessmentController',
'isms' => 'IsmsModelController', // Modello Organizzativo SGSI (ISO 27001/27017/27018 + SoA)
'dashboard' => 'DashboardController',
@@ -475,6 +478,38 @@ $actionMap = [
'POST:attachment' => 'attachment',
],
// ── InternalAuditController (Modulo A — Audit interni ISO 27001 §9.2) ──
'internal-audits' => [
'GET:list' => 'list',
'POST:create' => 'create',
'POST:items' => 'addItem',
'PUT:items/{subId}' => 'updateItem',
'GET:{id}' => 'get',
'PUT:{id}' => 'update',
'DELETE:{id}' => 'delete',
'POST:{id}/raiseNcr' => 'raiseNcr',
'GET:{id}/report' => 'report',
],
// ── ManagementReviewController — Riesame di Direzione (ISO 27001 §9.3, Modulo B) ──
'management-reviews' => [
'GET:list' => 'list',
'GET:gather' => 'gather',
'POST:create' => 'create',
'GET:{id}' => 'get',
'PUT:{id}' => 'update',
'POST:{id}/decisions' => 'addDecision',
'PUT:decisions/{subId}' => 'updateDecision',
'POST:{id}/approve' => 'approve',
'GET:{id}/report' => 'report',
],
// ── CalendarController (Modulo C — calendario unico scadenze, sola lettura) ──
'calendar' => [
'GET:events' => 'events',
'GET:summary' => 'summary',
],
// ── AuditController ─────────────────────────────
'audit' => [
'GET:controls' => 'listControls',
+2 -2
View File
@@ -910,8 +910,8 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
</main>
<script src="js/common.js?v=20260629"></script>
<script src="js/i18n.js?v=20260629"></script>
<script src="js/common.js?v=20260630"></script>
<script src="js/i18n.js?v=20260630"></script>
<script>
function showTab(id) {
document.querySelectorAll('.tab-btn').forEach((b, i) => {
+390
View File
@@ -0,0 +1,390 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<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">
<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; }
.ia-toolbar { display:flex; justify-content:flex-end; margin-bottom:12px; }
.ia-table { width:100%; border-collapse:collapse; font-size:.88rem; }
.ia-table th, .ia-table td { text-align:left; padding:9px 12px; border-top:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
.ia-table th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
.ia-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; white-space:nowrap; }
.ia-badge.s-planned { background:#e0f2fe; color:#075985; }
.ia-badge.s-in_progress { background:#fef3c7; color:#92400e; }
.ia-badge.s-completed { background:#dcfce7; color:#166534; }
.ia-badge.s-cancelled { background:#fee2e2; color:#991b1b; }
.ia-empty { text-align:center; padding:22px 16px; color:var(--gray-500,#6b7280); font-size:.86rem; }
.ia-overdue { color:#991b1b; font-weight:600; }
.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:760px; 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; flex-wrap:wrap; }
.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 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:150px; }
.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; }
.ck-group { margin-top:16px; }
.ck-group h4 { font-size:.82rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); margin:14px 0 6px; }
.ck-row { display:flex; gap:8px; align-items:flex-start; padding:8px 0; border-top:1px solid var(--gray-100,#f3f4f6); }
.ck-row:first-of-type { border-top:none; }
.ck-ref { flex:0 0 64px; font-size:.78rem; font-weight:700; color:#374151; padding-top:8px; }
.ck-main { flex:1 1 auto; }
.ck-cp { font-size:.86rem; }
.ck-controls { display:flex; gap:6px; align-items:center; margin-top:5px; flex-wrap:wrap; }
.ck-controls select { padding:5px 8px; border:1px solid var(--gray-200,#e5e7eb); border-radius:7px; font-size:.8rem; }
.ck-controls input[type=text] { padding:5px 8px; border:1px solid var(--gray-200,#e5e7eb); border-radius:7px; font-size:.8rem; flex:1 1 180px; }
.ck-nc { font-size:.74rem; background:none; border:1px solid #fecaca; color:#b91c1c; border-radius:6px; padding:4px 9px; cursor:pointer; }
.ck-nc[disabled] { opacity:.6; cursor:default; }
.ck-ncbadge { font-size:.72rem; color:#991b1b; font-weight:700; }
.det-meta { font-size:.85rem; color:#374151; margin-bottom:6px; }
.det-meta strong { color:#111827; }
</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="iaud.title">Audit interni</h2>
</header>
<div class="content-body">
<div class="ia-intro">
<strong>Audit interni del SGSI (ISO/IEC 27001 §9.2).</strong>
Programma gli audit, conduci la <em>checklist</em> (clausole 4-10 ISO 27001 + controlli Annex A applicabili dal tuo SoA),
registra gli esiti riga per riga, apri una <em>non conformità</em> sui punti non conformi e genera il <em>report</em> stampabile.
</div>
<div class="ia-note">
Ancoraggio: <strong>ISO/IEC 27001 §9.2</strong> (audit interni) — buona prassi di governance anche per NIS2
(<strong>GV.PO-02</strong>, monitoraggio del programma di sicurezza). Strumento di supporto organizzativo, non un parere legale.
</div>
<div class="ia-toolbar"><button class="btn btn-primary btn-sm" onclick="auditOpen()">+ Nuovo audit</button></div>
<div id="aud-list" aria-live="polite"></div>
</div>
</main>
</div>
<!-- Modale crea/modifica audit -->
<div class="sm-overlay" id="aud-modal" role="dialog" aria-modal="true" aria-labelledby="aud-modal-title">
<div class="sm-dialog">
<div class="sm-head"><h3 id="aud-modal-title">Nuovo audit</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="auditClose()">&times;</button></div>
<div class="sm-body">
<input type="hidden" id="aud-id">
<div class="sm-field"><label for="aud-title">Titolo *</label><input type="text" id="aud-title" maxlength="255" placeholder="Es. Audit interno SGSI 2026"></div>
<div class="sm-row">
<div class="sm-field"><label for="aud-status">Stato</label>
<select id="aud-status">
<option value="planned">Pianificato</option>
<option value="in_progress">In corso</option>
<option value="completed">Completato</option>
<option value="cancelled">Annullato</option>
</select>
</div>
<div class="sm-field"><label for="aud-planned">Data pianificata <span class="hint">(→ calendario)</span></label><input type="date" id="aud-planned"></div>
<div class="sm-field"><label for="aud-executed">Data esecuzione</label><input type="date" id="aud-executed"></div>
</div>
<div class="sm-field"><label for="aud-scope">Ambito (scope)</label><textarea id="aud-scope" maxlength="2000" placeholder="Es. SGSI, sede principale, processi IT"></textarea></div>
<div class="sm-field"><label for="aud-criteria">Criteri</label><textarea id="aud-criteria" maxlength="2000" placeholder="Es. ISO/IEC 27001:2022 cl.4-10 + Annex A">ISO/IEC 27001:2022 cl.4-10 + Annex A</textarea>
<span class="hint">Alla creazione la checklist viene pre-popolata con le clausole 4-10 e i controlli Annex A applicabili dal tuo SoA.</span>
</div>
<div class="sm-row">
<div class="sm-field"><label for="aud-lead-user">Auditor capo <span class="hint">(utente)</span></label><select id="aud-lead-user"><option value="">— Nessuno —</option></select></div>
<div class="sm-field"><label for="aud-lead-role">Auditor capo <span class="hint">(ruolo)</span></label><select id="aud-lead-role"><option value="">— Nessuno —</option></select></div>
</div>
<div class="sm-field" id="aud-wrap-conclusion"><label for="aud-conclusion">Conclusioni</label><textarea id="aud-conclusion" maxlength="2000"></textarea></div>
<div class="sm-err" id="aud-err" role="alert"></div>
</div>
<div class="sm-foot"><button class="btn btn-outline" type="button" onclick="auditClose()">Annulla</button><button class="btn btn-primary" type="button" id="aud-save" onclick="auditSave()">Salva</button></div>
</div>
</div>
<!-- Modale dettaglio audit (checklist) -->
<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 audit</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="detClose()">&times;</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>
<button class="btn btn-outline" type="button" onclick="detAddItem()">+ Voce custom</button>
<button class="btn btn-outline" type="button" onclick="detEdit()">Modifica audit</button>
<a class="btn btn-primary" id="det-report" href="#" target="_blank" rel="noopener">Report</a>
</div>
</div>
</div>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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>
'use strict';
/*
* NIS2 Agile - Audit interni (ISO 27001 §9.2) — MODULO A
* Programma audit + checklist (clausole 4-10 + Annex A dal SoA) + esiti per riga
* + apertura NC sui non conformi + report HTML stampabile.
*/
let IA = { audits: [], users: [], roles: [], detail: null };
const ST_LABELS = { planned: 'Pianificato', in_progress: 'In corso', completed: 'Completato', cancelled: 'Annullato' };
const RES_LABELS = { da_verificare: 'Da verificare', conforme: 'Conforme', non_conforme: 'Non conforme', osservazione: 'Osservazione', opportunita: 'Opportunità', non_applicabile: 'Non applicabile' };
const TYPE_LABELS = { clause: 'Clausole ISO 27001', annex_control: 'Controlli Annex A', nis2_measure: 'Misure NIS2', custom: 'Voci aggiuntive' };
const TYPE_ORDER = ['clause', 'annex_control', 'nis2_measure', 'custom'];
function el(id) { return document.getElementById(id); }
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
function escAttr(s) { return esc(s).replace(/"/g, '&quot;'); }
// Il report è HTML servito dall'API: token+org passati in query (no header su <a target=_blank>).
function reportUrl(id) {
const base = (api && api.baseUrl) ? api.baseUrl : '/api';
const qs = [];
if (api && api.token) qs.push('token=' + encodeURIComponent(api.token));
if (api && api.orgId) qs.push('org_id=' + encodeURIComponent(api.orgId));
return base + '/internal-audits/' + id + '/report' + (qs.length ? '?' + qs.join('&') : '');
}
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') { auditClose(); detClose(); } });
['aud-modal', 'det-modal'].forEach(m => { const o = el(m); if (o) o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); }); });
await iaLoadAll();
});
async function iaLoadAll() {
el('aud-list').innerHTML = '<div class="ia-empty">Caricamento…</div>';
try {
const [auds, users, roles] = await Promise.all([
api.intAuditList(),
api.orgRolesAssignableUsers().catch(() => ({ users: [] })),
api.orgRolesList().catch(() => ({ roles: [] }))
]);
IA.audits = (auds && auds.audits) || [];
IA.users = (users && (users.users || users.assignable_users)) || [];
IA.roles = (roles && (roles.roles || roles.org_roles)) || [];
fillSelects();
renderAudits();
} catch (e) {
el('aud-list').innerHTML = '<div class="ia-empty">Errore: ' + esc(e.message || e) + '</div>';
}
}
function fillSelects() {
el('aud-lead-user').innerHTML = '<option value="">— Nessuno —</option>'
+ IA.users.map(u => '<option value="' + u.id + '">' + esc(u.full_name || u.name || ('Utente ' + u.id)) + '</option>').join('');
el('aud-lead-role').innerHTML = '<option value="">— Nessuno —</option>'
+ IA.roles.map(r => '<option value="' + r.id + '">' + esc(r.role_name || r.name || ('Ruolo ' + r.id)) + '</option>').join('');
}
function todayStr() { return new Date().toISOString().slice(0, 10); }
function isOverdue(a) { return a.planned_date && a.planned_date < todayStr() && a.status !== 'completed' && a.status !== 'cancelled'; }
function renderAudits() {
if (!IA.audits.length) { el('aud-list').innerHTML = '<div class="ia-empty">Nessun audit. Creane uno con "+ Nuovo audit".</div>'; return; }
const rows = IA.audits.map(a => {
const dl = a.planned_date ? ('<span class="' + (isOverdue(a) ? 'ia-overdue' : '') + '">' + esc(a.planned_date) + '</span>') : '—';
const lead = a.lead_auditor_name ? esc(a.lead_auditor_name) : (a.lead_auditor_role_name ? esc(a.lead_auditor_role_name) : '—');
const ncTag = a.n_nc > 0 ? ' <span class="ia-badge s-cancelled">' + a.n_nc + ' NC</span>' : '';
return '<tr>'
+ '<td><strong>' + esc(a.code || '—') + '</strong></td>'
+ '<td><a href="#" onclick="detOpen(' + a.id + ');return false;"><strong>' + esc(a.title) + '</strong></a><div style="font-size:.78rem;color:#6b7280;">' + a.n_items + ' voci' + ncTag + '</div></td>'
+ '<td><span class="ia-badge s-' + a.status + '">' + (ST_LABELS[a.status] || a.status) + '</span></td>'
+ '<td>' + lead + '</td>'
+ '<td>' + dl + '</td>'
+ '<td style="white-space:nowrap;">'
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="detOpen(' + a.id + ')">Apri</button> '
+ '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;color:#b91c1c;border-color:#fecaca;" onclick="auditDelete(' + a.id + ')">Elimina</button>'
+ '</td></tr>';
}).join('');
el('aud-list').innerHTML = '<table class="ia-table"><thead><tr><th>Codice</th><th>Titolo</th><th>Stato</th><th>Auditor capo</th><th>Scadenza</th><th>Azioni</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
// ── Crea / modifica audit ──
function auditOpen(id) {
el('aud-err').textContent = '';
el('aud-id').value = id || '';
el('aud-modal-title').textContent = id ? 'Modifica audit' : 'Nuovo audit';
el('aud-wrap-conclusion').classList.toggle('hidden', !id);
if (!id) {
el('aud-title').value = ''; el('aud-status').value = 'planned';
el('aud-planned').value = ''; el('aud-executed').value = '';
el('aud-scope').value = ''; el('aud-criteria').value = 'ISO/IEC 27001:2022 cl.4-10 + Annex A';
el('aud-lead-user').value = ''; el('aud-lead-role').value = ''; el('aud-conclusion').value = '';
} else {
const a = IA.detail || IA.audits.find(x => x.id === id) || {};
el('aud-title').value = a.title || ''; el('aud-status').value = a.status || 'planned';
el('aud-planned').value = a.planned_date || ''; el('aud-executed').value = a.executed_date || '';
el('aud-scope').value = a.scope || ''; el('aud-criteria').value = a.criteria || '';
el('aud-lead-user').value = a.lead_auditor_user_id || ''; el('aud-lead-role').value = a.lead_auditor_role_id || '';
el('aud-conclusion').value = a.conclusion || '';
}
el('aud-modal').classList.add('open');
setTimeout(function () { var fe = el('aud-modal').querySelector('input,select,textarea'); if (fe) fe.focus(); }, 50);
}
function auditClose() { el('aud-modal').classList.remove('open'); }
async function auditSave() {
const id = el('aud-id').value;
const title = el('aud-title').value.trim();
if (!title) { el('aud-err').textContent = 'Il titolo è obbligatorio.'; return; }
const payload = {
title: title,
status: el('aud-status').value,
planned_date: el('aud-planned').value || null,
executed_date: el('aud-executed').value || null,
scope: el('aud-scope').value.trim() || null,
criteria: el('aud-criteria').value.trim() || null,
lead_auditor_user_id: el('aud-lead-user').value || null,
lead_auditor_role_id: el('aud-lead-role').value || null
};
if (id) { payload.conclusion = el('aud-conclusion').value.trim() || null; }
el('aud-save').disabled = true;
try {
if (id) {
await api.intAuditUpdate(id, payload);
} else {
const res = await api.intAuditCreate(payload);
if (res && res.items_seeded != null) { /* checklist pre-popolata */ }
}
auditClose();
await iaLoadAll();
} catch (e) {
el('aud-err').textContent = e.message || 'Errore di salvataggio.';
} finally {
el('aud-save').disabled = false;
}
}
async function auditDelete(id) {
if (!confirm('Eliminare questo audit e tutta la sua checklist?')) return;
try { await api.intAuditDelete(id); await iaLoadAll(); }
catch (e) { alert(e.message || 'Errore eliminazione.'); }
}
// ── Dettaglio (checklist) ──
async function detOpen(id) {
el('det-body').innerHTML = '<div class="ia-empty">Caricamento…</div>';
el('det-modal').classList.add('open');
setTimeout(function () { var fe = el('det-modal').querySelector('.sm-close, button'); if (fe) fe.focus(); }, 50);
try {
const a = await api.intAuditGet(id);
IA.detail = a;
el('det-report').href = reportUrl(id);
renderDetail(a);
} catch (e) {
el('det-body').innerHTML = '<div class="ia-empty">Errore: ' + esc(e.message || e) + '</div>';
}
}
function detClose() { el('det-modal').classList.remove('open'); IA.detail = null; }
function detEdit() { if (IA.detail) auditOpen(IA.detail.id); }
function renderDetail(a) {
el('det-title').textContent = (a.code ? a.code + ' — ' : '') + a.title;
const lead = a.lead_auditor_name || a.lead_auditor_role_name || '—';
let h = '<div class="det-meta"><strong>Stato:</strong> ' + esc(ST_LABELS[a.status] || a.status)
+ ' &middot; <strong>Auditor capo:</strong> ' + esc(lead)
+ ' &middot; <strong>Pianificata:</strong> ' + esc(a.planned_date || '—') + '</div>';
if (a.scope) h += '<div class="det-meta"><strong>Ambito:</strong> ' + esc(a.scope) + '</div>';
if (a.criteria) h += '<div class="det-meta"><strong>Criteri:</strong> ' + esc(a.criteria) + '</div>';
const items = a.items || [];
if (!items.length) { h += '<div class="ia-empty">Nessuna voce di checklist. Aggiungine una con "+ Voce custom".</div>'; el('det-body').innerHTML = h; return; }
const groups = {};
items.forEach(it => { (groups[it.ref_type] = groups[it.ref_type] || []).push(it); });
TYPE_ORDER.forEach(type => {
if (!groups[type] || !groups[type].length) return;
h += '<div class="ck-group"><h4>' + esc(TYPE_LABELS[type] || type) + ' (' + groups[type].length + ')</h4>';
groups[type].forEach(it => { h += renderItemRow(it); });
h += '</div>';
});
el('det-body').innerHTML = h;
}
function renderItemRow(it) {
const opts = Object.keys(RES_LABELS).map(k => '<option value="' + k + '"' + (it.result === k ? ' selected' : '') + '>' + esc(RES_LABELS[k]) + '</option>').join('');
let ncCtl;
if (it.ncr) {
ncCtl = '<span class="ck-ncbadge">NC ' + esc(it.ncr.ncr_code) + '</span>';
} else {
const dis = it.result === 'non_conforme' ? '' : ' disabled title="Apri NC disponibile sulle voci non conformi"';
ncCtl = '<button class="ck-nc" onclick="itemRaiseNcr(' + it.id + ')"' + dis + '>Apri NC</button>';
}
return '<div class="ck-row" id="ck-' + it.id + '">'
+ '<div class="ck-ref">' + esc(it.ref_code || '') + '</div>'
+ '<div class="ck-main">'
+ '<div class="ck-cp">' + esc(it.checkpoint) + '</div>'
+ '<div class="ck-controls">'
+ '<select onchange="itemSetResult(' + it.id + ', this.value)">' + opts + '</select>'
+ '<input type="text" placeholder="Note / evidenze" value="' + escAttr(it.note || '') + '" onchange="itemSetNote(' + it.id + ', this.value)">'
+ ncCtl
+ '</div></div></div>';
}
async function itemSetResult(itemId, value) {
try {
await api.intAuditUpdateItem(itemId, { result: value });
const it = findItem(itemId); if (it) { it.result = value; }
// ridisegna la riga per aggiornare il pulsante "Apri NC"
const row = el('ck-' + itemId); if (row && it) row.outerHTML = renderItemRow(it);
} catch (e) { alert(e.message || 'Errore aggiornamento esito.'); }
}
async function itemSetNote(itemId, value) {
try { await api.intAuditUpdateItem(itemId, { note: value }); const it = findItem(itemId); if (it) it.note = value; }
catch (e) { alert(e.message || 'Errore aggiornamento nota.'); }
}
async function itemRaiseNcr(itemId) {
if (!IA.detail) return;
if (!confirm('Aprire una non conformità (NC) collegata a questa voce?')) return;
try {
const res = await api.intAuditRaiseNcr(IA.detail.id, { item_id: itemId });
const it = findItem(itemId);
if (it) { it.ncr = { id: res.id, ncr_code: res.ncr_code }; const row = el('ck-' + itemId); if (row) row.outerHTML = renderItemRow(it); }
alert('Non conformità ' + (res.ncr_code || '') + (res.already ? ' (già esistente)' : ' creata') + '. La trovi nel modulo NCR/CAPA.');
await iaLoadAll();
} catch (e) { alert(e.message || 'Errore apertura NC.'); }
}
function findItem(itemId) { return (IA.detail && IA.detail.items || []).find(x => x.id === itemId); }
async function detAddItem() {
if (!IA.detail) return;
const cp = prompt('Testo del punto di verifica (voce custom):');
if (!cp || !cp.trim()) return;
try {
await api.intAuditAddItem({ audit_id: IA.detail.id, checkpoint: cp.trim(), ref_type: 'custom' });
await detOpen(IA.detail.id);
await iaLoadAll();
} catch (e) { alert(e.message || 'Errore aggiunta voce.'); }
}
</script>
</body>
</html>
+5 -5
View File
@@ -184,8 +184,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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/isms.js"></script>
</body>
</html>
+38
View File
@@ -221,6 +221,7 @@ class NIS2API {
// ═══════════════════════════════════════════════════════════════════
orgRolesList() { return this._acn(this.get('/org-roles/list')); }
orgRolesAssignableUsers() { return this._acn(this.get('/org-roles/assignable-users')); }
orgMembers() { return this._acn(this.get('/organizations/' + (this.orgId || '') + '/members')); }
orgRoleGet(id) { return this._acn(this.get(`/org-roles/${id}`)); }
orgRoleCreate(data) { return this._acn(this.post('/org-roles/create', data || {})); }
orgRoleUpdate(id, data) { return this._acn(this.put(`/org-roles/${id}`, data)); }
@@ -333,6 +334,43 @@ class NIS2API {
stkActAddComment(id, d) { return this._acn(this.post(`/stakeholder-activities/${id}/comments`, d || {})); }
stkActAttachments(id) { return this._acn(this.get(`/stakeholder-activities/${id}/attachments`)); }
// ── Audit interni (Modulo A — ISO 27001 §9.2) ──
intAuditList() { return this._acn(this.get('/internal-audits/list')); }
intAuditGet(id) { return this._acn(this.get(`/internal-audits/${id}`)); }
intAuditCreate(data) { return this._acn(this.post('/internal-audits/create', data || {})); }
intAuditUpdate(id, data) { return this._acn(this.put(`/internal-audits/${id}`, data)); }
intAuditDelete(id) { return this._acn(this.del(`/internal-audits/${id}`)); }
intAuditAddItem(data) { return this._acn(this.post('/internal-audits/items', data || {})); }
intAuditUpdateItem(itemId, data) { return this._acn(this.put(`/internal-audits/items/${itemId}`, data)); }
intAuditRaiseNcr(id, data) { return this._acn(this.post(`/internal-audits/${id}/raise-ncr`, data || {})); }
// ── Riesame di Direzione (ISO 27001 §9.3, Modulo B) ──
mgmtReviewList() { return this._acn(this.get('/management-reviews/list')); }
mgmtReviewGet(id) { return this._acn(this.get(`/management-reviews/${id}`)); }
mgmtReviewCreate(data) { return this._acn(this.post('/management-reviews/create', data || {})); }
mgmtReviewUpdate(id, data) { return this._acn(this.put(`/management-reviews/${id}`, data || {})); }
mgmtReviewGather() { return this._acn(this.get('/management-reviews/gather')); }
mgmtReviewAddDecision(id, data) { return this._acn(this.post(`/management-reviews/${id}/decisions`, data || {})); }
mgmtReviewUpdateDecision(subId, data) { return this._acn(this.put(`/management-reviews/decisions/${subId}`, data || {})); }
mgmtReviewApprove(id) { return this._acn(this.post(`/management-reviews/${id}/approve`, {})); }
// ── Calendario unico scadenze (Modulo C) — aggregatore sola lettura ──
calendarEvents(params) {
const qs = new URLSearchParams();
if (params && params.from) qs.set('from', params.from);
if (params && params.to) qs.set('to', params.to);
if (params && params.types) qs.set('types', Array.isArray(params.types) ? params.types.join(',') : params.types);
const q = qs.toString();
return this._acn(this.get('/calendar/events' + (q ? '?' + q : '')));
}
calendarSummary(params) {
const qs = new URLSearchParams();
if (params && params.from) qs.set('from', params.from);
if (params && params.to) qs.set('to', params.to);
const q = qs.toString();
return this._acn(this.get('/calendar/summary' + (q ? '?' + q : '')));
}
// ═══════════════════════════════════════════════════════════════════
// Dashboard
// ═══════════════════════════════════════════════════════════════════
+3
View File
@@ -62,6 +62,7 @@
label: 'Principale', i18nKey: 'nav.main',
items: [
{ name: 'Dashboard', href: 'dashboard.html', icon: iconGrid(), i18nKey: 'nav.dashboard' },
{ name: 'Calendario', href: 'calendario.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.calendar' },
{ name: 'Compliance Journey', href: 'workflow.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>' },
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
{ name: 'Misure e Requisiti', href: 'misure-requisiti.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h8a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/></svg>', i18nKey: 'nav.framework' },
@@ -90,6 +91,8 @@
items: [
{ 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: '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' }
]
},
+3
View File
@@ -186,6 +186,7 @@ function loadSidebar() {
label: 'Principale', i18nKey: 'nav.main',
items: [
{ name: 'Dashboard', href: 'dashboard.html', icon: iconGrid(), i18nKey: 'nav.dashboard' },
{ name: 'Calendario', href: 'calendario.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.calendar' },
{ name: 'Compliance Journey', href: 'workflow.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>` },
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
{ name: 'Misure e Requisiti', href: 'misure-requisiti.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h8a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.framework' },
@@ -216,6 +217,8 @@ function loadSidebar() {
items: [
{ 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: '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' },
]
},
+120
View File
@@ -400,6 +400,120 @@ 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.'
]
},
'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.',
sections: [
{
heading: 'Programma audit',
items: [
'Crea un <strong>audit</strong> indicando titolo, ambito (scope), criteri (es. "ISO/IEC 27001:2022 cl.4-10 + Annex A"), data pianificata, data di esecuzione e auditor capo (un utente dell\'organizzazione oppure un ruolo dell\'organigramma).',
'Il sistema assegna automaticamente un <strong>codice progressivo</strong> (AUD-001, AUD-002, …) per organizzazione.',
'La <strong>data pianificata</strong> compare automaticamente nel <strong>calendario NIS2</strong> (Scadenziario).'
]
},
{
heading: 'Checklist di conduzione',
items: [
'Alla creazione la checklist viene <strong>pre-popolata</strong>: le <strong>clausole 4-10</strong> della ISO 27001 e i <strong>controlli Annex A</strong> marcati come applicabili nel tuo <strong>SoA</strong> (Modello SGSI). Parti già con la checklist, non da foglio bianco.',
'Per ogni voce scegli l\'<strong>esito</strong> (Da verificare, Conforme, Non conforme, Osservazione, Opportunità, Non applicabile) e annoti note/evidenze; il salvataggio è immediato.',
'Puoi aggiungere <strong>voci custom</strong> non previste dalle clausole o dall\'Annex A.'
]
},
{
heading: 'Non conformità e report',
items: [
'Sulle voci marcate <strong>Non conforme</strong> puoi premere <strong>Apri NC</strong>: il sistema crea una non conformità collegata (sorgente "audit") che confluisce nel modulo <strong>NCR/CAPA</strong>, dove ne gestisci root cause e azioni correttive.',
'Il pulsante <strong>Report</strong> apre un verbale di audit <strong>stampabile</strong> (HTML) con metadati, sintesi degli esiti, checklist completa e conclusioni: usa "Stampa / Salva PDF" del browser.'
]
},
{
heading: 'Riferimenti normativi',
items: [
'<strong>ISO/IEC 27001:2022 §9.2</strong>: l\'organizzazione conduce audit interni a intervalli pianificati per verificare che il SGSI sia conforme ai requisiti e attuato efficacemente.',
'<strong>NIST CSF 2.0 / GV.PO-02</strong>: la politica di sicurezza e i programmi di gestione sono rivisti e migliorati nel tempo (buona prassi anche per NIS2).',
'Le <strong>azioni</strong> correttive/preventive si gestiscono nel modulo NCR/CAPA.'
]
}
],
references: [
'ISO/IEC 27001:2022 - Clausola 9.2 Audit interni',
'ISO/IEC 27001:2022 - Clausole 4-10 e Annex A (controlli del SoA)',
'NIST CSF 2.0 / GV.PO-02 - Revisione e miglioramento del programma di sicurezza',
'Gli audit interni e il riesame periodico sono buona prassi di governance; gli obblighi normativi in Italia derivano dal D.Lgs. 138/2024.',
'NOTA: strumento di supporto organizzativo, non un parere legale.'
]
},
'management-review': {
title: 'Guida - Riesame di Direzione (ISO 27001 §9.3)',
intro: 'Il Riesame di Direzione e il momento in cui la direzione valuta periodicamente l\'idoneita, l\'adeguatezza e l\'efficacia del Sistema di Gestione della Sicurezza delle Informazioni (SGSI). Il modulo produce il VERBALE con gli elementi in ingresso (aggregati automaticamente dai moduli esistenti) e in uscita (le decisioni). E\' uno strumento di SUPPORTO e PRE-AUDIT: non sostituisce l\'auditor ne costituisce una certificazione.',
sections: [
{
heading: 'Come si compila un verbale',
items: [
'<strong>1. Crea il riesame</strong>: indica data, periodo di riferimento (es. "2026 H1"), presidente e partecipanti. Il codice RD-AAAA-NN viene generato automaticamente.',
'<strong>2. Aggrega i dati</strong>: il pulsante "Aggrega dati" raccoglie gli elementi in ingresso §9.3.2 da tutta la piattaforma (non conformita, audit interni, rischi, SoA, obiettivi, formazione, stakeholder, normativa, scadenze).',
'<strong>3. Rivedi e completa</strong>: gli input aggregati sono una proposta da rivedere; aggiungi le conclusioni della direzione.',
'<strong>4. Registra le decisioni</strong>: ogni decisione (elemento in uscita §9.3.3) ha un responsabile (ruolo dell\'organigramma), una scadenza e uno stato. Puo essere collegata a un\'azione CAPA.',
'<strong>5. Approva e congela</strong>: l\'approvazione (riservata all\'org_admin) salva uno snapshot immutabile degli input al momento del verbale e blocca ogni ulteriore modifica.'
]
},
{
heading: 'Elementi in ingresso aggregati automaticamente',
items: [
'Le sezioni vengono lette in modo difensivo: se un modulo non e ancora attivo (es. Audit interni) o non ha dati, la sezione mostra "dato non disponibile" senza bloccare le altre.',
'Tutti i dati sono filtrati per la tua organizzazione e fotografati nello snapshot al momento dell\'approvazione, cosi il verbale resta coerente nel tempo.',
'Dal verbale puoi aprire la versione stampabile (pulsante "Verbale") da archiviare o presentare in audit.'
]
}
],
references: [
'ISO/IEC 27001:2022 §9.3 - Riesame di direzione (elementi in ingresso 9.3.2 / in uscita 9.3.3)',
'D.Lgs. 138/2024 art. 23 - Obblighi di governance degli organi di amministrazione e direttivi (NIS2 GV.PO-02)',
'NOTA: ISO e best practice non vincolante; gli obblighi italiani derivano da NIS2 / D.Lgs. 138/2024 / Determinazioni ACN.'
]
},
'calendario': {
title: 'Guida - Calendario scadenze',
intro: 'Il Calendario raccoglie in un unico posto ogni scadenza del sistema: incidenti Art.23, revisioni di policy e controlli, trattamenti di rischio, non conformita e azioni correttive, formazione, attivita stakeholder, scadenziario delle revisioni periodiche, audit interni e decisioni del riesame di direzione. E\' un aggregatore in SOLA LETTURA, sempre coerente con i moduli sorgente: ogni scadenza viene calcolata in tempo reale e rimanda direttamente all\'elemento che la genera. E\' uno strumento di supporto operativo, non un parere legale.',
sections: [
{
heading: 'Le viste',
items: [
'<strong>Griglia mensile</strong>: ogni giorno mostra dei pallini colorati, uno per scadenza. Il colore indica il tipo, mentre la navigazione &lsaquo; / &rsaquo; sposta il mese e "Oggi" torna al mese corrente. Clic su un giorno apre l\'elenco delle sue scadenze.',
'<strong>Lista</strong>: tutte le scadenze ordinate per data, con quelle <strong>in ritardo</strong> in cima. Clic sul nome apre l\'elemento sorgente (incidente, policy, rischio, ...).'
]
},
{
heading: 'Stato delle scadenze',
items: [
'<strong>In ritardo</strong> (rosso): la data e\' gia\' passata e la scadenza non risulta assolta.',
'<strong>In scadenza</strong> (arancione): mancano 14 giorni o meno.',
'<strong>Futura</strong> (blu): oltre 14 giorni.',
'<strong>Assolta</strong> (verde): la scadenza e\' stata chiusa/completata nel modulo di origine.'
]
},
{
heading: 'Filtri',
items: [
'Puoi filtrare per <strong>tipo</strong> (incidenti, policy, rischi, controlli, NC/CAPA, formazione, stakeholder, revisioni, audit interni, riesame) e per <strong>stato</strong>, combinando le due dimensioni.',
'I conteggi in alto (in ritardo / in scadenza / future / assolte) danno il colpo d\'occhio complessivo sul periodo caricato.'
]
},
{
heading: 'Da dove arrivano le scadenze',
items: [
'Il calendario non duplica i dati: legge le scadenze direttamente dai moduli. Per modificarne una, apri l\'elemento dal link e aggiornala nel suo modulo (es. la data di revisione di una policy si cambia in Policy).',
'Le scadenze dei moduli Audit interni e Riesame di direzione compaiono automaticamente non appena quei moduli sono attivi.'
]
}
],
references: [
'ISO/IEC 27001:2022, cl. 9.1 (monitoraggio e misurazione) e 9.3 (riesame di direzione) - buona prassi',
'NIST CSF 2.0 / GV.PO-02, GV.SC-07, DE.CM (best practice) - revisione periodica e monitoraggio continuo',
'NOTA: il calendario e\' uno strumento di supporto al monitoraggio delle scadenze; gli obblighi normativi derivano da NIS2 (Dir. UE 2022/2555) e dal D.Lgs. 138/2024.'
]
},
'stakeholder-activities': {
title: 'Guida - Attività stakeholder',
intro: 'Le attività verso gli stakeholder ti permettono di inviare questionari (da compilare o per firma di avvenuta lettura) e pianificare azioni, collegandoli alle procedure e alle misure/requisiti, con data e scadenza sul calendario NIS2. Strumento di supporto organizzativo, non un parere legale.',
@@ -1283,6 +1397,12 @@ const HelpSystem = (function () {
'stakeholders.html': 'stakeholders',
'stakeholders': 'stakeholders',
'stakeholder-activities.html': 'stakeholder-activities',
'internal-audits.html': 'internal-audits',
'internal-audits': 'internal-audits',
'management-review.html': 'management-review',
'management-review': 'management-review',
'calendario.html': 'calendario',
'calendario': 'calendario',
'stakeholder-activities': 'stakeholder-activities',
'risks.html': 'risks',
'risks': 'risks',
+6
View File
@@ -76,6 +76,12 @@ const I18n = (function () {
'rev.title': { it: 'Scadenziario', en: 'Review Schedule' },
'stk.title': { it: 'Stakeholder', en: 'Stakeholders' },
'sact.title': { it: 'Attività stakeholder', en: 'Stakeholder activities' },
'nav.calendar': { it: 'Calendario', en: 'Calendar' },
'cal.title': { it: 'Calendario scadenze', en: 'Deadlines calendar' },
'nav.internal_audits': { it: 'Audit interni', en: 'Internal audits' },
'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.risks': { it: 'Rischi', en: 'Risks' },
'nav.incidents': { it: 'Incidenti', en: 'Incidents' },
'nav.policies': { it: 'Policy', en: 'Policies' },
+5 -5
View File
@@ -151,8 +151,8 @@
</div>
<!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). -->
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260630"></script>
<script src="/js/api.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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/kb.js"></script>
<script>
// Gate auth + chrome come le altre pagine -bi.
+1 -1
View File
@@ -515,7 +515,7 @@ body { background: var(--bg-main); }
<div id="toast"></div>
<script src="js/api.js?v=20260629"></script>
<script src="js/api.js?v=20260630"></script>
<script>
// ══════════════════════════════════════════════════════
// CONFIG
+2 -2
View File
@@ -94,8 +94,8 @@
</script>
<!-- Stessa logica della login attuale: ZERO modifiche backend -->
<script src="js/api.js?v=20260629"></script>
<script src="js/common.js?v=20260629"></script>
<script src="js/api.js?v=20260630"></script>
<script src="js/common.js?v=20260630"></script>
<script>
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
+424
View File
@@ -0,0 +1,424 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<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">
<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; }
.mr-toolbar { display:flex; justify-content:flex-end; margin-bottom:12px; }
.mr-table { width:100%; border-collapse:collapse; font-size:.88rem; }
.mr-table th, .mr-table td { text-align:left; padding:9px 12px; border-top:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
.mr-table th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
.mr-table tr.click { cursor:pointer; } .mr-table tr.click:hover { background:#f8fafc; }
.mr-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; white-space:nowrap; }
.mr-badge.s-draft { background:#f3f4f6; color:#6b7280; }
.mr-badge.s-approved { background:#dcfce7; color:#166534; }
.mr-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:820px; 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; flex-wrap:wrap; }
.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 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:150px; }
.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; }
.mr-section { border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; margin-bottom:10px; }
.mr-section h4 { margin:0; padding:10px 14px; font-size:.86rem; background:#f8fafc; border-bottom:1px solid var(--gray-100,#f3f4f6); }
.mr-section .body { padding:10px 14px; font-size:.84rem; }
.mr-section .body ul { margin:6px 0 0; padding-left:18px; }
.mr-section.na h4 { color:#9ca3af; }
.mr-pill { display:inline-block; background:#eef2ff; color:#3730a3; font-size:.72rem; font-weight:700; padding:1px 8px; border-radius:6px; margin-left:6px; }
.dec-row { display:flex; gap:8px; align-items:flex-start; margin-bottom:8px; flex-wrap:wrap; }
.dec-row .grow { flex:1 1 200px; } .dec-row select, .dec-row input[type=date] { padding:7px 9px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.84rem; }
.dec-del { background:none; border:1px solid #fecaca; color:#b91c1c; border-radius:6px; padding:6px 9px; cursor:pointer; }
.dec-stat { font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; }
.dec-open { background:#f3f4f6; color:#6b7280; } .dec-in_progress { background:#fef3c7; color:#92400e; } .dec-done { background:#dcfce7; color:#166534; }
</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="mreview.title">Riesame di Direzione</h2>
</header>
<div class="content-body">
<div class="mr-intro">
<strong>Riesame periodico del SGSI da parte della direzione.</strong>
Produce il <em>verbale</em> con gli elementi in ingresso (aggregati automaticamente dai moduli esistenti) e in uscita
(le <em>decisioni</em>: azioni con responsabile e scadenza). Una volta approvato, il verbale viene congelato e reso immutabile.
</div>
<div class="mr-note">
Ancoraggio: <strong>ISO/IEC 27001 §9.3</strong> (riesame di direzione) e buona prassi di governance NIS2 (<strong>GV.PO-02</strong>,
art. 23 D.Lgs. 138/2024). Strumento di supporto e pre-audit: non sostituisce l'auditor.
</div>
<div class="mr-toolbar"><button class="btn btn-primary btn-sm" onclick="mrOpen()">+ Nuovo riesame</button></div>
<div id="mr-list" aria-live="polite"></div>
</div>
</main>
</div>
<!-- Modale dettaglio / verbale -->
<div class="sm-overlay" id="mr-modal" role="dialog" aria-modal="true" aria-labelledby="mr-modal-title">
<div class="sm-dialog">
<div class="sm-head"><h3 id="mr-modal-title">Nuovo riesame</h3><button class="sm-close" type="button" aria-label="Chiudi" onclick="mrClose()">&times;</button></div>
<div class="sm-body">
<input type="hidden" id="mr-id">
<div class="sm-row">
<div class="sm-field"><label for="mr-date">Data riesame</label><input type="date" id="mr-date"></div>
<div class="sm-field"><label for="mr-period">Periodo di riferimento <span class="hint">(es. 2026 H1)</span></label><input type="text" id="mr-period" maxlength="100"></div>
</div>
<div class="sm-field"><label for="mr-chair">Presidente del riesame</label><select id="mr-chair"><option value="">— Nessuno —</option></select></div>
<div class="sm-field"><label for="mr-attendees">Partecipanti <span class="hint">(uno per riga: Nome — ruolo)</span></label><textarea id="mr-attendees" placeholder="Mario Rossi — CISO&#10;Anna Bianchi — DPO"></textarea></div>
<div class="sm-field">
<label>Elementi in ingresso (§9.3.2)</label>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">
<button class="btn btn-outline btn-sm" type="button" id="mr-gather-btn" onclick="mrGather()">Aggrega dati</button>
<span class="hint" id="mr-gather-info"></span>
</div>
<div id="mr-inputs"><div class="mr-empty">Premi "Aggrega dati" per raccogliere automaticamente gli input dai moduli.</div></div>
</div>
<div class="sm-field"><label for="mr-conclusions">Conclusioni</label><textarea id="mr-conclusions" style="min-height:90px;"></textarea></div>
<div class="sm-field" id="mr-dec-wrap">
<label>Decisioni e azioni (elementi in uscita §9.3.3)</label>
<div id="mr-decisions"></div>
<button class="btn btn-outline btn-sm" type="button" id="mr-dec-add" onclick="decAdd()">+ Aggiungi decisione</button>
</div>
<div class="sm-err" id="mr-err" role="alert"></div>
</div>
<div class="sm-foot">
<a class="btn btn-outline hidden" id="mr-report-link" href="#" target="_blank" rel="noopener">Verbale (stampabile)</a>
<button class="btn btn-outline hidden" id="mr-approve" type="button" onclick="mrApprove()">Approva e congela</button>
<button class="btn btn-outline" type="button" onclick="mrClose()">Chiudi</button>
<button class="btn btn-primary" type="button" id="mr-save" onclick="mrSave()">Salva</button>
</div>
</div>
</div>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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>
'use strict';
/*
* NIS2 Agile - Riesame di Direzione (ISO 27001 §9.3) — Modulo B
* Lista riesami + form verbale: input aggregati (gather), decisioni, draft→approvato, report.
*/
let MR = { reviews: [], members: [], roles: [], detail: null, gather: null, decisions: [] };
const ST_LABELS = { draft: 'Bozza', approved: 'Approvato' };
const DEC_LABELS = { open: 'Aperta', in_progress: 'In corso', done: 'Conclusa' };
const SEC_TITLES = {
corrective_actions: 'Azioni dal riesame precedente / CAPA aperte',
stakeholders: 'Contesto e parti interessate',
internal_audits: 'Risultati audit interni',
nonconformities: 'Non conformità e azioni correttive',
compliance_score: 'Monitoraggio (avanzamento SoA / KPI)',
objectives: 'Obiettivi del SGSI',
risks: 'Rischi e stato del trattamento',
normative: 'Aggiornamenti normativi non riscontrati',
training: 'Formazione e consapevolezza',
upcoming_deadlines: 'Scadenze imminenti (90 gg)'
};
function el(id) { return document.getElementById(id); }
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
function escAttr(s) { return esc(s).replace(/"/g, '&quot;'); }
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') mrClose(); });
const ov = el('mr-modal'); if (ov) ov.addEventListener('click', e => { if (e.target === ov) mrClose(); });
await mrLoadAll();
});
async function mrLoadAll() {
el('mr-list').innerHTML = '<div class="mr-empty">Caricamento…</div>';
try {
const [revs, members, roles] = await Promise.all([
api.mgmtReviewList(),
safe(() => api.orgMembers ? api.orgMembers() : null),
safe(() => api.orgRolesList())
]);
MR.reviews = (revs && revs.reviews) || [];
MR.members = membersFrom(members);
MR.roles = (roles && roles.roles) || (roles && roles.org_roles) || [];
renderList();
} catch (e) {
el('mr-list').innerHTML = '<div class="mr-empty">Errore: ' + esc(e.message || e) + '</div>';
}
}
async function safe(fn) { try { return await fn(); } catch (e) { return null; } }
function membersFrom(m) {
if (!m) return [];
const arr = m.members || m.users || m.items || (Array.isArray(m) ? m : []);
return (arr || []).map(u => ({ id: u.user_id || u.id, name: u.full_name || u.name || u.email || ('Utente ' + (u.user_id || u.id)) }));
}
function renderList() {
if (!MR.reviews.length) { el('mr-list').innerHTML = '<div class="mr-empty">Nessun riesame. Creane uno con "+ Nuovo riesame".</div>'; return; }
const rows = MR.reviews.map(r =>
'<tr class="click" onclick="mrOpen(' + r.id + ')">'
+ '<td><strong>' + esc(r.code || '—') + '</strong></td>'
+ '<td>' + esc(r.review_date || '—') + '</td>'
+ '<td>' + esc(r.period_label || '—') + '</td>'
+ '<td>' + esc(r.chair_name || '—') + '</td>'
+ '<td>' + r.n_done + '/' + r.n_decisions + '</td>'
+ '<td><span class="mr-badge s-' + r.status + '">' + (ST_LABELS[r.status] || r.status) + '</span></td>'
+ '</tr>'
).join('');
el('mr-list').innerHTML = '<table class="mr-table"><thead><tr>'
+ '<th>Codice</th><th>Data</th><th>Periodo</th><th>Presidente</th><th>Decisioni</th><th>Stato</th>'
+ '</tr></thead><tbody>' + rows + '</tbody></table>';
}
function fillChair(selectedId) {
el('mr-chair').innerHTML = '<option value="">— Nessuno —</option>'
+ MR.members.map(u => '<option value="' + u.id + '"' + (String(u.id) === String(selectedId) ? ' selected' : '') + '>' + esc(u.name) + '</option>').join('');
}
function roleOptions(selectedId) {
return '<option value="">— Nessun ruolo —</option>'
+ MR.roles.map(r => '<option value="' + r.id + '"' + (String(r.id) === String(selectedId) ? ' selected' : '') + '>' + esc(r.role_name || r.name) + '</option>').join('');
}
async function mrOpen(id) {
el('mr-id').value = id || '';
el('mr-err').textContent = '';
el('mr-gather-info').textContent = '';
MR.detail = null; MR.gather = null; MR.decisions = [];
let r = null;
if (id) { try { r = await api.mgmtReviewGet(id); MR.detail = r; } catch (e) { el('mr-err').textContent = e.message; } }
el('mr-modal-title').textContent = id ? ('Riesame ' + (r ? (r.code || '') : '')) : 'Nuovo riesame';
el('mr-date').value = r ? (r.review_date || '') : '';
el('mr-period').value = r ? (r.period_label || '') : '';
fillChair(r ? r.chair_user_id : '');
el('mr-attendees').value = r && r.attendees ? r.attendees.map(a => a.name + (a.role ? ' — ' + a.role : '')).join('\n') : '';
el('mr-conclusions').value = r ? (r.conclusions || '') : '';
MR.gather = r && r.snapshot ? r.snapshot : null;
renderInputs();
MR.decisions = r && r.decisions ? r.decisions.slice() : [];
renderDecisions();
const approved = r && r.status === 'approved';
const fields = ['mr-date','mr-period','mr-chair','mr-attendees','mr-conclusions'];
fields.forEach(f => { el(f).disabled = !!approved; });
el('mr-save').classList.toggle('hidden', !!approved);
el('mr-gather-btn').classList.toggle('hidden', !!approved);
el('mr-dec-add').classList.toggle('hidden', !!approved);
el('mr-approve').classList.toggle('hidden', !id || !!approved);
const rl = el('mr-report-link');
if (id) {
const orgQ = api && api.orgId ? ('&org_id=' + encodeURIComponent(api.orgId)) : '';
rl.href = (api.baseUrl || '/api') + '/management-reviews/' + id + '/report?token=' + encodeURIComponent(token()) + orgQ;
rl.classList.remove('hidden');
} else { rl.classList.add('hidden'); }
el('mr-modal').classList.add('open');
setTimeout(function () { var fe = el('mr-modal').querySelector('input,select,textarea'); if (fe) fe.focus(); }, 50);
}
function mrClose() { el('mr-modal').classList.remove('open'); }
function token() { try { return (api && api.token) || localStorage.getItem('nis2_access_token') || ''; } catch (e) { return ''; } }
function renderInputs() {
const sections = (MR.gather && MR.gather.sections) || null;
if (!sections) { el('mr-inputs').innerHTML = '<div class="mr-empty">Nessun dato aggregato. Premi "Aggrega dati".</div>'; return; }
let html = '';
Object.keys(SEC_TITLES).forEach(key => {
const sec = sections[key];
const na = !sec || !sec.available;
html += '<div class="mr-section' + (na ? ' na' : '') + '"><h4>' + esc(SEC_TITLES[key]) + (na ? '<span class="mr-pill">n/d</span>' : '') + '</h4>';
html += '<div class="body">';
if (na) { html += 'Dato non disponibile.'; }
else {
html += esc(summarize(key, sec));
const items = sec.items || [];
if (items.length) {
html += '<ul>' + items.slice(0, 8).map(it => {
const label = it.title || it.code || '';
const extra = ['code','status','severity','due_date','next_review_date','impact','reference']
.filter(f => it[f] && it[f] !== label).map(f => esc(it[f])).join(' · ');
return '<li>' + esc(label) + (extra ? ' <span class="hint">(' + extra + ')</span>' : '') + '</li>';
}).join('') + (items.length > 8 ? '<li class="hint">… e altri ' + (items.length - 8) + '</li>' : '') + '</ul>';
}
}
html += '</div></div>';
});
el('mr-inputs').innerHTML = html;
}
function summarize(key, sec) {
switch (key) {
case 'nonconformities': return (sec.open_count || 0) + ' non conformità aperte.';
case 'corrective_actions': return (sec.open_count || 0) + ' azioni correttive in corso.';
case 'internal_audits': return (sec.total || 0) + ' audit interni (' + (sec.completed || 0) + ' completati).';
case 'risks': { const t = sec.treatments || {}; return (sec.total || 0) + ' rischi; trattamenti ' + (t.completed || 0) + ' completati, ' + (t.overdue || 0) + ' in ritardo.'; }
case 'compliance_score': return 'Avanzamento medio SoA: ' + (sec.soa_avg_implementation != null ? sec.soa_avg_implementation + '%' : 'n/d') + '.';
case 'objectives': return (sec.count || 0) + ' obiettivi SGSI.';
case 'training': return (sec.completed || 0) + '/' + (sec.total || 0) + ' completate, ' + (sec.overdue || 0) + ' in ritardo.';
case 'stakeholders': return (sec.total || 0) + ' attività stakeholder (' + (sec.completed || 0) + ' completate).';
case 'normative': return (sec.pending_count || 0) + ' aggiornamenti normativi da riscontrare.';
case 'upcoming_deadlines': return (sec.count || 0) + ' scadenze nei prossimi 90 giorni.';
}
return '';
}
async function mrGather() {
const btn = el('mr-gather-btn'); btn.disabled = true;
el('mr-gather-info').textContent = 'Aggregazione in corso…';
try {
MR.gather = await api.mgmtReviewGather();
renderInputs();
el('mr-gather-info').textContent = 'Dati aggregati: ' + ((MR.gather && MR.gather.generated_at) || '');
} catch (e) {
el('mr-gather-info').textContent = 'Errore: ' + (e.message || e);
} finally { btn.disabled = false; }
}
// ── Decisioni ──
function renderDecisions() {
const wrap = el('mr-decisions');
const approved = MR.detail && MR.detail.status === 'approved';
if (!MR.decisions.length) { wrap.innerHTML = '<div class="mr-empty">Nessuna decisione. Aggiungine una.</div>'; return; }
wrap.innerHTML = MR.decisions.map((d, i) => {
if (approved) {
return '<div class="dec-row"><div class="grow">' + esc(d.decision) + '</div>'
+ '<span class="dec-stat dec-' + d.status + '">' + (DEC_LABELS[d.status] || d.status) + '</span>'
+ (d.owner_role_name ? '<span class="hint">' + esc(d.owner_role_name) + '</span>' : '')
+ (d.due_date ? '<span class="hint">' + esc(d.due_date) + '</span>' : '')
+ (d.capa_code ? '<span class="mr-pill">' + esc(d.capa_code) + '</span>' : '') + '</div>';
}
return '<div class="dec-row" data-i="' + i + '">'
+ '<input type="text" class="grow" value="' + escAttr(d.decision) + '" oninput="decEdit(' + i + ',\'decision\',this.value)" placeholder="Decisione…">'
+ '<select onchange="decEdit(' + i + ',\'owner_role_id\',this.value)">' + roleOptions(d.owner_role_id) + '</select>'
+ '<input type="date" value="' + escAttr(d.due_date || '') + '" onchange="decEdit(' + i + ',\'due_date\',this.value)">'
+ '<select onchange="decEdit(' + i + ',\'status\',this.value)">'
+ ['open','in_progress','done'].map(s => '<option value="' + s + '"' + (d.status === s ? ' selected' : '') + '>' + DEC_LABELS[s] + '</option>').join('')
+ '</select>'
+ '<button class="dec-del" type="button" onclick="decDel(' + i + ')" aria-label="Rimuovi">&times;</button>'
+ '</div>';
}).join('');
}
function decAdd() {
if (!el('mr-id').value) { el('mr-err').textContent = 'Salva prima il riesame, poi aggiungi le decisioni.'; return; }
MR.decisions.push({ id: null, decision: '', owner_role_id: null, due_date: '', status: 'open', capa_code: null, _new: true });
renderDecisions();
}
function decEdit(i, field, val) { if (MR.decisions[i]) { MR.decisions[i][field] = val; MR.decisions[i]._dirty = true; } }
function decDel(i) {
const d = MR.decisions[i]; if (!d) return;
// Niente endpoint di cancellazione lato server: si rimuovono solo le decisioni NON ancora salvate.
if (d.id) { el('mr-err').textContent = 'Una decisione salvata non può essere cancellata: impostala su "Conclusa".'; return; }
MR.decisions.splice(i, 1); renderDecisions();
}
async function mrSave() {
const id = el('mr-id').value;
const payload = {
review_date: el('mr-date').value || null,
period_label: el('mr-period').value.trim() || null,
chair_user_id: el('mr-chair').value ? parseInt(el('mr-chair').value, 10) : null,
attendees: parseAttendees(el('mr-attendees').value),
conclusions: el('mr-conclusions').value.trim() || null
};
if (MR.gather) payload.snapshot = MR.gather;
const btn = el('mr-save'); btn.disabled = true;
try {
let reviewId;
if (id) { await api.mgmtReviewUpdate(parseInt(id, 10), payload); reviewId = parseInt(id, 10); }
else { const r = await api.mgmtReviewCreate(payload); reviewId = r.id; el('mr-id').value = reviewId; }
await syncDecisions(reviewId);
showNotification(id ? 'Riesame aggiornato.' : 'Riesame creato.', 'success');
await mrLoadAll();
await mrOpen(reviewId);
} catch (e) {
el('mr-err').textContent = e.message || String(e);
} finally { btn.disabled = false; }
}
async function syncDecisions(reviewId) {
for (const d of MR.decisions) {
const body = { decision: (d.decision || '').trim(), owner_role_id: d.owner_role_id ? parseInt(d.owner_role_id, 10) : null, due_date: d.due_date || null, status: d.status || 'open' };
if (!body.decision) continue;
if (d.id) { if (d._dirty) await api.mgmtReviewUpdateDecision(d.id, body); }
else { await api.mgmtReviewAddDecision(reviewId, body); }
}
}
function parseAttendees(text) {
return (text || '').split('\n').map(l => l.trim()).filter(Boolean).map(l => {
const parts = l.split('—');
if (parts.length >= 2) return { name: parts[0].trim(), role: parts.slice(1).join('—').trim() };
const d = l.split(' - ');
if (d.length >= 2) return { name: d[0].trim(), role: d.slice(1).join(' - ').trim() };
return { name: l };
});
}
async function mrApprove() {
const id = el('mr-id').value; if (!id) return;
if (!confirm('Approvare e congelare il verbale? Dopo l\'approvazione non sarà più modificabile.')) return;
const btn = el('mr-approve'); btn.disabled = true;
try {
// salva eventuali modifiche pendenti prima del congelamento
await mrSilentSave(parseInt(id, 10));
await api.mgmtReviewApprove(parseInt(id, 10));
showNotification('Riesame approvato e congelato.', 'success');
await mrLoadAll();
await mrOpen(parseInt(id, 10));
} catch (e) {
el('mr-err').textContent = e.message || String(e);
} finally { btn.disabled = false; }
}
async function mrSilentSave(reviewId) {
const payload = {
review_date: el('mr-date').value || null,
period_label: el('mr-period').value.trim() || null,
chair_user_id: el('mr-chair').value ? parseInt(el('mr-chair').value, 10) : null,
attendees: parseAttendees(el('mr-attendees').value),
conclusions: el('mr-conclusions').value.trim() || null
};
if (MR.gather) payload.snapshot = MR.gather;
await api.mgmtReviewUpdate(reviewId, payload);
await syncDecisions(reviewId);
}
</script>
</body>
</html>
+6 -6
View File
@@ -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=20260629">
<link rel="stylesheet" href="/css/style.css?v=20260630">
<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=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
'use strict';
/*
+1 -1
View File
@@ -424,6 +424,6 @@ curl https://nis2.agile.software/api/services/status</pre>
</div>
</div>
<script src="js/common.js?v=20260629"></script>
<script src="js/common.js?v=20260630"></script>
</body>
</html>
+5 -5
View File
@@ -112,8 +112,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
+2 -2
View File
@@ -494,8 +494,8 @@
</script>
<!-- Stessa logica della onboarding.html: ZERO modifiche backend -->
<script src="js/api.js?v=20260629"></script>
<script src="js/common.js?v=20260629"></script>
<script src="js/api.js?v=20260630"></script>
<script src="js/common.js?v=20260630"></script>
<script>
// ── Auth check ──────────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+5 -5
View File
@@ -138,12 +138,12 @@
</div>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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/organigramma.js?v=20260617"></script>
</body>
</html>
+5 -5
View File
@@ -333,8 +333,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+5 -5
View File
@@ -131,12 +131,12 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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/raci.js?v=20260619"></script>
</body>
</html>
+2 -2
View File
@@ -268,8 +268,8 @@
</script>
<!-- Stessa logica della register.html: ZERO modifiche backend -->
<script src="js/api.js?v=20260629"></script>
<script src="js/common.js?v=20260629"></script>
<script src="js/api.js?v=20260630"></script>
<script src="js/common.js?v=20260630"></script>
<script>
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
+5 -5
View File
@@ -443,8 +443,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+5 -5
View File
@@ -141,12 +141,12 @@
</div>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
'use strict';
/*
+5 -5
View File
@@ -494,8 +494,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
// ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+5 -5
View File
@@ -672,8 +672,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+2 -2
View File
@@ -165,8 +165,8 @@
</main>
</div>
<script src="js/api.js?v=20260629"></script>
<script src="js/common.js?v=20260629"></script>
<script src="js/api.js?v=20260630"></script>
<script src="js/common.js?v=20260630"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+6 -6
View File
@@ -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=20260629">
<link rel="stylesheet" href="/css/style.css?v=20260630">
<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=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
'use strict';
/*
+6 -6
View File
@@ -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=20260629">
<link rel="stylesheet" href="/css/style.css?v=20260630">
<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=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
'use strict';
/*
+5 -5
View File
@@ -477,8 +477,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+1 -1
View File
@@ -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.21.3';
const CACHE = 'nis2-shell-v1.22.0';
const PRECACHE = [
'/offline.html',
+5 -5
View File
@@ -292,8 +292,8 @@
</main>
</div>
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
+1 -1
View File
@@ -1 +1 @@
{"version": "1.21.3", "build": "2026-06-16-v1.21.3", "date": "2026-06-16", "changelog": "Epic C / C5 chiusura open-items: aggancio invio email questionari/firma-lettura in send() (GATED dal kill-switch EMAIL_SENDING_ENABLED=false -> nessun invio finora, pronto per go-live; magic-link assoluto via APP_URL). nis2_sources.php: nota di provenienza 92 vs 87 (prevale dato referente Simon). [host] EMAIL_MS_URL spostato sul path interno; pre-equip PHP-CA DB confermato (TLS1.3 attivo). Additivo."}
{"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."}
+5 -5
View File
@@ -207,8 +207,8 @@
<!-- Report Detail Modal handled by common.js showModal -->
<script src="/js/api.js?v=20260629"></script>
<script src="/js/common.js?v=20260629"></script>
<script src="/js/api.js?v=20260630"></script>
<script src="/js/common.js?v=20260630"></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=20260629"></script>
<script src="/js/i18n.js?v=20260629"></script>
<script src="/js/help.js?v=20260629"></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>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
+3 -3
View File
@@ -319,9 +319,9 @@
</main>
</div>
<script src="js/api.js?v=20260629"></script>
<script src="js/common.js?v=20260629"></script>
<script src="js/i18n.js?v=20260629"></script>
<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>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();