[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:
co-authored by
Claude Opus 4.8
parent
b0fc190704
commit
a4150b5389
+166
-147
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user