Files
DevEnv nis2-agileandClaude Opus 4.8 15010f7dc3 [FEAT] Cruscotto Studio (frontend): companies.html vista d'insieme + menu
companies.html diventa il cruscotto dello studio consulente: barra KPI (aziende,
compliance media, scadenze in ritardo/in arrivo), griglia aziende (compliance/
rischi/incidenti + badge ritardo, clic = entra nell'azienda) e sezione "Scadenze
generali" aggregata su TUTTE le aziende ordinata per data. Una sola chiamata
api.consultantPortfolio(). Voce menu V3 "Le mie aziende" (icona briefcase) gata a
consultant/super_admin. help.js + i18n (nav.studio + companies.*) aggiornati.
Cache-buster api/topnav-v3/i18n/help ?v=20260729. version 1.25.9.

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

355 lines
23 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ═══════════════════════════════════════════════════════════════════════════
* 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 <i data-lucide> + 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 ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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 '<a class="mega-link"' + navAttr(l.href) + ' href="' + esc(l.href) + '">' +
'<span class="mega-ic"><i data-lucide="' + esc(l.ic) + '" class="ic"></i></span>' +
'<span><b' + i18nAttr(I18N_BY_HREF[l.href]) + '>' + esc(l.b) + '</b><span>' + esc(l.s) + '</span></span></a>';
}).join('');
return '<div class="mega-col"><p class="mega-head">' + esc(col.head) + '</p>' + links + '</div>';
}).join('');
return '<div class="mega no-promo">' + colsHtml + '</div>';
}
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 '<div class="nav-item">' +
'<a class="' + cls + '"' + aria + navAttr(g.href) + ' href="' + esc(g.href) + '">' +
'<i data-lucide="' + esc(g.icon) + '" class="ic"></i>' +
'<span' + i18n + '>' + esc(g.label) + '</span></a></div>';
}
// Gruppo con mega-menu. Il link "testa" punta alla prima voce del gruppo.
var firstHref = g.cols[0].links[0].href;
return '<div class="nav-item">' +
'<a class="' + cls + '"' + aria + navAttr(firstHref) + ' href="' + esc(firstHref) + '">' +
'<i data-lucide="' + esc(g.icon) + '" class="ic"></i>' +
'<span' + i18n + '>' + esc(g.label) + '</span>' +
'<i data-lucide="chevron-down" class="caret"></i></a>' +
renderMega(g) + '</div>';
}
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 =
'<header class="appbar">' +
'<div class="bar-top"><div class="bar-top-in">' +
'<span class="flag" aria-hidden="true"><i class="g"></i><i class="w"></i><i class="r"></i></span>' +
'<a class="logo-mark" href="dashboard.html" aria-label="NIS2 Agile — Dashboard">N2</a>' +
'<div class="brand-txt"><b class="sidebar-logo-text">NIS2 Agile</b>' +
'<span>Conformità Direttiva (UE) 2022/2555 · D.Lgs. 138/2024</span></div>' +
'<label class="search"><i data-lucide="search" class="ic"></i>' +
'<input id="topnav-search" placeholder="Cerca misura, rischio, incidente…" aria-label="Cerca"></label>' +
'<span class="spacer"></span>' +
// Org-switcher (consulenti / multi-org) — popolato da _loadUserInfo() via stesso id.
'<span id="sidebar-org-switcher" style="display:none"></span>' +
// Lingua IT/EN (riusa switchLang di common.js)
'<span id="sidebar-lang-toggle" class="topnav-lang" role="group" aria-label="Lingua">' +
'<button class="lang-btn ' + (lang === 'it' ? 'active' : '') + '" onclick="switchLang(\'it\')" title="Italiano" aria-label="Italiano">IT</button>' +
'<button class="lang-btn ' + (lang === 'en' ? 'active' : '') + '" onclick="switchLang(\'en\')" title="English" aria-label="English">EN</button>' +
'</span>' +
'<a class="btn-incident" href="incidents.html"><i data-lucide="siren" class="ic"></i><span data-i18n="nav.register_incident">Registra incidente</span></a>' +
// User menu (avatar+nome+ruolo via _loadUserInfo; click = menu)
'<span class="user" id="topnav-user" tabindex="0" role="button" aria-haspopup="true" aria-expanded="false">' +
'<span class="avatar" id="sidebar-user-avatar">--</span>' +
'<span class="sidebar-user-name" id="sidebar-user-name" style="max-width:140px;overflow:hidden;text-overflow:ellipsis">…</span>' +
'<span id="sidebar-user-role" hidden></span>' +
'<i data-lucide="chevron-down" class="ic"></i>' +
'</span>' +
'<button class="nav-toggle" id="topnav-toggle" aria-label="Apri menu" aria-expanded="false"><i data-lucide="menu" class="ic"></i></button>' +
'</div></div>' +
'<nav class="bar-nav" id="topnav-bar" aria-label="Navigazione principale"><div class="bar-nav-in">' +
navItems +
'</div></nav>' +
'</header>';
// ── 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 =
'<div id="sidebar-version" role="button" tabindex="0" title="Versione applicazione" ' +
'style="padding:8px 10px;font-size:12px;color:var(--muted);cursor:pointer;border-radius:8px">v—</div>' +
'<a href="settings.html" style="display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:8px;font-size:13px;font-weight:600">' +
'<i data-lucide="settings" style="width:15px;height:15px"></i>Impostazioni</a>' +
'<button type="button" id="topnav-logout" style="display:flex;align-items:center;gap:8px;width:100%;text-align:left;' +
'border:0;background:none;font:inherit;font-size:13px;font-weight:600;color:var(--c-red);padding:9px 10px;border-radius:8px;cursor:pointer">' +
'<i data-lucide="log-out" style="width:15px;height:15px"></i>Esci</button>';
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;
})();