/**
* 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 = `
${content}
${options.footer ? `` : ''}
`;
// 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', href: 'integrations/index.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 = `
${orgs.map(org => {
const orgId = org.id || org.organization_id;
const isActive = orgId === currentOrgId;
return `
`;
}).join('')}
`;
// 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);
})();