/** * NIS2 Agile - Common Utilities * * Funzioni condivise tra tutte le pagine del frontend. */ // ═══════════════════════════════════════════════════════════════════ // Notifications (Toast) // ═══════════════════════════════════════════════════════════════════ /** * Mostra una notifica toast. * @param {string} message - Testo del messaggio * @param {string} type - 'success' | 'error' | 'warning' | 'info' * @param {number} duration - Durata in ms (default 4000) */ function showNotification(message, type = 'info', duration = 4000) { let container = document.querySelector('.notification-container'); if (!container) { container = document.createElement('div'); container.className = 'notification-container'; document.body.appendChild(container); } const icons = { success: '', error: '', warning: '', info: '' }; const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.innerHTML = ` ${icons[type] || icons.info} ${escapeHtml(message)} `; container.appendChild(notification); setTimeout(() => { notification.classList.add('fade-out'); setTimeout(() => notification.remove(), 300); }, duration); } // ═══════════════════════════════════════════════════════════════════ // Modal // ═══════════════════════════════════════════════════════════════════ /** * Mostra un dialogo modale. * @param {string} title - Titolo del modale * @param {string} content - Contenuto HTML del body * @param {object} options - { footer: HTML, size: 'sm'|'md'|'lg' } */ function showModal(title, content, options = {}) { closeModal(); // Chiudi eventuale modale aperto const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'modal-overlay'; const sizeClass = options.size === 'lg' ? 'style="max-width:720px"' : options.size === 'sm' ? 'style="max-width:400px"' : ''; overlay.innerHTML = ` `; // Chiudi cliccando fuori dal modale overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); }); document.body.appendChild(overlay); // Trigger animation requestAnimationFrame(() => { overlay.classList.add('active'); }); // Chiudi con ESC document.addEventListener('keydown', _modalEscHandler); } function closeModal() { const overlay = document.getElementById('modal-overlay'); if (overlay) { overlay.classList.remove('active'); setTimeout(() => overlay.remove(), 250); } document.removeEventListener('keydown', _modalEscHandler); } function _modalEscHandler(e) { if (e.key === 'Escape') closeModal(); } // ═══════════════════════════════════════════════════════════════════ // Date Formatting // ═══════════════════════════════════════════════════════════════════ /** * Formatta una data in formato italiano (dd/mm/yyyy). * @param {string|Date} date * @returns {string} */ function formatDate(date) { if (!date) return '-'; const d = new Date(date); if (isNaN(d.getTime())) return '-'; return d.toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric' }); } /** * Formatta data e ora in formato italiano. * @param {string|Date} date * @returns {string} */ function formatDateTime(date) { if (!date) return '-'; const d = new Date(date); if (isNaN(d.getTime())) return '-'; return d.toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } /** * Restituisce un'etichetta relativa (es. "2 ore fa", "ieri"). * @param {string|Date} date * @returns {string} */ function timeAgo(date) { if (!date) return ''; const d = new Date(date); const now = new Date(); const diffMs = now - d; const diffMin = Math.floor(diffMs / 60000); const diffHrs = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMin < 1) return 'Adesso'; if (diffMin < 60) return `${diffMin} min fa`; if (diffHrs < 24) return `${diffHrs} ore fa`; if (diffDays === 1) return 'Ieri'; if (diffDays < 7) return `${diffDays} giorni fa`; return formatDate(date); } // ═══════════════════════════════════════════════════════════════════ // Sidebar // ═══════════════════════════════════════════════════════════════════ /** * Inietta la sidebar di navigazione nell'elemento #sidebar. */ function loadSidebar() { const container = document.getElementById('sidebar'); if (!container) return; // Determina la pagina corrente const currentPage = window.location.pathname.split('/').pop() || 'dashboard.html'; const navItems = [ { label: 'Principale', i18nKey: 'nav.main', items: [ { name: 'Dashboard', href: 'dashboard.html', icon: iconGrid(), i18nKey: 'nav.dashboard' }, { name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' }, ] }, { label: 'Gestione', i18nKey: 'nav.management', items: [ { 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: 'Segnalazioni', href: 'whistleblowing.html', icon: `` }, { name: 'Normative', href: 'normative.html', icon: `` }, ] }, { label: 'Operativo', i18nKey: 'nav.operations', items: [ { name: 'Formazione', href: 'training.html', icon: iconAcademicCap(), i18nKey: 'nav.training' }, { name: 'Asset', href: 'assets.html', icon: iconServer(), i18nKey: 'nav.assets' }, { name: 'Audit & Report',href: 'reports.html', icon: iconChartBar(), i18nKey: 'nav.audit' }, ] }, { label: 'Sistema', i18nKey: 'nav.system', items: [ { name: 'Impostazioni', href: 'settings.html', icon: iconCog(), i18nKey: 'nav.settings' }, { name: 'Architettura', href: 'architecture.html', icon: iconCubeTransparent() }, { name: 'Simulazione Demo', href: 'simulate.html', icon: `` }, { name: 'Integrazioni Esterne', href: 'integrazioniext.html', icon: `` }, ] } ]; let navHTML = ''; // Brand navHTML += ` `; // Org-switcher area (populated async for consultants) navHTML += ``; // Nav sections navHTML += ''; // Footer with language toggle + user info navHTML += ` `; container.innerHTML = navHTML; // Carica info utente (e org-switcher se consulente) _loadUserInfo(); // Mobile toggle _setupMobileToggle(); } const _roleLabels = { super_admin: 'Super Admin', org_admin: 'Admin Org', compliance_manager: 'Compliance Manager', board_member: 'Membro CDA', auditor: 'Auditor', employee: 'Utente', consultant: 'Consulente', }; async function _loadUserInfo() { try { const result = await api.getMe(); if (result.success && result.data) { const user = result.data; const nameEl = document.getElementById('sidebar-user-name'); const avatarEl = document.getElementById('sidebar-user-avatar'); const roleEl = document.getElementById('sidebar-user-role'); if (nameEl) nameEl.textContent = user.full_name || user.email; if (avatarEl) { const initials = (user.full_name || user.email || '--') .split(' ') .map(w => w[0]) .slice(0, 2) .join('') .toUpperCase(); avatarEl.textContent = initials; } if (roleEl) roleEl.textContent = _roleLabels[user.role] || user.role || 'Utente'; // Save role to localStorage (ensures isConsultant() works across pages) if (user.role) api.setUserRole(user.role); // For consultants: render org-switcher if (user.role === 'consultant') { _loadConsultantOrgSwitcher(); } } } catch (e) { // Silenzioso } } async function _loadConsultantOrgSwitcher() { try { const result = await api.listOrganizations(); if (!result.success || !result.data || result.data.length === 0) return; const orgs = result.data; const currentOrgId = parseInt(api.orgId); const currentOrg = orgs.find(o => (o.id || o.organization_id) === currentOrgId); const switcher = document.getElementById('sidebar-org-switcher'); if (!switcher) return; const dropdownId = 'org-dropdown-' + Date.now(); switcher.style.display = 'block'; switcher.innerHTML = `
`; // Close on outside click document.addEventListener('click', (e) => { const dd = document.getElementById(dropdownId); if (dd && !switcher.contains(e.target)) dd.style.display = 'none'; }); // Override toggle to handle display const ddEl = document.getElementById(dropdownId); if (ddEl) { const btn = switcher.querySelector('button'); if (btn) btn.onclick = () => { ddEl.style.display = ddEl.style.display === 'none' ? 'block' : 'none'; }; } } catch (e) { // Silenzioso } } function _switchOrg(orgId) { api.setOrganization(orgId); window.location.reload(); } function _setupMobileToggle() { // Crea pulsante toggle se non esiste if (!document.querySelector('.sidebar-toggle')) { const toggle = document.createElement('button'); toggle.className = 'sidebar-toggle'; toggle.setAttribute('aria-label', 'Apri menu'); toggle.innerHTML = ''; toggle.addEventListener('click', () => _toggleSidebar()); document.body.appendChild(toggle); } // Crea backdrop per mobile if (!document.querySelector('.sidebar-backdrop')) { const backdrop = document.createElement('div'); backdrop.className = 'sidebar-backdrop'; backdrop.addEventListener('click', () => _toggleSidebar(false)); document.body.appendChild(backdrop); } } function _toggleSidebar(forceState) { const sidebar = document.getElementById('sidebar'); const backdrop = document.querySelector('.sidebar-backdrop'); if (!sidebar) return; const isOpen = typeof forceState === 'boolean' ? forceState : !sidebar.classList.contains('open'); if (isOpen) { sidebar.classList.add('open'); if (backdrop) backdrop.classList.add('visible'); } else { sidebar.classList.remove('open'); if (backdrop) backdrop.classList.remove('visible'); } } // ═══════════════════════════════════════════════════════════════════ // Auth Check // ═══════════════════════════════════════════════════════════════════ /** * Verifica che l'utente sia autenticato. Se non lo e', redirige al login. * Attiva automaticamente il timeout di sessione per inattivita'. * @returns {boolean} */ function checkAuth() { if (!api.isAuthenticated()) { window.location.href = 'login.html'; return false; } if (!_idleInitialized) { _idleInitialized = true; initIdleTimeout(); } return true; } // ═══════════════════════════════════════════════════════════════════ // Idle Session Timeout // ═══════════════════════════════════════════════════════════════════ let _idleTimer = null; let _idleWarningTimer = null; let _idleCountdownInterval = null; let _idleInitialized = false; const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minuti totali const IDLE_WARNING_MS = 5 * 60 * 1000; // avviso 5 minuti prima della scadenza /** * Avvia il monitoraggio di inattivita' della sessione. * Mostra un avviso 5 minuti prima del logout automatico. */ function initIdleTimeout() { _resetIdleTimer(); ['mousemove', 'keydown', 'click', 'scroll', 'touchstart'].forEach(evt => { document.addEventListener(evt, _resetIdleTimer, { passive: true }); }); } function _resetIdleTimer() { clearTimeout(_idleTimer); clearTimeout(_idleWarningTimer); clearInterval(_idleCountdownInterval); // Chiudi avviso se gia' aperto const existingWarning = document.getElementById('idle-warning-overlay'); if (existingWarning) existingWarning.remove(); // Timer per mostrare avviso _idleWarningTimer = setTimeout(_showIdleWarning, IDLE_TIMEOUT_MS - IDLE_WARNING_MS); // Timer per logout automatico _idleTimer = setTimeout(() => { if (typeof api !== 'undefined') api.logout(); }, IDLE_TIMEOUT_MS); } function _showIdleWarning() { const existing = document.getElementById('idle-warning-overlay'); if (existing) existing.remove(); let remaining = Math.floor(IDLE_WARNING_MS / 1000); const overlay = document.createElement('div'); overlay.id = 'idle-warning-overlay'; overlay.style.cssText = [ 'position:fixed', 'top:0', 'left:0', 'right:0', 'bottom:0', 'z-index:9999', 'background:rgba(0,0,0,0.55)', 'display:flex', 'align-items:center', 'justify-content:center' ].join(';'); const _t = (key, fallback) => (typeof I18n !== 'undefined' ? I18n.t(key) : fallback); overlay.innerHTML = `

${_t('session.expiring_title', 'Sessione in scadenza')}

${_t('session.expiring_msg', 'Per motivi di sicurezza, verrai disconnesso tra')}
5:00 ${_t('session.idle_reason', "a causa di inattivita'.")}

`; document.body.appendChild(overlay); document.getElementById('idle-stay-btn').addEventListener('click', () => { overlay.remove(); _resetIdleTimer(); }); _idleCountdownInterval = setInterval(() => { remaining--; const min = Math.floor(remaining / 60); const sec = remaining % 60; const el = document.getElementById('idle-countdown'); if (el) el.textContent = `${min}:${sec.toString().padStart(2, '0')}`; if (remaining <= 0) clearInterval(_idleCountdownInterval); }, 1000); } // ═══════════════════════════════════════════════════════════════════ // Utilities // ═══════════════════════════════════════════════════════════════════ /** * Escape HTML per prevenire XSS. * @param {string} str * @returns {string} */ function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML; } /** * Debounce: ritarda l'esecuzione di fn fino a delay ms dopo l'ultima chiamata. * @param {Function} fn * @param {number} delay * @returns {Function} */ function debounce(fn, delay = 300) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } /** * Restituisce la classe CSS per un punteggio di conformita'. * @param {number} score - Valore 0-100 * @returns {string} */ function getScoreClass(score) { if (score >= 80) return 'score-excellent'; if (score >= 60) return 'score-good'; if (score >= 40) return 'score-medium'; if (score >= 20) return 'score-low'; return 'score-critical'; } /** * Restituisce il colore esadecimale per un punteggio. * @param {number} score - Valore 0-100 * @returns {string} */ function getScoreColor(score) { if (score >= 80) return '#34a853'; if (score >= 60) return '#84cc16'; if (score >= 40) return '#fbbc04'; if (score >= 20) return '#f97316'; return '#ea4335'; } /** * Crea l'SVG per il gauge circolare del punteggio. * @param {number} score - Valore 0-100 * @param {number} size - Dimensione in px (default 180) * @returns {string} HTML */ function renderScoreGauge(score, size = 180) { const radius = (size / 2) - 14; const circumference = 2 * Math.PI * radius; const offset = circumference - (score / 100) * circumference; const color = getScoreColor(score); const cls = getScoreClass(score); return `
${Math.round(score)}
Compliance
`; } // ═══════════════════════════════════════════════════════════════════ // SVG Icons (inline, nessuna dipendenza esterna) // ═══════════════════════════════════════════════════════════════════ function iconGrid() { return ''; } function iconClipboardCheck() { return ''; } function iconShieldExclamation() { return ''; } function iconBell() { return ''; } function iconDocumentText() { return ''; } function iconLink() { return ''; } function iconAcademicCap() { return ''; } function iconServer() { return ''; } function iconChartBar() { return ''; } function iconCubeTransparent() { return ''; } function iconQuestionMarkCircle() { return ''; } function iconCog() { return ''; } /** * Imposta lo stato di caricamento su un bottone. * @param {HTMLElement|string} btn - Elemento o ID * @param {boolean} loading - true per attivare, false per disattivare * @param {string} [originalText] - Testo originale da ripristinare */ function setButtonLoading(btn, loading, originalText) { if (typeof btn === 'string') btn = document.getElementById(btn); if (!btn) return; if (loading) { btn._originalText = btn.textContent; btn.classList.add('loading'); btn.disabled = true; } else { btn.classList.remove('loading'); btn.disabled = false; if (originalText) btn.textContent = originalText; else if (btn._originalText) btn.textContent = btn._originalText; } } function iconPlus() { return ''; } function iconSparkles() { return ''; } // ═══════════════════════════════════════════════════════════════════ // Language Switcher // ═══════════════════════════════════════════════════════════════════ function _getSavedLang() { return localStorage.getItem('nis2_lang') || 'it'; } function switchLang(lang) { if (typeof I18n !== 'undefined') { I18n.setLang(lang); } localStorage.setItem('nis2_lang', lang); // Aggiorna bottoni document.querySelectorAll('.lang-btn').forEach(btn => { btn.classList.toggle('active', btn.textContent.trim().toLowerCase() === lang); }); } // Inject language toggle CSS (function _injectLangCSS() { if (document.getElementById('lang-toggle-css')) return; const style = document.createElement('style'); style.id = 'lang-toggle-css'; style.textContent = ` .sidebar-lang-toggle { display: flex; justify-content: center; gap: 4px; padding: 8px 16px 4px; } .lang-btn { padding: 4px 14px; font-size: 0.75rem; font-weight: 700; border: 1px solid var(--gray-300); background: var(--gray-50); color: var(--gray-500); border-radius: 4px; cursor: pointer; transition: all var(--transition-fast); font-family: inherit; } .lang-btn:hover { background: var(--gray-100); color: var(--gray-700); } .lang-btn.active { background: var(--primary); color: #fff; border-color: var(--primary); } `; document.head.appendChild(style); })();