Files
nis2-agile/public/js/common-bi.js
T
DevEnv nis2-agileandClaude Opus 4.8 4fdeae7cee [FEAT] Modulo Controlli periodici (control monitoring §9.1/A.8.16) — v1.23.0
Registro controlli ricorrenti (owner/frequenza/metodo/controllo SoA-NIS2 collegato)
+ registro esecuzioni come EVIDENZA (data/esito/note) con avanzamento automatico
della scadenza; da esito non conforme generazione NC o NC+azione correttiva
(riuso non_conformities/capa_actions, nuova source 'monitoring'); report
scheda+storico stampabile; scadenze nel Calendario unico.

mig.057: periodic_controls + periodic_control_executions (FK CASCADE, UNIQUE
(organization_id, code)). PeriodicControlController (slug periodic-controls) +
controlli-periodici.html + CalendarController.srcPeriodicControls.

Fix da flotta di verifica avversariale (9 finding confermati):
- advance(): correzione overflow strtotime '+1 month' (31/01 -> 28/02, no salto
  mese) via DateTimeImmutable con clamp all'ultimo giorno valido (mensile/
  trimestrale/semestrale/annuale); giornaliero/settimanale/custom invariati.
- create(): retry-on-duplicate del codice CTL-NNN con UNIQUE (org_id, code).
- update(): array_key_exists per azzerare owner_role_id / next_due_date.
- delete(): rimossa cleanup morta su review_schedule (il calendario legge diretto).
- CalendarController.srcReviewSchedule: esclude internal_audit/stakeholder_activity
  (gia' con sorgente diretta) -> fix doppia comparsa nel calendario.
- calendario.html: TYPE_META 'periodic_control' (chip filtro + label + colore).

Additivo. Cache-buster ?v= bump (sidebar) + sw cache nis2-shell-v1.23.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:21:03 +02:00

218 lines
19 KiB
JavaScript

/* ═══════════════════════════════════════════════════════════════════
* common-bi.js — Override Bootstrap Italia per la chrome (sidebar)
* ───────────────────────────────────────────────────────────────────
* PILOTA "App-pages onda B" (T5).
*
* Regole rispettate:
* - ADDITIVO: NON sovrascrive common.js. Va caricato DOPO common.js e
* ridefinisce SOLO loadSidebar() con markup Bootstrap Italia.
* - ZERO backend: riusa tutti gli helper di common.js (_loadUserInfo,
* _setupMobileToggle, _loadVersionFooter, _loadFirmBranding,
* _renderImpersonateBanner, switchLang, _getSavedLang, icon*()).
* - Stessi ID DOM della sidebar originale (sidebar-user-name,
* sidebar-user-avatar, sidebar-user-role, sidebar-org-switcher,
* sidebar-version, .lang-btn) → gli helper esistenti funzionano
* invariati senza toccare api.js.
* - Accessibilità: <nav aria-label>, link active con aria-current,
* bottoni con aria-label, focus nativo BI.
* - I link puntano alle varianti ".html" quando esistono, con
* fallback all'originale per le pagine non ancora convertite.
* ═══════════════════════════════════════════════════════════════════ */
(function () {
'use strict';
// Pagine già convertite a Bootstrap Italia in questa onda (+ login).
// Per queste la navigazione resta in contesto "-bi". Le altre puntano
// ancora all'originale (parità funzionale, nessun link rotto).
var BI_PAGES = {
'login.html': 'login.html',
'dashboard.html': 'dashboard.html',
'assessment.html': 'assessment.html',
'isms.html': 'isms.html',
'risks.html': 'risks.html',
'incidents.html': 'incidents.html',
'policies.html': 'policies.html',
'supply-chain.html': 'supply-chain.html',
'training.html': 'training.html',
'assets.html': 'assets.html',
'reports.html': 'reports.html',
'settings.html': 'settings.html',
'whistleblowing.html': 'whistleblowing.html',
'organigramma.html': 'organigramma.html',
'competenze.html': 'competenze.html'
};
function biHref(href) {
return BI_PAGES[href] || href;
}
function loadSidebarBI() {
var container = document.getElementById('sidebar');
if (!container) return;
// La pagina corrente può essere sia "x.html" sia "x.html":
// normalizziamo togliendo il suffisso -bi per l'evidenziazione active.
var raw = window.location.pathname.split('/').pop() || 'dashboard.html';
var currentBase = raw.replace(/-bi\.html$/, '.html');
// Stesso albero di navigazione di common.js (icone riusate dagli
// helper globali icon*()). label/href/i18nKey identici.
var navItems = [
{
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' },
{ name: 'Modello SGSI (ISO 27001)', href: 'isms.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>', i18nKey: 'nav.isms' }
]
},
{
label: 'Gestione', i18nKey: 'nav.management',
items: [
{ name: 'Organigramma', href: 'organigramma.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M8 2.5h4a.5.5 0 01.5.5v2a.5.5 0 01-.5.5h-1.25v2.5H15a.5.5 0 01.5.5v1.25H17a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-4a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1.25V9.5H6v1.25H7.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-4a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H4.5V9.5a.5.5 0 01.5-.5h4.75V6H8a.5.5 0 01-.5-.5V3a.5.5 0 01.5-.5z"/></svg>', i18nKey: 'nav.org_chart' },
{ name: 'Competenze', href: 'competenze.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 11-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 8.134a1 1 0 010 1.732l-3.354.934-1.18 4.455a1 1 0 01-1.933 0L9.854 10.8 6.5 9.866a1 1 0 010-1.732l3.354-.934 1.179-4.456A1 1 0 0112 2z"/></svg>', i18nKey: 'nav.competences' },
{ name: 'Rischi', href: 'risks.html', icon: iconShieldExclamation(), i18nKey: 'nav.risks' },
{ name: 'Incidenti', href: 'incidents.html', icon: iconBell(), i18nKey: 'nav.incidents' },
{ name: 'Policy', href: 'policies.html', icon: iconDocumentText(), i18nKey: 'nav.policies' },
{ name: 'Supply Chain', href: 'supply-chain.html', icon: iconLink(), i18nKey: 'nav.supply_chain' },
{ name: 'Stakeholder', href: 'stakeholders.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"/><path d="M5.5 7.5a2 2 0 11-2.999.001A2 2 0 015.5 7.5zM17.5 7.5a2 2 0 11-2.999.001A2 2 0 0117.5 7.5z"/></svg>', i18nKey: 'nav.stakeholders' },
{ name: 'Attività stakeholder', href: 'stakeholder-activities.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" clip-rule="evenodd"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/></svg>', i18nKey: 'nav.stk_activities' },
{ name: 'Segnalazioni', href: 'whistleblowing.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>' },
{ name: 'Normative', href: 'normative.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-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>' },
{ name: 'AI Cross-Analysis', href: 'cross-analysis.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"/></svg>' },
{ name: 'Knowledge Base', href: 'kb.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z"/></svg>' }
]
},
{
label: 'Operativo', i18nKey: 'nav.operations',
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: 'Controlli periodici', href: 'controlli-periodici.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.5 2.5a1 1 0 001.414-1.414L11 9.586V6z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.periodic_controls' },
{ name: 'Riesame di Direzione', href: 'management-review.html', icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>`, i18nKey: 'nav.management_review' },
{ name: 'Audit & Report', href: 'reports.html', icon: iconChartBar(), i18nKey: 'nav.audit' }
]
},
{
label: 'Sistema', i18nKey: 'nav.system',
items: [
{ name: 'Guida all\'uso', href: 'guida.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>' },
{ name: 'Impostazioni', href: 'settings.html', icon: iconCog(), i18nKey: 'nav.settings' },
{ name: 'Architettura', href: 'architecture.html', icon: iconCubeTransparent() },
{ name: 'Simulazione Demo', href: 'simulate.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd"/></svg>' },
{ name: 'Integrazioni Esterne', href: 'integrazioniext.html', icon: '<svg viewBox="0 0 20 20" fill="currentColor"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"/><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"/></svg>' }
]
}
];
var html = '';
// Brand
html += '' +
'<div class="sidebar-brand">' +
'<div class="sidebar-brand-icon">' +
'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/><path d="M10 12.5l-2-2-1.41 1.41L10 15.32l5.41-5.41L14 8.5l-4 4z"/></svg>' +
'</div>' +
'<div>' +
'<h1 class="sidebar-logo-text">NIS2 Agile</h1>' +
'<span>Compliance Platform</span>' +
'</div>' +
'</div>';
// Org-switcher (popolato async per consulenti — stesso id)
html += '<div id="sidebar-org-switcher" style="display:none;padding:8px 12px;border-bottom:1px solid var(--gray-100);"></div>';
// Nav — markup BI: <nav> con aria-label, link con aria-current
html += '<nav class="sidebar-nav" aria-label="Navigazione principale">';
navItems.forEach(function (section) {
var sectionI18n = section.i18nKey ? ' data-i18n="' + section.i18nKey + '"' : '';
html += '<div class="sidebar-nav-label"' + sectionI18n + '>' + section.label + '</div>';
section.items.forEach(function (item) {
var isActive = currentBase === item.href;
var cls = isActive ? 'active' : '';
var aria = isActive ? ' aria-current="page"' : '';
var spanI18n = item.i18nKey ? ' data-i18n="' + item.i18nKey + '"' : '';
html += '<a href="' + biHref(item.href) + '" class="' + cls + '"' + aria + '>' +
item.icon + '<span' + spanI18n + '>' + item.name + '</span>' +
'</a>';
});
});
html += '</nav>';
// Footer: lang toggle + user info + version (stessi id degli helper)
html += '' +
'<div class="sidebar-footer">' +
'<div class="sidebar-lang-toggle" id="sidebar-lang-toggle" role="group" aria-label="Lingua">' +
'<button class="lang-btn ' + (_getSavedLang() === 'it' ? 'active' : '') + '" onclick="switchLang(\'it\')" title="Italiano" aria-label="Italiano">IT</button>' +
'<button class="lang-btn ' + (_getSavedLang() === 'en' ? 'active' : '') + '" onclick="switchLang(\'en\')" title="English" aria-label="English">EN</button>' +
'</div>' +
'<div class="sidebar-user">' +
'<div class="sidebar-user-avatar" id="sidebar-user-avatar" aria-hidden="true">--</div>' +
'<div class="sidebar-user-info">' +
'<div class="sidebar-user-name" id="sidebar-user-name">Caricamento...</div>' +
'<div class="sidebar-user-role" id="sidebar-user-role">Utente</div>' +
'</div>' +
'<button class="sidebar-logout-btn" onclick="api.logout()" title="Esci" aria-label="Esci dall\'applicazione">' +
'<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M3 3a1 1 0 00-1 1v12a1 1 0 001 1h6a1 1 0 100-2H4V5h5a1 1 0 100-2H3zm11.707 3.293a1 1 0 010 1.414L12.414 10l2.293 2.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z" clip-rule="evenodd"/><path fill-rule="evenodd" d="M16 10a1 1 0 00-1-1H8a1 1 0 100 2h7a1 1 0 001-1z" clip-rule="evenodd"/></svg>' +
'</button>' +
'</div>' +
'<div class="sidebar-version" id="sidebar-version" title="Versione applicazione" tabindex="0" role="button"' +
' style="text-align:center; padding:6px 12px; font-size:.68rem; color:var(--gray-400); cursor:pointer; border-top:1px solid var(--gray-100); margin-top:4px;"' +
' onclick="_showVersionChangelog()" onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();_showVersionChangelog();}">' +
'v—' +
'</div>' +
'</div>';
container.innerHTML = html;
// Riuso integrale degli helper di common.js
if (typeof _loadUserInfo === 'function') _loadUserInfo();
if (typeof _setupMobileToggle === 'function') _setupMobileToggle();
if (typeof _loadVersionFooter === 'function') _loadVersionFooter();
if (typeof _loadFirmBranding === 'function') _loadFirmBranding();
if (typeof _renderImpersonateBanner === 'function') _renderImpersonateBanner();
}
// Override globale: le pagine -bi chiamano loadSidebar() come le originali.
window.loadSidebar = loadSidebarBI;
})();
// ── Avatar di prodotto: hook demo-mode (product-demo-protocol v1.0.1) ──
// Solo in presenza di ?demo=<session_id> inietta gli asset demo (no-op per utenti normali).
(function () {
try {
if (!new URLSearchParams(window.location.search).get('demo')) return;
// Auto-auth SINCRONA: il demo_jwt arriva nel fragment (#djwt=) dello spa_iframe_url.
// Lo impostiamo come token API PRIMA che la pagina chiami checkAuth() (common-bi.js gira
// prima dello script inline della pagina). Origin demo separato → nessuna pollution di sessioni reali.
try {
var h = new URLSearchParams((window.location.hash || '').replace(/^#/, ''));
var djwt = h.get('djwt');
if (djwt) {
localStorage.setItem('nis2_access_token', djwt);
localStorage.setItem('nis2_user_role', 'compliance_manager');
var orgv = null;
try {
var pl = JSON.parse(atob(djwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
if (pl && pl.org_id) { orgv = String(pl.org_id); localStorage.setItem('nis2_org_id', orgv); }
} catch (e2) { /* jwt opaco: org via credentials */ }
// CRITICO: api.js ha già creato l'istanza `api` leggendo localStorage VUOTO al load
// (api.js è incluso PRIMA di common-bi.js). Senza questo sync in-memory, checkAuth()
// vede api.token=null e redirige a login. Aggiorniamo l'istanza viva.
try { if (typeof api !== 'undefined' && api) { api.token = djwt; if (orgv) api.orgId = orgv; } } catch (e3) { /* noop */ }
}
} catch (e1) { /* noop */ }
var css = document.createElement('link'); css.rel = 'stylesheet'; css.href = '/css/demo.css?v=20260613'; document.head.appendChild(css);
var inject = function () {
var s1 = document.createElement('script'); s1.src = '/js/demo-selectors.js?v=20260613';
s1.onload = function () { var s2 = document.createElement('script'); s2.src = '/js/demo-mode.js?v=20260613'; document.body.appendChild(s2); };
document.body.appendChild(s1);
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', inject); else inject();
} catch (e) { /* noop */ }
})();