[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
+86 -65
View File
@@ -3,61 +3,62 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Competenze - NIS2 Agile</title>
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="/css/style.css?v=20260618">
<title>Competenze · 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>
.cmp-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; }
.cmp-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 18px; }
/* V3+ page-specific (componenti non coperti da v3.css): tab interni, tabella, gap card, modali. Token V3. */
.cmp-intro { background:#EFF4FB; border-left:4px solid var(--c-blue); padding:14px 18px; border-radius:10px; margin-bottom:14px; font-size:13.5px; line-height:1.6; color:var(--ink); }
.cmp-note { font-size:12.5px; color:#9A6418; background:#FBF1E2; border:1px solid #f3d9a8; border-radius:10px; padding:10px 12px; margin:0 0 18px; }
.cmp-tabs { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:18px; }
.cmp-tab { flex:1 1 160px; min-width:140px; padding:10px 12px; border:1.5px solid var(--gray-200,#e5e7eb); border-radius:10px; background:#fff; cursor:pointer; font-size:.85rem; font-weight:600; text-align:center; }
.cmp-tab.active { border-color:var(--primary,#2563eb); background:#eff6ff; color:var(--primary,#2563eb); }
.cmp-tab { flex:1 1 160px; min-width:140px; padding:11px 12px; border:1.5px solid var(--line); border-radius:10px; background:#fff; cursor:pointer; font:inherit; font-size:13px; font-weight:600; text-align:center; color:var(--ink); }
.cmp-tab.active { border-color:var(--c-blue); background:#EFF4FB; color:var(--c-blue); }
.cmp-panel { display:none; }
.cmp-panel.active { display:block; }
.cmp-table { width:100%; border-collapse:collapse; font-size:.88rem; }
.cmp-table th, .cmp-table td { text-align:left; padding:9px 10px; border-bottom:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
.cmp-table th { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
.cmp-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; }
.cmp-badge.global { background:#ede9fe; color:#5b21b6; }
.cmp-badge.area { background:#f1f5f9; color:#475569; }
.cmp-badge.lvl { background:#e0f2fe; color:#075985; }
.cmp-table { width:100%; border-collapse:collapse; font-size:13.5px; }
.cmp-table th, .cmp-table td { text-align:left; padding:10px 11px; border-bottom:1px solid var(--line); vertical-align:top; }
.cmp-table th { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); font-weight:700; }
.cmp-badge { display:inline-block; font-size:10.5px; font-weight:700; padding:2px 8px; border-radius:6px; }
.cmp-badge.global { background:#EFE7FB; color:#5b21b6; }
.cmp-badge.area { background:#EEF2F7; color:#5b6b80; }
.cmp-badge.lvl { background:#E7F0FB; color:#1F5FA8; }
.cmp-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
.cmp-toolbar select, .cmp-inline select, .cmp-inline input { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; }
.cmp-inline { display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end; background:#f9fafb; border:1px solid var(--gray-100,#f3f4f6); border-radius:10px; padding:12px; margin-bottom:14px; }
.cmp-toolbar select, .cmp-inline select, .cmp-inline input { padding:8px 10px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
.cmp-inline { display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end; background:#FAFCFE; border:1px solid var(--line); border-radius:10px; padding:12px; margin-bottom:14px; }
.cmp-inline .fld { display:flex; flex-direction:column; gap:3px; }
.cmp-inline .fld label { font-size:.72rem; font-weight:600; color:var(--gray-500,#6b7280); }
.cmp-inline .fld label { font-size:11px; font-weight:700; color:var(--muted); }
.cmp-actions-cell { display:flex; gap:6px; flex-wrap:wrap; }
.cmp-actions-cell .btn, .cmp-toolbar .btn { padding:4px 9px; font-size:.76rem; }
/* Gap cards */
.gap-role { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; }
.gap-role-head { background:var(--gray-50,#f9fafb); padding:11px 14px; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:6px; }
.gap-role { border:1px solid var(--line); border-radius:11px; margin-bottom:14px; overflow:hidden; }
.gap-role-head { background:#FAFCFE; padding:12px 14px; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:6px; }
.gap-role-title { font-weight:700; }
.gap-role-sub { font-size:.82rem; color:var(--gray-500,#6b7280); }
.gap-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; padding:10px 14px; border-top:1px solid var(--gray-100,#f3f4f6); }
.gap-row .skill { flex:1 1 220px; font-weight:600; font-size:.9rem; }
.gap-meter { font-size:.82rem; }
.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.bad { background:#fee2e2; color:#991b1b; }
.gap-role-sub { font-size:12px; color:var(--muted); }
.gap-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; padding:11px 14px; border-top:1px solid var(--line); }
.gap-row .skill { flex:1 1 220px; font-weight:600; font-size:13.5px; }
.gap-meter { font-size:12.5px; color:var(--muted); }
.gap-pill { display:inline-block; font-size:10.5px; font-weight:800; padding:2px 9px; border-radius:20px; }
.gap-pill.ok { background:#E6F4EC; color:#1E7A4E; }
.gap-pill.bad { background:#FBE9E9; color:#B23636; }
.gap-row .ctas { display:flex; gap:6px; flex-wrap:wrap; }
.gap-row .ctas .btn { padding:4px 9px; font-size:.76rem; }
.cmp-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); }
.cmp-empty { text-align:center; padding:34px 16px; color:var(--muted); }
/* Modale */
.cm-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; }
.cm-overlay { display:none; position:fixed; inset:0; background:rgba(15,38,60,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
.cm-overlay.open { display:flex; }
.cm-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(0,0,0,.25); }
.cm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
.cm-head h3 { margin:0; font-size:1.05rem; }
.cm-dialog { background:#fff; border-radius:14px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(11,40,80,.25); }
.cm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--line); }
.cm-head h3 { margin:0; font-size:17px; font-weight:800; }
.cm-body { padding:18px 20px; }
.cm-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; }
.cm-foot { padding:14px 20px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px; }
.cm-field { margin-bottom:14px; }
.cm-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; }
.cm-field input[type=text], .cm-field select, .cm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; }
.cm-field label { display:block; font-weight:600; font-size:12.5px; margin-bottom:5px; }
.cm-field input[type=text], .cm-field select, .cm-field textarea { width:100%; padding:9px 11px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
.cm-field textarea { min-height:64px; resize:vertical; }
.cm-err { color:#b91c1c; font-size:.82rem; min-height:18px; }
.cm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); }
.cm-err { color:var(--c-red); font-size:12.5px; min-height:18px; }
.cm-close { background:none; border:none; font-size:22px; line-height:1; cursor:pointer; color:#9aa8ba; }
.cm-maplist { margin-top:10px; }
.cm-maprow { display:flex; justify-content:space-between; align-items:center; padding:7px 0; border-top:1px solid var(--gray-100,#f3f4f6); font-size:.86rem; }
.cm-maprow { display:flex; justify-content:space-between; align-items:center; padding:8px 0; border-top:1px solid var(--line); font-size:13px; }
.savehint { font-size:12px; color:var(--muted); }
</style>
<!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest">
@@ -74,17 +75,24 @@
<!-- 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="comp.title">Competenze</h2>
<div class="content-header-actions">
<span class="savehint" id="cmp-hint" role="status" aria-live="polite"></span>
</div>
</header>
<div id="topnav"></div>
<div class="content-body">
<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="comp.title">Competenze</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="comp.title">Competenze</h1>
<p class="sub">Skill matrix, requisiti per ruolo e copertura dei gap</p>
</div>
<span class="savehint" id="cmp-hint" role="status" aria-live="polite"></span>
</div>
<div class="stack">
<section class="card card-pad">
<div class="cmp-intro">
<strong>Competenze, requisiti e gap.</strong>
Definisci le competenze necessarie, assegnale ai ruoli dell'organigramma, registra quelle possedute dalle persone e individua i divari (gap) da colmare con formazione o azioni correttive.
@@ -103,7 +111,7 @@
<!-- TAB 1 — Catalogo competenze -->
<div class="cmp-panel active" id="panel-catalog">
<div class="cmp-toolbar">
<button class="btn btn-primary btn-sm" onclick="cmpOpenSkill()">+ Nuova competenza</button>
<button class="btn-primary" onclick="cmpOpenSkill()"><i data-lucide="plus" class="ic"></i>Nuova competenza</button>
<span class="gap-role-sub" id="cmp-catalog-count"></span>
</div>
<div id="cmp-catalog"></div>
@@ -112,13 +120,13 @@
<!-- TAB 2 — Requisiti per ruolo -->
<div class="cmp-panel" id="panel-roles">
<div class="cmp-toolbar">
<label for="cmp-role-sel" style="font-weight:600;font-size:.86rem;">Ruolo:</label>
<label for="cmp-role-sel" style="font-weight:600;font-size:13px;">Ruolo:</label>
<select id="cmp-role-sel" onchange="cmpLoadRoleSkills()"></select>
</div>
<div class="cmp-inline" id="cmp-role-add" style="display:none;">
<div class="fld"><label>Competenza</label><select id="cmp-rs-skill"></select></div>
<div class="fld"><label>Livello richiesto</label><select id="cmp-rs-level"></select></div>
<button class="btn btn-primary btn-sm" onclick="cmpAddRoleSkill()">Aggiungi requisito</button>
<button class="btn-primary" onclick="cmpAddRoleSkill()">Aggiungi requisito</button>
</div>
<div id="cmp-role-skills"></div>
</div>
@@ -126,7 +134,7 @@
<!-- TAB 3 — Competenze persone -->
<div class="cmp-panel" id="panel-users">
<div class="cmp-toolbar">
<label for="cmp-user-sel" style="font-weight:600;font-size:.86rem;">Persona:</label>
<label for="cmp-user-sel" style="font-weight:600;font-size:13px;">Persona:</label>
<select id="cmp-user-sel" onchange="cmpLoadUserSkills()"></select>
</div>
<div class="cmp-inline" id="cmp-user-add" style="display:none;">
@@ -134,7 +142,7 @@
<div class="fld"><label>Livello posseduto</label><select id="cmp-us-level"></select></div>
<div class="fld"><label>Corso (opz.)</label><select id="cmp-us-course"></select></div>
<div class="fld"><label>Evidenza (opz.)</label><input type="text" id="cmp-us-evidence" maxlength="500" placeholder="es. attestato, data"></div>
<button class="btn btn-primary btn-sm" onclick="cmpAddUserSkill()">Registra</button>
<button class="btn-primary" onclick="cmpAddUserSkill()">Registra</button>
</div>
<div id="cmp-user-skills"></div>
</div>
@@ -142,14 +150,14 @@
<!-- TAB 4 — Gap & azioni -->
<div class="cmp-panel" id="panel-gap">
<div class="cmp-toolbar">
<button class="btn btn-outline btn-sm" onclick="cmpLoadGap()">Ricalcola gap</button>
<button class="btn-ghost" onclick="cmpLoadGap()"><i data-lucide="refresh-cw" class="ic"></i>Ricalcola gap</button>
<span class="gap-role-sub" id="cmp-gap-summary"></span>
</div>
<div id="cmp-gap"></div>
</div>
</div>
</main>
</div>
</section>
</div>
</main>
<!-- Modale competenza (crea/modifica) -->
<div class="cm-overlay" id="cmp-skill-modal" role="dialog" aria-modal="true" aria-labelledby="cmp-skill-title">
@@ -162,7 +170,7 @@
<div class="cm-field"><label for="cmp-skill-desc">Descrizione</label><textarea id="cmp-skill-desc" maxlength="2000"></textarea></div>
<div class="cm-err" id="cmp-skill-err" role="alert"></div>
</div>
<div class="cm-foot"><button class="btn btn-outline" type="button" onclick="cmpClose('cmp-skill-modal')">Annulla</button><button class="btn btn-primary" type="button" id="cmp-skill-save" onclick="cmpSaveSkill()">Salva</button></div>
<div class="cm-foot"><button class="btn-ghost" type="button" onclick="cmpClose('cmp-skill-modal')">Annulla</button><button class="btn-primary" type="button" id="cmp-skill-save" onclick="cmpSaveSkill()">Salva</button></div>
</div>
</div>
@@ -174,21 +182,34 @@
<input type="hidden" id="cmp-courses-skill-id">
<div class="cm-field" style="display:flex;gap:8px;align-items:flex-end;">
<div style="flex:1;"><label for="cmp-courses-sel">Aggiungi corso</label><select id="cmp-courses-sel"></select></div>
<button class="btn btn-primary" type="button" onclick="cmpMapCourse()">Aggiungi</button>
<button class="btn-primary" type="button" onclick="cmpMapCourse()">Aggiungi</button>
</div>
<div class="cm-maplist" id="cmp-courses-list"></div>
<div class="cm-err" id="cmp-courses-err" role="alert"></div>
</div>
<div class="cm-foot"><button class="btn btn-outline" type="button" onclick="cmpClose('cmp-courses-modal')">Chiudi</button></div>
<div class="cm-foot"><button class="btn-ghost" type="button" onclick="cmpClose('cmp-courses-modal')">Chiudi</button></div>
</div>
</div>
<!-- Scripts V3. competenze.js si auto-inizializza su DOMContentLoaded (checkAuth/loadSidebar/I18n/Help). -->
<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 src="/js/competenze.js?v=20260617"></script>
<script src="/js/competenze.js?v=20260627"></script>
<script>
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
v3Icons();
document.addEventListener('DOMContentLoaded', function () {
v3Icons();
var root = document.querySelector('main.wrap');
if (root && window.MutationObserver) {
var mo = new MutationObserver(function () { v3Icons(); });
mo.observe(root, { childList: true, subtree: true });
}
});
</script>
</body>
</html>