/* ═══════════════════════════════════════════════════════════════════════════ * topnav-v3.js — Chrome V3 (appbar + mega-menu) data-driven dalla mappa IA (§6). * ─────────────────────────────────────────────────────────────────────────── * Sostituisce la sidebar V2 (common-bi.js) con la top-nav del mockup * "docs/mockup Ui/index.html". REGOLE: * - SOLO UI. Riusa gli helper di common.js: _loadUserInfo, _loadVersionFooter, * _loadConsultantOrgSwitcher (via _loadUserInfo), _loadFirmBranding, * _renderImpersonateBanner, switchLang, _getSavedLang, api.logout. * - Stessi ID DOM degli helper V2 (#sidebar-user-name/-avatar/-role, * #sidebar-org-switcher, #sidebar-version, .lang-btn) → gli helper esistenti * funzionano invariati senza toccare api.js. * - Espone window.loadSidebar = loadTopnavV3 → l'init delle pagine resta invariato. * - I 3 FAB reali (campana notifiche + bug-reporter + ARIA) restano quelli di * common.js (qui NON si reimplementano: si applica solo lo stile .fab-stack/.fab * al trittico già montato, per fedeltà al mockup). * - aria-current sulla voce/gruppo attivo (da location.pathname). * - Icone Lucide via + lucide.createIcons() (chiamato dall'init pagina). * ═══════════════════════════════════════════════════════════════════════════ */ (function () { 'use strict'; // ── Mappa IA (§6 dello spec). 9 gruppi → voci/href reali. ─────────────── // top-level: voce diretta (href) oppure gruppo con mega-menu (cols). var NAV_GROUPS = [ { label: 'Dashboard', i18n: 'nav.dashboard', icon: 'layout-dashboard', href: 'dashboard.html' }, // Voce riservata a consulenti/super_admin — gate applicato in loadTopnavV3(). { label: 'Le mie aziende', i18n: 'nav.studio', icon: 'briefcase', href: 'companies.html', studioOnly: true }, { label: 'Calendario', i18n: 'nav.calendar', icon: 'calendar', href: 'calendario.html' }, { label: 'Stakeholders', icon: 'users', cols: [{ head: 'RELAZIONI', links: [ { b: 'Stakeholders', s: 'Soggetti interni ed esterni rilevanti', href: 'stakeholders.html', ic: 'handshake' }, { b: 'Supply-Chain', s: 'Rischio fornitori e terze parti', href: 'supply-chain.html', ic: 'link' }, { b: 'Attività su Stakeholders', s: 'Interazioni, richieste e impegni', href: 'stakeholder-activities.html', ic: 'activity' } ]}] }, { label: 'Struttura interna', icon: 'building-2', cols: [{ head: 'RUOLI E COMPETENZE', links: [ { b: 'Organigramma', s: 'Ruoli, deleghe e responsabilità', href: 'organigramma.html', ic: 'network' }, { b: 'Competenze', s: 'Skill matrix e copertura dei ruoli', href: 'competenze.html', ic: 'award' }, { b: 'GAP Analysis', s: 'Scostamento tra stato attuale e atteso', href: 'assessment.html', ic: 'git-compare' }, { b: 'Matrice RACI', s: 'Responsabilità per attività (R/A/C/I)', href: 'raci.html', ic: 'grid-3x3' }, { b: 'Formazione', s: 'Piani formativi e completamento', href: 'training.html', ic: 'graduation-cap' } ]}] }, { label: 'Rischi', icon: 'shield-alert', cols: [{ head: 'GESTIONE RISCHIO', links: [ { b: 'Mappa dei rischi', s: 'Registro rischi e piani di trattamento', href: 'risks.html', ic: 'map' }, { b: 'Inventario', s: 'Asset e perimetro tecnico', href: 'assets.html', ic: 'boxes' }, { b: 'Policy', s: 'Politiche approvate e in revisione', href: 'policies.html', ic: 'file-text' }, { b: 'Connettori Discovery', s: 'Scansione rete/cloud → inventario', href: 'connettori-discovery.html', ic: 'plug-zap' } ]}] }, { label: 'Monitoraggio', icon: 'radar', cols: [{ head: 'VIGILANZA E AUDIT', links: [ { b: 'Segnalazioni ricevute', s: 'Whistleblowing e segnalazioni interne', href: 'whistleblowing.html', ic: 'megaphone' }, { b: 'Audit interni', s: 'Programma e esiti degli audit', href: 'internal-audits.html', ic: 'clipboard-check' }, { b: 'Audit ACN', s: 'Verifiche e ispezioni dell’autorità', href: 'reports.html', ic: 'file-check' }, { b: 'Controlli periodici', s: 'Verifiche ricorrenti e check-list', href: 'controlli-periodici.html', ic: 'list-checks' }, { b: 'Riesame di Direzione', s: 'Esiti del riesame §9.3', href: 'management-review.html', ic: 'gauge' }, { b: 'Scadenziario', s: 'Revisioni e adempimenti ricorrenti', href: 'review-schedule.html', ic: 'calendar-clock' } ]}] }, { label: 'Lex e ISO', icon: 'scale', cols: [{ head: 'NORMATIVA E ISO', links: [ { b: 'Normative', s: 'Direttiva, decreto e riferimenti applicabili', href: 'normative.html', ic: 'scroll-text' }, { b: 'Modello SGSI (ISO 27001)', s: 'Sistema di gestione sicurezza informazioni', href: 'isms.html', ic: 'shield-check' }, { b: 'Misure e requisiti ACN', s: 'Misure tecniche e organizzative Art. 21', href: 'misure-requisiti.html', ic: 'list-checks' } ]}] }, { label: 'AI', icon: 'sparkles', cols: [{ head: 'ASSISTENTE', links: [ { b: 'AI Cross-Analysis', s: 'Analisi trasversale di rischi e misure', href: 'cross-analysis.html', ic: 'sparkles' }, { b: 'Knowledge Base', s: 'Procedure, linee guida e riferimenti', href: 'kb.html', ic: 'book-open' } ]}] }, { label: 'Guida', icon: 'life-buoy', cols: [ { head: 'GUIDA', links: [ { b: 'Guida all’uso', s: 'Manuale e istruzioni della piattaforma', href: 'guida.html', ic: 'life-buoy' }, { b: 'Compliance Journey', s: 'Percorso guidato di conformità', href: 'workflow.html', ic: 'route' }, { b: 'Integrazioni Esterne', s: 'Connettori e servizi collegati', href: 'integrazioniext.html', ic: 'plug' } ]}, { head: 'SISTEMA', links: [ { b: 'Impostazioni', s: 'Configurazione del workspace', href: 'settings.html', ic: 'settings' }, { b: 'Architettura', s: 'Modello dati e struttura del sistema', href: 'architecture.html', ic: 'layers' }, { b: 'Simulazione Demo', s: 'Scenari guidati di prova', href: 'simulate.html', ic: 'play-circle' } ]} ] } ]; // ── i18n: traduzione voci nav (IT/EN) come nel V2. Chiave per href/gruppo. ── var I18N_BY_HREF = { 'dashboard.html': 'nav.dashboard', 'calendario.html': 'nav.calendar', 'stakeholders.html': 'nav.stakeholders', 'supply-chain.html': 'nav.supply_chain', 'stakeholder-activities.html': 'nav.stk_activities', 'organigramma.html': 'nav.org_chart', 'competenze.html': 'nav.competences', 'assessment.html': 'nav.gap_analysis', 'raci.html': 'nav.raci', 'training.html': 'nav.training', 'risks.html': 'nav.risks', 'assets.html': 'nav.assets', 'policies.html': 'nav.policies', 'connettori-discovery.html': 'nav.connectors', 'whistleblowing.html': 'nav.whistleblowing', 'internal-audits.html': 'nav.internal_audits', 'reports.html': 'nav.audit_acn', 'controlli-periodici.html': 'nav.periodic_controls', 'management-review.html': 'nav.management_review', 'review-schedule.html': 'nav.review_schedule', 'normative.html': 'nav.normative', 'isms.html': 'nav.isms', 'misure-requisiti.html': 'nav.framework', 'cross-analysis.html': 'nav.cross_analysis', 'kb.html': 'nav.kb', 'guida.html': 'nav.help', 'workflow.html': 'nav.workflow', 'integrazioniext.html': 'nav.integrations', 'settings.html': 'nav.settings', 'architecture.html': 'nav.architecture', 'simulate.html': 'nav.simulate' }; var GROUP_I18N = { 'Stakeholders': 'nav.grp_stakeholders', 'Struttura interna': 'nav.grp_internal', 'Rischi': 'nav.grp_risks', 'Monitoraggio': 'nav.grp_monitoring', 'Lex e ISO': 'nav.grp_lexiso', 'AI': 'nav.grp_ai', 'Guida': 'nav.grp_guide' }; function i18nAttr(key) { return key ? ' data-i18n="' + key + '"' : ''; } // data-nav = href senza .html → aggancio per il puntatore guida ARIA (aria-guide.js). function navAttr(href) { return ' data-nav="' + esc(String(href).replace(/\.html$/, '')) + '"'; } function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]; }); } function currentPage() { return (window.location.pathname.split('/').pop() || 'dashboard.html'); } // È attivo il gruppo che contiene la pagina corrente (direct href o una delle links). function isGroupActive(g, page) { if (g.href) return g.href === page; if (g.cols) { for (var i = 0; i < g.cols.length; i++) { var lk = g.cols[i].links; for (var j = 0; j < lk.length; j++) if (lk[j].href === page) return true; } } return false; } function renderMega(g) { var colsHtml = g.cols.map(function (col) { var links = col.links.map(function (l) { return '' + '' + '' + esc(l.b) + '' + esc(l.s) + ''; }).join(''); return '

' + esc(col.head) + '

' + links + '
'; }).join(''); return '
' + colsHtml + '
'; } function renderNavItem(g, page) { var active = isGroupActive(g, page); var cls = 'nav-link' + (active ? ' active' : ''); var aria = active ? ' aria-current="page"' : ''; var i18n = i18nAttr(g.i18n || GROUP_I18N[g.label]); if (g.href && !g.cols) { // Voce diretta (Dashboard, Calendario) return ''; } // Gruppo con mega-menu. Il link "testa" punta alla prima voce del gruppo. var firstHref = g.cols[0].links[0].href; return ''; } function loadTopnavV3() { // Container: usa #topnav se presente, altrimenti riusa #sidebar (compat), // altrimenti crea un host e lo prepende al body. var host = document.getElementById('topnav') || document.getElementById('sidebar'); if (!host) { host = document.createElement('div'); host.id = 'topnav'; document.body.insertBefore(host, document.body.firstChild); } // Se riusiamo #sidebar (aside), neutralizziamo eventuali stili layout V2. host.removeAttribute('class'); host.id = 'topnav'; var page = currentPage(); var lang = (typeof _getSavedLang === 'function') ? _getSavedLang() : 'it'; var navItems = NAV_GROUPS.map(function (g) { return renderNavItem(g, page); }).join(''); host.innerHTML = '
' + '
' + '' + 'N2' + '
NIS2 Agile' + 'Conformità Direttiva (UE) 2022/2555 · D.Lgs. 138/2024
' + '' + '' + // Org-switcher (consulenti / multi-org) — popolato da _loadUserInfo() via stesso id. '' + // Lingua IT/EN (riusa switchLang di common.js) '' + '' + '' + '' + 'Registra incidente' + // User menu (avatar+nome+ruolo via _loadUserInfo; click = menu) '' + '' + '
' + '' + '
'; // ── Gate voce "Le mie aziende": visibile solo a consulenti / super_admin ── try { var role = (typeof api !== 'undefined' && api.getUserRole) ? api.getUserRole() : null; if (['consultant', 'super_admin'].indexOf(role) === -1) { var studioLink = host.querySelector('#topnav-bar a[data-nav="companies"]'); var studioItem = studioLink && studioLink.closest('.nav-item'); if (studioItem) studioItem.remove(); } } catch (e) { /* in caso di errore lasciamo la voce (backend gate comunque presente) */ } // ── Riuso helper common.js (utente/avatar/ruolo + org-switcher + versione + branding + impersonate) ── if (typeof _loadUserInfo === 'function') _loadUserInfo(); if (typeof _loadVersionFooter === 'function') _loadVersionFooter(); if (typeof _loadFirmBranding === 'function') _loadFirmBranding(); if (typeof _renderImpersonateBanner === 'function') _renderImpersonateBanner(); // ── Menu utente: dropdown con versione + logout (riusa api.logout) ── _wireUserMenu(); // ── Mobile: toggle nav + accordion mega ── _wireMobileNav(); // ── Stile + montaggio del trittico FAB reale dentro .fab-stack ── _styleRealFabs(); // Lucide: prova subito (se già caricato) — l'init pagina lo richiama comunque. if (window.lucide && typeof lucide.createIcons === 'function') { try { lucide.createIcons(); } catch (e) {} } } // Dropdown del menu utente: versione (changelog) + logout. function _wireUserMenu() { var u = document.getElementById('topnav-user'); if (!u) return; var menu = document.createElement('div'); menu.id = 'topnav-user-menu'; menu.style.cssText = 'position:fixed;display:none;min-width:200px;background:#fff;color:var(--ink);' + 'border:1px solid var(--line);border-radius:10px;box-shadow:0 14px 36px rgba(11,40,80,.20);' + 'padding:6px;z-index:90'; menu.innerHTML = '' + '' + 'Impostazioni' + ''; document.body.appendChild(menu); function place() { var r = u.getBoundingClientRect(); menu.style.top = (r.bottom + 8) + 'px'; menu.style.right = Math.max(8, window.innerWidth - r.right) + 'px'; } function open() { place(); menu.style.display = 'block'; u.setAttribute('aria-expanded', 'true'); if (window.lucide) try { lucide.createIcons(); } catch (e) {} } function close() { menu.style.display = 'none'; u.setAttribute('aria-expanded', 'false'); } function toggle() { (menu.style.display === 'block') ? close() : open(); } u.addEventListener('click', toggle); u.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } }); document.addEventListener('click', function (e) { if (!u.contains(e.target) && !menu.contains(e.target)) close(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); }); var lo = menu.querySelector('#topnav-logout'); if (lo) lo.addEventListener('click', function () { if (typeof api !== 'undefined' && api.logout) api.logout(); }); var ver = menu.querySelector('#sidebar-version'); if (ver) { ver.addEventListener('click', function () { if (typeof _showVersionChangelog === 'function') _showVersionChangelog(); }); ver.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (typeof _showVersionChangelog === 'function') _showVersionChangelog(); } }); } } // Mobile (≤900): hamburger apre/chiude la nav; tap su un gruppo apre l'accordion mega. function _wireMobileNav() { var toggle = document.getElementById('topnav-toggle'); var bar = document.getElementById('topnav-bar'); if (toggle && bar) { toggle.addEventListener('click', function () { var open = bar.classList.toggle('open'); toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); }); } // Accordion per i gruppi con mega (solo in modalità mobile via CSS). document.querySelectorAll('#topnav-bar .nav-item').forEach(function (item) { var hasMega = item.querySelector('.mega'); if (!hasMega) return; var link = item.querySelector('.nav-link'); if (!link) return; link.addEventListener('click', function (e) { // Solo in layout mobile (nav-toggle visibile) intercettiamo il tap per aprire l'accordion. if (toggle && getComputedStyle(toggle).display !== 'none') { e.preventDefault(); item.classList.toggle('open'); } }); }); } // Trittico FAB reale (campana/bug-reporter ARIA) montato da common.js: lo // raggruppiamo in .fab-stack e gli diamo la veste V3, senza reimplementarlo. // common.js crea: #nx-bar (bug-reporter widget) e #ai-chat-fab (ARIA viola). // Qui aggiungiamo SOLO un contenitore visivo se non presente; gli elementi // restano gestiti dal loro codice (eventi/voce/puntatore invariati). function _styleRealFabs() { // I FAB reali sono position:fixed indipendenti (z-index alti). Non li // spostiamo nel DOM per non romperne gli handler; il loro look resta // quello di common.js. Questa funzione è un hook no-op di compatibilità // mantenuto per chiarezza: la veste .fab del mockup NON deve sovrascrivere // i FAB funzionanti (ARIA con voce + bug-reporter widget). return; } // CSS one-shot per il lang-toggle e user menu nella appbar (token V3). (function injectChromeCss() { if (document.getElementById('topnav-v3-css')) return; var s = document.createElement('style'); s.id = 'topnav-v3-css'; s.textContent = '.topnav-lang{display:inline-flex;gap:3px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.16);border-radius:8px;padding:3px}' + '.topnav-lang .lang-btn{border:0;background:transparent;color:#cfe0f1;font:inherit;font-size:11px;font-weight:700;padding:4px 8px;border-radius:6px;cursor:pointer}' + '.topnav-lang .lang-btn:hover{background:rgba(255,255,255,.12);color:#fff}' + '.topnav-lang .lang-btn.active{background:var(--gold);color:#1a1a1a}' + '#topnav-user .sidebar-user-name{font-size:12px;font-weight:600}' + '@media (max-width:560px){#topnav-user .sidebar-user-name{display:none}.topnav-lang{display:none}}'; document.head.appendChild(s); })(); // Override globale: l'init delle pagine chiama loadSidebar() invariato. window.loadTopnavV3 = loadTopnavV3; window.loadSidebar = loadTopnavV3; })();