[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
+101 -85
View File
@@ -3,51 +3,60 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Scadenziario - NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="/css/style.css?v=20260618">
<title>Scadenziario · 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>
.rev-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:16px; font-size:.92rem; line-height:1.6; }
.rev-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 18px; }
.rev-stats { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:18px; }
.rev-stat { flex:1 1 150px; min-width:130px; background:#fff; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:12px 14px; }
.rev-stat .n { font-size:1.5rem; font-weight:800; line-height:1; }
.rev-stat .l { font-size:.78rem; color:var(--gray-500,#6b7280); margin-top:4px; }
/* V3 — specifico pagina Scadenziario (non coperto da v3.css) */
.rev-intro { background:#EAF1FA; border-left:4px solid var(--c-blue); padding:14px 18px; border-radius:10px; margin-bottom:16px; font-size:13.5px; line-height:1.6; color:var(--ink); }
.rev-note { font-size:12.5px; color:#92400e; background:#FBF1E2; border:1px solid #fde68a; border-radius:10px; padding:10px 12px; margin:0 0 18px; }
.rev-stats { display:flex; flex-wrap:wrap; gap:12px; margin-bottom:18px; }
.rev-stat { flex:1 1 150px; min-width:130px; background:#fff; border:1px solid var(--line); border-radius:11px; padding:14px 16px; box-shadow:var(--shadow); }
.rev-stat .n { font-size:27px; font-weight:800; line-height:1; }
.rev-stat .l { font-size:11px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#7a8aa0; margin-top:6px; }
.rev-stat.s-overdue .n { color:#991b1b; }
.rev-stat.s-due .n { color:#b45309; }
.rev-stat.s-ok .n { color:#166534; }
.rev-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
.rev-toolbar select { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; }
.rev-toolbar { display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
.rev-toolbar select { padding:8px 11px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
.rev-toolbar .grow { flex:1 1 auto; }
.rev-table { width:100%; border-collapse:collapse; font-size:.88rem; }
.rev-table th, .rev-table td { text-align:left; padding:9px 10px; border-bottom:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
.rev-table th { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
.rev-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; }
.rev-badge.type { background:#f1f5f9; color:#475569; }
.gap-pill { display:inline-block; font-size:.72rem; font-weight:800; padding:2px 9px; border-radius:20px; }
.gap-pill.ok { background:#dcfce7; color:#166534; }
.gap-pill.due { background:#fef3c7; color:#92400e; }
.gap-pill.bad { background:#fee2e2; color:#991b1b; }
.rev-table { width:100%; border-collapse:collapse; font-size:13px; }
.rev-table th, .rev-table td { text-align:left; padding:10px 12px; border-bottom:1px solid var(--line); vertical-align:top; }
.rev-table th { font-size:10.5px; text-transform:uppercase; letter-spacing:.04em; color:#7a8aa0; }
.rev-badge { display:inline-block; font-size:10.5px; font-weight:700; padding:2px 8px; border-radius:6px; }
.rev-badge.type { background:#EEF2F7; color:#475569; }
.gap-pill { display:inline-block; font-size:11px; font-weight:800; padding:2px 9px; border-radius:20px; }
.gap-pill.ok { background:#E6F4EC; color:#166534; }
.gap-pill.due { background:#FBF1E2; color:#92400e; }
.gap-pill.bad { background:#FBE9E9; color:#991b1b; }
.rev-actions-cell { display:flex; gap:6px; flex-wrap:wrap; }
.rev-actions-cell .btn, .rev-toolbar .btn { padding:4px 9px; font-size:.76rem; }
.rev-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); }
/* Modale */
.rm-overlay { display:none; position:fixed; inset:0; background:rgba(15,23,42,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
.rev-empty { text-align:center; padding:34px 16px; color:var(--muted); }
/* Modale (page-owned) */
.rm-overlay { display:none; position:fixed; inset:0; background:rgba(15,27,46,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
.rm-overlay.open { display:flex; }
.rm-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(0,0,0,.25); }
.rm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
.rm-head h3 { margin:0; font-size:1.05rem; }
.rm-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(11,40,80,.25); }
.rm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--line); }
.rm-head h3 { margin:0; font-size:16px; }
.rm-body { padding:18px 20px; }
.rm-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; }
.rm-foot { padding:14px 20px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px; }
.rm-field { margin-bottom:14px; }
.rm-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; }
.rm-field input[type=text], .rm-field input[type=number], .rm-field input[type=date], .rm-field select, .rm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; }
.rm-field label { display:block; font-weight:600; font-size:13px; margin-bottom:4px; }
.rm-field input[type=text], .rm-field input[type=number], .rm-field input[type=date], .rm-field select, .rm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:14px; }
.rm-field textarea { min-height:64px; resize:vertical; }
.rm-row { display:flex; gap:12px; }
.rm-row > .rm-field { flex:1 1 0; }
.rm-err { color:#b91c1c; font-size:.82rem; min-height:18px; }
.rm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); }
.rm-err { color:#b91c1c; font-size:12.5px; min-height:18px; }
.rm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--muted); }
/* Toast prodotti da common.js (style.css rimosso): stile minimo coerente V3 */
.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">
@@ -64,49 +73,55 @@
<!-- PWA:end -->
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="rev.title">Scadenziario</h2>
<div class="content-header-actions">
<span class="savehint" id="rev-hint" role="status" aria-live="polite"></span>
</div>
</header>
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
<div id="topnav"></div>
<div class="content-body">
<div class="rev-intro">
<strong>Revisioni e scadenze in un unico posto.</strong>
Tieni traccia delle revisioni periodiche di ruoli, competenze, inventario, procedure, rischi, fornitori e misure. Ogni voce mostra quando è prevista la prossima revisione e ti avvisa quando è in scadenza o scaduta.
</div>
<div class="rev-note">
Ancoraggio: <strong>GV.PO-02</strong> (riesame periodico delle policy), <strong>GV.SC-07</strong> (revisione dei fornitori), <strong>PR.AT</strong>, <strong>DE.CM</strong> (monitoraggio continuo). Gli obblighi di gestione del rischio fanno capo all'<strong>art. 24 D.Lgs. 138/2024</strong>. La periodicità puntuale è una buona prassi dove non fissata da norma o contratto. Strumento di supporto, non un parere legale.
</div>
<div class="rev-stats" id="rev-stats" style="display:none;">
<div class="rev-stat"><div class="n" id="rev-stat-total">0</div><div class="l">Voci totali</div></div>
<div class="rev-stat s-overdue"><div class="n" id="rev-stat-overdue">0</div><div class="l">Scadute</div></div>
<div class="rev-stat s-due"><div class="n" id="rev-stat-due">0</div><div class="l">In scadenza (30 gg)</div></div>
<div class="rev-stat s-ok"><div class="n" id="rev-stat-ok">0</div><div class="l">In regola</div></div>
</div>
<div class="rev-toolbar">
<button class="btn btn-primary btn-sm" onclick="revOpen()">+ Nuova scadenza</button>
<button class="btn btn-outline btn-sm" id="rev-sync-btn" onclick="revDoSync()">Sincronizza scadenze esistenti</button>
<span class="grow"></span>
<label for="rev-filter" style="font-weight:600;font-size:.82rem;">Stato:</label>
<select id="rev-filter" onchange="revRender()">
<option value="all">Tutti</option>
<option value="overdue">Scadute</option>
<option value="due">In scadenza</option>
<option value="ok">In regola</option>
</select>
</div>
<div id="rev-list" aria-live="polite"></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>Monitoraggio</span>
<i data-lucide="chevron-right" class="ic"></i><span>Scadenziario</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="rev.title">Scadenziario</h1>
<p class="sub">Revisioni periodiche di ruoli, competenze, inventario, procedure, rischi, fornitori e misure</p>
</div>
</main>
</div>
<span class="savehint" id="rev-hint" role="status" aria-live="polite" style="font-size:12.5px;color:var(--muted)"></span>
</div>
<section class="card card-pad">
<div class="rev-intro">
<strong>Revisioni e scadenze in un unico posto.</strong>
Tieni traccia delle revisioni periodiche di ruoli, competenze, inventario, procedure, rischi, fornitori e misure. Ogni voce mostra quando è prevista la prossima revisione e ti avvisa quando è in scadenza o scaduta.
</div>
<div class="rev-note">
Ancoraggio: <strong>GV.PO-02</strong> (riesame periodico delle policy), <strong>GV.SC-07</strong> (revisione dei fornitori), <strong>PR.AT</strong>, <strong>DE.CM</strong> (monitoraggio continuo). Gli obblighi di gestione del rischio fanno capo all'<strong>art. 24 D.Lgs. 138/2024</strong>. La periodicità puntuale è una buona prassi dove non fissata da norma o contratto. Strumento di supporto, non un parere legale.
</div>
<div class="rev-stats" id="rev-stats" style="display:none;">
<div class="rev-stat"><div class="n" id="rev-stat-total">0</div><div class="l">Voci totali</div></div>
<div class="rev-stat s-overdue"><div class="n" id="rev-stat-overdue">0</div><div class="l">Scadute</div></div>
<div class="rev-stat s-due"><div class="n" id="rev-stat-due">0</div><div class="l">In scadenza (30 gg)</div></div>
<div class="rev-stat s-ok"><div class="n" id="rev-stat-ok">0</div><div class="l">In regola</div></div>
</div>
<div class="rev-toolbar">
<button class="btn-primary" onclick="revOpen()"><i data-lucide="plus" class="ic"></i>Nuova scadenza</button>
<button class="btn-ghost" id="rev-sync-btn" onclick="revDoSync()"><i data-lucide="refresh-cw" class="ic"></i>Sincronizza scadenze esistenti</button>
<span class="grow"></span>
<label for="rev-filter" style="font-weight:600;font-size:13px;">Stato:</label>
<select id="rev-filter" onchange="revRender()">
<option value="all">Tutti</option>
<option value="overdue">Scadute</option>
<option value="due">In scadenza</option>
<option value="ok">In regola</option>
</select>
</div>
<div id="rev-list" aria-live="polite"></div>
</section>
</main>
<!-- Modale scadenza (crea/modifica) -->
<div class="rm-overlay" id="rev-modal" role="dialog" aria-modal="true" aria-labelledby="rev-modal-title">
@@ -137,14 +152,15 @@
<div class="rm-field"><label for="rev-f-notes">Note</label><textarea id="rev-f-notes" maxlength="2000"></textarea></div>
<div class="rm-err" id="rev-err" role="alert"></div>
</div>
<div class="rm-foot"><button class="btn btn-outline" type="button" onclick="revClose()">Annulla</button><button class="btn btn-primary" type="button" id="rev-save" onclick="revSave()">Salva</button></div>
<div class="rm-foot"><button class="btn-ghost" type="button" onclick="revClose()">Annulla</button><button class="btn-primary" type="button" id="rev-save" onclick="revSave()">Salva</button></div>
</div>
</div>
<!-- Scripts V3 (ordine ESATTO come dashboard.html) -->
<script src="/js/api.js?v=20260625d"></script>
<script src="/js/common.js?v=20260624g"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></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>
@@ -180,6 +196,7 @@
if (window.I18n && I18n.init) I18n.init('it');
if (typeof loadSidebar === 'function') loadSidebar();
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') revClose(); });
const ov = el('rev-modal');
@@ -195,7 +212,6 @@
REV.roles = (rolesData && rolesData.roles) || [];
revFillOwnerSelect();
} catch (e) {
// l'organigramma può non essere ancora popolato: non bloccare la pagina
REV.roles = [];
}
}
@@ -210,7 +226,7 @@
}
async function revLoad() {
el('rev-list').innerHTML = '<div class="rev-empty">Caricamento…</div>';
el('rev-list').innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>';
try {
const data = await api.revList();
REV.items = (data && data.items) || [];
@@ -250,16 +266,16 @@
const freq = (i.frequency_months != null && i.frequency_months !== '') ? (esc(i.frequency_months) + ' mesi') : '';
return '<tr>'
+ '<td><strong>' + esc(i.title) + '</strong>'
+ (i.notes ? '<div class="gap-role-sub" style="font-size:.8rem;color:var(--gray-500,#6b7280);margin-top:3px;">' + esc(i.notes) + '</div>' : '') + '</td>'
+ (i.notes ? '<div style="font-size:12px;color:var(--muted);margin-top:3px;">' + esc(i.notes) + '</div>' : '') + '</td>'
+ '<td><span class="rev-badge type">' + esc(typeLabel(i.entity_type)) + '</span></td>'
+ '<td>' + owner + '</td>'
+ '<td>' + fmtDate(i.last_reviewed_at) + '</td>'
+ '<td>' + fmtDate(i.next_review_date) + (freq ? '<div style="font-size:.78rem;color:var(--gray-500,#6b7280);">ogni ' + freq + '</div>' : '') + '</td>'
+ '<td>' + fmtDate(i.next_review_date) + (freq ? '<div style="font-size:12px;color:var(--muted);">ogni ' + freq + '</div>' : '') + '</td>'
+ '<td><span class="gap-pill ' + meta.cls + '">' + meta.label + '</span></td>'
+ '<td><div class="rev-actions-cell">'
+ '<button class="btn btn-outline" onclick="revComplete(' + i.id + ')">Segna revisionato</button>'
+ '<button class="btn btn-outline" onclick="revOpen(' + i.id + ')">Modifica</button>'
+ '<button class="btn btn-outline" onclick="revDelete(' + i.id + ')">Elimina</button>'
+ '<button class="btn-ghost" onclick="revComplete(' + i.id + ')">Segna revisionato</button>'
+ '<button class="btn-ghost" onclick="revOpen(' + i.id + ')">Modifica</button>'
+ '<button class="btn-ghost" onclick="revDelete(' + i.id + ')">Elimina</button>'
+ '</div></td>'
+ '</tr>';
}).join('');