/**
* 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',
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 '';
}