[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
+139 -247
View File
@@ -3,222 +3,112 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Formazione - 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 (tabelle/modali/form) -> 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>Formazione · NIS2 Agile</title>
<!-- UI V3: Inter self-host + design system v3.css. No external CDN, no Bootstrap Italia. -->
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<style>
/* V3+ page-specific (componenti non coperti da v3.css): tab interni, card corso,
tabella, badge stato, barra compliance. + componenti prodotti dalla JS inline via
common.js (showModal/showNotification): modal-* / form-* / .btn-secondary, mappati a token V3
perche' style.css e' rimosso. */
/* ── Tab Navigation ─────────────────────────────────────── */
.tab-nav {
display: flex;
gap: 0;
border-bottom: 2px solid var(--gray-200);
margin-bottom: 24px;
}
.tab-nav button {
padding: 12px 24px;
background: none;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
color: var(--gray-500);
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
}
.tab-nav button:hover {
color: var(--gray-700);
background: var(--gray-50);
}
.tab-nav button.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.tab-panel { display: none; }
.tab-panel.active { display: block; }
.tab-nav { display:flex; gap:0; border-bottom:2px solid var(--line); margin-bottom:24px; }
.tab-nav button { padding:12px 22px; background:none; border:none; border-bottom:2px solid transparent; margin-bottom:-2px; color:var(--muted); font:inherit; font-size:13.5px; font-weight:600; cursor:pointer; }
.tab-nav button:hover { color:var(--ink); background:#F5F8FC; }
.tab-nav button.active { color:var(--c-blue); border-bottom-color:var(--c-blue); }
.tab-panel { display:none; }
.tab-panel.active { display:block; }
/* ── Course Cards Grid ──────────────────────────────────── */
.courses-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
}
.course-card {
background: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
padding: 20px;
transition: box-shadow var(--transition);
position: relative;
display: flex;
flex-direction: column;
}
.course-card:hover {
box-shadow: var(--card-shadow-hover);
}
.course-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.course-card-title {
font-size: 1rem;
font-weight: 600;
color: var(--gray-800);
margin: 0;
}
.course-card-desc {
font-size: 0.85rem;
color: var(--gray-500);
margin-bottom: 16px;
flex-grow: 1;
line-height: 1.5;
}
.course-card-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
font-size: 0.8rem;
}
.course-meta-item {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--gray-500);
background: var(--gray-100);
padding: 4px 10px;
border-radius: 20px;
}
.course-meta-item svg { width: 14px; height: 14px; }
.badge-mandatory {
background: var(--danger-bg);
color: var(--danger);
font-weight: 600;
padding: 3px 10px;
border-radius: 20px;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.courses-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(320px, 1fr)); gap:18px; }
.course-card { background:#fff; border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow); padding:18px 20px; position:relative; display:flex; flex-direction:column; }
.course-card:hover { box-shadow:0 8px 20px rgba(16,40,80,.13); }
.course-card-header { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; margin-bottom:12px; }
.course-card-title { font-size:15px; font-weight:700; color:var(--ink); margin:0; }
.course-card-desc { font-size:13px; color:var(--muted); margin-bottom:16px; flex-grow:1; line-height:1.5; }
.course-card-meta { display:flex; flex-wrap:wrap; gap:8px; align-items:center; font-size:12px; }
.course-meta-item { display:inline-flex; align-items:center; gap:4px; color:var(--muted); background:#EEF2F7; padding:4px 10px; border-radius:20px; }
.course-meta-item svg { width:14px; height:14px; }
.badge-mandatory { background:#FBE9E9; color:#B23636; font-weight:700; padding:3px 10px; border-radius:20px; font-size:10.5px; text-transform:uppercase; letter-spacing:.04em; }
/* ── Assignments Table ──────────────────────────────────── */
.data-table {
width: 100%;
border-collapse: collapse;
background: var(--card-bg);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--card-shadow);
}
.data-table thead {
background: var(--gray-50);
}
.data-table th {
text-align: left;
padding: 12px 16px;
font-size: 0.8rem;
font-weight: 600;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--gray-200);
}
.data-table td {
padding: 12px 16px;
font-size: 0.875rem;
color: var(--gray-700);
border-bottom: 1px solid var(--gray-100);
}
.data-table tr:last-child td { border-bottom: none; }
.data-table tr:hover td { background: var(--gray-50); }
/* ── Tables ─────────────────────────────────────────────── */
.data-table { width:100%; border-collapse:collapse; background:#fff; border:1px solid var(--line); border-radius:var(--radius); overflow:hidden; box-shadow:var(--shadow); }
.data-table thead { background:#FAFCFE; }
.data-table th { text-align:left; padding:12px 16px; font-size:11px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.04em; border-bottom:1px solid var(--line); }
.data-table td { padding:12px 16px; font-size:13.5px; color:var(--ink); border-bottom:1px solid var(--line); }
.data-table tr:last-child td { border-bottom:none; }
.data-table tr:hover td { background:#FAFCFE; }
/* ── Status Badges ──────────────────────────────────────── */
.status-badge {
display: inline-flex;
align-items: center;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 500;
gap: 4px;
}
.status-badge.assigned { background: var(--gray-100); color: var(--gray-600); }
.status-badge.in_progress { background: var(--info-bg); color: var(--info); }
.status-badge.completed { background: var(--secondary-bg); color: var(--secondary); }
.status-badge.overdue { background: var(--danger-bg); color: var(--danger); }
.status-badge { display:inline-flex; align-items:center; padding:4px 12px; border-radius:20px; font-size:11px; font-weight:700; gap:4px; }
.status-badge.assigned { background:#EEF2F7; color:#5b6b80; }
.status-badge.in_progress { background:#E0F2FE; color:#075985; }
.status-badge.completed { background:#E6F4EC; color:#1E7A4E; }
.status-badge.overdue { background:#FBE9E9; color:#B23636; }
/* ── Compliance Percentage Bar ──────────────────────────── */
.compliance-bar-container {
display: flex;
align-items: center;
gap: 10px;
}
.compliance-bar {
flex-grow: 1;
height: 8px;
background: var(--gray-200);
border-radius: 4px;
overflow: hidden;
}
.compliance-bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.compliance-pct {
font-size: 0.85rem;
font-weight: 600;
min-width: 45px;
text-align: right;
}
.compliance-pct.high { color: var(--secondary); }
.compliance-pct.medium { color: var(--warning); }
.compliance-pct.low { color: var(--danger); }
/* ── Compliance Bar ─────────────────────────────────────── */
.compliance-bar-container { display:flex; align-items:center; gap:10px; }
.compliance-bar { flex-grow:1; height:8px; background:#EAF0F6; border-radius:4px; overflow:hidden; }
.compliance-bar-fill { height:100%; border-radius:4px; transition:width .3s ease; }
.compliance-pct { font-size:13px; font-weight:700; min-width:45px; text-align:right; }
.compliance-pct.high { color:var(--c-green); }
.compliance-pct.medium { color:var(--c-amber); }
.compliance-pct.low { color:var(--c-red); }
/* ── Spinner/Loading ────────────────────────────────────── */
.loading-state {
text-align: center;
padding: 48px 20px;
color: var(--gray-400);
}
.loading-state .spinner {
width: 32px;
height: 32px;
border: 3px solid var(--gray-200);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 12px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Loading / Empty ────────────────────────────────────── */
.loading-state { text-align:center; padding:48px 20px; color:var(--muted); }
.loading-state .spinner { width:32px; height:32px; border:3px solid var(--line); border-top-color:var(--c-blue); border-radius:50%; animation:spin .8s linear infinite; margin:0 auto 12px; }
@keyframes spin { to { transform:rotate(360deg); } }
@media (prefers-reduced-motion:reduce){ .loading-state .spinner{ animation:none; } }
.empty-state-box { text-align:center; padding:48px 20px; color:var(--muted); }
.empty-state-box svg { width:48px; height:48px; margin-bottom:12px; opacity:.45; }
.empty-state-box h4 { color:var(--ink); margin:0 0 4px; font-size:15px; font-weight:700; }
.text-muted { color:var(--muted); }
/* ── Empty State ────────────────────────────────────────── */
.empty-state-box {
text-align: center;
padding: 48px 20px;
color: var(--gray-400);
}
.empty-state-box svg {
width: 48px;
height: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.empty-state-box h4 {
color: var(--gray-600);
margin-bottom: 4px;
}
/* ── Modale (prodotta da common.js showModal — style.css rimosso) ── */
.modal-overlay { position:fixed; inset:0; background:rgba(15,38,60,.6); backdrop-filter:blur(4px); z-index:1000; display:flex; align-items:center; justify-content:center; opacity:0; visibility:hidden; transition:opacity .2s,visibility .2s; }
.modal-overlay.active { opacity:1; visibility:visible; }
.modal { background:#fff; border-radius:14px; box-shadow:0 20px 60px rgba(11,40,80,.25); width:90%; max-width:560px; max-height:85vh; overflow-y:auto; transform:translateY(20px) scale(.97); transition:transform .2s; }
.modal-overlay.active .modal { transform:translateY(0) scale(1); }
.modal-header { padding:18px 22px; border-bottom:1px solid var(--line); display:flex; align-items:center; justify-content:space-between; }
.modal-header h3 { font-size:17px; font-weight:800; color:var(--ink); margin:0; }
.modal-close { background:none; border:none; color:#9aa8ba; cursor:pointer; padding:4px; border-radius:8px; line-height:1; font-size:22px; }
.modal-close:hover { color:var(--ink); }
.modal-body { padding:22px; }
.modal-footer { padding:14px 22px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px; }
/* ── Form (dentro le modali) ────────────────────────────── */
.form-group { margin-bottom:14px; }
.form-label { display:block; font-weight:600; font-size:12.5px; margin-bottom:5px; color:var(--ink); }
.form-input, .form-select { width:100%; padding:9px 11px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
.form-input:focus, .form-select:focus { outline:2px solid var(--gold); outline-offset:1px; }
/* ── Bottoni extra (btn-secondary/btn-sm) usati dalla JS inline ── */
.btn-secondary { display:inline-flex; align-items:center; gap:7px; border:1px solid var(--line); background:#fff; border-radius:8px; padding:8px 13px; font:inherit; font-weight:600; font-size:12.5px; color:var(--ink); cursor:pointer; }
.btn-secondary:hover { background:#F5F8FC; border-color:#c7d4e4; }
.btn-sm { padding:6px 11px; font-size:12px; }
/* ── Notifiche (prodotte da common.js showNotification — style.css rimosso) ── */
.notification-container { position:fixed; top:20px; right:20px; z-index:2000; 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,.16); padding:12px 14px; min-width:260px; max-width:380px; font-size:13px; transition:opacity .3s,transform .3s; }
.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.fade-out { opacity:0; transform:translateX(12px); }
.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; color:var(--ink); }
.notification-close { background:none; border:none; font-size:18px; line-height:1; cursor:pointer; color:#9aa8ba; }
/* ── Responsive ─────────────────────────────────────────── */
@media (max-width: 768px) {
.courses-grid { grid-template-columns: 1fr; }
.tab-nav button { padding: 10px 14px; font-size: 0.82rem; }
.data-table { font-size: 0.8rem; }
.courses-grid { grid-template-columns:1fr; }
.tab-nav button { padding:10px 14px; font-size:12.5px; }
.data-table { font-size:12.5px; }
}
</style>
<!-- PWA:start -->
@@ -236,19 +126,24 @@
<!-- PWA:end -->
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<div id="topnav"></div>
<main class="main-content">
<header class="content-header">
<h1 class="h2" data-i18n="training.title">Formazione e Awareness</h1>
<div class="content-header-actions">
<span class="text-muted" style="font-size:0.8rem; margin-right:8px;">Art. 20 NIS2</span>
<button class="btn btn-primary" onclick="showCreateCourseModal()">+ Nuovo Corso</button>
</div>
</header>
<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>Struttura interna</span>
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="training.title">Formazione</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="training.title">Formazione e Awareness</h1>
<p class="sub">Piani formativi, assegnazioni e compliance — Art. 20 NIS2</p>
</div>
<button class="btn-primary" onclick="showCreateCourseModal()"><i data-lucide="plus" class="ic"></i>Nuovo Corso</button>
</div>
<div class="content-body">
<div class="stack">
<section class="card card-pad">
<!-- Tab Navigation -->
<div class="tab-nav">
<button class="active" onclick="switchTab('courses', this)">Corsi</button>
@@ -279,7 +174,7 @@
<!-- Tab: Compliance Formativa -->
<div class="tab-panel" id="tab-compliance">
<div style="display:flex; justify-content:flex-end; margin-bottom:16px;">
<button class="btn btn-primary" onclick="showAssignCourseModal()">Assegna Corso</button>
<button class="btn-primary" onclick="showAssignCourseModal()"><i data-lucide="user-plus" class="ic"></i>Assegna Corso</button>
</div>
<div id="compliance-container">
<div class="loading-state">
@@ -288,30 +183,27 @@
</div>
</div>
</div>
</div>
</main>
</div>
</section>
</div>
</main>
<!-- Scripts V3: api.js → common.js (helper+FAB reali+showModal/showNotification) →
Lucide self-host → topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help.
La logica della pagina e' inline (nessun training.js): preservata invariata. -->
<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 & Init ─────────────────────────────────────────
// ── Auth & Init (ordine V3) ─────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init();
HelpSystem.init();
loadSidebar(); // = loadTopnavV3() (topnav-v3.js)
if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
v3Icons();
// ── Labels ──────────────────────────────────────────────
const roleLabels = {
@@ -460,8 +352,8 @@
`, {
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="createCourse()">Crea Corso</button>
<button class="btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn-primary" onclick="createCourse()">Crea Corso</button>
`
});
}
@@ -549,18 +441,18 @@
let actionBtn = '';
if (displayStatus === 'assigned' || displayStatus === 'overdue') {
actionBtn = `<button class="btn btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'in_progress')">Inizia</button>`;
actionBtn = `<button class="btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'in_progress')">Inizia</button>`;
} else if (displayStatus === 'in_progress') {
actionBtn = `<button class="btn btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'completed')">Completa</button>`;
actionBtn = `<button class="btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'completed')">Completa</button>`;
} else {
actionBtn = `<span style="color:var(--secondary); font-weight:500; font-size:0.85rem;">Completato</span>`;
actionBtn = `<span style="color:var(--c-green); font-weight:600; font-size:13px;">Completato</span>`;
}
html += `
<tr>
<td>
<div style="font-weight:500;">${escapeHtml(a.title || '')}</div>
${a.is_mandatory == 1 ? '<span style="font-size:0.75rem; color:var(--danger);">Obbligatorio</span>' : ''}
<div style="font-weight:600;">${escapeHtml(a.title || '')}</div>
${a.is_mandatory == 1 ? '<span style="font-size:11px; color:var(--c-red);">Obbligatorio</span>' : ''}
</td>
<td><span class="status-badge ${displayStatus}">${statusLabels[displayStatus] || displayStatus}</span></td>
<td>${a.due_date ? formatDate(a.due_date) : '-'}</td>
@@ -638,11 +530,11 @@
const total = assignments.length;
const pct = m.mandatory_compliance != null ? m.mandatory_compliance : 100;
const pctClass = pct >= 80 ? 'high' : pct >= 50 ? 'medium' : 'low';
const barColor = pct >= 80 ? 'var(--secondary)' : pct >= 50 ? 'var(--warning)' : 'var(--danger)';
const barColor = pct >= 80 ? 'var(--c-green)' : pct >= 50 ? 'var(--c-amber)' : 'var(--c-red)';
html += `
<tr>
<td style="font-weight:500;">${escapeHtml(m.full_name || '-')}</td>
<td style="font-weight:600;">${escapeHtml(m.full_name || '-')}</td>
<td>${escapeHtml(m.email || '-')}</td>
<td>${escapeHtml(m.role || '-')}</td>
<td>${completed} / ${total}</td>
@@ -689,7 +581,7 @@
<label style="display:flex; align-items:center; gap:8px; padding:6px 0; cursor:pointer;">
<input type="checkbox" class="assign-user-cb" value="${m.id}">
<span>${escapeHtml(m.full_name || m.email)}</span>
<span style="color:var(--gray-400); font-size:0.8rem;">(${escapeHtml(m.role || '')})</span>
<span style="color:var(--muted); font-size:12px;">(${escapeHtml(m.role || '')})</span>
</label>
`).join('');
}
@@ -705,15 +597,15 @@
</div>
<div class="form-group">
<label class="form-label">Seleziona Utenti</label>
<div style="max-height:200px; overflow-y:auto; border:1px solid var(--gray-200); border-radius:var(--border-radius-sm); padding:8px 12px;">
<div style="max-height:200px; overflow-y:auto; border:1px solid var(--line); border-radius:8px; padding:8px 12px;">
${membersHtml}
</div>
</div>
`, {
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="doAssignCourse()">Assegna</button>
<button class="btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn-primary" onclick="doAssignCourse()">Assegna</button>
`
});
}