/** * 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', items: [ { name: 'Dashboard', href: 'dashboard.html', icon: iconGrid() }, { name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck() }, ] }, { label: 'Gestione', items: [ { name: 'Rischi', href: 'risks.html', icon: iconShieldExclamation() }, { name: 'Incidenti', href: 'incidents.html', icon: iconBell() }, { name: 'Policy', href: 'policies.html', icon: iconDocumentText() }, { name: 'Supply Chain', href: 'supply-chain.html', icon: iconLink() }, ] }, { label: 'Operativo', items: [ { name: 'Formazione', href: 'training.html', icon: iconAcademicCap() }, { name: 'Asset', href: 'assets.html', icon: iconServer() }, { name: 'Audit & Report', href: 'audit.html', icon: iconChartBar() }, ] }, { label: 'Sistema', items: [ { name: 'Impostazioni', href: 'settings.html', icon: iconCog() }, ] } ]; let navHTML = ''; // Brand navHTML += ` `; // Nav sections navHTML += ''; // Footer with user info navHTML += ` `; container.innerHTML = navHTML; // Carica info utente _loadUserInfo(); // Mobile toggle _setupMobileToggle(); } 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 = user.role === 'admin' ? 'Amministratore' : 'Utente'; } } catch (e) { // Silenzioso } } 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. * @returns {boolean} */ function checkAuth() { if (!api.isAuthenticated()) { window.location.href = 'login.html'; return false; } return true; } // ═══════════════════════════════════════════════════════════════════ // 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 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 ''; }