[FEAT] UI V3 — flotta: chrome top-nav + design system su 32 pagine app

Migrazione UI dal V2 (Bootstrap Italia, sidebar) al V3 (top-nav mega-menu
navy/oro, Inter+Lucide self-host) fedele ai mockup docs/mockup Ui/.
- 32 pagine app convertite (incl. admin/*): head Inter+v3.css, chrome
  topnav-v3, contenuti in card V3, dati REALI preservati (id/data-* intatti),
  i18n/help/FAB reali invariati, nessun CDN esterno.
- v3.css: aggiunti widget condivisi (modal/toast/form/btn-variant/spinner/
  progress/tag/badge) + utility (.btn base, .text-muted, .sr-only) + alias
  variabili V2->token V3, così modali/form/toast di common.js funzionano
  ovunque senza style.css.
- maintenance.html: finestra estesa a ~12:15.
- docs/UI_V3_FLEET_BRIEF.md: ricetta operativa della flotta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-27 15:57:35 +02:00
co-authored by Claude Opus 4.8
parent b0fc190704
commit a4150b5389
37 changed files with 4655 additions and 4922 deletions
+166 -147
View File
@@ -3,29 +3,63 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Aggiornamenti Normativi - NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
via JS (card aggiornamenti/badge/modali) -> parita' funzionale assoluta. BI veste la chrome. -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="/css/style.css?v=20260618">
<title>Aggiornamenti Normativi · NIS2 Agile</title>
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia). -->
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<style>
.impact-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; }
.impact-critical { background: var(--danger-bg); color: var(--danger); }
.impact-high { background: #fff7ed; color: #c2410c; }
.impact-medium { background: var(--warning-bg); color: #a16207; }
.impact-low { background: var(--gray-100); color: var(--gray-600); }
.impact-informational { background: rgba(6,182,212,0.1); color: #0891b2; }
.ack-badge { display: inline-flex; align-items: center; gap: 5px; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 600; }
.ack-done { background: var(--success-bg); color: var(--success); }
.ack-pending { background: var(--warning-bg); color: #a16207; }
.source-chip { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 700; background: var(--gray-100); color: var(--gray-600); font-family: monospace; }
.update-card { border: 1px solid var(--gray-200); border-radius: var(--border-radius-lg); padding: 20px; margin-bottom: 16px; transition: all var(--transition-fast); }
.update-card:hover { border-color: var(--primary); box-shadow: 0 2px 8px rgba(6,182,212,0.1); }
.update-card.action-required { border-left: 3px solid var(--warning); }
.update-card.acked { opacity: 0.7; }
.ack-progress { height: 6px; background: var(--gray-200); border-radius: 3px; overflow: hidden; }
.ack-progress-bar { height: 100%; background: var(--primary); border-radius: 3px; transition: width 0.5s ease; }
/* V3 — specifico pagina Normative (non coperto da v3.css) */
.nrm-bars{display:grid;gap:18px}
.nrm-progress-track{height:8px;background:#EAF0F6;border-radius:99px;overflow:hidden}
.nrm-progress-fill{height:100%;background:var(--c-blue);border-radius:99px;transition:width .5s ease}
.nrm-filters{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
.nrm-select{padding:8px 12px;border:1px solid var(--line);border-radius:8px;background:#fff;font:inherit;font-size:13px;color:var(--ink)}
.nrm-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;color:var(--ink)}
.nrm-check input{accent-color:var(--c-blue)}
.update-card{border:1px solid var(--line);border-radius:11px;padding:18px;margin-bottom:14px;transition:border-color .12s,box-shadow .12s;background:#fff}
.update-card:hover{border-color:#9FBEDD;box-shadow:0 4px 12px rgba(16,40,80,.08)}
.update-card.action-required{border-left:3px solid var(--c-amber)}
.update-card.acked{opacity:.72}
.source-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:6px;font-size:10.5px;font-weight:700;background:#EEF2F7;color:#5b6b80;font-family:'Inter',monospace}
.impact-badge{display:inline-flex;align-items:center;padding:2px 9px;border-radius:6px;font-size:10px;font-weight:700;letter-spacing:.03em;text-transform:uppercase}
.impact-critical{background:#FBE9E9;color:#B23636}
.impact-high{background:#FBF1E2;color:#C2410C}
.impact-medium{background:#FBF1E2;color:#9A6418}
.impact-low{background:#EEF2F7;color:#5b6b80}
.impact-informational{background:#E7F0FB;color:#1F5FA8}
.ack-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:6px;font-size:10.5px;font-weight:700}
.ack-done{background:#E6F4EC;color:#1E7A4E}
.ack-pending{background:#FBF1E2;color:#9A6418}
.nrm-uupd-h3{font-size:15px;font-weight:700;color:var(--ink);margin:0 0 6px}
.nrm-ref{font-size:11.5px;color:var(--muted);margin:0 0 8px}
.nrm-ref code{background:#EEF2F7;border-radius:5px;padding:1px 6px}
.nrm-summary{font-size:13px;color:var(--muted);line-height:1.6;margin:0 0 10px}
.nrm-card-grid{display:flex;justify-content:space-between;gap:14px;flex-wrap:wrap}
.nrm-card-side{display:flex;flex-direction:column;gap:8px;align-items:flex-end;flex-shrink:0}
.nrm-eff{font-size:11.5px;color:var(--muted)}
.nrm-action-tag{font-size:10px;font-weight:700;color:#9A6418;background:#FBF1E2;padding:2px 8px;border-radius:6px;text-transform:uppercase;letter-spacing:.03em}
/* Modal + toast prodotti da common.js (style.css rimosso): stile minimo coerente V3 */
.modal-overlay{position:fixed;inset:0;background:rgba(15,27,46,.5);display:flex;align-items:flex-start;justify-content:center;z-index:1050;opacity:0;transition:opacity .2s;padding:48px 16px;overflow:auto}
.modal-overlay.active{opacity:1}
.modal-overlay .modal{background:#fff;border-radius:12px;width:100%;max-width:560px;box-shadow:0 20px 50px rgba(11,40,80,.25);transform:translateY(-8px);transition:transform .2s}
.modal-overlay.active .modal{transform:none}
.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--line)}
.modal-header h3{margin:0;font-size:16px;font-weight:700}
.modal-close{background:none;border:0;font-size:1.5rem;line-height:1;cursor:pointer;color:var(--muted)}
.modal-body{padding:18px 20px}
.modal-footer{padding:14px 20px;border-top:1px solid var(--line);display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap}
.form-group{margin-bottom:14px}
.form-label{display:block;font-weight:600;font-size:13px;margin-bottom:5px}
.form-control{width:100%;padding:9px 10px;border:1px solid var(--line);border-radius:8px;font:inherit;font-size:14px}
textarea.form-control{min-height:64px;resize:vertical}
.notification-container{position:fixed;top:18px;right:18px;z-index:1100;display:flex;flex-direction:column;gap:10px}
.notification{display:flex;align-items:center;gap:10px;background:#fff;border:1px solid var(--line);border-left:4px solid var(--c-blue);border-radius:10px;box-shadow:0 8px 24px rgba(16,40,80,.14);padding:12px 14px;min-width:240px;max-width:360px;font-size:13px}
.notification.success{border-left-color:var(--c-green)}.notification.error{border-left-color:var(--c-red)}.notification.warning{border-left-color:var(--c-amber)}
.notification-icon{width:18px;height:18px;flex:0 0 auto}.notification-icon svg{width:18px;height:18px}
.notification.success .notification-icon{color:var(--c-green)}.notification.error .notification-icon{color:var(--c-red)}.notification.warning .notification-icon{color:var(--c-amber)}.notification.info .notification-icon{color:var(--c-blue)}
.notification-message{flex:1;min-width:0}
.notification-close{background:none;border:0;font-size:1.2rem;line-height:1;cursor:pointer;color:var(--muted)}
.notification.fade-out{opacity:0;transition:opacity .3s}
</style>
<!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest">
@@ -42,95 +76,82 @@
<!-- PWA:end -->
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<div class="page-header">
<div class="page-header-content">
<h1 class="page-title">Aggiornamenti Normativi</h1>
<p class="page-subtitle">Feed NIS2 / ACN / DORA — Presa visione obbligatoria per audit compliance</p>
</div>
<div class="page-header-actions">
<button class="btn btn-secondary" onclick="loadPending()">
<svg class="icon icon-sm" aria-hidden="true" style="margin-right:6px;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-clock"></use></svg>
Solo Pendenti
</button>
</div>
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
<div id="topnav"></div>
<main class="wrap">
<nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>Lex e ISO</span>
<i data-lucide="chevron-right" class="ic"></i><span>Normative</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="normative.title">Aggiornamenti Normativi</h1>
<p class="sub">Feed NIS2 / ACN / DORA — Presa visione obbligatoria per audit compliance</p>
</div>
<button class="btn-ghost" onclick="loadPending()"><i data-lucide="clock" class="ic"></i>Solo pendenti</button>
</div>
<div class="stack">
<!-- Stats (dati reali da /normative/stats) -->
<div class="grid3" id="stats-grid">
<section class="card card-pad stat blue"><div style="flex:1;min-width:0"><div class="lbl">Caricamento…</div><div class="val">…</div></div></section>
</div>
<!-- Stats -->
<div class="stats-grid mb-24" id="stats-grid">
<div class="stat-card"><div class="spinner"></div></div>
</div>
<!-- Avanzamento presa visione -->
<section class="card card-pad">
<div class="sec-head"><h3>Avanzamento presa visione</h3><span class="hint" id="ack-pct">—</span></div>
<div class="nrm-progress-track"><div class="nrm-progress-fill" id="ack-bar" style="width:0%"></div></div>
<p class="sub" style="margin:8px 0 0;font-size:12px;color:var(--muted)">Documentare la presa visione degli aggiornamenti normativi è richiesto per la compliance NIS2 Art. 21.</p>
</section>
<!-- Ack Progress bar -->
<div class="card mb-24">
<div class="card-body" style="padding:16px 20px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
<span style="font-size:0.875rem; font-weight:600; color:var(--gray-700);">Avanzamento presa visione</span>
<span id="ack-pct" style="font-size:0.875rem; font-weight:700; color:var(--primary);">—</span>
</div>
<div class="ack-progress">
<div class="ack-progress-bar" id="ack-bar" style="width:0%"></div>
</div>
<p style="font-size:0.75rem; color:var(--gray-500); margin-top:6px;">Documentare la presa visione degli aggiornamenti normativi è richiesto per la compliance NIS2 Art.21.</p>
<!-- Filtri -->
<section class="card card-pad">
<div class="nrm-filters">
<label for="filter-source" class="visually-hidden" style="position:absolute;width:1px;height:1px;overflow:hidden">Filtra per fonte normativa</label>
<select id="filter-source" class="nrm-select" onchange="loadUpdates()" aria-label="Filtra per fonte normativa">
<option value="">Tutte le fonti</option>
<option value="dlgs_138_2024">D.Lgs. 138/2024</option>
<option value="nis2_directive">Direttiva NIS2</option>
<option value="acn_guideline">ACN Guideline</option>
<option value="dora">DORA</option>
<option value="enisa">ENISA</option>
<option value="iso27001">ISO 27001</option>
</select>
<label for="filter-impact" class="visually-hidden" style="position:absolute;width:1px;height:1px;overflow:hidden">Filtra per livello di impatto</label>
<select id="filter-impact" class="nrm-select" onchange="loadUpdates()" aria-label="Filtra per livello di impatto">
<option value="">Tutti i livelli</option>
<option value="critical">Critico</option>
<option value="high">Alto</option>
<option value="medium">Medio</option>
<option value="low">Basso</option>
<option value="informational">Informativo</option>
</select>
<label class="nrm-check"><input type="checkbox" id="filter-action" onchange="loadUpdates()"> Solo azione richiesta</label>
</div>
</div>
</section>
<!-- Filters -->
<div class="card mb-16">
<div class="card-body" style="padding:14px 20px;">
<div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
<label for="filter-source" class="visually-hidden">Filtra per fonte normativa</label>
<select id="filter-source" class="form-control" style="width:auto;" onchange="loadUpdates()" aria-label="Filtra per fonte normativa">
<option value="">Tutte le fonti</option>
<option value="dlgs_138_2024">D.Lgs. 138/2024</option>
<option value="nis2_directive">Direttiva NIS2</option>
<option value="acn_guideline">ACN Guideline</option>
<option value="dora">DORA</option>
<option value="enisa">ENISA</option>
<option value="iso27001">ISO 27001</option>
</select>
<label for="filter-impact" class="visually-hidden">Filtra per livello di impatto</label>
<select id="filter-impact" class="form-control" style="width:auto;" onchange="loadUpdates()" aria-label="Filtra per livello di impatto">
<option value="">Tutti i livelli</option>
<option value="critical">Critico</option>
<option value="high">Alto</option>
<option value="medium">Medio</option>
<option value="low">Basso</option>
<option value="informational">Informativo</option>
</select>
<label style="display:flex; align-items:center; gap:8px; font-size:0.875rem; cursor:pointer;">
<input type="checkbox" id="filter-action" onchange="loadUpdates()" style="accent-color:var(--primary);">
Solo azione richiesta
</label>
</div>
</div>
</div>
<div id="updates-container"><div class="spinner" style="margin:60px auto;"></div></div>
</main>
</div>
<div id="updates-container"><div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div></div>
</div>
</main>
<!-- Scripts V3 (ordine ESATTO come dashboard.html) -->
<script src="/js/api.js?v=20260625d"></script>
<script src="/js/common.js?v=20260624g"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
riusando tutti gli helper di common.js (zero backend). -->
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script>
if (window.bootstrap && bootstrap.loadFonts) {
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
}
</script>
<script src="/js/common-bi.js?v=20260625d"></script>
<script src="/js/common.js?v=20260627v3"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627"></script>
<script src="/js/i18n.js?v=20260631"></script>
<script src="/js/help.js?v=20260625d"></script>
<script>
// ── Auth + chrome + i18n + help ──────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init();
loadSidebar(); // = loadTopnavV3()
if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
function refreshIcons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
const sourceLabels = {
nis2_directive: 'NIS2 Directive', dlgs_138_2024: 'D.Lgs. 138/2024',
@@ -149,30 +170,27 @@
function renderStats(d) {
document.getElementById('stats-grid').innerHTML = `
<div class="stat-card">
<div class="stat-label">Aggiornamenti Totali</div>
<div class="stat-value">${d.total_updates || 0}</div>
</div>
<div class="stat-card">
<div class="stat-label">Prese Visione</div>
<div class="stat-value" style="color:var(--success);">${d.acknowledged || 0}</div>
</div>
<div class="stat-card">
<div class="stat-label">Pendenti</div>
<div class="stat-value" style="color:var(--warning);">${d.pending || 0}</div>
</div>
<div class="stat-card">
<div class="stat-label">Compliance ACK</div>
<div class="stat-value" style="color:var(--primary);">${d.ack_rate || 0}%</div>
</div>`;
<a class="card card-pad stat blue" style="cursor:default">
<div class="ibox"><i data-lucide="layers" class="ic"></i></div>
<div style="flex:1;min-width:0"><div class="lbl">Aggiornamenti totali</div><div class="val blue">${d.total_updates || 0}</div></div>
</a>
<a class="card card-pad stat green" style="cursor:default">
<div class="ibox"><i data-lucide="check-check" class="ic"></i></div>
<div style="flex:1;min-width:0"><div class="lbl">Prese visione</div><div class="val green">${d.acknowledged || 0}</div></div>
</a>
<a class="card card-pad stat amber" style="cursor:default">
<div class="ibox"><i data-lucide="clock" class="ic"></i></div>
<div style="flex:1;min-width:0"><div class="lbl">Pendenti</div><div class="val amber">${d.pending || 0}</div><div class="sub">Compliance ACK ${d.ack_rate || 0}%</div></div>
</a>`;
document.getElementById('ack-pct').textContent = (d.ack_rate || 0) + '%';
document.getElementById('ack-bar').style.width = (d.ack_rate || 0) + '%';
refreshIcons();
}
async function loadUpdates() {
const container = document.getElementById('updates-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;"></div>';
container.innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>';
try {
const params = new URLSearchParams();
const source = document.getElementById('filter-source').value;
@@ -185,30 +203,36 @@
const result = await api.request('GET', '/normative/list?' + params);
if (result.success) renderUpdates(result.data.updates || []);
} catch (e) {
container.innerHTML = '<div class="empty-state"><h4>Errore caricamento aggiornamenti</h4></div>';
container.innerHTML = '<div class="v3-error"><i data-lucide="alert-triangle" class="ic"></i><b>Errore di caricamento</b><span>Riprova tra poco.</span></div>';
refreshIcons();
}
}
async function loadPending() {
const container = document.getElementById('updates-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;"></div>';
container.innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>';
try {
const result = await api.request('GET', '/normative/pending');
if (result.success) renderUpdates(result.data.pending || [], true);
} catch (e) { container.innerHTML = '<div class="empty-state"><h4>Errore.</h4></div>'; }
} catch (e) {
container.innerHTML = '<div class="v3-error"><i data-lucide="alert-triangle" class="ic"></i><b>Errore di caricamento</b><span>Riprova tra poco.</span></div>';
refreshIcons();
}
}
function renderUpdates(updates, pendingOnly) {
const container = document.getElementById('updates-container');
if (!updates.length) {
container.innerHTML = `<div class="empty-state">
<svg class="icon" aria-hidden="true" style="width:40px;height:40px;color:var(--gray-300)"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-file"></use></svg>
<h4>${pendingOnly ? 'Nessun aggiornamento pendente — tutto preso in visione!' : 'Nessun aggiornamento trovato'}</h4>
</div>`;
container.innerHTML = `<section class="card card-pad"><div class="v3-empty">
<i data-lucide="file-text" class="ic"></i>
<b>${pendingOnly ? 'Nessun aggiornamento pendente — tutto preso in visione!' : 'Nessun aggiornamento trovato'}</b>
<span>I prossimi aggiornamenti normativi appariranno qui.</span>
</div></section>`;
refreshIcons();
return;
}
let html = '';
let html = '<div class="stack">';
updates.forEach(u => {
const acked = u.is_acknowledged || !!(u.acknowledged_at);
const actionClass = u.action_required ? 'action-required' : '';
@@ -218,51 +242,46 @@
).join(' ');
html += `<div class="update-card ${actionClass} ${ackedClass}" id="update-${u.id}">
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; flex-wrap:wrap;">
<div style="flex:1;">
<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:8px;">
<div class="nrm-card-grid">
<div style="flex:1;min-width:0;">
<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:10px;">
<span class="source-chip">${escapeHtml(sourceLabels[u.source] || u.source)}</span>
<span class="impact-badge impact-${u.impact_level}">${escapeHtml(impactLabels[u.impact_level] || u.impact_level)}</span>
${u.action_required ? '<span style="font-size:0.7rem; font-weight:700; color:var(--warning); background:var(--warning-bg); padding:2px 8px; border-radius:4px;">Azione richiesta</span>' : ''}
${u.action_required ? '<span class="nrm-action-tag">Azione richiesta</span>' : ''}
${acked
? `<span class="ack-badge ack-done"><svg class="icon" aria-hidden="true" style="width:11px;height:11px;fill:currentColor;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>Preso in visione ${u.ack_by_name ? 'da ' + escapeHtml(u.ack_by_name) : ''}</span>`
? `<span class="ack-badge ack-done"><i data-lucide="check" style="width:12px;height:12px"></i>Preso in visione ${u.ack_by_name ? 'da ' + escapeHtml(u.ack_by_name) : ''}</span>`
: '<span class="ack-badge ack-pending">Da prendere in visione</span>'
}
</div>
<h3 style="font-size:1rem; font-weight:700; color:var(--gray-900); margin-bottom:6px;">${escapeHtml(u.title)}</h3>
${u.reference ? `<p style="font-size:0.75rem; color:var(--gray-500); margin-bottom:8px;"><code>${escapeHtml(u.reference)}</code></p>` : ''}
<p style="font-size:0.8125rem; color:var(--gray-600); line-height:1.6; margin-bottom:10px;">${escapeHtml(u.summary)}</p>
<h3 class="nrm-uupd-h3">${escapeHtml(u.title)}</h3>
${u.reference ? `<p class="nrm-ref"><code>${escapeHtml(u.reference)}</code></p>` : ''}
<p class="nrm-summary">${escapeHtml(u.summary)}</p>
${domains ? `<div style="display:flex; gap:6px; flex-wrap:wrap;">${domains}</div>` : ''}
</div>
<div style="display:flex; flex-direction:column; gap:8px; align-items:flex-end; flex-shrink:0;">
${u.effective_date ? `<p style="font-size:0.75rem; color:var(--gray-500);">In vigore: <strong>${formatDate(u.effective_date)}</strong></p>` : ''}
${u.url ? `<a href="${escapeHtml(u.url)}" target="_blank" rel="noopener" class="btn btn-sm btn-secondary" style="font-size:0.75rem;">
<svg class="icon" aria-hidden="true" style="width:12px;height:12px;fill:currentColor;margin-right:4px;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-external-link"></use></svg>
Fonte
</a>` : ''}
${!acked ? `<button class="btn btn-sm btn-primary" onclick="acknowledgeUpdate(${u.id})">
<svg class="icon" aria-hidden="true" style="width:14px;height:14px;fill:currentColor;margin-right:4px;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>
Prendi in visione
</button>` : ''}
<div class="nrm-card-side">
${u.effective_date ? `<p class="nrm-eff">In vigore: <strong>${formatDate(u.effective_date)}</strong></p>` : ''}
${u.url ? `<a href="${escapeHtml(u.url)}" target="_blank" rel="noopener" class="btn-ghost"><i data-lucide="external-link" class="ic"></i>Fonte</a>` : ''}
${!acked ? `<button class="btn-primary" onclick="acknowledgeUpdate(${u.id})"><i data-lucide="check" class="ic"></i>Prendi in visione</button>` : ''}
</div>
</div>
</div>`;
});
html += '</div>';
container.innerHTML = html;
refreshIcons();
}
async function acknowledgeUpdate(id) {
showModal('Presa Visione', `
<p style="font-size:0.875rem; color:var(--gray-600); margin-bottom:16px;">
<p style="font-size:13px; color:var(--muted); margin-bottom:16px;">
Documentare la presa visione dell'aggiornamento normativo. Opzionalmente aggiungere note sull'impatto per la tua organizzazione.
</p>
<div class="form-group">
<label for="ack-notes" class="form-label">Note (opzionale)</label>
<textarea id="ack-notes" class="form-control" rows="3" placeholder="Es. Analizzato con il team di compliance. Nessun impatto diretto rilevato..."></textarea>
</div>
`, `<button class="btn btn-primary" onclick="confirmAck(${id})">Conferma presa visione</button>
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>`);
`, { footer: `<button class="btn-primary" onclick="confirmAck(${id})">Conferma presa visione</button>
<button class="btn-ghost" onclick="closeModal()">Annulla</button>` });
}
async function confirmAck(id) {
@@ -278,7 +297,7 @@
} catch (e) { showNotification('Errore di connessione.', 'error'); }
}
// Init
// ── Init dati reali ──────────────────────────────────────
loadStats();
loadUpdates();
</script>