[FEAT] UI V3 — foundation (v3.css + topnav-v3.js + Inter/Lucide self-host) + dashboard pilota fedele al mockup
Fase 0 della migrazione UI V3 (sidebar V2 → top-nav navy/oro, font Inter, icone Lucide). - public/css/v3.css: design system portato fedelmente dal mockup index.html (token :root, chrome appbar+mega-menu, card/stat/calendario/anag/doc/fab, @media ≤900/≤560), + aggiunte minime V3+ per stati loading/empty/error, mobile nav toggle e touch≥44px (AGID/WCAG). - public/vendor/inter/: Inter self-host (variable woff2 latin+latin-ext) + inter.css + README (NO Google CDN). - public/vendor/lucide/lucide.min.js: Lucide UMD self-host (NO unpkg CDN). - public/js/topnav-v3.js: appbar + mega-menu data-driven dalla mappa IA (§6, 9 gruppi), aria-current sulla voce attiva, riusa gli helper di common.js (utente/logout/versione/org-switcher, branding, impersonate) e i 3 FAB reali (ARIA + bug-reporter + notifiche). window.loadSidebar=loadTopnavV3. - public/dashboard.html: convertita al V3 fedele al mockup, cablata sui DATI REALI via api.js (overview/compliance-score/upcoming-deadlines/recent-activity/organizations-current); monitor giornaliero/settimanale alimentati dagli eventi reali; documenti istituzionali = stato vuoto onesto. - public/js/dashboard.js (nuovo): wiring dati reali + render calendario/scadenze/attività/anagrafica; stati loading/empty/error; zero dati finti del mockup. node --check OK su topnav-v3.js e dashboard.js. Solo UI: nessun PHP/DB/api.js toccato. 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
2a5d90f8bb
commit
b0fc190704
@@ -0,0 +1,265 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
* NIS2 Agile — UI V3 design system
|
||||||
|
* Portato FEDELMENTE dal <style> del mockup "docs/mockup Ui/index.html".
|
||||||
|
* Token + chrome (appbar/mega-menu) + componenti (card/stat/calendario/anag/doc/fab).
|
||||||
|
* Font: Inter self-host (/vendor/inter/inter.css). Icone: Lucide self-host.
|
||||||
|
* Aggiunte minime (segnate "V3+") per stati loading/empty/error e touch≥44px.
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
:root{
|
||||||
|
--navy-top:#0A2E54; --navy-nav:#103A61; --navy-hover:#1B4A75; --gold:#F0B429;
|
||||||
|
--bg:#EFF4FA; --card:#FFFFFF; --ink:#15263C; --muted:#64748B; --line:#E3E9F0;
|
||||||
|
--c-green:#1E8A5A; --c-blue:#1F5FA8; --c-amber:#D98324; --c-red:#C62828;
|
||||||
|
--radius:12px; --shadow:0 1px 2px rgba(16,40,80,.06),0 8px 24px rgba(16,40,80,.06);
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box} html{-webkit-text-size-adjust:100%}
|
||||||
|
body{margin:0;background:var(--bg);color:var(--ink);
|
||||||
|
font-family:'Inter',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;font-size:14px;line-height:1.5}
|
||||||
|
a{color:inherit;text-decoration:none}
|
||||||
|
:focus-visible{outline:2px solid var(--gold);outline-offset:2px;border-radius:4px}
|
||||||
|
|
||||||
|
/* ── Chrome: appbar ─────────────────────────────────────────────────────── */
|
||||||
|
.appbar{position:sticky;top:0;z-index:50}
|
||||||
|
.bar-top{background:var(--navy-top);color:#fff}
|
||||||
|
.bar-top-in{max-width:1280px;margin:0 auto;display:flex;align-items:center;gap:16px;padding:10px 24px}
|
||||||
|
.flag{width:18px;height:13px;border-radius:2px;overflow:hidden;display:flex;flex:0 0 auto;box-shadow:0 0 0 1px rgba(255,255,255,.25)}
|
||||||
|
.flag i{flex:1}.flag .g{background:#1f8a4c}.flag .w{background:#fff}.flag .r{background:#cd2b2b}
|
||||||
|
.logo-mark{width:34px;height:34px;border-radius:8px;background:var(--c-green);display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:13px;flex:0 0 auto}
|
||||||
|
.brand-txt{line-height:1.1;min-width:0}
|
||||||
|
.brand-txt b{font-size:15px;font-weight:700}
|
||||||
|
.brand-txt span{display:block;font-size:10.5px;color:#A9C2DC;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.search{flex:1;max-width:340px;display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.10);border:1px solid rgba(255,255,255,.16);border-radius:8px;padding:7px 12px;color:#cfe0f1;min-width:120px}
|
||||||
|
.search input{flex:1;border:0;background:transparent;color:#fff;font:inherit;outline:none;min-width:0}
|
||||||
|
.search input::placeholder{color:#9db8d2}
|
||||||
|
.spacer{flex:1}
|
||||||
|
.btn-incident{display:inline-flex;align-items:center;gap:8px;background:#D24545;color:#fff;border:0;border-radius:8px;padding:9px 15px;font:inherit;font-weight:600;font-size:13px;cursor:pointer;box-shadow:0 1px 2px rgba(120,20,20,.35);white-space:nowrap}
|
||||||
|
.btn-incident:hover{background:#BF3A3A}
|
||||||
|
.btn-incident .ic{width:16px;height:16px}
|
||||||
|
.user{display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.16);border-radius:8px;padding:5px 10px 5px 5px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}
|
||||||
|
.user:hover{background:rgba(255,255,255,.16)}
|
||||||
|
.avatar{width:24px;height:24px;border-radius:6px;background:var(--c-green);color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700}
|
||||||
|
|
||||||
|
/* ── Chrome: nav + mega-menu ────────────────────────────────────────────── */
|
||||||
|
.bar-nav{background:var(--navy-nav);border-top:1px solid rgba(255,255,255,.06)}
|
||||||
|
.bar-nav-in{max-width:1280px;margin:0 auto;padding:0 16px;display:flex;align-items:stretch;gap:2px;flex-wrap:wrap}
|
||||||
|
.nav-item{position:relative}
|
||||||
|
.nav-link{display:flex;align-items:center;gap:7px;color:#cfe0f1;font-weight:600;font-size:13px;padding:13px 14px;white-space:nowrap;border-bottom:3px solid transparent;cursor:pointer}
|
||||||
|
.nav-link .ic{width:15px;height:15px;opacity:.9}
|
||||||
|
.nav-link .caret{width:13px;height:13px;opacity:.7;margin-left:-2px}
|
||||||
|
.nav-item:hover>.nav-link{color:#fff;background:rgba(255,255,255,.06)}
|
||||||
|
.nav-link.active{color:#fff;border-bottom-color:var(--gold)}
|
||||||
|
.mega{position:absolute;top:100%;left:0;background:#fff;color:var(--ink);border-radius:0 0 14px 14px;box-shadow:0 18px 40px rgba(11,40,80,.18);border:1px solid var(--line);border-top:0;padding:22px;display:none;grid-template-columns:auto auto 250px;gap:30px;min-width:560px;z-index:60}
|
||||||
|
.nav-item:hover .mega,.nav-item:focus-within .mega{display:grid}
|
||||||
|
.nav-item:nth-last-child(-n+3) .mega{left:auto;right:0}
|
||||||
|
.mega.no-promo{min-width:auto}
|
||||||
|
.nav-item:hover .mega.no-promo,.nav-item:focus-within .mega.no-promo{display:flex;gap:30px;width:auto}
|
||||||
|
.mega-col{min-width:190px}
|
||||||
|
.mega-head{font-size:10.5px;font-weight:700;letter-spacing:.12em;color:#94A3B8;text-transform:uppercase;margin:0 0 12px}
|
||||||
|
.mega-link{display:flex;gap:11px;padding:8px;border-radius:9px;align-items:flex-start}
|
||||||
|
.mega-link:hover{background:#F3F7FC}
|
||||||
|
.mega-link .mega-ic{width:36px;height:36px;border-radius:10px;background:#EAF1FA;color:var(--c-blue);display:flex;align-items:center;justify-content:center;flex:0 0 auto}
|
||||||
|
.mega-ic .ic{width:21px;height:21px;display:block}
|
||||||
|
.mega-link b{display:block;font-size:13.5px;font-weight:700}
|
||||||
|
.mega-link span{display:block;font-size:11.5px;color:var(--muted);margin-top:2px}
|
||||||
|
.promo{background:linear-gradient(160deg,#1F5FA8,#0F3F77);color:#fff;border-radius:12px;padding:18px;display:flex;flex-direction:column;gap:10px}
|
||||||
|
.promo .badge{align-self:flex-start;background:rgba(255,255,255,.18);font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:4px 9px;border-radius:999px}
|
||||||
|
.promo b{font-size:16px}
|
||||||
|
.promo p{margin:0;font-size:12px;color:#cfe0f3;line-height:1.45}
|
||||||
|
.promo .go{margin-top:auto;align-self:flex-end;width:38px;height:38px;border-radius:9px;background:#fff;color:var(--c-blue);display:flex;align-items:center;justify-content:center}
|
||||||
|
|
||||||
|
/* ── Layout / page head ─────────────────────────────────────────────────── */
|
||||||
|
.wrap{max-width:1280px;margin:0 auto;padding:22px 24px 80px}
|
||||||
|
.crumb{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--muted);margin-bottom:14px}
|
||||||
|
.crumb .ic{width:13px;height:13px}.crumb a:hover{color:var(--ink)}
|
||||||
|
.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:24px;flex-wrap:wrap}
|
||||||
|
.page-head h1{margin:0;font-size:26px;font-weight:800;letter-spacing:-.4px}
|
||||||
|
.page-head .sub{margin:4px 0 0;color:var(--muted);font-size:13.5px}
|
||||||
|
.date{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px;background:#fff;border:1px solid var(--line);border-radius:9px;padding:8px 12px}
|
||||||
|
.date .ic{width:15px;height:15px}
|
||||||
|
|
||||||
|
/* ── Card / stack ───────────────────────────────────────────────────────── */
|
||||||
|
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}
|
||||||
|
.card-pad{padding:20px}
|
||||||
|
.stack{display:grid;gap:18px}
|
||||||
|
|
||||||
|
/* ── Calendario (dashboard widgets) ─────────────────────────────────────── */
|
||||||
|
.cal-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:16px;flex-wrap:wrap}
|
||||||
|
.cal-head h3{margin:0;font-size:15px;font-weight:700}
|
||||||
|
.cal-ctrl{display:flex;gap:6px;align-items:center}
|
||||||
|
.ctrl-btn{width:32px;height:32px;border:1px solid var(--line);border-radius:8px;background:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer}
|
||||||
|
.ctrl-btn:hover{background:#F5F8FC}
|
||||||
|
.ctrl-btn .ic{width:16px;height:16px;color:var(--muted)}
|
||||||
|
.ctrl-today{height:32px;padding:0 13px;border:1px solid var(--line);border-radius:8px;background:#fff;font:inherit;font-weight:600;font-size:12px;display:flex;align-items:center;gap:6px;cursor:pointer;color:var(--ink)}
|
||||||
|
.ctrl-today:hover{background:#F5F8FC}
|
||||||
|
.ctrl-today .ic{width:14px;height:14px;color:var(--c-blue)}
|
||||||
|
.daystrip{display:flex;gap:8px;overflow-x:auto;padding-bottom:4px;scrollbar-width:thin}
|
||||||
|
.daycell{flex:1 1 0;min-width:64px;border:1px solid var(--line);border-radius:11px;background:#fff;padding:11px 6px 9px;text-align:center;position:relative;cursor:pointer;transition:border-color .12s,transform .12s,box-shadow .12s}
|
||||||
|
.daycell:hover{border-color:#9FBEDD;box-shadow:0 4px 12px rgba(16,40,80,.08)}
|
||||||
|
.daycell .wd{font-size:10.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:#8093A8}
|
||||||
|
.daycell .dn{font-size:21px;font-weight:800;line-height:1.15;margin-top:3px}
|
||||||
|
.daycell .mo{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin-top:1px}
|
||||||
|
.daycell.weekend{background:#F5F8FC}
|
||||||
|
.daycell.weekend .wd{color:#A6B4C6}
|
||||||
|
.daycell.today{background:var(--navy-nav);border-color:var(--navy-nav)}
|
||||||
|
.daycell.today:hover{border-color:var(--navy-hover);box-shadow:0 6px 16px rgba(16,40,80,.22)}
|
||||||
|
.daycell.today .wd{color:#C7D9EC}.daycell.today .dn,.daycell.today .mo{color:#fff}
|
||||||
|
.daycell.today::before{content:"";position:absolute;left:0;right:0;top:0;height:3px;background:var(--gold);border-radius:11px 11px 0 0}
|
||||||
|
.dots{display:flex;gap:3px;justify-content:center;margin-top:7px;min-height:6px}
|
||||||
|
.ev{width:6px;height:6px;border-radius:50%;display:inline-block}
|
||||||
|
.ev.green{background:var(--c-green)}.ev.blue{background:var(--c-blue)}.ev.amber{background:var(--c-amber)}.ev.red{background:var(--c-red)}
|
||||||
|
.weekstrip{display:grid;grid-template-columns:1fr 1.25fr 1fr;gap:14px;align-items:start}
|
||||||
|
.weekblock{border:1px solid var(--line);border-radius:12px;background:#FAFCFE;padding:13px;cursor:pointer;transition:border-color .12s,box-shadow .12s}
|
||||||
|
.weekblock:hover{border-color:#9FBEDD;box-shadow:0 6px 16px rgba(16,40,80,.10)}
|
||||||
|
.weekblock.current{background:#fff;border-color:#B9CFE6;box-shadow:0 6px 18px rgba(16,40,80,.08)}
|
||||||
|
.weekblock .wl{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:8px}
|
||||||
|
.weekblock .wl b{font-size:12.5px;font-weight:700;color:var(--muted)}
|
||||||
|
.weekblock.current .wl b{color:var(--c-blue)}
|
||||||
|
.weekblock .wl span{font-size:9.5px;color:#A6B4C6;text-transform:uppercase;letter-spacing:.07em;font-weight:700;white-space:nowrap}
|
||||||
|
.week7{display:grid;grid-template-columns:repeat(7,1fr);gap:3px}
|
||||||
|
.mini{text-align:center;border-radius:8px;padding:6px 0 5px}
|
||||||
|
.mini .mi-wd{font-size:9.5px;font-weight:700;color:#9aa8ba;text-transform:uppercase}
|
||||||
|
.mini .mi-dn{font-size:13px;font-weight:700;margin-top:1px}
|
||||||
|
.mini.weekend .mi-wd,.mini.weekend .mi-dn{color:#A6B4C6}
|
||||||
|
.mini.today{background:var(--navy-nav)}.mini.today .mi-wd{color:#C7D9EC}.mini.today .mi-dn{color:#fff}
|
||||||
|
.mini .mi-ev{display:flex;gap:2px;justify-content:center;min-height:5px;margin-top:3px}
|
||||||
|
.mini .mi-ev .ev{width:4px;height:4px}
|
||||||
|
.cal-pop{position:fixed;z-index:80;display:none;width:252px;background:#fff;border:1px solid var(--line);border-radius:11px;box-shadow:0 14px 36px rgba(11,40,80,.20);padding:13px 14px;pointer-events:none;opacity:0;transform:translateY(5px);transition:opacity .12s ease,transform .12s ease}
|
||||||
|
.cal-pop.show{opacity:1;transform:none}
|
||||||
|
.cal-pop h4{margin:0 0 9px;font-size:12.5px;font-weight:700;text-transform:capitalize}
|
||||||
|
.cal-pop .pe{display:flex;gap:9px;align-items:flex-start;padding:6px 0;border-top:1px solid #EFF3F8}
|
||||||
|
.cal-pop .pe:first-of-type{border-top:0;padding-top:0}
|
||||||
|
.cal-pop .pe .ev{margin-top:5px;flex:0 0 auto}
|
||||||
|
.cal-pop .pe .tt{font-size:10.5px;color:var(--muted);font-variant-numeric:tabular-nums;display:block}
|
||||||
|
.cal-pop .pe b{font-size:12px;font-weight:600;line-height:1.3;display:block}
|
||||||
|
.cal-pop .empty{font-size:12px;color:#94A3B8}
|
||||||
|
|
||||||
|
/* ── FAB ────────────────────────────────────────────────────────────────── */
|
||||||
|
.fab-stack{position:fixed;right:22px;bottom:22px;display:flex;flex-direction:column;gap:12px;z-index:40}
|
||||||
|
.fab{width:46px;height:46px;border-radius:50%;border:0;color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 8px 20px rgba(16,40,80,.22)}
|
||||||
|
.fab .ic{width:20px;height:20px}
|
||||||
|
.fab.blue{background:var(--c-blue)}.fab.red{background:var(--c-red)}.fab.violet{background:#7C4DD6}
|
||||||
|
|
||||||
|
/* ── Grid helpers ───────────────────────────────────────────────────────── */
|
||||||
|
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
|
||||||
|
.split2{display:grid;grid-template-columns:1fr 1fr;gap:18px}
|
||||||
|
.split-anag{display:grid;grid-template-columns:1.35fr 1fr;gap:18px}
|
||||||
|
.sec-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}
|
||||||
|
.sec-head h3{margin:0;font-size:15px;font-weight:700}
|
||||||
|
.sec-head .hint{font-size:11.5px;color:var(--muted)}
|
||||||
|
|
||||||
|
/* ── Stat cards ─────────────────────────────────────────────────────────── */
|
||||||
|
.stat{display:flex;gap:14px;align-items:flex-start;border-left:4px solid var(--c-blue);transition:box-shadow .12s,transform .12s}
|
||||||
|
a.stat{color:inherit}
|
||||||
|
a.stat:hover{box-shadow:0 8px 20px rgba(16,40,80,.13);transform:translateY(-1px)}
|
||||||
|
.stat.green{border-left-color:var(--c-green)}.stat.amber{border-left-color:var(--c-amber)}.stat.blue{border-left-color:var(--c-blue)}
|
||||||
|
.stat .ibox{width:42px;height:42px;border-radius:11px;display:flex;align-items:center;justify-content:center;flex:0 0 auto;background:#EAF1FA;color:var(--c-blue)}
|
||||||
|
.stat.green .ibox{background:#E6F4EC;color:var(--c-green)}.stat.amber .ibox{background:#FBF1E2;color:var(--c-amber)}
|
||||||
|
.stat .ibox .ic{width:20px;height:20px}
|
||||||
|
.stat .lbl{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#7a8aa0}
|
||||||
|
.stat .val{font-size:27px;font-weight:800;line-height:1.1;margin:3px 0 1px}
|
||||||
|
.stat.green .val{color:var(--c-green)}.stat.amber .val{color:var(--c-amber)}.stat.blue .val{color:var(--c-blue)}
|
||||||
|
.stat .sub{font-size:12px;color:var(--muted)}
|
||||||
|
.bar{height:7px;background:#EAF0F6;border-radius:99px;overflow:hidden;margin-top:9px;max-width:180px}
|
||||||
|
.bar>i{display:block;height:100%;background:var(--c-amber);border-radius:99px}
|
||||||
|
|
||||||
|
/* ── Buttons ────────────────────────────────────────────────────────────── */
|
||||||
|
.btn-primary{display:inline-flex;align-items:center;gap:7px;background:var(--c-blue);color:#fff;border:0;border-radius:8px;padding:8px 13px;font:inherit;font-weight:600;font-size:12.5px;cursor:pointer}
|
||||||
|
.btn-primary:hover{background:#1A5494}
|
||||||
|
.btn-primary .ic{width:15px;height:15px}
|
||||||
|
.btn-ghost{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--line);background:#fff;border-radius:8px;padding:7px 12px;font:inherit;font-weight:600;font-size:12.5px;color:var(--ink);cursor:pointer}
|
||||||
|
.btn-ghost:hover{background:#F5F8FC;border-color:#c7d4e4}
|
||||||
|
.btn-ghost .ic{width:15px;height:15px;color:var(--c-blue)}
|
||||||
|
|
||||||
|
/* ── Scadenze (deadlines) ───────────────────────────────────────────────── */
|
||||||
|
.dpill{flex:0 0 auto;width:46px;text-align:center;background:#EFF4FB;border:1px solid var(--line);border-radius:9px;padding:6px 0}
|
||||||
|
.dpill b{display:block;font-size:15px;font-weight:800;line-height:1}
|
||||||
|
.dpill span{font-size:9.5px;font-weight:700;letter-spacing:.05em;color:var(--muted);text-transform:uppercase}
|
||||||
|
.sca{display:flex;gap:13px;align-items:flex-start;padding:12px 0;border-top:1px solid var(--line)}
|
||||||
|
.sca:first-of-type{border-top:0;padding-top:0}
|
||||||
|
.sca .body{flex:1;min-width:0}
|
||||||
|
.sca .body b{font-size:13px;font-weight:600;display:block}
|
||||||
|
.meta{display:flex;gap:8px;align-items:center;margin-top:5px;flex-wrap:wrap}
|
||||||
|
.who{font-size:11.5px;color:var(--muted)}
|
||||||
|
|
||||||
|
/* ── Badge ──────────────────────────────────────────────────────────────── */
|
||||||
|
.badge-t{font-size:10px;font-weight:700;letter-spacing:.03em;text-transform:uppercase;padding:2px 7px;border-radius:6px;background:#EEF2F7;color:#5b6b80;white-space:nowrap}
|
||||||
|
.badge-t.blue{background:#E7F0FB;color:#1F5FA8}.badge-t.amber{background:#FBF1E2;color:#9A6418}.badge-t.red{background:#FBE9E9;color:#B23636}.badge-t.green{background:#E6F4EC;color:#1E7A4E}.badge-t.gray{background:#EEF2F7;color:#5b6b80}
|
||||||
|
|
||||||
|
/* ── Attività (open activities) ─────────────────────────────────────────── */
|
||||||
|
.act-scroll{max-height:330px;overflow-y:auto;padding-right:4px;scrollbar-width:thin}
|
||||||
|
.act{display:flex;gap:12px;align-items:flex-start;justify-content:space-between;padding:12px;border:1px solid var(--line);border-radius:10px;margin-bottom:8px;background:#fff}
|
||||||
|
.act:last-child{margin-bottom:0}
|
||||||
|
.act .body{flex:1;min-width:0}.act .body b{font-size:13px;font-weight:600;display:block}
|
||||||
|
|
||||||
|
/* ── Anagrafica ─────────────────────────────────────────────────────────── */
|
||||||
|
.anag{display:flex;gap:16px;align-items:flex-start}
|
||||||
|
.anag-logo{width:60px;height:60px;border-radius:13px;background:linear-gradient(135deg,#143A5C,#1F5FA8);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;font-size:17px;letter-spacing:.5px;flex:0 0 auto}
|
||||||
|
.anag .info{flex:1;min-width:0}
|
||||||
|
.anag .nm{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||||
|
.anag .nm h3{margin:0;font-size:16px;font-weight:800}
|
||||||
|
.badge-nis{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:3px 8px;border-radius:6px;background:#E7F0FB;color:#1F5FA8}
|
||||||
|
.anag-grid{display:grid;grid-template-columns:1fr 1fr;gap:11px 18px;margin-top:14px}
|
||||||
|
.anag-grid .f span{display:block;font-size:10.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:#8093A8}
|
||||||
|
.anag-grid .f b{font-size:13px;font-weight:600}
|
||||||
|
|
||||||
|
/* ── Documenti istituzionali ────────────────────────────────────────────── */
|
||||||
|
.doc{display:flex;align-items:center;gap:12px;width:100%;text-align:left;border:1px solid var(--line);background:#fff;border-radius:10px;padding:12px 14px;cursor:pointer;font:inherit;margin-bottom:10px}
|
||||||
|
.doc:last-child{margin-bottom:0}
|
||||||
|
.doc:hover{border-color:#c7d4e4;background:#FAFCFE}
|
||||||
|
.doc .di{width:36px;height:36px;border-radius:9px;background:#EAF1FA;color:var(--c-blue);display:flex;align-items:center;justify-content:center;flex:0 0 auto}
|
||||||
|
.doc .di .ic{width:18px;height:18px}
|
||||||
|
.doc .dt{flex:1;min-width:0}
|
||||||
|
.doc .dt b{font-size:13.5px;font-weight:700;display:block}
|
||||||
|
.doc .dt small{font-size:11px;color:var(--muted)}
|
||||||
|
.doc .st{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:700;padding:5px 10px;border-radius:999px;white-space:nowrap}
|
||||||
|
.doc .st .ic{width:14px;height:14px}
|
||||||
|
.doc.has .st{background:#E6F4EC;color:#1E7A4E}
|
||||||
|
.doc.miss .st{background:#FBF1E2;color:#9A6418}
|
||||||
|
|
||||||
|
/* ── V3+ : stati loading / empty / error (dati reali via api.js) ────────── */
|
||||||
|
.v3-loading{display:flex;align-items:center;justify-content:center;gap:10px;padding:28px 12px;color:var(--muted);font-size:13px}
|
||||||
|
.v3-spin{width:18px;height:18px;border:2px solid var(--line);border-top-color:var(--c-blue);border-radius:50%;animation:v3spin .7s linear infinite}
|
||||||
|
@keyframes v3spin{to{transform:rotate(360deg)}}
|
||||||
|
.v3-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:26px 12px;color:var(--muted);text-align:center}
|
||||||
|
.v3-empty .ic{width:26px;height:26px;opacity:.6}
|
||||||
|
.v3-empty b{font-size:13px;font-weight:700;color:var(--ink)}
|
||||||
|
.v3-empty span{font-size:12px}
|
||||||
|
.v3-error{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:26px 12px;color:var(--c-red);text-align:center}
|
||||||
|
.v3-error .ic{width:26px;height:26px}
|
||||||
|
.v3-error b{font-size:13px;font-weight:700}
|
||||||
|
.v3-error span{font-size:12px;color:var(--muted)}
|
||||||
|
@media (prefers-reduced-motion:reduce){.v3-spin{animation:none}}
|
||||||
|
|
||||||
|
/* ── V3+ : mobile nav toggle (hamburger) ────────────────────────────────── */
|
||||||
|
.nav-toggle{display:none;align-items:center;justify-content:center;width:38px;height:38px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.08);border-radius:8px;color:#fff;cursor:pointer;flex:0 0 auto}
|
||||||
|
.nav-toggle .ic{width:20px;height:20px}
|
||||||
|
|
||||||
|
/* ── Responsive (dal mockup) + V3+ mobile nav ──────────────────────────── */
|
||||||
|
@media (max-width:900px){
|
||||||
|
.weekstrip{grid-template-columns:1fr}
|
||||||
|
.grid3,.split2,.split-anag{grid-template-columns:1fr}
|
||||||
|
.mega{grid-template-columns:1fr;min-width:280px}
|
||||||
|
.search{display:none}
|
||||||
|
.nav-toggle{display:flex}
|
||||||
|
.bar-nav-in{display:none;flex-direction:column;align-items:stretch;padding:6px 12px 10px}
|
||||||
|
.bar-nav.open .bar-nav-in{display:flex}
|
||||||
|
.nav-item{position:static}
|
||||||
|
.nav-link{border-bottom:0;padding:11px 8px}
|
||||||
|
.nav-link.active{background:rgba(255,255,255,.06);border-radius:8px;border-left:3px solid var(--gold)}
|
||||||
|
.nav-link .caret{margin-left:auto}
|
||||||
|
.mega,.mega.no-promo{position:static;display:none!important;box-shadow:none;border:0;border-radius:0;padding:0 0 8px 18px;min-width:auto}
|
||||||
|
.nav-item.open .mega,.nav-item.open .mega.no-promo{display:flex!important;flex-direction:column;gap:0}
|
||||||
|
.mega-col{min-width:auto}
|
||||||
|
}
|
||||||
|
@media (max-width:560px){
|
||||||
|
.brand-txt span{display:none}
|
||||||
|
.btn-incident span{display:none}
|
||||||
|
.wrap{padding:16px 14px 80px}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── V3+ : touch target ≥44px su pointer coarse (AGID/WCAG) ─────────────── */
|
||||||
|
@media (pointer:coarse){
|
||||||
|
.nav-link,.ctrl-today,.btn-primary,.btn-ghost,.btn-incident,.user,.doc{min-height:44px}
|
||||||
|
.ctrl-btn{min-width:44px;min-height:44px}
|
||||||
|
.fab{width:52px;height:52px}
|
||||||
|
}
|
||||||
+105
-324
@@ -3,12 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<title>Dashboard - NIS2 Agile</title>
|
<title>Dashboard · NIS2 Agile</title>
|
||||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg CDN). -->
|
||||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||||
via JS (card/gauge/stat/modali) -> parita' funzionale assoluta. BI veste la chrome. -->
|
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
|
||||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
|
||||||
<!-- PWA:start -->
|
<!-- PWA:start -->
|
||||||
<link rel="manifest" href="/manifest.webmanifest">
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
<meta name="theme-color" content="#0066CC">
|
<meta name="theme-color" content="#0066CC">
|
||||||
@@ -24,338 +22,121 @@
|
|||||||
<!-- PWA:end -->
|
<!-- PWA:end -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-layout">
|
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||||
<!-- Sidebar -->
|
<div id="topnav"></div>
|
||||||
<aside class="sidebar" id="sidebar"></aside>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
<main class="wrap">
|
||||||
<main class="main-content">
|
<nav class="crumb" aria-label="breadcrumb">
|
||||||
<header class="content-header">
|
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||||
<h1 class="h2" data-i18n="dashboard.title">Dashboard</h1>
|
<i data-lucide="chevron-right" class="ic"></i><span>Dashboard</span>
|
||||||
<div class="content-header-actions">
|
</nav>
|
||||||
<span class="text-muted" id="header-date"></span>
|
<div class="page-head">
|
||||||
</div>
|
<div>
|
||||||
</header>
|
<h1 data-i18n="dashboard.title">Dashboard</h1>
|
||||||
|
<p class="sub">Stato di conformità NIS2 del tuo perimetro</p>
|
||||||
<div class="content-body">
|
|
||||||
<!-- Compliance Score + Stats -->
|
|
||||||
<div class="grid-3 mb-24" style="grid-template-columns: 1fr 2fr;">
|
|
||||||
<!-- Gauge -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<div id="compliance-gauge">
|
|
||||||
<div class="spinner-lg" style="margin:40px auto;"></div>
|
|
||||||
</div>
|
|
||||||
<p class="text-muted mt-8" style="font-size:0.8rem;">Avanzamento implementazione misure Art.21 NIS2</p>
|
|
||||||
<p class="text-muted" style="font-size:0.7rem; margin-top:2px;">Misura l'implementazione tecnica, non la conformita' legale</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stats Cards -->
|
|
||||||
<div>
|
|
||||||
<div class="stats-grid" id="stats-grid">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-card-icon danger">
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd"/></svg>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card-content">
|
|
||||||
<h4>Rischi Aperti</h4>
|
|
||||||
<div class="stat-card-value" id="stat-risks">--</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-card-icon warning">
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z"/></svg>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card-content">
|
|
||||||
<h4>Incidenti Attivi</h4>
|
|
||||||
<div class="stat-card-value" id="stat-incidents">--</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-card-icon success">
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card-content">
|
|
||||||
<h4>Policy Approvate</h4>
|
|
||||||
<div class="stat-card-value" id="stat-policies">--</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-card-icon primary">
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0z"/></svg>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card-content">
|
|
||||||
<h4>Formazione</h4>
|
|
||||||
<div class="stat-card-value" id="stat-training">--%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
|
||||||
<div class="card mb-24" id="nis2-dashboard-quick-actions">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3>Azioni Rapide</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="quick-actions">
|
|
||||||
<button class="quick-action-btn" id="nis2-dashboard-qa-assessment" onclick="window.location.href='assessment.html'">
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20"><path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd"/></svg>
|
|
||||||
Nuovo Assessment
|
|
||||||
</button>
|
|
||||||
<button class="quick-action-btn" id="nis2-dashboard-qa-incidente" onclick="window.location.href='incidents.html'">
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20"><path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6z"/></svg>
|
|
||||||
Registra Incidente
|
|
||||||
</button>
|
|
||||||
<button class="quick-action-btn" onclick="generateAIPolicy()">
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor" width="20" height="20"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
|
|
||||||
Genera Policy AI
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Deadlines + Activity -->
|
|
||||||
<div class="grid-2">
|
|
||||||
<!-- Scadenze -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3>Prossime Scadenze</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="deadlines-list">
|
|
||||||
<div class="spinner" style="margin:20px auto;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Attivita' Recenti -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3>Attivita' Recenti</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body" id="activity-list">
|
|
||||||
<div class="spinner" style="margin:20px auto;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
<div class="date"><i data-lucide="calendar" class="ic"></i><span id="header-date"></span></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="stack">
|
||||||
|
<!-- Monitor Giornaliero (eventi reali: scadenze + attività) -->
|
||||||
|
<section class="card card-pad">
|
||||||
|
<div class="cal-head">
|
||||||
|
<h3>Monitor Giornaliero</h3>
|
||||||
|
<div class="cal-ctrl">
|
||||||
|
<button class="ctrl-btn" id="d-prev" title="Giorni precedenti" aria-label="Giorni precedenti"><i data-lucide="chevron-left" class="ic"></i></button>
|
||||||
|
<button class="ctrl-today" id="d-today"><i data-lucide="calendar-check" class="ic"></i>Oggi</button>
|
||||||
|
<button class="ctrl-btn" id="d-next" title="Giorni successivi" aria-label="Giorni successivi"><i data-lucide="chevron-right" class="ic"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="daystrip" id="daystrip"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Monitor Settimanale -->
|
||||||
|
<section class="card card-pad">
|
||||||
|
<div class="cal-head">
|
||||||
|
<h3>Monitor Settimanale</h3>
|
||||||
|
<div class="cal-ctrl">
|
||||||
|
<button class="ctrl-btn" id="w-prev" title="Settimana precedente" aria-label="Settimana precedente"><i data-lucide="chevron-left" class="ic"></i></button>
|
||||||
|
<button class="ctrl-today" id="w-today"><i data-lucide="calendar-check" class="ic"></i>Questa settimana</button>
|
||||||
|
<button class="ctrl-btn" id="w-next" title="Settimana successiva" aria-label="Settimana successiva"><i data-lucide="chevron-right" class="ic"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="weekstrip" id="weekstrip"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3 stat card (dati reali) -->
|
||||||
|
<div class="grid3">
|
||||||
|
<a href="assessment.html" class="card card-pad stat blue" id="stat-gap">
|
||||||
|
<div class="ibox"><i data-lucide="git-compare" class="ic"></i></div>
|
||||||
|
<div style="flex:1;min-width:0"><div class="lbl">Gap analysis in corso</div><div class="val">…</div><div class="sub">caricamento…</div></div>
|
||||||
|
</a>
|
||||||
|
<a href="training.html" class="card card-pad stat green" id="stat-training">
|
||||||
|
<div class="ibox"><i data-lucide="graduation-cap" class="ic"></i></div>
|
||||||
|
<div style="flex:1;min-width:0"><div class="lbl">Formazione</div><div class="val">…</div><div class="sub">caricamento…</div></div>
|
||||||
|
</a>
|
||||||
|
<a href="risks.html" class="card card-pad stat amber" id="stat-risk">
|
||||||
|
<div class="ibox"><i data-lucide="shield-check" class="ic"></i></div>
|
||||||
|
<div style="flex:1;min-width:0"><div class="lbl">Rischi valutati</div><div class="val">…</div><div class="sub">caricamento…</div><div class="bar"><i style="width:0%"></i></div></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scadenze + Attività aperte -->
|
||||||
|
<div class="split2">
|
||||||
|
<section class="card card-pad">
|
||||||
|
<div class="sec-head"><h3>Scadenze · prossimi 10 giorni</h3><span class="hint" id="sca-hint"></span></div>
|
||||||
|
<div id="sca-list"><div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div></div>
|
||||||
|
</section>
|
||||||
|
<section class="card card-pad">
|
||||||
|
<div class="sec-head"><h3>Attività aperte</h3>
|
||||||
|
<a class="btn-primary" href="stakeholder-activities.html"><i data-lucide="plus" class="ic"></i>Nuova attività</a></div>
|
||||||
|
<div class="act-scroll" id="act-scroll"><div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Anagrafica + Documenti istituzionali -->
|
||||||
|
<div class="split-anag">
|
||||||
|
<section class="card card-pad">
|
||||||
|
<div class="sec-head"><h3>Anagrafica aziendale</h3>
|
||||||
|
<a class="btn-ghost" href="settings.html"><i data-lucide="pencil" class="ic"></i>Modifica</a></div>
|
||||||
|
<div id="anag-card"><div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div></div>
|
||||||
|
</section>
|
||||||
|
<section class="card card-pad">
|
||||||
|
<div class="sec-head"><h3>Documenti istituzionali</h3></div>
|
||||||
|
<div id="doc-card"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Scripts V3: api.js (client REST, prima) → common.js (helper+FAB reali) →
|
||||||
|
Lucide self-host → topnav-v3.js (chrome, ridefinisce loadSidebar) →
|
||||||
|
i18n → help → dashboard.js -->
|
||||||
<script src="/js/api.js?v=20260625d"></script>
|
<script src="/js/api.js?v=20260625d"></script>
|
||||||
<script src="/js/common.js?v=20260624g"></script>
|
<script src="/js/common.js?v=20260627v3"></script>
|
||||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||||
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/i18n.js?v=20260631"></script>
|
<script src="/js/i18n.js?v=20260631"></script>
|
||||||
<script src="/js/help.js?v=20260625d"></script>
|
<script src="/js/help.js?v=20260625d"></script>
|
||||||
|
<script src="/js/dashboard.js?v=20260627"></script>
|
||||||
<script>
|
<script>
|
||||||
// ── Auth check ───────────────────────────────────────────
|
// ── Auth check ───────────────────────────────────────────
|
||||||
if (!checkAuth()) throw new Error('Not authenticated');
|
if (!checkAuth()) throw new Error('Not authenticated');
|
||||||
|
|
||||||
// ── Init ─────────────────────────────────────────────────
|
// ── Init chrome + i18n + help ────────────────────────────
|
||||||
loadSidebar();
|
loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
|
||||||
I18n.init();
|
if (typeof I18n !== 'undefined') I18n.init();
|
||||||
HelpSystem.init();
|
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
|
||||||
|
|
||||||
// Data corrente nell'header
|
// Data corrente nell'header
|
||||||
document.getElementById('header-date').textContent = new Date().toLocaleDateString('it-IT', {
|
var hd = document.getElementById('header-date');
|
||||||
weekday: 'long', day: 'numeric', month: 'long', year: 'numeric'
|
if (hd) hd.textContent = new Date().toLocaleDateString('it-IT', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
|
||||||
});
|
|
||||||
|
|
||||||
// ── Caricamento dati ─────────────────────────────────────
|
// Icone Lucide (chrome + pagina)
|
||||||
loadDashboard();
|
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||||
|
|
||||||
async function loadDashboard() {
|
// ── Dati reali ───────────────────────────────────────────
|
||||||
try {
|
if (typeof initDashboard === 'function') initDashboard();
|
||||||
const result = await api.getDashboardOverview();
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
const data = result.data;
|
|
||||||
|
|
||||||
// Compliance gauge
|
|
||||||
const score = data.compliance_score != null ? Math.round(data.compliance_score) : 0;
|
|
||||||
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(score, 180);
|
|
||||||
|
|
||||||
// Stats - map backend response structure to UI
|
|
||||||
const openRisks = data.risks ? (parseInt(data.risks.total) || 0) : 0;
|
|
||||||
const activeIncidents = data.active_incidents || 0;
|
|
||||||
const approvedPolicies = Array.isArray(data.policies)
|
|
||||||
? data.policies.reduce((sum, p) => p.status === 'approved' || p.status === 'published' ? sum + parseInt(p.count) : sum, 0)
|
|
||||||
: 0;
|
|
||||||
const trainingTotal = data.training ? (parseInt(data.training.total) || 0) : 0;
|
|
||||||
const trainingCompleted = data.training ? (parseInt(data.training.completed) || 0) : 0;
|
|
||||||
const trainingPct = trainingTotal > 0 ? Math.round((trainingCompleted / trainingTotal) * 100) : 0;
|
|
||||||
|
|
||||||
document.getElementById('stat-risks').textContent = openRisks;
|
|
||||||
document.getElementById('stat-incidents').textContent = activeIncidents;
|
|
||||||
document.getElementById('stat-policies').textContent = approvedPolicies;
|
|
||||||
document.getElementById('stat-training').textContent = trainingPct + '%';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Dashboard load error:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always load deadlines and activity from dedicated endpoints
|
|
||||||
loadIndividualData();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadIndividualData() {
|
|
||||||
// Compliance score
|
|
||||||
try {
|
|
||||||
const scoreRes = await api.getComplianceScore();
|
|
||||||
if (scoreRes.success && scoreRes.data) {
|
|
||||||
// Preferisci la % controlli implementati; se 0/assente (org con gap
|
|
||||||
// analysis fatta ma senza modulo controlli) ripiega sul punteggio assessment.
|
|
||||||
const d = scoreRes.data;
|
|
||||||
const score = (d.avg_implementation && d.avg_implementation > 0) ? d.avg_implementation : (d.score || 0);
|
|
||||||
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(score, 180);
|
|
||||||
} else {
|
|
||||||
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(0, 180);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
document.getElementById('compliance-gauge').innerHTML = renderScoreGauge(0, 180);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deadlines
|
|
||||||
try {
|
|
||||||
const dlRes = await api.getUpcomingDeadlines();
|
|
||||||
renderDeadlines(dlRes.success ? (dlRes.data || []) : []);
|
|
||||||
} catch (e) {
|
|
||||||
renderDeadlines([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activity
|
|
||||||
try {
|
|
||||||
const actRes = await api.getRecentActivity();
|
|
||||||
renderActivity(actRes.success ? (actRes.data || []) : []);
|
|
||||||
} catch (e) {
|
|
||||||
renderActivity([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDeadlines(deadlines) {
|
|
||||||
const container = document.getElementById('deadlines-list');
|
|
||||||
|
|
||||||
if (!deadlines || deadlines.length === 0) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="empty-state">
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"/></svg>
|
|
||||||
<h4>Nessuna scadenza imminente</h4>
|
|
||||||
<p>Le prossime scadenze appariranno qui.</p>
|
|
||||||
</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const months = ['GEN','FEB','MAR','APR','MAG','GIU','LUG','AGO','SET','OTT','NOV','DIC'];
|
|
||||||
let html = '';
|
|
||||||
|
|
||||||
deadlines.slice(0, 5).forEach(dl => {
|
|
||||||
const d = new Date(dl.due_date || dl.date);
|
|
||||||
const now = new Date();
|
|
||||||
const diffDays = Math.ceil((d - now) / 86400000);
|
|
||||||
let urgency = 'normal';
|
|
||||||
if (diffDays <= 3) urgency = 'urgent';
|
|
||||||
else if (diffDays <= 7) urgency = 'soon';
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<div class="deadline-item">
|
|
||||||
<div class="deadline-date ${urgency}">
|
|
||||||
<span class="day">${d.getDate()}</span>
|
|
||||||
<span class="month">${months[d.getMonth()]}</span>
|
|
||||||
</div>
|
|
||||||
<div class="deadline-info">
|
|
||||||
<div class="deadline-title">${escapeHtml(dl.title || dl.description || 'Scadenza')}</div>
|
|
||||||
<div class="deadline-desc">${escapeHtml(dl.category || dl.type || '')}</div>
|
|
||||||
</div>
|
|
||||||
${diffDays <= 3 ? '<span class="badge badge-danger">Urgente</span>' : diffDays <= 7 ? '<span class="badge badge-warning">Prossima</span>' : ''}
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderActivity(activities) {
|
|
||||||
const container = document.getElementById('activity-list');
|
|
||||||
|
|
||||||
if (!activities || activities.length === 0) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="empty-state">
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
|
|
||||||
<h4>Nessuna attivita' recente</h4>
|
|
||||||
<p>Le attivita' appariranno qui quando inizierai ad usare la piattaforma.</p>
|
|
||||||
</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '<ul class="activity-list">';
|
|
||||||
|
|
||||||
activities.slice(0, 8).forEach(act => {
|
|
||||||
html += `
|
|
||||||
<li class="activity-item">
|
|
||||||
<div class="activity-icon">
|
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/></svg>
|
|
||||||
</div>
|
|
||||||
<div class="activity-content">
|
|
||||||
<div class="activity-text">${escapeHtml(act.description || act.message || act.action || '')}</div>
|
|
||||||
<div class="activity-time">${timeAgo(act.created_at || act.timestamp || act.date)}</div>
|
|
||||||
</div>
|
|
||||||
</li>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
html += '</ul>';
|
|
||||||
container.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateAIPolicy() {
|
|
||||||
showModal('Genera Policy con AI', `
|
|
||||||
<p style="margin-bottom:16px;">Seleziona la categoria per cui generare una policy automatica tramite intelligenza artificiale.</p>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Categoria Policy</label>
|
|
||||||
<select class="form-select" id="ai-policy-category">
|
|
||||||
<option value="risk_management">Gestione del Rischio</option>
|
|
||||||
<option value="incident_response">Risposta agli Incidenti</option>
|
|
||||||
<option value="business_continuity">Continuita' Operativa</option>
|
|
||||||
<option value="supply_chain">Sicurezza Supply Chain</option>
|
|
||||||
<option value="access_control">Controllo degli Accessi</option>
|
|
||||||
<option value="encryption">Crittografia</option>
|
|
||||||
<option value="hr_security">Sicurezza del Personale</option>
|
|
||||||
<option value="asset_management">Gestione degli Asset</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
`, {
|
|
||||||
footer: `
|
|
||||||
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
|
||||||
<button class="btn btn-primary" onclick="doGeneratePolicy()">Genera</button>
|
|
||||||
`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doGeneratePolicy() {
|
|
||||||
const category = document.getElementById('ai-policy-category').value;
|
|
||||||
closeModal();
|
|
||||||
showNotification('Generazione policy in corso...', 'info');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await api.aiGeneratePolicy(category);
|
|
||||||
if (result.success) {
|
|
||||||
showNotification('Policy generata con successo!', 'success');
|
|
||||||
setTimeout(() => window.location.href = 'policies.html', 1500);
|
|
||||||
} else {
|
|
||||||
showNotification(result.message || 'Errore nella generazione.', 'error');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showNotification('Errore di connessione.', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
* dashboard.js — Dashboard V3 (cablata sui DATI REALI via api.js).
|
||||||
|
* ───────────────────────────────────────────────────────────────────────────
|
||||||
|
* Markup/classi FEDELI al mockup "docs/mockup Ui/index.html" (.daystrip/.weekstrip/
|
||||||
|
* .stat/.sca/.act/.anag/.doc), ma ZERO dati finti: tutto da
|
||||||
|
* /dashboard/overview · /dashboard/compliance-score · /dashboard/upcoming-deadlines
|
||||||
|
* /dashboard/recent-activity · /organizations/current
|
||||||
|
* Stati loading/empty/error gestiti. NESSUN endpoint nuovo.
|
||||||
|
* I monitor giornaliero/settimanale usano gli EVENTI reali (scadenze + attività);
|
||||||
|
* "Documenti istituzionali" non ha endpoint → stato vuoto onesto (no invenzione).
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ── Date helpers (porting dal mockup, ma TODAY = data reale) ────────────
|
||||||
|
var TODAY = new Date(); TODAY.setHours(0, 0, 0, 0);
|
||||||
|
var MO = ['gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', 'set', 'ott', 'nov', 'dic'];
|
||||||
|
var WDa = ['Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom'];
|
||||||
|
var WDi = ['L', 'M', 'M', 'G', 'V', 'S', 'D'];
|
||||||
|
var DAY = 86400000;
|
||||||
|
function mi(d) { return (d.getDay() + 6) % 7; }
|
||||||
|
function addD(d, n) { return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n); }
|
||||||
|
function iso(d) { return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); }
|
||||||
|
function monday(d) { return addD(d, -mi(d)); }
|
||||||
|
function same(a, b) { return iso(a) === iso(b); }
|
||||||
|
function weekLabel(m) { var s = addD(m, 6); return m.getMonth() === s.getMonth() ? (m.getDate() + ' – ' + s.getDate() + ' ' + MO[s.getMonth()]) : (m.getDate() + ' ' + MO[m.getMonth()] + ' – ' + s.getDate() + ' ' + MO[s.getMonth()]); }
|
||||||
|
|
||||||
|
function esc(s) { return (typeof escapeHtml === 'function') ? escapeHtml(s) : String(s == null ? '' : s); }
|
||||||
|
|
||||||
|
// ── EVENTI reali: dizionario iso-date → [{t, c, time}] ──────────────────
|
||||||
|
// Popolato da scadenze (api.getUpcomingDeadlines) + attività (recent-activity).
|
||||||
|
var EVENTS = {};
|
||||||
|
function pushEvent(dateVal, title, color, time) {
|
||||||
|
if (!dateVal) return;
|
||||||
|
var d = new Date(dateVal);
|
||||||
|
if (isNaN(d.getTime())) return;
|
||||||
|
var k = iso(d);
|
||||||
|
(EVENTS[k] = EVENTS[k] || []).push({ t: title || 'Evento', c: color || 'blue', time: time || '' });
|
||||||
|
}
|
||||||
|
function evOf(d) { return EVENTS[iso(d)] || []; }
|
||||||
|
function dots(list) { return list.slice(0, 4).map(function (e) { return '<i class="ev ' + e.c + '"></i>'; }).join(''); }
|
||||||
|
|
||||||
|
// ── Popover (porting fedele dal mockup) ─────────────────────────────────
|
||||||
|
var POP = document.createElement('div'); POP.className = 'cal-pop'; document.body.appendChild(POP);
|
||||||
|
var popTimer;
|
||||||
|
function showPop(anchor, html) {
|
||||||
|
clearTimeout(popTimer);
|
||||||
|
POP.innerHTML = html; POP.style.display = 'block';
|
||||||
|
var r = anchor.getBoundingClientRect();
|
||||||
|
var pw = POP.offsetWidth, ph = POP.offsetHeight;
|
||||||
|
var left = r.left + r.width / 2 - pw / 2;
|
||||||
|
left = Math.max(10, Math.min(left, window.innerWidth - pw - 10));
|
||||||
|
var top = r.top - ph - 10; if (top < 10) top = r.bottom + 10;
|
||||||
|
POP.style.left = left + 'px'; POP.style.top = top + 'px';
|
||||||
|
requestAnimationFrame(function () { POP.classList.add('show'); });
|
||||||
|
}
|
||||||
|
function hidePop() { POP.classList.remove('show'); popTimer = setTimeout(function () { POP.style.display = 'none'; }, 150); }
|
||||||
|
function dayPop(d) {
|
||||||
|
var list = evOf(d);
|
||||||
|
var head = WDa[mi(d)] + ' ' + d.getDate() + ' ' + MO[d.getMonth()];
|
||||||
|
var body = list.length
|
||||||
|
? list.map(function (e) { return '<div class="pe"><span class="ev ' + e.c + '"></span><div>' + (e.time ? '<span class="tt">' + esc(e.time) + '</span>' : '') + '<b>' + esc(e.t) + '</b></div></div>'; }).join('')
|
||||||
|
: '<div class="empty">Nessun appuntamento.</div>';
|
||||||
|
return '<h4>' + head + '</h4>' + body;
|
||||||
|
}
|
||||||
|
function weekPop(m) {
|
||||||
|
var items = [];
|
||||||
|
for (var i = 0; i < 7; i++) { var d = addD(m, i); evOf(d).forEach(function (e) { items.push({ d: d, e: e }); }); }
|
||||||
|
var body = items.length
|
||||||
|
? items.slice(0, 6).map(function (o) { return '<div class="pe"><span class="ev ' + o.e.c + '"></span><div><span class="tt">' + WDa[mi(o.d)] + ' ' + o.d.getDate() + '</span><b>' + esc(o.e.t) + '</b></div></div>'; }).join('')
|
||||||
|
: '<div class="empty">Nessun appuntamento.</div>';
|
||||||
|
if (items.length > 6) body += '<div class="empty" style="margin-top:6px">+' + (items.length - 6) + ' altri…</div>';
|
||||||
|
return '<h4>' + weekLabel(m) + '</h4>' + body;
|
||||||
|
}
|
||||||
|
function openCal() { window.location.href = 'calendario.html'; }
|
||||||
|
|
||||||
|
var dayOffset = 0, weekOffset = 0;
|
||||||
|
function renderDaily() {
|
||||||
|
var center = addD(TODAY, dayOffset);
|
||||||
|
var wrap = document.getElementById('daystrip'); if (!wrap) return; wrap.innerHTML = '';
|
||||||
|
for (var i = -6; i <= 6; i++) {
|
||||||
|
(function (d) {
|
||||||
|
var cell = document.createElement('div');
|
||||||
|
cell.className = 'daycell' + (mi(d) >= 5 ? ' weekend' : '') + (same(d, TODAY) ? ' today' : '');
|
||||||
|
cell.innerHTML = '<div class="wd">' + WDa[mi(d)] + '</div><div class="dn">' + d.getDate() + '</div><div class="mo">' + MO[d.getMonth()] + '</div><div class="dots">' + dots(evOf(d)) + '</div>';
|
||||||
|
cell.addEventListener('mouseenter', function () { showPop(cell, dayPop(d)); });
|
||||||
|
cell.addEventListener('mouseleave', hidePop);
|
||||||
|
cell.addEventListener('click', openCal);
|
||||||
|
wrap.appendChild(cell);
|
||||||
|
})(addD(center, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function renderWeekly() {
|
||||||
|
var curMon = monday(TODAY);
|
||||||
|
var centerMon = addD(curMon, weekOffset * 7);
|
||||||
|
var wrap = document.getElementById('weekstrip'); if (!wrap) return; wrap.innerHTML = '';
|
||||||
|
for (var w = -1; w <= 1; w++) {
|
||||||
|
(function (m) {
|
||||||
|
var isCur = same(m, curMon);
|
||||||
|
var diff = Math.round((m - curMon) / (7 * DAY));
|
||||||
|
var tag = diff === 0 ? 'Settimana in corso' : diff === -1 ? 'Settimana precedente' : diff === 1 ? 'Settimana successiva' : diff < 0 ? (-diff) + ' settimane fa' : 'Tra ' + diff + ' settimane';
|
||||||
|
var block = document.createElement('div');
|
||||||
|
block.className = 'weekblock' + (isCur ? ' current' : '');
|
||||||
|
var minis = '';
|
||||||
|
for (var i = 0; i < 7; i++) { var d = addD(m, i); minis += '<div class="mini' + (mi(d) >= 5 ? ' weekend' : '') + (same(d, TODAY) ? ' today' : '') + '"><div class="mi-wd">' + WDi[mi(d)] + '</div><div class="mi-dn">' + d.getDate() + '</div><div class="mi-ev">' + dots(evOf(d)) + '</div></div>'; }
|
||||||
|
block.innerHTML = '<div class="wl"><b>' + weekLabel(m) + '</b><span>' + tag + '</span></div><div class="week7">' + minis + '</div>';
|
||||||
|
block.addEventListener('mouseenter', function () { showPop(block, weekPop(m)); });
|
||||||
|
block.addEventListener('mouseleave', hidePop);
|
||||||
|
block.addEventListener('click', openCal);
|
||||||
|
wrap.appendChild(block);
|
||||||
|
})(addD(centerMon, w * 7));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function wireCalControls() {
|
||||||
|
var map = [['d-prev', function () { dayOffset--; renderDaily(); }], ['d-next', function () { dayOffset++; renderDaily(); }],
|
||||||
|
['d-today', function () { dayOffset = 0; renderDaily(); }], ['w-prev', function () { weekOffset--; renderWeekly(); }],
|
||||||
|
['w-next', function () { weekOffset++; renderWeekly(); }], ['w-today', function () { weekOffset = 0; renderWeekly(); }]];
|
||||||
|
map.forEach(function (p) { var el = document.getElementById(p[0]); if (el) el.onclick = p[1]; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stato cella generico (loading/empty/error) ──────────────────────────
|
||||||
|
function setLoading(id) { var el = document.getElementById(id); if (el) el.innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>'; }
|
||||||
|
function setEmpty(id, title, sub, icon) { var el = document.getElementById(id); if (el) el.innerHTML = '<div class="v3-empty"><i data-lucide="' + (icon || 'inbox') + '" class="ic"></i><b>' + esc(title) + '</b><span>' + esc(sub || '') + '</span></div>'; refreshIcons(); }
|
||||||
|
function setError(id, msg) { var el = document.getElementById(id); if (el) el.innerHTML = '<div class="v3-error"><i data-lucide="alert-triangle" class="ic"></i><b>Errore di caricamento</b><span>' + esc(msg || 'Riprova tra poco.') + '</span></div>'; refreshIcons(); }
|
||||||
|
function refreshIcons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
|
||||||
|
|
||||||
|
// ── Mappa categoria → colore badge (coerente col mockup) ────────────────
|
||||||
|
function catColor(cat) {
|
||||||
|
var c = String(cat || '').toLowerCase();
|
||||||
|
if (/incid/.test(c)) return 'red';
|
||||||
|
if (/formaz|training/.test(c)) return 'green';
|
||||||
|
if (/govern|riesame|board|direzion/.test(c)) return 'blue';
|
||||||
|
if (/control|rischi|risk|audit/.test(c)) return 'amber';
|
||||||
|
return 'gray';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 1) Stat cards (3) — dati reali da overview + compliance-score ───────
|
||||||
|
async function loadStats() {
|
||||||
|
var elGap = document.getElementById('stat-gap');
|
||||||
|
var elTrain = document.getElementById('stat-training');
|
||||||
|
var elRisk = document.getElementById('stat-risk');
|
||||||
|
try {
|
||||||
|
var ov = await api.getDashboardOverview();
|
||||||
|
var d = (ov && ov.success && ov.data) ? ov.data : {};
|
||||||
|
|
||||||
|
// Formazione: % completata (overview.training {total, completed})
|
||||||
|
var tTot = d.training ? (parseInt(d.training.total) || 0) : 0;
|
||||||
|
var tDone = d.training ? (parseInt(d.training.completed) || 0) : 0;
|
||||||
|
var tPct = tTot > 0 ? Math.round((tDone / tTot) * 100) : 0;
|
||||||
|
if (elTrain) {
|
||||||
|
elTrain.querySelector('.val').textContent = tTot > 0 ? (tPct + '%') : '0%';
|
||||||
|
elTrain.querySelector('.sub').textContent = tTot > 0 ? (tDone + ' di ' + tTot + ' completati') : 'Nessun corso assegnato';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rischi valutati: total rischi (overview.risks.total) + % implementazione misure
|
||||||
|
var riskTotal = d.risks ? (parseInt(d.risks.total) || 0) : 0;
|
||||||
|
// compliance-score per la percentuale "valutati/implementati"
|
||||||
|
var pct = 0;
|
||||||
|
try {
|
||||||
|
var sc = await api.getComplianceScore();
|
||||||
|
if (sc && sc.success && sc.data) {
|
||||||
|
pct = (sc.data.avg_implementation && sc.data.avg_implementation > 0) ? Math.round(sc.data.avg_implementation) : Math.round(sc.data.score || 0);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
if (elRisk) {
|
||||||
|
elRisk.querySelector('.val').textContent = pct + '%';
|
||||||
|
elRisk.querySelector('.sub').textContent = riskTotal > 0 ? ('su ' + riskTotal + ' del registro rischi') : 'Registro rischi vuoto';
|
||||||
|
var barI = elRisk.querySelector('.bar > i'); if (barI) barI.style.width = Math.max(0, Math.min(100, pct)) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// GAP analysis: numero assessment in corso (overview non sempre lo espone → usa list)
|
||||||
|
if (elGap) { fillGapCard(elGap); }
|
||||||
|
} catch (e) {
|
||||||
|
if (elGap) { elGap.querySelector('.val').textContent = '—'; elGap.querySelector('.sub').textContent = 'Dati non disponibili'; }
|
||||||
|
if (elTrain) { elTrain.querySelector('.val').textContent = '—'; elTrain.querySelector('.sub').textContent = 'Dati non disponibili'; }
|
||||||
|
if (elRisk) { elRisk.querySelector('.val').textContent = '—'; elRisk.querySelector('.sub').textContent = 'Dati non disponibili'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fillGapCard(elGap) {
|
||||||
|
try {
|
||||||
|
var r = await api.listAssessments();
|
||||||
|
var list = (r && r.success && Array.isArray(r.data)) ? r.data : (r && Array.isArray(r.data && r.data.items) ? r.data.items : []);
|
||||||
|
if (!Array.isArray(list)) list = [];
|
||||||
|
var inProgress = list.filter(function (a) { var s = String(a.status || '').toLowerCase(); return s && s !== 'completed' && s !== 'completato'; }).length;
|
||||||
|
elGap.querySelector('.val').textContent = list.length ? String(inProgress) : '0';
|
||||||
|
elGap.querySelector('.sub').textContent = list.length ? ('su ' + list.length + ' assessment') : 'Nessun assessment';
|
||||||
|
} catch (e) {
|
||||||
|
elGap.querySelector('.val').textContent = '—';
|
||||||
|
elGap.querySelector('.sub').textContent = 'Dati non disponibili';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2) Scadenze (prossimi 10 giorni) — dati reali ───────────────────────
|
||||||
|
async function loadDeadlines() {
|
||||||
|
setLoading('sca-list');
|
||||||
|
try {
|
||||||
|
var r = await api.getUpcomingDeadlines();
|
||||||
|
var list = (r && r.success && Array.isArray(r.data)) ? r.data : [];
|
||||||
|
// alimenta i monitor calendario con eventi reali
|
||||||
|
list.forEach(function (dl) {
|
||||||
|
var when = dl.due_date || dl.date || dl.deadline;
|
||||||
|
pushEvent(when, dl.title || dl.description || 'Scadenza', catColor(dl.category || dl.type), '');
|
||||||
|
});
|
||||||
|
renderDaily(); renderWeekly();
|
||||||
|
|
||||||
|
var hint = document.getElementById('sca-hint');
|
||||||
|
if (hint) hint.textContent = list.length + (list.length === 1 ? ' in agenda' : ' in agenda');
|
||||||
|
|
||||||
|
if (!list.length) { setEmpty('sca-list', 'Nessuna scadenza imminente', 'Le prossime scadenze appariranno qui.', 'calendar-check'); return; }
|
||||||
|
|
||||||
|
var html = list.slice(0, 10).map(function (dl) {
|
||||||
|
var d = new Date(dl.due_date || dl.date || dl.deadline);
|
||||||
|
var day = isNaN(d.getTime()) ? '—' : d.getDate();
|
||||||
|
var mon = isNaN(d.getTime()) ? '' : MO[d.getMonth()].charAt(0).toUpperCase() + MO[d.getMonth()].slice(1);
|
||||||
|
var cat = dl.category || dl.type || '';
|
||||||
|
var who = dl.owner || dl.assignee || dl.responsible || '';
|
||||||
|
return '<div class="sca"><div class="dpill"><b>' + day + '</b><span>' + esc(mon) + '</span></div>' +
|
||||||
|
'<div class="body"><b>' + esc(dl.title || dl.description || 'Scadenza') + '</b>' +
|
||||||
|
'<div class="meta">' + (cat ? '<span class="badge-t ' + catColor(cat) + '">' + esc(cat) + '</span>' : '') +
|
||||||
|
(who ? '<span class="who">' + esc(who) + '</span>' : '') + '</div></div></div>';
|
||||||
|
}).join('');
|
||||||
|
document.getElementById('sca-list').innerHTML = html;
|
||||||
|
} catch (e) {
|
||||||
|
setError('sca-list');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3) Attività aperte — dati reali da recent-activity ──────────────────
|
||||||
|
async function loadActivities() {
|
||||||
|
setLoading('act-scroll');
|
||||||
|
try {
|
||||||
|
var r = await api.getRecentActivity();
|
||||||
|
var list = (r && r.success && Array.isArray(r.data)) ? r.data : [];
|
||||||
|
// alimenta i monitor con eventi reali (data attività)
|
||||||
|
list.forEach(function (a) {
|
||||||
|
var when = a.created_at || a.timestamp || a.date;
|
||||||
|
pushEvent(when, a.description || a.message || a.action || 'Attività', catColor(a.category || a.type || a.entity_type), '');
|
||||||
|
});
|
||||||
|
renderDaily(); renderWeekly();
|
||||||
|
|
||||||
|
if (!list.length) { setEmpty('act-scroll', 'Nessuna attività recente', 'Le attività appariranno qui.', 'activity'); return; }
|
||||||
|
|
||||||
|
var html = list.slice(0, 30).map(function (a) {
|
||||||
|
var title = a.description || a.message || a.action || 'Attività';
|
||||||
|
var who = a.user_name || a.actor || a.user || '';
|
||||||
|
var when = a.created_at || a.timestamp || a.date;
|
||||||
|
var ago = (typeof timeAgo === 'function' && when) ? timeAgo(when) : '';
|
||||||
|
var st = a.status || a.entity_type || a.type || '';
|
||||||
|
var stColor = catColor(st);
|
||||||
|
return '<div class="act"><div class="body"><b>' + esc(title) + '</b>' +
|
||||||
|
'<div class="meta">' + (who ? '<span class="who">' + esc(who) + '</span>' : '') +
|
||||||
|
(ago ? '<span class="who">· ' + esc(ago) + '</span>' : '') + '</div></div>' +
|
||||||
|
(st ? '<span class="badge-t ' + stColor + '">' + esc(st) + '</span>' : '') + '</div>';
|
||||||
|
}).join('');
|
||||||
|
document.getElementById('act-scroll').innerHTML = html;
|
||||||
|
} catch (e) {
|
||||||
|
setError('act-scroll');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4) Anagrafica aziendale — dati reali da organizations/current ───────
|
||||||
|
var ENTITY_LABEL = { essential: 'Soggetto essenziale', important: 'Soggetto importante', not_applicable: 'Fuori ambito', voluntary: 'Adesione volontaria' };
|
||||||
|
function initials(name) {
|
||||||
|
return String(name || '').trim().split(/\s+/).map(function (w) { return w[0]; }).filter(Boolean).slice(0, 2).join('').toUpperCase() || '—';
|
||||||
|
}
|
||||||
|
async function loadAnag() {
|
||||||
|
var card = document.getElementById('anag-card'); if (!card) return;
|
||||||
|
try {
|
||||||
|
var r = await api.getCurrentOrg();
|
||||||
|
if (!r || !r.success || !r.data) { setEmpty('anag-card', 'Nessuna organizzazione', 'Completa l’onboarding per visualizzare l’anagrafica.', 'building-2'); return; }
|
||||||
|
var o = r.data;
|
||||||
|
var nis = ENTITY_LABEL[o.entity_type] || (o.entity_type ? esc(o.entity_type) : 'Da classificare');
|
||||||
|
var fields = [
|
||||||
|
['Settore', o.sector || o.nis2_sector || '—'],
|
||||||
|
['Partita IVA', o.vat_number || '—'],
|
||||||
|
['Sede legale', [o.city, o.country].filter(Boolean).join(', ') || o.legal_address || '—'],
|
||||||
|
['Dipendenti', (o.employee_count != null && o.employee_count !== '') ? o.employee_count : '—'],
|
||||||
|
['Codice fiscale', o.fiscal_code || '—'],
|
||||||
|
['Classificazione NIS2', nis]
|
||||||
|
];
|
||||||
|
var grid = fields.map(function (f) { return '<div class="f"><span>' + esc(f[0]) + '</span><b>' + esc(f[1]) + '</b></div>'; }).join('');
|
||||||
|
card.innerHTML =
|
||||||
|
'<div class="anag"><div class="anag-logo">' + esc(initials(o.name)) + '</div>' +
|
||||||
|
'<div class="info"><div class="nm"><h3>' + esc(o.name || 'Organizzazione') + '</h3>' +
|
||||||
|
'<span class="badge-nis">' + nis + '</span></div>' +
|
||||||
|
'<div class="anag-grid">' + grid + '</div></div></div>';
|
||||||
|
} catch (e) {
|
||||||
|
setError('anag-card');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 5) Documenti istituzionali — nessun endpoint reale → stato vuoto onesto
|
||||||
|
function loadDocs() {
|
||||||
|
var card = document.getElementById('doc-card'); if (!card) return;
|
||||||
|
setEmpty('doc-card', 'Documenti istituzionali', 'Gestisci Mission, Vision ed Etica nelle Impostazioni.', 'file-text');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Init pubblico (chiamato dall'init pagina dopo checkAuth/loadSidebar) ─
|
||||||
|
function initDashboard() {
|
||||||
|
wireCalControls();
|
||||||
|
renderDaily(); renderWeekly(); // render iniziale (eventi reali arriveranno e ri-renderizzano)
|
||||||
|
loadStats();
|
||||||
|
loadDeadlines();
|
||||||
|
loadActivities();
|
||||||
|
loadAnag();
|
||||||
|
loadDocs();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.initDashboard = initDashboard;
|
||||||
|
})();
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
/* ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
* topnav-v3.js — Chrome V3 (appbar + mega-menu) data-driven dalla mappa IA (§6).
|
||||||
|
* ───────────────────────────────────────────────────────────────────────────
|
||||||
|
* Sostituisce la sidebar V2 (common-bi.js) con la top-nav del mockup
|
||||||
|
* "docs/mockup Ui/index.html". REGOLE:
|
||||||
|
* - SOLO UI. Riusa gli helper di common.js: _loadUserInfo, _loadVersionFooter,
|
||||||
|
* _loadConsultantOrgSwitcher (via _loadUserInfo), _loadFirmBranding,
|
||||||
|
* _renderImpersonateBanner, switchLang, _getSavedLang, api.logout.
|
||||||
|
* - Stessi ID DOM degli helper V2 (#sidebar-user-name/-avatar/-role,
|
||||||
|
* #sidebar-org-switcher, #sidebar-version, .lang-btn) → gli helper esistenti
|
||||||
|
* funzionano invariati senza toccare api.js.
|
||||||
|
* - Espone window.loadSidebar = loadTopnavV3 → l'init delle pagine resta invariato.
|
||||||
|
* - I 3 FAB reali (campana notifiche + bug-reporter + ARIA) restano quelli di
|
||||||
|
* common.js (qui NON si reimplementano: si applica solo lo stile .fab-stack/.fab
|
||||||
|
* al trittico già montato, per fedeltà al mockup).
|
||||||
|
* - aria-current sulla voce/gruppo attivo (da location.pathname).
|
||||||
|
* - Icone Lucide via <i data-lucide> + lucide.createIcons() (chiamato dall'init pagina).
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════ */
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ── Mappa IA (§6 dello spec). 9 gruppi → voci/href reali. ───────────────
|
||||||
|
// top-level: voce diretta (href) oppure gruppo con mega-menu (cols).
|
||||||
|
var NAV_GROUPS = [
|
||||||
|
{ label: 'Dashboard', i18n: 'nav.dashboard', icon: 'layout-dashboard', href: 'dashboard.html' },
|
||||||
|
{ label: 'Calendario', i18n: 'nav.calendar', icon: 'calendar', href: 'calendario.html' },
|
||||||
|
{
|
||||||
|
label: 'Stakeholders', icon: 'users',
|
||||||
|
cols: [{ head: 'RELAZIONI', links: [
|
||||||
|
{ b: 'Stakeholders', s: 'Soggetti interni ed esterni rilevanti', href: 'stakeholders.html', ic: 'handshake' },
|
||||||
|
{ b: 'Supply-Chain', s: 'Rischio fornitori e terze parti', href: 'supply-chain.html', ic: 'link' },
|
||||||
|
{ b: 'Attività su Stakeholders', s: 'Interazioni, richieste e impegni', href: 'stakeholder-activities.html', ic: 'activity' }
|
||||||
|
]}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Struttura interna', icon: 'building-2',
|
||||||
|
cols: [{ head: 'RUOLI E COMPETENZE', links: [
|
||||||
|
{ b: 'Organigramma', s: 'Ruoli, deleghe e responsabilità', href: 'organigramma.html', ic: 'network' },
|
||||||
|
{ b: 'Competenze', s: 'Skill matrix e copertura dei ruoli', href: 'competenze.html', ic: 'award' },
|
||||||
|
{ b: 'GAP Analysis', s: 'Scostamento tra stato attuale e atteso', href: 'assessment.html', ic: 'git-compare' },
|
||||||
|
{ b: 'Formazione', s: 'Piani formativi e completamento', href: 'training.html', ic: 'graduation-cap' }
|
||||||
|
]}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Rischi', icon: 'shield-alert',
|
||||||
|
cols: [{ head: 'GESTIONE RISCHIO', links: [
|
||||||
|
{ b: 'Mappa dei rischi', s: 'Registro rischi e piani di trattamento', href: 'risks.html', ic: 'map' },
|
||||||
|
{ b: 'Inventario', s: 'Asset e perimetro tecnico', href: 'assets.html', ic: 'boxes' },
|
||||||
|
{ b: 'Policy', s: 'Politiche approvate e in revisione', href: 'policies.html', ic: 'file-text' },
|
||||||
|
{ b: 'Connettori Discovery', s: 'Scansione rete/cloud → inventario', href: 'connettori-discovery.html', ic: 'plug-zap' }
|
||||||
|
]}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Monitoraggio', icon: 'radar',
|
||||||
|
cols: [{ head: 'VIGILANZA E AUDIT', links: [
|
||||||
|
{ b: 'Segnalazioni ricevute', s: 'Whistleblowing e segnalazioni interne', href: 'whistleblowing.html', ic: 'megaphone' },
|
||||||
|
{ b: 'Audit interni', s: 'Programma e esiti degli audit', href: 'internal-audits.html', ic: 'clipboard-check' },
|
||||||
|
{ b: 'Audit ACN', s: 'Verifiche e ispezioni dell’autorità', href: 'reports.html', ic: 'file-check' },
|
||||||
|
{ b: 'Controlli periodici', s: 'Verifiche ricorrenti e check-list', href: 'controlli-periodici.html', ic: 'list-checks' },
|
||||||
|
{ b: 'Riesame di Direzione', s: 'Esiti del riesame §9.3', href: 'management-review.html', ic: 'gauge' }
|
||||||
|
]}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Lex e ISO', icon: 'scale',
|
||||||
|
cols: [{ head: 'NORMATIVA E ISO', links: [
|
||||||
|
{ b: 'Normative', s: 'Direttiva, decreto e riferimenti applicabili', href: 'normative.html', ic: 'scroll-text' },
|
||||||
|
{ b: 'Modello SGSI (ISO 27001)', s: 'Sistema di gestione sicurezza informazioni', href: 'isms.html', ic: 'shield-check' },
|
||||||
|
{ b: 'Misure e requisiti ACN', s: 'Misure tecniche e organizzative Art. 21', href: 'misure-requisiti.html', ic: 'list-checks' }
|
||||||
|
]}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'AI', icon: 'sparkles',
|
||||||
|
cols: [{ head: 'ASSISTENTE', links: [
|
||||||
|
{ b: 'AI Cross-Analysis', s: 'Analisi trasversale di rischi e misure', href: 'cross-analysis.html', ic: 'sparkles' },
|
||||||
|
{ b: 'Knowledge Base', s: 'Procedure, linee guida e riferimenti', href: 'kb.html', ic: 'book-open' }
|
||||||
|
]}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Guida', icon: 'life-buoy',
|
||||||
|
cols: [
|
||||||
|
{ head: 'GUIDA', links: [
|
||||||
|
{ b: 'Guida all’uso', s: 'Manuale e istruzioni della piattaforma', href: 'guida.html', ic: 'life-buoy' },
|
||||||
|
{ b: 'Integrazioni Esterne', s: 'Connettori e servizi collegati', href: 'integrazioniext.html', ic: 'plug' }
|
||||||
|
]},
|
||||||
|
{ head: 'SISTEMA', links: [
|
||||||
|
{ b: 'Impostazioni', s: 'Configurazione del workspace', href: 'settings.html', ic: 'settings' },
|
||||||
|
{ b: 'Architettura', s: 'Modello dati e struttura del sistema', href: 'architecture.html', ic: 'layers' },
|
||||||
|
{ b: 'Simulazione Demo', s: 'Scenari guidati di prova', href: 'simulate.html', ic: 'play-circle' }
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||||||
|
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentPage() {
|
||||||
|
return (window.location.pathname.split('/').pop() || 'dashboard.html');
|
||||||
|
}
|
||||||
|
|
||||||
|
// È attivo il gruppo che contiene la pagina corrente (direct href o una delle links).
|
||||||
|
function isGroupActive(g, page) {
|
||||||
|
if (g.href) return g.href === page;
|
||||||
|
if (g.cols) {
|
||||||
|
for (var i = 0; i < g.cols.length; i++) {
|
||||||
|
var lk = g.cols[i].links;
|
||||||
|
for (var j = 0; j < lk.length; j++) if (lk[j].href === page) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMega(g) {
|
||||||
|
var colsHtml = g.cols.map(function (col) {
|
||||||
|
var links = col.links.map(function (l) {
|
||||||
|
return '<a class="mega-link" href="' + esc(l.href) + '">' +
|
||||||
|
'<span class="mega-ic"><i data-lucide="' + esc(l.ic) + '" class="ic"></i></span>' +
|
||||||
|
'<span><b>' + esc(l.b) + '</b><span>' + esc(l.s) + '</span></span></a>';
|
||||||
|
}).join('');
|
||||||
|
return '<div class="mega-col"><p class="mega-head">' + esc(col.head) + '</p>' + links + '</div>';
|
||||||
|
}).join('');
|
||||||
|
return '<div class="mega no-promo">' + colsHtml + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNavItem(g, page) {
|
||||||
|
var active = isGroupActive(g, page);
|
||||||
|
var cls = 'nav-link' + (active ? ' active' : '');
|
||||||
|
var aria = active ? ' aria-current="page"' : '';
|
||||||
|
var i18n = g.i18n ? ' data-i18n="' + g.i18n + '"' : '';
|
||||||
|
|
||||||
|
if (g.href && !g.cols) {
|
||||||
|
// Voce diretta (Dashboard, Calendario)
|
||||||
|
return '<div class="nav-item">' +
|
||||||
|
'<a class="' + cls + '"' + aria + ' href="' + esc(g.href) + '">' +
|
||||||
|
'<i data-lucide="' + esc(g.icon) + '" class="ic"></i>' +
|
||||||
|
'<span' + i18n + '>' + esc(g.label) + '</span></a></div>';
|
||||||
|
}
|
||||||
|
// Gruppo con mega-menu. Il link "testa" punta alla prima voce del gruppo.
|
||||||
|
var firstHref = g.cols[0].links[0].href;
|
||||||
|
return '<div class="nav-item">' +
|
||||||
|
'<a class="' + cls + '"' + aria + ' href="' + esc(firstHref) + '">' +
|
||||||
|
'<i data-lucide="' + esc(g.icon) + '" class="ic"></i>' +
|
||||||
|
'<span' + i18n + '>' + esc(g.label) + '</span>' +
|
||||||
|
'<i data-lucide="chevron-down" class="caret"></i></a>' +
|
||||||
|
renderMega(g) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTopnavV3() {
|
||||||
|
// Container: usa #topnav se presente, altrimenti riusa #sidebar (compat),
|
||||||
|
// altrimenti crea un host e lo prepende al body.
|
||||||
|
var host = document.getElementById('topnav') || document.getElementById('sidebar');
|
||||||
|
if (!host) {
|
||||||
|
host = document.createElement('div');
|
||||||
|
host.id = 'topnav';
|
||||||
|
document.body.insertBefore(host, document.body.firstChild);
|
||||||
|
}
|
||||||
|
// Se riusiamo #sidebar (aside), neutralizziamo eventuali stili layout V2.
|
||||||
|
host.removeAttribute('class');
|
||||||
|
host.id = 'topnav';
|
||||||
|
|
||||||
|
var page = currentPage();
|
||||||
|
var lang = (typeof _getSavedLang === 'function') ? _getSavedLang() : 'it';
|
||||||
|
|
||||||
|
var navItems = NAV_GROUPS.map(function (g) { return renderNavItem(g, page); }).join('');
|
||||||
|
|
||||||
|
host.innerHTML =
|
||||||
|
'<header class="appbar">' +
|
||||||
|
'<div class="bar-top"><div class="bar-top-in">' +
|
||||||
|
'<span class="flag" aria-hidden="true"><i class="g"></i><i class="w"></i><i class="r"></i></span>' +
|
||||||
|
'<a class="logo-mark" href="dashboard.html" aria-label="NIS2 Agile — Dashboard">N2</a>' +
|
||||||
|
'<div class="brand-txt"><b class="sidebar-logo-text">NIS2 Agile</b>' +
|
||||||
|
'<span>Conformità Direttiva (UE) 2022/2555 · D.Lgs. 138/2024</span></div>' +
|
||||||
|
'<label class="search"><i data-lucide="search" class="ic"></i>' +
|
||||||
|
'<input id="topnav-search" placeholder="Cerca misura, rischio, incidente…" aria-label="Cerca"></label>' +
|
||||||
|
'<span class="spacer"></span>' +
|
||||||
|
// Org-switcher (consulenti / multi-org) — popolato da _loadUserInfo() via stesso id.
|
||||||
|
'<span id="sidebar-org-switcher" style="display:none"></span>' +
|
||||||
|
// Lingua IT/EN (riusa switchLang di common.js)
|
||||||
|
'<span id="sidebar-lang-toggle" class="topnav-lang" role="group" aria-label="Lingua">' +
|
||||||
|
'<button class="lang-btn ' + (lang === 'it' ? 'active' : '') + '" onclick="switchLang(\'it\')" title="Italiano" aria-label="Italiano">IT</button>' +
|
||||||
|
'<button class="lang-btn ' + (lang === 'en' ? 'active' : '') + '" onclick="switchLang(\'en\')" title="English" aria-label="English">EN</button>' +
|
||||||
|
'</span>' +
|
||||||
|
'<a class="btn-incident" href="incidents.html"><i data-lucide="siren" class="ic"></i><span>Registra incidente</span></a>' +
|
||||||
|
// User menu (avatar+nome+ruolo via _loadUserInfo; click = menu)
|
||||||
|
'<span class="user" id="topnav-user" tabindex="0" role="button" aria-haspopup="true" aria-expanded="false">' +
|
||||||
|
'<span class="avatar" id="sidebar-user-avatar">--</span>' +
|
||||||
|
'<span class="sidebar-user-name" id="sidebar-user-name" style="max-width:140px;overflow:hidden;text-overflow:ellipsis">…</span>' +
|
||||||
|
'<span id="sidebar-user-role" hidden></span>' +
|
||||||
|
'<i data-lucide="chevron-down" class="ic"></i>' +
|
||||||
|
'</span>' +
|
||||||
|
'<button class="nav-toggle" id="topnav-toggle" aria-label="Apri menu" aria-expanded="false"><i data-lucide="menu" class="ic"></i></button>' +
|
||||||
|
'</div></div>' +
|
||||||
|
'<nav class="bar-nav" id="topnav-bar" aria-label="Navigazione principale"><div class="bar-nav-in">' +
|
||||||
|
navItems +
|
||||||
|
'</div></nav>' +
|
||||||
|
'</header>';
|
||||||
|
|
||||||
|
// ── Riuso helper common.js (utente/avatar/ruolo + org-switcher + versione + branding + impersonate) ──
|
||||||
|
if (typeof _loadUserInfo === 'function') _loadUserInfo();
|
||||||
|
if (typeof _loadVersionFooter === 'function') _loadVersionFooter();
|
||||||
|
if (typeof _loadFirmBranding === 'function') _loadFirmBranding();
|
||||||
|
if (typeof _renderImpersonateBanner === 'function') _renderImpersonateBanner();
|
||||||
|
|
||||||
|
// ── Menu utente: dropdown con versione + logout (riusa api.logout) ──
|
||||||
|
_wireUserMenu();
|
||||||
|
// ── Mobile: toggle nav + accordion mega ──
|
||||||
|
_wireMobileNav();
|
||||||
|
// ── Stile + montaggio del trittico FAB reale dentro .fab-stack ──
|
||||||
|
_styleRealFabs();
|
||||||
|
|
||||||
|
// Lucide: prova subito (se già caricato) — l'init pagina lo richiama comunque.
|
||||||
|
if (window.lucide && typeof lucide.createIcons === 'function') {
|
||||||
|
try { lucide.createIcons(); } catch (e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropdown del menu utente: versione (changelog) + logout.
|
||||||
|
function _wireUserMenu() {
|
||||||
|
var u = document.getElementById('topnav-user');
|
||||||
|
if (!u) return;
|
||||||
|
var menu = document.createElement('div');
|
||||||
|
menu.id = 'topnav-user-menu';
|
||||||
|
menu.style.cssText = 'position:fixed;display:none;min-width:200px;background:#fff;color:var(--ink);' +
|
||||||
|
'border:1px solid var(--line);border-radius:10px;box-shadow:0 14px 36px rgba(11,40,80,.20);' +
|
||||||
|
'padding:6px;z-index:90';
|
||||||
|
menu.innerHTML =
|
||||||
|
'<div id="sidebar-version" role="button" tabindex="0" title="Versione applicazione" ' +
|
||||||
|
'style="padding:8px 10px;font-size:12px;color:var(--muted);cursor:pointer;border-radius:8px">v—</div>' +
|
||||||
|
'<a href="settings.html" style="display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:8px;font-size:13px;font-weight:600">' +
|
||||||
|
'<i data-lucide="settings" style="width:15px;height:15px"></i>Impostazioni</a>' +
|
||||||
|
'<button type="button" id="topnav-logout" style="display:flex;align-items:center;gap:8px;width:100%;text-align:left;' +
|
||||||
|
'border:0;background:none;font:inherit;font-size:13px;font-weight:600;color:var(--c-red);padding:9px 10px;border-radius:8px;cursor:pointer">' +
|
||||||
|
'<i data-lucide="log-out" style="width:15px;height:15px"></i>Esci</button>';
|
||||||
|
document.body.appendChild(menu);
|
||||||
|
|
||||||
|
function place() {
|
||||||
|
var r = u.getBoundingClientRect();
|
||||||
|
menu.style.top = (r.bottom + 8) + 'px';
|
||||||
|
menu.style.right = Math.max(8, window.innerWidth - r.right) + 'px';
|
||||||
|
}
|
||||||
|
function open() { place(); menu.style.display = 'block'; u.setAttribute('aria-expanded', 'true'); if (window.lucide) try { lucide.createIcons(); } catch (e) {} }
|
||||||
|
function close() { menu.style.display = 'none'; u.setAttribute('aria-expanded', 'false'); }
|
||||||
|
function toggle() { (menu.style.display === 'block') ? close() : open(); }
|
||||||
|
|
||||||
|
u.addEventListener('click', toggle);
|
||||||
|
u.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } });
|
||||||
|
document.addEventListener('click', function (e) { if (!u.contains(e.target) && !menu.contains(e.target)) close(); });
|
||||||
|
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
|
||||||
|
|
||||||
|
var lo = menu.querySelector('#topnav-logout');
|
||||||
|
if (lo) lo.addEventListener('click', function () { if (typeof api !== 'undefined' && api.logout) api.logout(); });
|
||||||
|
var ver = menu.querySelector('#sidebar-version');
|
||||||
|
if (ver) {
|
||||||
|
ver.addEventListener('click', function () { if (typeof _showVersionChangelog === 'function') _showVersionChangelog(); });
|
||||||
|
ver.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (typeof _showVersionChangelog === 'function') _showVersionChangelog(); } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile (≤900): hamburger apre/chiude la nav; tap su un gruppo apre l'accordion mega.
|
||||||
|
function _wireMobileNav() {
|
||||||
|
var toggle = document.getElementById('topnav-toggle');
|
||||||
|
var bar = document.getElementById('topnav-bar');
|
||||||
|
if (toggle && bar) {
|
||||||
|
toggle.addEventListener('click', function () {
|
||||||
|
var open = bar.classList.toggle('open');
|
||||||
|
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Accordion per i gruppi con mega (solo in modalità mobile via CSS).
|
||||||
|
document.querySelectorAll('#topnav-bar .nav-item').forEach(function (item) {
|
||||||
|
var hasMega = item.querySelector('.mega');
|
||||||
|
if (!hasMega) return;
|
||||||
|
var link = item.querySelector('.nav-link');
|
||||||
|
if (!link) return;
|
||||||
|
link.addEventListener('click', function (e) {
|
||||||
|
// Solo in layout mobile (nav-toggle visibile) intercettiamo il tap per aprire l'accordion.
|
||||||
|
if (toggle && getComputedStyle(toggle).display !== 'none') {
|
||||||
|
e.preventDefault();
|
||||||
|
item.classList.toggle('open');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trittico FAB reale (campana/bug-reporter ARIA) montato da common.js: lo
|
||||||
|
// raggruppiamo in .fab-stack e gli diamo la veste V3, senza reimplementarlo.
|
||||||
|
// common.js crea: #nx-bar (bug-reporter widget) e #ai-chat-fab (ARIA viola).
|
||||||
|
// Qui aggiungiamo SOLO un contenitore visivo se non presente; gli elementi
|
||||||
|
// restano gestiti dal loro codice (eventi/voce/puntatore invariati).
|
||||||
|
function _styleRealFabs() {
|
||||||
|
// I FAB reali sono position:fixed indipendenti (z-index alti). Non li
|
||||||
|
// spostiamo nel DOM per non romperne gli handler; il loro look resta
|
||||||
|
// quello di common.js. Questa funzione è un hook no-op di compatibilità
|
||||||
|
// mantenuto per chiarezza: la veste .fab del mockup NON deve sovrascrivere
|
||||||
|
// i FAB funzionanti (ARIA con voce + bug-reporter widget).
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS one-shot per il lang-toggle e user menu nella appbar (token V3).
|
||||||
|
(function injectChromeCss() {
|
||||||
|
if (document.getElementById('topnav-v3-css')) return;
|
||||||
|
var s = document.createElement('style');
|
||||||
|
s.id = 'topnav-v3-css';
|
||||||
|
s.textContent =
|
||||||
|
'.topnav-lang{display:inline-flex;gap:3px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.16);border-radius:8px;padding:3px}' +
|
||||||
|
'.topnav-lang .lang-btn{border:0;background:transparent;color:#cfe0f1;font:inherit;font-size:11px;font-weight:700;padding:4px 8px;border-radius:6px;cursor:pointer}' +
|
||||||
|
'.topnav-lang .lang-btn:hover{background:rgba(255,255,255,.12);color:#fff}' +
|
||||||
|
'.topnav-lang .lang-btn.active{background:var(--gold);color:#1a1a1a}' +
|
||||||
|
'#topnav-user .sidebar-user-name{font-size:12px;font-weight:600}' +
|
||||||
|
'@media (max-width:560px){#topnav-user .sidebar-user-name{display:none}.topnav-lang{display:none}}';
|
||||||
|
document.head.appendChild(s);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Override globale: l'init delle pagine chiama loadSidebar() invariato.
|
||||||
|
window.loadTopnavV3 = loadTopnavV3;
|
||||||
|
window.loadSidebar = loadTopnavV3;
|
||||||
|
})();
|
||||||
Vendored
+31
@@ -0,0 +1,31 @@
|
|||||||
|
# Inter (self-hosted) — NIS2 Agile UI V3
|
||||||
|
|
||||||
|
Font **Inter** ospitato localmente. **NON** usare Google Fonts CDN nelle pagine
|
||||||
|
(motivi: AGID/privacy, CSP, PWA offline). Le pagine includono solo
|
||||||
|
`/vendor/inter/inter.css`, con fallback `system-ui`.
|
||||||
|
|
||||||
|
## File
|
||||||
|
|
||||||
|
- `inter-latin.woff2` — subset **latin** (variabile, asse peso 100–900).
|
||||||
|
- `inter-latin-ext.woff2` — subset **latin-ext** (accentate aggiuntive).
|
||||||
|
- `inter.css` — `@font-face` che puntano ai due `.woff2` locali.
|
||||||
|
|
||||||
|
Inter v20 è un **variable font**: un solo file copre tutti i pesi (400–800 usati
|
||||||
|
dal design system V3) tramite `font-weight: 100 900`.
|
||||||
|
|
||||||
|
## URL di origine (per rigenerare)
|
||||||
|
|
||||||
|
CSS sorgente (User-Agent browser → woff2):
|
||||||
|
```
|
||||||
|
https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap
|
||||||
|
```
|
||||||
|
|
||||||
|
woff2 estratti dal CSS sopra (subset latin / latin-ext, Inter v20):
|
||||||
|
```
|
||||||
|
latin : https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2
|
||||||
|
latin-ext : https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2
|
||||||
|
```
|
||||||
|
|
||||||
|
> Nota: gli hash nelle URL gstatic possono cambiare a nuove versioni di Inter.
|
||||||
|
> Se serve rigenerare, riscaricare la CSS con UA browser e ripescare i blocchi
|
||||||
|
> `/* latin */` e `/* latin-ext */`.
|
||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/* ───────────────────────────────────────────────────────────────────────────
|
||||||
|
* Inter — self-hosted (NIS2 Agile UI V3)
|
||||||
|
* Variable font (weight axis 100–900) scaricato da fonts.gstatic.com (Inter v20).
|
||||||
|
* Self-host obbligatorio: NO Google Fonts CDN (AGID/privacy/CSP/PWA-offline).
|
||||||
|
* Coperti: latin + latin-ext (à è é ì ò ù ... — set sufficiente per IT/EN).
|
||||||
|
* Fallback: system-ui. Vedi README.md per le URL di origine e come rigenerare.
|
||||||
|
* ─────────────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/inter/inter-latin.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/vendor/inter/inter-latin-ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
Vendored
+12
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user