[FEAT] UI V3 — flotta: chrome top-nav + design system su 32 pagine app

Migrazione UI dal V2 (Bootstrap Italia, sidebar) al V3 (top-nav mega-menu
navy/oro, Inter+Lucide self-host) fedele ai mockup docs/mockup Ui/.
- 32 pagine app convertite (incl. admin/*): head Inter+v3.css, chrome
  topnav-v3, contenuti in card V3, dati REALI preservati (id/data-* intatti),
  i18n/help/FAB reali invariati, nessun CDN esterno.
- v3.css: aggiunti widget condivisi (modal/toast/form/btn-variant/spinner/
  progress/tag/badge) + utility (.btn base, .text-muted, .sr-only) + alias
  variabili V2->token V3, così modali/form/toast di common.js funzionano
  ovunque senza style.css.
- maintenance.html: finestra estesa a ~12:15.
- docs/UI_V3_FLEET_BRIEF.md: ricetta operativa della flotta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-27 15:57:35 +02:00
co-authored by Claude Opus 4.8
parent b0fc190704
commit a4150b5389
37 changed files with 4655 additions and 4922 deletions
+93
View File
@@ -0,0 +1,93 @@
# UI V3 — BRIEF OPERATIVO FLOTTA (FASE 1)
Migri pagine NIS2 dal V2 (Bootstrap Italia, sidebar) al **V3** (top-nav navy/oro, Inter, Lucide).
La **FASE 0 è già fatta e in prod**: `public/css/v3.css`, `public/js/topnav-v3.js`, `public/vendor/inter/`,
`public/vendor/lucide/lucide.min.js`, e la **pilota `public/dashboard.html` + `public/js/dashboard.js`**.
**Replica quel pattern.** Il sito è in MANUTENZIONE: testa SEMPRE con `?mnt=efc088d042032889b64b887d2e562ec8`.
## REGOLE D'ORO (vincolanti)
- **NON usare git** (no add/commit/push). **Solo scrivere file.** Il commit lo fa l'orchestratore.
- **NON toccare**: `public/css/v3.css`, `public/js/topnav-v3.js`, `public/js/common.js`, `public/js/api.js`,
`public/js/i18n.js`, `public/js/help.js`, qualunque `.php`, DB, migrazioni. Sono condivisi/già pronti.
- **NON toccare** `/var/www/nis2-agile/.maintenance` né `/tmp/agent-working.lock`.
- **Modifichi SOLO le pagine assegnate** (e, se serve, il loro `<pagina>.js` dedicato — non condiviso).
- **Dati REALI** via `api.js` (quelli che la pagina già usa). Zero numeri finti dei mockup.
Se un dato non esiste nel backend → placeholder "—"/empty, **non inventarlo**.
- **Self-host only**: nessun `googleapis.com`, `gstatic.com`, `unpkg.com`, CDN Bootstrap Italia.
- Mantieni: `checkAuth`, i18n IT/EN (`data-i18n`), help "?", **trittico FAB reale** (lo monta `common.js`).
- A11y/AGID: `focus-visible`, contrasto, touch ≥44px, `aria-label`, zoom non bloccato, responsive ≤900/≤560.
## RICETTA PER PAGINA — "reskin a rischio minimo"
L'obiettivo è cambiare **chrome + look**, **senza rompere il wiring**: **preserva ogni `id`/`data-*`
che il `<pagina>.js` legge** (non rinominarli). Cambi guscio e classi, non la logica.
### 1) `<head>` — sostituisci il caricamento CSS/font con (vedi `dashboard.html` righe 3–23):
```html
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>NomePagina · NIS2 Agile</title>
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<!-- mantieni il blocco PWA: manifest, theme-color #0066CC, icone, pwa.js (copia da dashboard.html) -->
```
**Rimuovi**: bundle Bootstrap Italia (CSS/JS), `/css/style.css`, Google Fonts, unpkg.
Se la pagina ha un `<style>` inline con regole essenziali non coperte da `v3.css`, puoi **tenerne una
versione minima** (nessun CDN), ma preferisci le classi di `v3.css`.
### 2) `<body>` — struttura V3:
```html
<div id="topnav"></div> <!-- host chrome: lo riempie topnav-v3.js -->
<main class="wrap">
<nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>NomePagina</span>
</nav>
<div class="page-head">
<div><h1 data-i18n="...">Titolo</h1><p class="sub">sottotitolo breve</p></div>
<!-- eventuali bottoni pagina: <button class="btn-primary">…</button> -->
</div>
<!-- CONTENUTO: avvolgi i blocchi in <section class="card card-pad">… ; preserva gli id dinamici -->
</main>
```
Mappa i componenti vecchi → classi `v3.css`: contenitori → `.card`/`.card-pad`; KPI → `.stat`
(`.green/.amber/.blue/.red`, `.ibox`,`.val`,`.lbl`,`.bar`); bottoni → `.btn-primary`/`.btn-ghost`;
stati → `.badge-t` (varianti colore); intestazioni sezione → `.sec-head`; griglie → `.grid3`/`.split2`.
Tabelle/liste: tienile dentro una `.card`; usa stili tabella di `v3.css` se presenti, altrimenti markup sobrio.
Stati **loading/empty/error**: usa `.v3-loading`/`.v3-empty`/`.v3-error` (come la dashboard) dove la pagina carica dati.
### 3) Scripts in fondo (ordine ESATTO come `dashboard.html` righe 112–140):
```html
<script src="/js/api.js?v=20260625d"></script>
<script src="/js/common.js?v=20260627v3"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627"></script>
<script src="/js/i18n.js?v=20260631"></script>
<script src="/js/help.js?v=20260625d"></script>
<script src="/js/NOMEPAGINA.js?v=20260627"></script>
<script>
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); // = loadTopnavV3()
if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch(e){} }
/* poi l'init reale della pagina, es: if (typeof initPagina==='function') initPagina(); */
</script>
```
**Rimuovi** `common-bi.js` e il bundle Bootstrap Italia JS. Se il vecchio init chiamava `loadSidebar()`
funziona identico (topnav-v3.js ridefinisce `window.loadSidebar`). Mantieni l'init dati della pagina.
### 4) Pagine con MOCKUP (massima fedeltà)
- `assessment.html` → segui `docs/mockup Ui/assessment.html` (pattern pagina-modulo/GAP).
- `calendario.html` → segui `docs/mockup Ui/calendario.html` (app full-page: `.cal-app/.cal-main/.cal-side/.mv-grid/.view-switch/.legend`).
Le altre pagine: applica lo stile dashboard/card in modo **coerente** (non esiste un mockup dedicato).
## VALIDAZIONE (obbligatoria prima di riportare "fatto")
- `node --check public/js/<ogni-js-toccato>.js` → deve passare.
- HTTP: `curl -s -o /dev/null -w "%{http_code}\n" "https://nis2.agile.software/<pagina>?mnt=efc088d042032889b64b887d2e562ec8"` → **200**.
- HTML servito non deve contenere `googleapis|gstatic|unpkg|bootstrap-italia` (CDN):
`curl -s "https://nis2.agile.software/<pagina>?mnt=efc088d042032889b64b887d2e562ec8" | grep -Eo 'googleapis|gstatic|unpkg|cdn[^"]*bootstrap' | sort -u` → vuoto.
## REPORT FINALE
Elenca: pagine convertite (✓), eventuali `<pagina>.js` adattati e perché, gap/placeholder usati (dato mockup
inesistente nel backend), componenti `v3.css` che mancavano (NON aggiungerli tu — **segnalali**), e qualsiasi
pagina che NON sei riuscito a convertire fedelmente (spiega, non improvvisare).
+101 -261
View File
@@ -3,184 +3,34 @@
<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>Pannello Amministrazione - NIS2 Agile</title> <title>Pannello Amministrazione · 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. Pagina in /admin/: asset con path ASSOLUTI. -->
cosi' style.css resta autorevole per i widget renderizzati via JS -> parita' funzionale. <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
Pagina in /admin/: gli asset usano path ASSOLUTI (/vendor, /js, /css) serviti dal web root. --> <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">
<style> <style>
.admin-stats-grid { /* ── Grafico distribuzione piani (non coperto da v3.css) ────────── */
display: grid; .chart-container { padding:8px 4px 4px; }
grid-template-columns: repeat(4, 1fr); .bar-chart { display:flex; align-items:flex-end; gap:32px; height:200px; padding:0 20px; border-bottom:2px solid var(--line); }
gap: 16px; .bar-chart-item { display:flex; flex-direction:column; align-items:center; flex:1; height:100%; justify-content:flex-end; }
margin-bottom: 24px; .bar-chart-bar { width:60px; border-radius:6px 6px 0 0; transition:height .8s ease; min-height:4px; position:relative; }
} .bar-chart-bar.free { background:#94A3B8; }
.admin-stat-card { .bar-chart-bar.professional { background:var(--c-blue); }
background: var(--card-bg); .bar-chart-bar.enterprise { background:#7C4DD6; }
border-radius: var(--border-radius-lg); .bar-chart-value { font-size:.875rem; font-weight:700; color:var(--ink); margin-bottom:8px; }
box-shadow: var(--card-shadow); .bar-chart-label { font-size:.75rem; color:var(--muted); margin-top:12px; font-weight:600; text-transform:uppercase; letter-spacing:.03em; }
padding: 20px; @media (prefers-reduced-motion:reduce) { .bar-chart-bar{transition:none;} }
transition: box-shadow var(--transition), transform var(--transition);
} /* Quick links */
.admin-stat-card:hover { .quick-links-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:16px; }
box-shadow: var(--card-shadow-hover); .quick-link-card { display:flex; align-items:center; gap:16px; padding:18px 20px; background:#fff; border:1px solid var(--line); border-radius:var(--radius); cursor:pointer; transition:border-color .12s, box-shadow .12s, transform .12s; text-decoration:none; color:inherit; }
transform: translateY(-2px); .quick-link-card:hover { border-color:#9FBEDD; box-shadow:0 6px 16px rgba(16,40,80,.10); transform:translateY(-1px); }
} .quick-link-icon { width:46px; height:46px; border-radius:11px; background:#EAF1FA; color:var(--c-blue); display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.admin-stat-label { .quick-link-icon .ic { width:22px; height:22px; }
font-size: 0.7rem; .quick-link-info h4 { font-size:.9375rem; font-weight:700; color:var(--ink); margin:0 0 2px; }
font-weight: 600; .quick-link-info p { font-size:.8125rem; color:var(--muted); margin:0; }
text-transform: uppercase;
letter-spacing: 0.04em; @media (max-width:1024px) { .grid3 { grid-template-columns:repeat(2,1fr); } }
color: var(--gray-500); @media (max-width:768px) { .grid3 { grid-template-columns:1fr; } .quick-links-grid { grid-template-columns:1fr; } .bar-chart { gap:16px; } .bar-chart-bar { width:40px; } }
margin-bottom: 8px;
}
.admin-stat-value {
font-size: 2rem;
font-weight: 800;
color: var(--gray-900);
line-height: 1;
}
.admin-stat-icon {
width: 40px;
height: 40px;
border-radius: var(--border-radius);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.admin-stat-icon svg {
width: 20px;
height: 20px;
}
.admin-stat-icon.blue { background: var(--primary-bg); color: var(--primary); }
.admin-stat-icon.green { background: var(--secondary-bg); color: var(--secondary); }
.admin-stat-icon.orange { background: var(--warning-bg); color: #d97706; }
.admin-stat-icon.red { background: var(--danger-bg); color: var(--danger); }
.admin-stat-icon.purple { background: #f3e8ff; color: #7c3aed; }
/* Le icone BI usano fill=currentColor: garantiamo che il colore del wrapper sia ereditato. */
.admin-stat-icon svg.icon { fill: currentColor; width: 20px; height: 20px; }
.chart-container {
padding: 24px;
}
.bar-chart {
display: flex;
align-items: flex-end;
gap: 32px;
height: 200px;
padding: 0 20px;
border-bottom: 2px solid var(--gray-200);
}
.bar-chart-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
height: 100%;
justify-content: flex-end;
}
.bar-chart-bar {
width: 60px;
border-radius: 6px 6px 0 0;
transition: height 0.8s ease;
min-height: 4px;
position: relative;
}
.bar-chart-bar.free { background: var(--gray-400); }
.bar-chart-bar.professional { background: var(--primary); }
.bar-chart-bar.enterprise { background: #7c3aed; }
.bar-chart-value {
font-size: 0.875rem;
font-weight: 700;
color: var(--gray-700);
margin-bottom: 8px;
}
.bar-chart-label {
font-size: 0.75rem;
color: var(--gray-500);
margin-top: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.quick-link-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px 24px;
background: var(--card-bg);
border: 1px solid var(--gray-200);
border-radius: var(--border-radius-lg);
cursor: pointer;
transition: all var(--transition-fast);
text-decoration: none;
color: inherit;
}
.quick-link-card:hover {
border-color: var(--primary);
background: var(--primary-bg);
color: var(--primary);
transform: translateY(-2px);
box-shadow: var(--card-shadow-hover);
}
.quick-link-card:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.quick-link-icon {
width: 48px;
height: 48px;
border-radius: var(--border-radius);
background: var(--primary-bg);
color: var(--primary);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.quick-link-icon svg {
width: 24px;
height: 24px;
fill: currentColor;
}
.quick-link-info h4 {
font-size: 0.9375rem;
font-weight: 600;
color: var(--gray-800);
margin-bottom: 2px;
}
.quick-link-info p {
font-size: 0.8125rem;
color: var(--gray-500);
}
.quick-link-card:hover .quick-link-info h4 {
color: var(--primary);
}
.quick-links-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
@media (max-width: 1024px) {
.admin-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.admin-stats-grid {
grid-template-columns: 1fr;
}
.quick-links-grid {
grid-template-columns: 1fr;
}
.bar-chart {
gap: 16px;
}
.bar-chart-bar {
width: 40px;
}
}
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -197,144 +47,134 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2">Pannello Amministrazione</h1> <i data-lucide="home" class="ic"></i><a href="/dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span>Amministrazione</span>
<span class="badge badge-danger">Super Admin</span> </nav>
<div class="page-head">
<div>
<h1>Pannello Amministrazione</h1>
<p class="sub">Panoramica della piattaforma e gestione globale</p>
</div>
<span class="badge-t red">Super Admin</span>
</div> </div>
</header>
<div class="content-body"> <div class="stack">
<!-- Stats Row 1 --> <!-- Stats Row 1 -->
<div class="admin-stats-grid" id="stats-row-1"> <div class="grid3" id="stats-row-1" style="grid-template-columns:repeat(4,1fr)">
<div class="admin-stat-card"> <div class="card card-pad stat blue">
<div class="admin-stat-icon blue" aria-hidden="true"> <div class="ibox"><i data-lucide="building-2" class="ic"></i></div>
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-pa"></use></svg> <div style="flex:1;min-width:0">
<div class="lbl" id="lbl-total-orgs">Totale Organizzazioni</div>
<div class="val" id="stat-total-orgs" aria-labelledby="lbl-total-orgs">--</div>
</div> </div>
<div class="admin-stat-label" id="lbl-total-orgs">Totale Organizzazioni</div>
<div class="admin-stat-value" id="stat-total-orgs" aria-labelledby="lbl-total-orgs">--</div>
</div> </div>
<div class="admin-stat-card"> <div class="card card-pad stat green">
<div class="admin-stat-icon green" aria-hidden="true"> <div class="ibox"><i data-lucide="check-circle" class="ic"></i></div>
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check-circle"></use></svg> <div style="flex:1;min-width:0">
<div class="lbl" id="lbl-active-orgs">Organizzazioni Attive</div>
<div class="val" id="stat-active-orgs" aria-labelledby="lbl-active-orgs">--</div>
</div> </div>
<div class="admin-stat-label" id="lbl-active-orgs">Organizzazioni Attive</div>
<div class="admin-stat-value" id="stat-active-orgs" aria-labelledby="lbl-active-orgs">--</div>
</div> </div>
<div class="admin-stat-card"> <div class="card card-pad stat blue">
<div class="admin-stat-icon blue" aria-hidden="true"> <div class="ibox"><i data-lucide="users" class="ic"></i></div>
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg> <div style="flex:1;min-width:0">
<div class="lbl" id="lbl-total-users">Totale Utenti</div>
<div class="val" id="stat-total-users" aria-labelledby="lbl-total-users">--</div>
</div> </div>
<div class="admin-stat-label" id="lbl-total-users">Totale Utenti</div>
<div class="admin-stat-value" id="stat-total-users" aria-labelledby="lbl-total-users">--</div>
</div> </div>
<div class="admin-stat-card"> <div class="card card-pad stat green">
<div class="admin-stat-icon green" aria-hidden="true"> <div class="ibox"><i data-lucide="user-check" class="ic"></i></div>
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg> <div style="flex:1;min-width:0">
<div class="lbl" id="lbl-active-users">Utenti Attivi</div>
<div class="val" id="stat-active-users" aria-labelledby="lbl-active-users">--</div>
</div> </div>
<div class="admin-stat-label" id="lbl-active-users">Utenti Attivi</div>
<div class="admin-stat-value" id="stat-active-users" aria-labelledby="lbl-active-users">--</div>
</div> </div>
</div> </div>
<!-- Stats Row 2 --> <!-- Stats Row 2 -->
<div class="admin-stats-grid" id="stats-row-2"> <div class="grid3" id="stats-row-2" style="grid-template-columns:repeat(4,1fr)">
<div class="admin-stat-card"> <div class="card card-pad stat blue">
<div class="admin-stat-icon purple" aria-hidden="true"> <div class="ibox"><i data-lucide="clipboard-check" class="ic"></i></div>
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg> <div style="flex:1;min-width:0">
<div class="lbl" id="lbl-assessments">Assessment Completati</div>
<div class="val" id="stat-assessments" aria-labelledby="lbl-assessments">--</div>
</div> </div>
<div class="admin-stat-label" id="lbl-assessments">Assessment Completati</div>
<div class="admin-stat-value" id="stat-assessments" aria-labelledby="lbl-assessments">--</div>
</div> </div>
<div class="admin-stat-card"> <div class="card card-pad stat amber">
<div class="admin-stat-icon orange" aria-hidden="true"> <div class="ibox"><i data-lucide="alert-triangle" class="ic"></i></div>
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-warning-circle"></use></svg> <div style="flex:1;min-width:0">
<div class="lbl" id="lbl-incidents">Incidenti Aperti</div>
<div class="val" id="stat-incidents" aria-labelledby="lbl-incidents">--</div>
</div> </div>
<div class="admin-stat-label" id="lbl-incidents">Incidenti Aperti</div>
<div class="admin-stat-value" id="stat-incidents" aria-labelledby="lbl-incidents">--</div>
</div> </div>
<div class="admin-stat-card"> <div class="card card-pad stat amber">
<div class="admin-stat-icon red" aria-hidden="true"> <div class="ibox"><i data-lucide="shield-alert" class="ic"></i></div>
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-error"></use></svg> <div style="flex:1;min-width:0">
<div class="lbl" id="lbl-risks">Totale Rischi</div>
<div class="val" id="stat-risks" aria-labelledby="lbl-risks">--</div>
</div> </div>
<div class="admin-stat-label" id="lbl-risks">Totale Rischi</div>
<div class="admin-stat-value" id="stat-risks" aria-labelledby="lbl-risks">--</div>
</div> </div>
<div class="admin-stat-card"> <div class="card card-pad stat blue">
<div class="admin-stat-icon purple" aria-hidden="true"> <div class="ibox"><i data-lucide="sparkles" class="ic"></i></div>
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-software"></use></svg> <div style="flex:1;min-width:0">
<div class="lbl" id="lbl-ai">Interazioni AI</div>
<div class="val" id="stat-ai" aria-labelledby="lbl-ai">--</div>
</div> </div>
<div class="admin-stat-label" id="lbl-ai">Interazioni AI</div>
<div class="admin-stat-value" id="stat-ai" aria-labelledby="lbl-ai">--</div>
</div> </div>
</div> </div>
<!-- Plans Distribution --> <!-- Plans Distribution -->
<div class="card mb-24"> <section class="card card-pad">
<div class="card-header"> <div class="sec-head"><h3>Distribuzione Piani</h3></div>
<h3>Distribuzione Piani</h3>
</div>
<div class="chart-container"> <div class="chart-container">
<div class="bar-chart" id="plans-chart" role="img" aria-label="Grafico a barre: distribuzione delle organizzazioni per piano di sottoscrizione"> <div class="bar-chart" id="plans-chart" role="img" aria-label="Grafico a barre: distribuzione delle organizzazioni per piano di sottoscrizione">
<div class="spinner" style="margin:40px auto;"></div> <div class="v3-loading" style="flex:1"><span class="v3-spin"></span>Caricamento…</div>
</div>
</div> </div>
</div> </div>
</section>
<!-- Quick Links --> <!-- Quick Links -->
<div class="card"> <section class="card card-pad">
<div class="card-header"> <div class="sec-head"><h3>Gestione Piattaforma</h3></div>
<h3>Gestione Piattaforma</h3>
</div>
<div class="card-body">
<nav class="quick-links-grid" aria-label="Collegamenti rapidi gestione piattaforma"> <nav class="quick-links-grid" aria-label="Collegamenti rapidi gestione piattaforma">
<a href="/admin/organizations.html" class="quick-link-card"> <a href="/admin/organizations.html" class="quick-link-card">
<div class="quick-link-icon" aria-hidden="true"> <div class="quick-link-icon" aria-hidden="true"><i data-lucide="building-2" class="ic"></i></div>
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-pa"></use></svg>
</div>
<div class="quick-link-info"> <div class="quick-link-info">
<h4>Gestisci Organizzazioni</h4> <h4>Gestisci Organizzazioni</h4>
<p>Visualizza e gestisci tutte le organizzazioni registrate sulla piattaforma.</p> <p>Visualizza e gestisci tutte le organizzazioni registrate sulla piattaforma.</p>
</div> </div>
</a> </a>
<a href="/admin/users.html" class="quick-link-card"> <a href="/admin/users.html" class="quick-link-card">
<div class="quick-link-icon" aria-hidden="true"> <div class="quick-link-icon" aria-hidden="true"><i data-lucide="users" class="ic"></i></div>
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg>
</div>
<div class="quick-link-info"> <div class="quick-link-info">
<h4>Gestisci Utenti</h4> <h4>Gestisci Utenti</h4>
<p>Visualizza e gestisci tutti gli utenti registrati sulla piattaforma.</p> <p>Visualizza e gestisci tutti gli utenti registrati sulla piattaforma.</p>
</div> </div>
</a> </a>
</nav> </nav>
</div> </section>
</div>
</div> </div>
</main> </main>
</div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide → topnav-v3.js (chrome) → i18n → init pagina -->
<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="/js/i18n.js?v=20260631"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/js/help.js?v=20260625d"></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=20260620"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar();
I18n.init(); if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
// ── Admin check ────────────────────────────────────────── // ── Admin check ──────────────────────────────────────────
(async function checkAdmin() { (async function checkAdmin() {
+72 -138
View File
@@ -3,103 +3,47 @@
<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>Gestione Organizzazioni - NIS2 Agile</title> <title>Gestione Organizzazioni · 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. Pagina in /admin/: asset con path ASSOLUTI. -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (tabella org/paginazione/badge) -> parita' funzionale assoluta. BI veste la chrome. <link rel="stylesheet" href="/css/v3.css?v=20260627">
Path relativi ../ perche' la pagina vive sotto public/admin/. -->
<link rel="stylesheet" href="../vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="../css/style.css?v=20260618">
<style> <style>
.entity-type-badge { /* ── Tabella admin (v3.css non include stili tabella; markup sobrio, no CDN) ── */
display: inline-flex; .table-container { width:100%; overflow-x:auto; }
padding: 3px 10px; table { width:100%; border-collapse:collapse; font-size:13px; }
border-radius: 100px; thead th { text-align:left; font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:#7a8aa0; padding:10px 14px; border-bottom:1px solid var(--line); white-space:nowrap; }
font-size: 0.7rem; tbody td { padding:12px 14px; border-bottom:1px solid var(--line); vertical-align:middle; }
font-weight: 600; tbody tr:last-child td { border-bottom:0; }
text-transform: uppercase; tbody tr:hover { background:#F7FAFD; }
letter-spacing: 0.03em; td strong { font-weight:700; }
} .text-muted { color:var(--muted); }
.entity-type-badge.essential { background: var(--danger-bg); color: var(--danger); }
.entity-type-badge.important { background: var(--warning-bg); color: #a16207; } .entity-type-badge, .plan-badge { display:inline-flex; padding:2px 7px; border-radius:6px; font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.03em; white-space:nowrap; }
.entity-type-badge.not_applicable { background: var(--gray-100); color: var(--gray-600); } .entity-type-badge.essential { background:#FBE9E9; color:#B23636; }
.plan-badge { .entity-type-badge.important { background:#FBF1E2; color:#9A6418; }
display: inline-flex; .entity-type-badge.not_applicable { background:#EEF2F7; color:#5b6b80; }
padding: 3px 10px; .plan-badge.free { background:#EEF2F7; color:#5b6b80; }
border-radius: 100px; .plan-badge.professional { background:#E7F0FB; color:#1F5FA8; }
font-size: 0.7rem; .plan-badge.enterprise { background:#F0E9FB; color:#7C4DD6; }
font-weight: 600;
text-transform: uppercase; .status-dot { display:inline-flex; align-items:center; gap:6px; font-size:.8125rem; }
letter-spacing: 0.03em; .status-dot::before { content:''; width:8px; height:8px; border-radius:50%; flex-shrink:0; }
} .status-dot.active::before { background:var(--c-green); }
.plan-badge.free { background: var(--gray-100); color: var(--gray-600); } .status-dot.inactive::before { background:#94A3B8; }
.plan-badge.professional { background: var(--primary-bg); color: var(--primary); }
.plan-badge.enterprise { background: #f3e8ff; color: #7c3aed; } .score-inline { display:inline-flex; align-items:center; gap:6px; font-size:.8125rem; font-weight:600; }
.status-dot { .score-inline .score-dot { width:8px; height:8px; border-radius:50%; }
display: inline-flex;
align-items: center; .pagination { display:flex; align-items:center; justify-content:space-between; padding:14px 14px 2px; border-top:1px solid var(--line); margin-top:6px; }
gap: 6px; .pagination-info { font-size:.8125rem; color:var(--muted); }
font-size: 0.8125rem; .pagination-controls { display:flex; gap:8px; }
} .btn-ghost[disabled] { opacity:.5; cursor:not-allowed; }
.status-dot::before { .btn-sm { padding:6px 11px; font-size:11.5px; }
content: '';
width: 8px; .empty-state { text-align:center; padding:60px 24px; color:var(--muted); }
height: 8px; .empty-state .ic { width:48px; height:48px; color:var(--line); margin:0 auto 16px; display:block; }
border-radius: 50%; .empty-state h4 { font-size:1rem; font-weight:700; color:var(--ink); margin:0 0 8px; }
flex-shrink: 0; .empty-state p { margin:0; }
}
.status-dot.active::before { background: var(--secondary); }
.status-dot.inactive::before { background: var(--gray-400); }
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-top: 1px solid var(--gray-100);
background: var(--gray-50);
}
.pagination-info {
font-size: 0.8125rem;
color: var(--gray-500);
}
.pagination-controls {
display: flex;
gap: 8px;
}
.score-inline {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.8125rem;
font-weight: 600;
}
.score-inline .score-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.admin-breadcrumb {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8125rem;
color: var(--gray-500);
margin-bottom: 20px;
}
.admin-breadcrumb a {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.admin-breadcrumb a:hover {
text-decoration: underline;
}
.admin-breadcrumb .icon {
width: 14px;
height: 14px;
color: var(--gray-400);
fill: currentColor;
}
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -116,55 +60,44 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2">Gestione Organizzazioni</h1> <i data-lucide="home" class="ic"></i><a href="/dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><a href="/admin/index.html">Amministrazione</a>
<span class="badge badge-danger">Super Admin</span> <i data-lucide="chevron-right" class="ic"></i><span>Organizzazioni</span>
</div>
</header>
<div class="content-body">
<!-- Breadcrumb -->
<nav class="admin-breadcrumb" aria-label="Percorso di navigazione">
<a href="../admin/index.html">Amministrazione</a>
<svg class="icon icon-sm" aria-hidden="true"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-chevron-right"></use></svg>
<span aria-current="page">Organizzazioni</span>
</nav> </nav>
<div class="page-head">
<div>
<h1>Gestione Organizzazioni</h1>
<p class="sub">Tutte le organizzazioni registrate sulla piattaforma</p>
</div>
<span class="badge-t red">Super Admin</span>
</div>
<!-- Table --> <section class="card card-pad">
<div class="card">
<div class="card-body" style="padding:0;">
<div id="orgs-container"> <div id="orgs-container">
<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento in corso"></div> <div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>
</div>
</div>
</div>
</div> </div>
</section>
</main> </main>
</div>
<script src="../js/api.js?v=20260625d"></script> <!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide → topnav-v3.js (chrome) → i18n → init pagina -->
<script src="../js/common.js?v=20260624g"></script> <script src="/js/api.js?v=20260625d"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <script src="/js/common.js?v=20260627v3"></script>
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, <script src="/vendor/lucide/lucide.min.js"></script>
riusando tutti gli helper di common.js (zero backend). --> <script src="/js/topnav-v3.js?v=20260627"></script>
<script src="../vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/js/i18n.js?v=20260631"></script>
<script> <script src="/js/help.js?v=20260625d"></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=20260620"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar();
I18n.init(); if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
let currentPage = 1; let currentPage = 1;
@@ -205,7 +138,7 @@
// ── Caricamento ────────────────────────────────────────── // ── Caricamento ──────────────────────────────────────────
async function loadOrganizations(page) { async function loadOrganizations(page) {
const container = document.getElementById('orgs-container'); const container = document.getElementById('orgs-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento in corso"></div>'; container.innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>';
currentPage = page; currentPage = page;
try { try {
@@ -231,10 +164,11 @@
if (!orgs || orgs.length === 0) { if (!orgs || orgs.length === 0) {
container.innerHTML = ` container.innerHTML = `
<div class="empty-state"> <div class="empty-state">
<svg class="icon" aria-hidden="true" style="width:48px;height:48px;fill:var(--gray-300);"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-pa"></use></svg> <i data-lucide="building-2" class="ic"></i>
<h4>Nessuna organizzazione trovata</h4> <h4>Nessuna organizzazione trovata</h4>
<p>Le organizzazioni registrate appariranno qui.</p> <p>Le organizzazioni registrate appariranno qui.</p>
</div>`; </div>`;
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
return; return;
} }
@@ -304,10 +238,10 @@
Visualizzati ${start}-${end} di ${total} organizzazioni Visualizzati ${start}-${end} di ${total} organizzazioni
</div> </div>
<div class="pagination-controls"> <div class="pagination-controls">
<button class="btn btn-secondary btn-sm" onclick="loadOrganizations(${page - 1})" ${page <= 1 ? 'disabled' : ''}> <button class="btn-ghost btn-sm" onclick="loadOrganizations(${page - 1})" ${page <= 1 ? 'disabled' : ''}>
&laquo; Precedente &laquo; Precedente
</button> </button>
<button class="btn btn-secondary btn-sm" onclick="loadOrganizations(${page + 1})" ${page >= totalPages ? 'disabled' : ''}> <button class="btn-ghost btn-sm" onclick="loadOrganizations(${page + 1})" ${page >= totalPages ? 'disabled' : ''}>
Successivo &raquo; Successivo &raquo;
</button> </button>
</div> </div>
+85 -165
View File
@@ -3,125 +3,54 @@
<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>Gestione Utenti - NIS2 Agile</title> <title>Gestione Utenti · 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. Pagina in /admin/: asset con path ASSOLUTI. -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (tabella utenti/azioni) -> parita' funzionale assoluta. BI veste la chrome. <link rel="stylesheet" href="/css/v3.css?v=20260627">
Path ../ perche' questa pagina vive in /admin/. -->
<link rel="stylesheet" href="../vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="../css/style.css?v=20260618">
<style> <style>
.role-badge { /* ── Tabella admin (v3.css non include stili tabella; markup sobrio, no CDN) ── */
display: inline-flex; .table-container { width:100%; overflow-x:auto; }
padding: 3px 10px; table { width:100%; border-collapse:collapse; font-size:13px; }
border-radius: 100px; thead th { text-align:left; font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:#7a8aa0; padding:10px 14px; border-bottom:1px solid var(--line); white-space:nowrap; }
font-size: 0.7rem; tbody td { padding:12px 14px; border-bottom:1px solid var(--line); vertical-align:middle; }
font-weight: 600; tbody tr:last-child td { border-bottom:0; }
text-transform: uppercase; tbody tr:hover { background:#F7FAFD; }
letter-spacing: 0.03em; td strong { font-weight:700; }
} .text-muted { color:var(--muted); }
.role-badge.super_admin { background: var(--danger-bg); color: var(--danger); }
.role-badge.org_admin { background: var(--primary-bg); color: var(--primary); } .visually-hidden { position:absolute !important; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
.role-badge.compliance_manager { background: var(--secondary-bg); color: #15803d; }
.role-badge.board_member { background: #f3e8ff; color: #7c3aed; } .role-badge { display:inline-flex; padding:2px 7px; border-radius:6px; font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.03em; white-space:nowrap; }
.role-badge.auditor { background: var(--info-bg); color: var(--primary); } .role-badge.super_admin { background:#FBE9E9; color:#B23636; }
.role-badge.employee { background: var(--gray-100); color: var(--gray-600); } .role-badge.org_admin { background:#E7F0FB; color:#1F5FA8; }
.active-toggle { .role-badge.compliance_manager { background:#E6F4EC; color:#1E7A4E; }
position: relative; .role-badge.board_member { background:#F0E9FB; color:#7C4DD6; }
display: inline-flex; .role-badge.auditor { background:#E7F0FB; color:#1F5FA8; }
align-items: center; .role-badge.employee { background:#EEF2F7; color:#5b6b80; }
cursor: pointer;
} .active-toggle { position:relative; display:inline-flex; align-items:center; cursor:pointer; }
.active-toggle input { .active-toggle input { position:absolute; opacity:0; width:0; height:0; }
position: absolute; .toggle-slider { width:36px; height:20px; background:#CBD5E1; border-radius:10px; transition:background .15s; position:relative; }
opacity: 0; .toggle-slider::after { content:''; position:absolute; width:16px; height:16px; background:#fff; border-radius:50%; top:2px; left:2px; transition:transform .15s; box-shadow:0 1px 3px rgba(0,0,0,0.2); }
width: 0; .active-toggle input:checked + .toggle-slider { background:var(--c-green); }
height: 0; .active-toggle input:checked + .toggle-slider::after { transform:translateX(16px); }
} .active-toggle input:focus-visible + .toggle-slider { outline:2px solid var(--gold); outline-offset:2px; }
.toggle-slider { @media (prefers-reduced-motion:reduce) { .toggle-slider, .toggle-slider::after { transition:none; } }
width: 36px;
height: 20px; .org-tags { display:flex; flex-wrap:wrap; gap:4px; }
background: var(--gray-300); .org-tag { display:inline-flex; padding:2px 8px; font-size:10.5px; font-weight:600; background:#EEF2F7; color:#5b6b80; border-radius:6px; max-width:120px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
border-radius: 10px;
transition: background var(--transition-fast); .pagination { display:flex; align-items:center; justify-content:space-between; padding:14px 14px 2px; border-top:1px solid var(--line); margin-top:6px; }
position: relative; .pagination-info { font-size:.8125rem; color:var(--muted); }
} .pagination-controls { display:flex; gap:8px; }
.toggle-slider::after { .btn-ghost[disabled] { opacity:.5; cursor:not-allowed; }
content: ''; .btn-sm { padding:6px 11px; font-size:11.5px; }
position: absolute; .btn-ghost .ic { color:var(--c-blue); }
width: 16px;
height: 16px; .empty-state { text-align:center; padding:60px 24px; color:var(--muted); }
background: #fff; .empty-state .ic { width:48px; height:48px; color:var(--line); margin:0 auto 16px; display:block; }
border-radius: 50%; .empty-state h4 { font-size:1rem; font-weight:700; color:var(--ink); margin:0 0 8px; }
top: 2px; .empty-state p { margin:0; }
left: 2px;
transition: transform var(--transition-fast);
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.active-toggle input:checked + .toggle-slider {
background: var(--secondary);
}
.active-toggle input:checked + .toggle-slider::after {
transform: translateX(16px);
}
/* Focus visibile WCAG su toggle keyboard-operabile */
.active-toggle input:focus-visible + .toggle-slider {
outline: 2px solid var(--primary, #06c);
outline-offset: 2px;
}
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-top: 1px solid var(--gray-100);
background: var(--gray-50);
}
.pagination-info {
font-size: 0.8125rem;
color: var(--gray-500);
}
.pagination-controls {
display: flex;
gap: 8px;
}
.org-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.org-tag {
display: inline-flex;
padding: 2px 8px;
font-size: 0.7rem;
font-weight: 500;
background: var(--gray-100);
color: var(--gray-600);
border-radius: var(--border-radius-sm);
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-breadcrumb {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8125rem;
color: var(--gray-500);
margin-bottom: 20px;
}
.admin-breadcrumb a {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.admin-breadcrumb a:hover {
text-decoration: underline;
}
.admin-breadcrumb .icon {
color: var(--gray-400);
}
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -138,55 +67,44 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2">Gestione Utenti</h1> <i data-lucide="home" class="ic"></i><a href="/dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><a href="/admin/index.html">Amministrazione</a>
<span class="badge badge-danger">Super Admin</span> <i data-lucide="chevron-right" class="ic"></i><span>Utenti</span>
</div>
</header>
<div class="content-body">
<!-- Breadcrumb -->
<nav class="admin-breadcrumb" aria-label="Percorso di navigazione">
<a href="../admin/index.html">Amministrazione</a>
<svg class="icon icon-sm" aria-hidden="true"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-chevron-right"></use></svg>
<span aria-current="page">Utenti</span>
</nav> </nav>
<div class="page-head">
<div>
<h1>Gestione Utenti</h1>
<p class="sub">Tutti gli utenti registrati sulla piattaforma</p>
</div>
<span class="badge-t red">Super Admin</span>
</div>
<!-- Table --> <section class="card card-pad">
<div class="card">
<div class="card-body" style="padding:0;">
<div id="users-container" aria-live="polite"> <div id="users-container" aria-live="polite">
<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento utenti"></div> <div class="v3-loading"><span class="v3-spin"></span>Caricamento utenti…</div>
</div>
</div>
</div>
</div> </div>
</section>
</main> </main>
</div>
<script src="../js/api.js?v=20260625d"></script> <!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide → topnav-v3.js (chrome) → i18n → init pagina -->
<script src="../js/common.js?v=20260624g"></script> <script src="/js/api.js?v=20260625d"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <script src="/js/common.js?v=20260627v3"></script>
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, <script src="/vendor/lucide/lucide.min.js"></script>
riusando tutti gli helper di common.js (zero backend). --> <script src="/js/topnav-v3.js?v=20260627"></script>
<script src="../vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/js/i18n.js?v=20260631"></script>
<script> <script src="/js/help.js?v=20260625d"></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=20260620"></script>
<script> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar();
I18n.init(); if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
let currentPage = 1; let currentPage = 1;
@@ -196,12 +114,12 @@
const me = await api.getMe(); const me = await api.getMe();
if (!me.success || !me.data || me.data.role !== 'super_admin') { if (!me.success || !me.data || me.data.role !== 'super_admin') {
showNotification('Accesso non autorizzato.', 'error'); showNotification('Accesso non autorizzato.', 'error');
setTimeout(() => { window.location.href = '../dashboard.html'; }, 1500); setTimeout(() => { window.location.href = '/dashboard.html'; }, 1500);
return; return;
} }
loadUsers(1); loadUsers(1);
} catch (e) { } catch (e) {
window.location.href = '../dashboard.html'; window.location.href = '/dashboard.html';
} }
})(); })();
@@ -218,7 +136,7 @@
// ── Caricamento ────────────────────────────────────────── // ── Caricamento ──────────────────────────────────────────
async function loadUsers(page) { async function loadUsers(page) {
const container = document.getElementById('users-container'); const container = document.getElementById('users-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento utenti"></div>'; container.innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento utenti…</div>';
currentPage = page; currentPage = page;
try { try {
@@ -244,10 +162,11 @@
if (!users || users.length === 0) { if (!users || users.length === 0) {
container.innerHTML = ` container.innerHTML = `
<div class="empty-state"> <div class="empty-state">
<svg class="icon icon-lg" aria-hidden="true" style="margin:0 auto;"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg> <i data-lucide="users" class="ic"></i>
<h4>Nessun utente trovato</h4> <h4>Nessun utente trovato</h4>
<p>Gli utenti registrati appariranno qui.</p> <p>Gli utenti registrati appariranno qui.</p>
</div>`; </div>`;
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
return; return;
} }
@@ -306,8 +225,8 @@
<td>${formatDate(user.created_at)}</td> <td>${formatDate(user.created_at)}</td>
<td> <td>
${role !== 'super_admin' && isActive ${role !== 'super_admin' && isActive
? `<button class="btn btn-secondary btn-sm" onclick="impersonateUser(${user.id}, '${escapeHtml(user.email)}')" title="Entra come questo utente per 1 ora"> ? `<button class="btn-ghost btn-sm" onclick="impersonateUser(${user.id}, '${escapeHtml(user.email)}')" title="Entra come questo utente per 1 ora">
<svg class="icon icon-sm" aria-hidden="true" style="vertical-align:-2px"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-exchange-circle"></use></svg> <i data-lucide="repeat" class="ic"></i>
Impersonate Impersonate
</button>` </button>`
: '<span class="text-muted">-</span>'} : '<span class="text-muted">-</span>'}
@@ -332,10 +251,10 @@
Visualizzati ${start}-${end} di ${total} utenti Visualizzati ${start}-${end} di ${total} utenti
</div> </div>
<div class="pagination-controls"> <div class="pagination-controls">
<button class="btn btn-secondary btn-sm" onclick="loadUsers(${page - 1})" ${page <= 1 ? 'disabled' : ''}> <button class="btn-ghost btn-sm" onclick="loadUsers(${page - 1})" ${page <= 1 ? 'disabled' : ''}>
&laquo; Precedente &laquo; Precedente
</button> </button>
<button class="btn btn-secondary btn-sm" onclick="loadUsers(${page + 1})" ${page >= totalPages ? 'disabled' : ''}> <button class="btn-ghost btn-sm" onclick="loadUsers(${page + 1})" ${page >= totalPages ? 'disabled' : ''}>
Successivo &raquo; Successivo &raquo;
</button> </button>
</div> </div>
@@ -351,6 +270,7 @@
} }
container.innerHTML = html; container.innerHTML = html;
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
} }
// ── Toggle Attivo/Inattivo ─────────────────────────────── // ── Toggle Attivo/Inattivo ───────────────────────────────
@@ -388,7 +308,7 @@
sessionStorage.setItem('nis2_impersonate_target_email', email); sessionStorage.setItem('nis2_impersonate_target_email', email);
// Sostituisce token con quello impersonate (no refresh per default) // Sostituisce token con quello impersonate (no refresh per default)
api.setTokens(res.data.access_token, ''); api.setTokens(res.data.access_token, '');
window.location.href = '../dashboard.html'; window.location.href = '/dashboard.html';
} else { } else {
showNotification(res.message || 'Impersonate fallito.', 'error'); showNotification(res.message || 'Impersonate fallito.', 'error');
} }
+57 -23
View File
@@ -3,11 +3,39 @@
<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>Architettura - NIS2 Agile</title> <title>Architettura · NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN), PRIMA di style.css cosi' il design system esistente resta autorevole --> <!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia). -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="css/style.css?v=20260618"> <link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
/* ── V3 compat bridge: token del vecchio style.css → token v3.css ──
Mantiene funzionanti gli stili inline e la page-CSS qui sotto senza
ricaricare style.css (rimosso col V3). Nessun CDN. */
:root{
--primary:#1F5FA8; --primary-light:#4a9af5; --primary-dark:#1557b0; --primary-bg:#E7F0FB;
--secondary:#1E8A5A; --secondary-light:#5cb876; --secondary-bg:#E6F4EC;
--warning:#D98324; --warning-light:#fdd663; --warning-bg:#FBF1E2;
--danger:#C62828; --danger-light:#f28b82; --danger-bg:#FBE9E9;
--info:#1F5FA8; --info-bg:#E7F0FB;
--gray-50:#f8fafc; --gray-100:#f1f5f9; --gray-200:#e2e8f0; --gray-300:#cbd5e1;
--gray-400:#94a3b8; --gray-500:#64748b; --gray-600:#475569; --gray-700:#334155;
--gray-800:#1e293b; --gray-900:#0f172a;
--card-bg:#fff; --content-bg:var(--bg);
--card-shadow:var(--shadow); --card-shadow-hover:0 4px 12px rgba(0,0,0,.1),0 2px 4px rgba(0,0,0,.06);
--border-radius:8px; --border-radius-lg:12px; --border-radius-sm:4px;
--font-mono:'SF Mono','Fira Code','Consolas',monospace;
--transition-fast:150ms ease; --transition:250ms ease;
}
/* Card di base (ex style.css) per le sezioni architettura */
.arch-section .card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);overflow:hidden}
.arch-section .card-header{padding:16px 20px;border-bottom:1px solid var(--line)}
.arch-section .card-header h3{margin:0;font-size:15px;font-weight:700}
.arch-section .card-body{padding:20px}
.badge{display:inline-flex;align-items:center;font-size:11px;font-weight:700;padding:3px 9px;border-radius:6px;background:#EEF2F7;color:#5b6b80}
.badge-danger{background:var(--danger-bg);color:var(--danger)}
.badge-warning{background:var(--warning-bg);color:#9A6418}
.badge-neutral{background:#EEF2F7;color:#5b6b80}
/* ── Architecture Page Styles ────────────────────────────────── */ /* ── Architecture Page Styles ────────────────────────────────── */
.arch-section { .arch-section {
@@ -634,18 +662,21 @@
<!-- 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>
<h2>Architettura del Sistema</h2> <i data-lucide="chevron-right" class="ic"></i><span>Architettura</span>
<div class="content-header-actions"> </nav>
<span class="badge badge-neutral">v1.0</span> <div class="page-head">
<div>
<h1>Architettura del Sistema</h1>
<p class="sub">Stack tecnologico, modello dati e moduli della piattaforma</p>
</div>
<span class="badge-t blue">v1.0</span>
</div> </div>
</header>
<div class="content-body"> <div class="content-body">
@@ -1088,18 +1119,21 @@
</div> </div>
</main> </main>
</div>
<script src="js/api.js?v=20260631"></script> <!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
<script src="js/common.js?v=20260624g"></script> topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help -->
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/js/api.js?v=20260625d"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/common.js?v=20260627v3"></script>
<script src="js/i18n.js?v=20260631"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="js/help.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></script>
<script src="/js/i18n.js?v=20260631"></script>
<script src="/js/help.js?v=20260625d"></script>
<script> <script>
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar(); // = loadTopnavV3()
I18n.init(); if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
</script> </script>
</body> </body>
</html> </html>
+267 -125
View File
@@ -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>Gap Analysis - NIS2 Agile</title> <title>GAP Analysis · 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. Self-host only: no external font/script hosts. -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (wizard/form/gauge) -> 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">
@@ -22,39 +20,162 @@
<meta name="application-name" content="NIS2 Agile"> <meta name="application-name" content="NIS2 Agile">
<script src="/js/pwa.js?v=20260614" defer></script> <script src="/js/pwa.js?v=20260614" defer></script>
<!-- PWA:end --> <!-- PWA:end -->
<style>
/* ── GAP Analysis V3 — solo widget NON coperti da v3.css (token condivisi). ──
Fonte visiva: docs/mockup Ui/assessment.html (.ga-intro, .stat KPI, .det/.crow,
.gbadge). Le viste reali (wizard domande, gauge, categorie) sono renderizzate
dal JS della pagina → qui ne forniamo lo stile minimo (no CDN, no Bootstrap). */
.ga-intro{display:flex;gap:12px;align-items:flex-start;background:#EEF4FB;border:1px solid #D5E3F2;color:#2C4A6B;border-radius:11px;padding:14px 16px;font-size:13px;margin-bottom:20px}
.ga-intro .ic{width:18px;height:18px;flex:0 0 auto;color:var(--c-blue);margin-top:1px}
.ga-kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:20px}
.center-card{text-align:center;padding:40px 24px}
.center-card h3{font-size:18px;font-weight:800;margin:0 0 8px}
.center-card p{color:var(--muted);margin:0 auto 22px;max-width:560px;font-size:13.5px}
.btn-lg{padding:12px 20px;font-size:14px;border-radius:10px}
/* tabella assessment esistenti */
.v3-table{width:100%;border-collapse:collapse;font-size:13px}
.v3-table th{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#8093A8;text-align:left;padding:9px 12px;border-bottom:1px solid var(--line)}
.v3-table td{padding:11px 12px;border-top:1px solid var(--line)}
.v3-table tr:first-child td{border-top:0}
/* wizard: indicatore step */
.steps{display:flex;align-items:center;flex-wrap:wrap;gap:4px 0;margin-bottom:18px}
.step{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:9px;cursor:pointer;color:var(--muted)}
.step:hover{background:#F3F7FC}
.step .step-number{width:24px;height:24px;border-radius:50%;background:#E7ECF2;color:#5b6b80;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex:0 0 auto}
.step .step-label{font-size:12.5px;font-weight:600;white-space:nowrap}
.step.active{color:var(--ink)}
.step.active .step-number{background:var(--c-blue);color:#fff}
.step.completed{color:var(--c-green)}
.step.completed .step-number{background:var(--c-green);color:#fff}
.step-connector{flex:1;min-width:14px;height:2px;background:var(--line);margin:0 2px}
/* progress bar avanzamento risposte */
.ga-prog-wrap{margin-bottom:16px}
.ga-prog-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}
.ga-prog-lab{font-size:11.5px;font-weight:700;color:#7a8aa0;text-transform:uppercase;letter-spacing:.04em}
.ga-prog-pct{font-size:12.5px;font-weight:800;color:var(--c-blue)}
.ga-prog-track{background:#EAF0F6;border-radius:99px;height:8px;overflow:hidden}
.ga-prog-fill{height:100%;background:var(--c-blue);border-radius:99px;transition:width .3s ease;width:0}
.ga-prog-sub{font-size:11px;color:var(--muted);margin-top:4px}
/* card domanda */
.qcard-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px;border-bottom:1px solid var(--line)}
.qcard-head h3{margin:0;font-size:15px;font-weight:700}
.qcard-foot{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:14px 20px;border-top:1px solid var(--line);flex-wrap:wrap}
/* domanda: testo + tag */
.q-text{font-size:15px;font-weight:600;color:var(--ink);margin:0 0 4px}
.q-guidance{color:var(--muted);font-size:13px;margin:0 0 8px}
.q-tag{display:inline-block;font-size:10.5px;font-weight:700;padding:3px 8px;border-radius:6px;background:#EEF2F7;color:#5b6b80;margin-top:6px;margin-right:4px}
/* form */
.form-group{margin-bottom:18px}
.form-label{display:block;font-size:11.5px;font-weight:700;color:#7a8aa0;text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px}
.form-radio-group{display:flex;flex-wrap:wrap;gap:9px}
.form-radio{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--line);border-radius:9px;padding:9px 13px;font-size:13px;font-weight:600;cursor:pointer;background:#fff}
.form-radio:hover{border-color:#9FBEDD}
.form-radio.selected{border-color:var(--c-blue);background:#EEF4FB;box-shadow:0 0 0 2px rgba(31,95,168,.15)}
.form-radio input{accent-color:var(--c-blue);cursor:pointer}
.form-range{width:100%;accent-color:var(--c-blue);cursor:pointer}
.form-range-labels{display:flex;justify-content:space-between;align-items:center;font-size:11.5px;color:var(--muted);margin-top:6px}
.form-range-labels #maturity-value{font-size:14px;font-weight:800;color:var(--c-blue)}
.form-textarea,.form-input{width:100%;border:1px solid var(--line);border-radius:8px;padding:9px 11px;font:inherit;font-size:13.5px;color:var(--ink);background:#fff;outline:none}
.form-textarea:focus,.form-input:focus{border-color:var(--c-blue);box-shadow:0 0 0 3px rgba(31,95,168,.12)}
.form-textarea{resize:vertical;min-height:70px}
/* badge generici (renderizzati dal JS) */
.badge{display:inline-block;font-size:10.5px;font-weight:700;padding:3px 9px;border-radius:999px;white-space:nowrap}
.badge-neutral{background:#EEF2F7;color:#5b6b80}
.badge-success{background:#E6F4EC;color:#1E7A4E}
.badge-warning{background:#FBF1E2;color:#9A6418}
.badge-info{background:#E7F0FB;color:#1F5FA8}
/* bottoni stato (JS riusa .btn-success per "Completa") */
.btn-success{display:inline-flex;align-items:center;gap:7px;background:var(--c-green);color:#fff;border:0;border-radius:8px;padding:8px 13px;font:inherit;font-weight:600;font-size:12.5px;cursor:pointer}
.btn-success:hover{background:#19764C}
.btn-secondary{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--line);background:#fff;border-radius:8px;padding:8px 13px;font:inherit;font-weight:600;font-size:12.5px;color:var(--ink);cursor:pointer}
.btn-secondary:hover{background:#F5F8FC}
.btn-secondary:disabled,.btn-primary:disabled,.btn-success:disabled{opacity:.5;cursor:not-allowed}
.btn-sm{padding:6px 11px;font-size:12px}
.btn-outline{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--c-blue);background:#fff;border-radius:8px;padding:7px 12px;font:inherit;font-weight:600;font-size:12.5px;color:var(--c-blue);cursor:pointer}
.btn-outline:hover{background:#EEF4FB}
/* risultati: gauge (renderizzato da common.renderScoreGauge) */
.score-gauge{position:relative;width:200px;height:200px;margin:0 auto}
.score-gauge svg{transform:rotate(-90deg)}
.score-gauge-circle-bg{fill:none;stroke:#EAF0F6;stroke-width:12}
.score-gauge-circle{fill:none;stroke-width:12;stroke-linecap:round;stroke:var(--score-color,var(--c-blue));transition:stroke-dashoffset 1s ease,stroke .5s ease}
.score-gauge-value{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}
.score-gauge-number{font-size:2.6rem;font-weight:800;color:var(--ink);line-height:1}
.score-gauge-label{font-size:.72rem;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-top:4px}
.score-critical{--score-color:var(--c-red)}.score-low{--score-color:#f97316}.score-medium{--score-color:var(--c-amber)}.score-good{--score-color:#84cc16}.score-excellent{--score-color:var(--c-green)}
/* risultati: punteggio per categoria */
.category-score{margin-bottom:14px}
.category-score:last-child{margin-bottom:0}
.category-score-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}
.category-score-name{font-size:13px;font-weight:600;color:var(--ink)}
.category-score-value{font-size:13px;font-weight:800}
.progress{background:#EAF0F6;border-radius:99px;height:8px;overflow:hidden}
.progress-bar{height:100%;border-radius:99px;background:var(--c-blue);transition:width .4s ease}
.progress-bar.success{background:var(--c-green)}.progress-bar.warning{background:var(--c-amber)}.progress-bar.danger{background:var(--c-red)}
/* spinner inline (riusato dal JS) */
.spinner-lg{width:36px;height:36px;border:3px solid var(--line);border-top-color:var(--c-blue);border-radius:50%;animation:v3spin .7s linear infinite}
.spinner{width:16px;height:16px;border:2px solid rgba(255,255,255,.4);border-top-color:#fff;border-radius:50%;animation:v3spin .7s linear infinite;display:inline-block;vertical-align:middle}
.mb-24{margin-bottom:24px}
.text-muted{color:var(--muted)}
.ai-block h4{margin:0 0 8px;font-size:14px;font-weight:700}
.ai-block ul,.ai-block ol{margin:0;padding-left:20px;color:var(--muted)}
@media (max-width:900px){.ga-kpis{grid-template-columns:1fr}}
@media (prefers-reduced-motion:reduce){.spinner-lg,.spinner{animation:none}}
</style>
</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="assessment.title">Gap Analysis NIS2</h1> <i data-lucide="chevron-right" class="ic"></i><span data-i18n="assessment.title">Gap Analysis NIS2</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="assessment.title">Gap Analysis NIS2</h1>
<p class="sub">Scostamento tra lo stato attuale e i requisiti della Direttiva (UE) 2022/2555</p>
</div>
<div class="content-header-actions"> <div class="content-header-actions">
<button class="btn btn-outline btn-sm" id="btn-ai-analyze" style="display:none;" onclick="aiAnalyze()"> <button class="btn-outline" id="btn-ai-analyze" style="display:none;" onclick="aiAnalyze()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><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> <i data-lucide="sparkles" class="ic"></i> Analisi AI
Analisi AI
</button> </button>
</div> </div>
</header> </div>
<div class="ga-intro">
<i data-lucide="info" class="ic"></i>
<div>L'assessment confronta il livello di conformità <b>effettivo</b> della tua organizzazione con quello <b>atteso</b> dai requisiti NIS2, evidenziando i gap su cui intervenire. I gap rilevati possono essere convertiti automaticamente in non conformità (NCR).</div>
</div>
<!-- KPI sintetici (dati reali, popolati dal JS) -->
<div class="ga-kpis" id="ga-kpis" style="display:none;"></div>
<div class="content-body">
<!-- Assessment Selection / Create --> <!-- Assessment Selection / Create -->
<div id="assessment-start" class="card mb-24"> <section class="card mb-24" id="assessment-start">
<div class="card-body text-center" style="padding:48px;"> <div class="center-card">
<h3 style="font-size:1.25rem; color:var(--gray-900); margin-bottom:8px;">Assessment di Conformita' NIS2</h3> <h3>Assessment di Conformità NIS2</h3>
<p class="text-muted mb-24">Valuta il livello di conformita' della tua organizzazione rispetto ai requisiti della direttiva NIS2.</p> <p>Valuta il livello di conformità della tua organizzazione rispetto ai requisiti della direttiva NIS2.</p>
<div id="existing-assessments" class="mb-24" style="display:none;"></div> <div id="existing-assessments" class="mb-24" style="display:none; text-align:left;"></div>
<button class="btn btn-primary btn-lg" id="btn-new-assessment" onclick="createNewAssessment()"> <button class="btn-primary btn-lg" id="btn-new-assessment" onclick="createNewAssessment()">
<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> <i data-lucide="plus" class="ic"></i> Nuovo Assessment
Nuovo Assessment
</button> </button>
</div> </div>
</div> </section>
<!-- Wizard Container --> <!-- Wizard Container -->
<div id="assessment-wizard" style="display:none;"> <div id="assessment-wizard" style="display:none;">
@@ -62,118 +183,110 @@
<div class="steps" id="steps-indicator"></div> <div class="steps" id="steps-indicator"></div>
<!-- Progress Bar --> <!-- Progress Bar -->
<div style="margin-bottom:16px;"> <div class="ga-prog-wrap">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;"> <div class="ga-prog-top">
<span style="font-size:0.78rem; font-weight:600; color:var(--gray-600);">Avanzamento risposta domande</span> <span class="ga-prog-lab">Avanzamento risposta domande</span>
<span style="font-size:0.78rem; font-weight:700; color:var(--primary);" id="progress-pct">0%</span> <span class="ga-prog-pct" id="progress-pct">0%</span>
</div> </div>
<div style="background:var(--gray-200); border-radius:99px; height:8px; overflow:hidden;"> <div class="ga-prog-track">
<div id="progress-bar-fill" style="height:100%; background:var(--primary); border-radius:99px; transition:width 0.3s ease; width:0%;"></div> <div id="progress-bar-fill" class="ga-prog-fill"></div>
</div> </div>
<div style="font-size:0.7rem; color:var(--gray-400); margin-top:4px;"> <div class="ga-prog-sub">
<span id="progress-answered">0</span> di <span id="progress-total">0</span> domande con risposta <span id="progress-answered">0</span> di <span id="progress-total">0</span> domande con risposta
</div> </div>
</div> </div>
<!-- Question Card --> <!-- Question Card -->
<div class="card mb-24"> <section class="card mb-24">
<div class="card-header"> <div class="qcard-head">
<h3 id="category-title">Caricamento...</h3> <h3 id="category-title">Caricamento...</h3>
<span class="badge badge-neutral" id="question-counter">0/0</span> <span class="badge badge-neutral" id="question-counter">0/0</span>
</div> </div>
<div class="card-body" id="question-area"> <div class="card-pad" id="question-area">
<div class="spinner-lg" style="margin:40px auto;"></div> <div class="spinner-lg" style="margin:40px auto;"></div>
</div> </div>
<div class="card-footer"> <div class="qcard-foot">
<div style="display:flex; justify-content:space-between; align-items:center;"> <button class="btn-secondary" id="btn-prev" onclick="prevQuestion()" disabled>
<button class="btn btn-secondary" id="btn-prev" onclick="prevQuestion()" disabled> <i data-lucide="chevron-left" class="ic"></i> Precedente
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
Precedente
</button> </button>
<button class="btn btn-ghost btn-sm" onclick="saveCurrentResponse()"> <button class="btn-ghost btn-sm" onclick="saveCurrentResponse()">
Salva Progresso Salva Progresso
</button> </button>
<button class="btn btn-primary" id="btn-next" onclick="nextQuestion()"> <button class="btn-primary" id="btn-next" onclick="nextQuestion()">
Successiva Successiva <i data-lucide="chevron-right" class="ic"></i>
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
</button> </button>
</div> </div>
</div> </section>
</div>
</div> </div>
<!-- Results Container --> <!-- Results Container -->
<div id="assessment-results" style="display:none;"> <div id="assessment-results" style="display:none;">
<div class="card mb-24"> <section class="card mb-24">
<div class="card-header"> <div class="qcard-head">
<h3>Risultati Assessment</h3> <h3>Risultati Assessment</h3>
<button class="btn btn-outline btn-sm" onclick="aiAnalyze()"> <button class="btn-outline btn-sm" onclick="aiAnalyze()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><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> <i data-lucide="sparkles" class="ic"></i> Analisi AI
Analisi AI
</button> </button>
</div> </div>
<div class="card-body"> <div class="card-pad">
<!-- Overall Score --> <!-- Overall Score -->
<div class="text-center mb-32" id="results-gauge"></div> <div style="text-align:center;" class="mb-24" id="results-gauge"></div>
<!-- Category Scores --> <!-- Category Scores -->
<h4 style="margin-bottom:16px; color:var(--gray-900);">Punteggio per Categoria</h4> <h4 style="margin:0 0 16px; font-size:15px; font-weight:700;">Punteggio per Categoria</h4>
<div id="category-scores"></div> <div id="category-scores"></div>
</div> </div>
</div> </section>
<!-- AI Analysis result --> <!-- AI Analysis result -->
<div class="card mb-24" id="ai-analysis-card" style="display:none;"> <section class="card mb-24" id="ai-analysis-card" style="display:none;">
<div class="card-header"> <div class="qcard-head">
<h3>Analisi AI</h3> <h3>Analisi AI</h3>
<span class="badge badge-info">Intelligenza Artificiale</span> <span class="badge badge-info">Intelligenza Artificiale</span>
</div> </div>
<div class="card-body" id="ai-analysis-content"></div> <div class="card-pad" id="ai-analysis-content"></div>
</div> </section>
<!-- Generate NCR from gaps --> <!-- Generate NCR from gaps -->
<div class="card mb-24" id="ncr-generation-card"> <section class="card mb-24" id="ncr-generation-card">
<div class="card-header"> <div class="qcard-head">
<h3>Non Conformita'</h3> <h3>Non Conformità</h3>
</div> </div>
<div class="card-body" style="text-align:center; padding:32px;"> <div class="center-card">
<p style="color:var(--gray-600); margin-bottom:16px; font-size:0.875rem;"> <p>
Genera automaticamente le non conformita' (NCR) dai gap identificati nell'assessment. Genera automaticamente le non conformità (NCR) dai gap identificati nell'assessment.
I controlli con stato "Non Implementato" saranno classificati come <strong>major</strong>, I controlli con stato "Non Implementato" saranno classificati come <strong>major</strong>,
quelli "Parzialmente Implementato" come <strong>minor</strong>. quelli "Parzialmente Implementato" come <strong>minor</strong>.
</p> </p>
<button class="btn btn-primary" id="btn-generate-ncr" onclick="generateNCRs()"> <button class="btn-primary" id="btn-generate-ncr" onclick="generateNCRs()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm5 6a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V8z" clip-rule="evenodd"/></svg> <i data-lucide="file-plus" class="ic"></i> Genera Non Conformità dai Gap
Genera Non Conformita' dai Gap
</button> </button>
<div id="ncr-generation-result" style="display:none; margin-top:16px;"></div> <div id="ncr-generation-result" style="display:none; margin-top:16px; text-align:left;"></div>
</div>
</div>
</div> </div>
</section>
</div> </div>
</main> </main>
</div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help. La logica
pagina (wizard GAP) resta inline: usa gli stessi id/funzioni di prima. -->
<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> <script>
// ── Auth check ─────────────────────────────────────────── // ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init(); // ── Init chrome + i18n + help ────────────────────────────
HelpSystem.init(); loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
function refreshIcons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
// ── State ──────────────────────────────────────────────── // ── State ────────────────────────────────────────────────
let currentAssessmentId = null; let currentAssessmentId = null;
@@ -183,7 +296,7 @@
let currentQuestionIdx = 0; let currentQuestionIdx = 0;
let responses = {}; // questionId -> { answer, maturity, notes, evidence } let responses = {}; // questionId -> { answer, maturity, notes, evidence }
// ── Init ───────────────────────────────────────────────── // ── Init (dati reali) ────────────────────────────────────
loadAssessments(); loadAssessments();
async function loadAssessments() { async function loadAssessments() {
@@ -193,8 +306,8 @@
const container = document.getElementById('existing-assessments'); const container = document.getElementById('existing-assessments');
container.style.display = 'block'; container.style.display = 'block';
let html = '<h4 style="margin-bottom:12px; color:var(--gray-700);">Assessment Esistenti</h4>'; let html = '<h4 style="margin:0 0 12px; font-size:13px; font-weight:700; color:#7a8aa0; text-transform:uppercase; letter-spacing:.04em;">Assessment Esistenti</h4>';
html += '<div class="table-container"><table><thead><tr><th>Data</th><th>Stato</th><th>Punteggio</th><th>Azioni</th></tr></thead><tbody>'; html += '<div class="card card-pad" style="padding:6px 14px;"><table class="v3-table"><thead><tr><th>Data</th><th>Stato</th><th>Punteggio</th><th>Azioni</th></tr></thead><tbody>';
result.data.forEach(a => { result.data.forEach(a => {
const statusBadge = a.status === 'completed' const statusBadge = a.status === 'completed'
@@ -204,11 +317,11 @@
<tr> <tr>
<td>${formatDate(a.created_at)}</td> <td>${formatDate(a.created_at)}</td>
<td>${statusBadge}</td> <td>${statusBadge}</td>
<td>${a.overall_score != null ? Math.round(a.overall_score) + '%' : '-'}</td> <td>${a.overall_score != null ? Math.round(a.overall_score) + '%' : '—'}</td>
<td> <td>
${a.status === 'completed' ${a.status === 'completed'
? `<button class="btn btn-sm btn-ghost" onclick="viewResults('${a.id}')">Risultati</button>` ? `<button class="btn-ghost btn-sm" onclick="viewResults('${a.id}')">Risultati</button>`
: `<button class="btn btn-sm btn-primary" onclick="resumeAssessment('${a.id}')">Riprendi</button>` : `<button class="btn-primary btn-sm" onclick="resumeAssessment('${a.id}')">Riprendi</button>`
} }
</td> </td>
</tr>`; </tr>`;
@@ -216,12 +329,38 @@
html += '</tbody></table></div>'; html += '</tbody></table></div>';
container.innerHTML = html; container.innerHTML = html;
refreshIcons();
// KPI sintetici (dati reali)
renderKpis(result.data);
} }
} catch (e) { } catch (e) {
// Silenzioso // Silenzioso
} }
} }
// ── KPI sintetici dai dati reali assessment ──────────────
function renderKpis(list) {
const wrap = document.getElementById('ga-kpis');
if (!wrap || !Array.isArray(list) || !list.length) return;
const total = list.length;
const completed = list.filter(a => a.status === 'completed').length;
const inProgress = total - completed;
const scored = list.filter(a => a.overall_score != null);
const avg = scored.length ? Math.round(scored.reduce((s, a) => s + Number(a.overall_score || 0), 0) / scored.length) : null;
const cards = [
['blue', 'clipboard-list', 'Assessment', total, 'totali'],
['amber', 'loader', 'In corso', inProgress, completed + ' completati'],
['green', 'target', 'Conformità media', avg != null ? (avg + '%') : '—', scored.length ? ('su ' + scored.length + ' completati') : 'nessun punteggio']
];
wrap.innerHTML = cards.map(c =>
'<div class="card card-pad stat ' + c[0] + '"><div class="ibox"><i data-lucide="' + c[1] + '" class="ic"></i></div>' +
'<div style="flex:1;min-width:0"><div class="lbl">' + c[2] + '</div><div class="val">' + c[3] + '</div><div class="sub">' + c[4] + '</div></div></div>'
).join('');
wrap.style.display = 'grid';
refreshIcons();
}
async function createNewAssessment() { async function createNewAssessment() {
const btn = document.getElementById('btn-new-assessment'); const btn = document.getElementById('btn-new-assessment');
btn.disabled = true; btn.disabled = true;
@@ -239,7 +378,8 @@
showNotification('Errore di connessione.', 'error'); showNotification('Errore di connessione.', 'error');
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.innerHTML = `<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`; btn.innerHTML = `<i data-lucide="plus" class="ic"></i> Nuovo Assessment`;
refreshIcons();
} }
} }
@@ -374,16 +514,14 @@
let html = ` let html = `
<div style="margin-bottom:20px;"> <div style="margin-bottom:20px;">
<p style="font-size:1rem; font-weight:600; color:var(--gray-900); margin-bottom:4px;"> <p class="q-text">${escapeHtml(q.question_text || q.text || q.title || '')}</p>
${escapeHtml(q.question_text || q.text || q.title || '')} ${q.guidance_it ? `<p class="q-guidance">${escapeHtml(q.guidance_it)}</p>` : ''}
</p> ${q.nis2_article ? `<span class="q-tag">Art. ${escapeHtml(q.nis2_article)}</span>` : ''}
${q.guidance_it ? `<p class="text-muted" style="font-size:0.8125rem;">${escapeHtml(q.guidance_it)}</p>` : ''} ${q.iso27001_control ? `<span class="q-tag">ISO ${escapeHtml(q.iso27001_control)}</span>` : ''}
${q.nis2_article ? `<span class="tag mt-8">Art. ${escapeHtml(q.nis2_article)}</span>` : ''}
${q.iso27001_control ? `<span class="tag mt-8" style="margin-left:4px;">ISO ${escapeHtml(q.iso27001_control)}</span>` : ''}
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Livello di Conformita'</label> <label class="form-label">Livello di Conformità</label>
<div class="form-radio-group"> <div class="form-radio-group">
${answers.map(a => ` ${answers.map(a => `
<label class="form-radio ${r.answer === a.value ? 'selected' : ''}"> <label class="form-radio ${r.answer === a.value ? 'selected' : ''}">
@@ -396,7 +534,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Livello di Maturita' (0-5)</label> <label class="form-label">Livello di Maturità (0-5)</label>
<input type="range" class="form-range" min="0" max="5" step="1" <input type="range" class="form-range" min="0" max="5" step="1"
value="${r.maturity != null ? r.maturity : 0}" id="maturity-slider" value="${r.maturity != null ? r.maturity : 0}" id="maturity-slider"
oninput="document.getElementById('maturity-value').textContent = this.value"> oninput="document.getElementById('maturity-value').textContent = this.value">
@@ -413,7 +551,7 @@
placeholder="Aggiungi eventuali note o osservazioni...">${escapeHtml(r.notes || '')}</textarea> placeholder="Aggiungi eventuali note o osservazioni...">${escapeHtml(r.notes || '')}</textarea>
</div> </div>
<div class="form-group"> <div class="form-group" style="margin-bottom:0;">
<label class="form-label">Descrizione Evidenza</label> <label class="form-label">Descrizione Evidenza</label>
<input type="text" class="form-input" id="question-evidence" <input type="text" class="form-input" id="question-evidence"
placeholder="Documenti, procedure, strumenti a supporto..." placeholder="Documenti, procedure, strumenti a supporto..."
@@ -430,12 +568,13 @@
const isLast = currentCategoryIdx === categories.length - 1 && currentQuestionIdx === cat.questions.length - 1; const isLast = currentCategoryIdx === categories.length - 1 && currentQuestionIdx === cat.questions.length - 1;
const nextBtn = document.getElementById('btn-next'); const nextBtn = document.getElementById('btn-next');
if (isLast) { if (isLast) {
nextBtn.innerHTML = 'Completa Assessment'; nextBtn.innerHTML = '<i data-lucide="check" class="ic"></i> Completa Assessment';
nextBtn.className = 'btn btn-success'; nextBtn.className = 'btn-success';
} else { } else {
nextBtn.innerHTML = 'Successiva <svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>'; nextBtn.innerHTML = 'Successiva <i data-lucide="chevron-right" class="ic"></i>';
nextBtn.className = 'btn btn-primary'; nextBtn.className = 'btn-primary';
} }
refreshIcons();
} }
function onAnswerChange(radio) { function onAnswerChange(radio) {
@@ -477,7 +616,7 @@
const r = responses[q.id]; const r = responses[q.id];
if (!r || !r.answer) { if (!r || !r.answer) {
showNotification('Seleziona un livello di conformita\' prima di salvare.', 'warning'); showNotification('Seleziona un livello di conformità prima di salvare.', 'warning');
return; return;
} }
@@ -570,6 +709,7 @@
document.getElementById('assessment-wizard').style.display = 'none'; document.getElementById('assessment-wizard').style.display = 'none';
document.getElementById('assessment-results').style.display = 'block'; document.getElementById('assessment-results').style.display = 'block';
document.getElementById('btn-ai-analyze').style.display = ''; document.getElementById('btn-ai-analyze').style.display = '';
refreshIcons();
try { try {
const result = await api.getAssessmentReport(currentAssessmentId); const result = await api.getAssessmentReport(currentAssessmentId);
@@ -637,29 +777,29 @@
let html = ''; let html = '';
if (analysis.summary) { if (analysis.summary) {
html += `<div style="margin-bottom:20px;"><h4 style="margin-bottom:8px;">Riepilogo</h4><p style="color:var(--gray-600); line-height:1.7;">${escapeHtml(analysis.summary)}</p></div>`; html += `<div class="ai-block" style="margin-bottom:20px;"><h4>Riepilogo</h4><p style="color:var(--muted); line-height:1.7;">${escapeHtml(analysis.summary)}</p></div>`;
} }
if (analysis.strengths && analysis.strengths.length > 0) { if (analysis.strengths && analysis.strengths.length > 0) {
html += `<div style="margin-bottom:20px;"><h4 style="margin-bottom:8px; color:var(--secondary);">Punti di Forza</h4><ul style="padding-left:20px; color:var(--gray-600);">`; html += `<div class="ai-block" style="margin-bottom:20px;"><h4 style="color:var(--c-green);">Punti di Forza</h4><ul>`;
analysis.strengths.forEach(s => { html += `<li style="margin-bottom:4px;">${escapeHtml(s)}</li>`; }); analysis.strengths.forEach(s => { html += `<li style="margin-bottom:4px;">${escapeHtml(s)}</li>`; });
html += '</ul></div>'; html += '</ul></div>';
} }
if (analysis.weaknesses && analysis.weaknesses.length > 0) { if (analysis.weaknesses && analysis.weaknesses.length > 0) {
html += `<div style="margin-bottom:20px;"><h4 style="margin-bottom:8px; color:var(--danger);">Aree di Miglioramento</h4><ul style="padding-left:20px; color:var(--gray-600);">`; html += `<div class="ai-block" style="margin-bottom:20px;"><h4 style="color:var(--c-red);">Aree di Miglioramento</h4><ul>`;
analysis.weaknesses.forEach(w => { html += `<li style="margin-bottom:4px;">${escapeHtml(w)}</li>`; }); analysis.weaknesses.forEach(w => { html += `<li style="margin-bottom:4px;">${escapeHtml(w)}</li>`; });
html += '</ul></div>'; html += '</ul></div>';
} }
if (analysis.recommendations && analysis.recommendations.length > 0) { if (analysis.recommendations && analysis.recommendations.length > 0) {
html += `<div><h4 style="margin-bottom:8px; color:var(--primary);">Raccomandazioni</h4><ol style="padding-left:20px; color:var(--gray-600);">`; html += `<div class="ai-block"><h4 style="color:var(--c-blue);">Raccomandazioni</h4><ol>`;
analysis.recommendations.forEach(r => { html += `<li style="margin-bottom:8px;">${escapeHtml(r)}</li>`; }); analysis.recommendations.forEach(r => { html += `<li style="margin-bottom:8px;">${escapeHtml(r)}</li>`; });
html += '</ol></div>'; html += '</ol></div>';
} }
if (!html) { if (!html) {
html = `<p style="color:var(--gray-600); line-height:1.7;">${escapeHtml(analysis.text || analysis.message || JSON.stringify(analysis))}</p>`; html = `<p style="color:var(--muted); line-height:1.7;">${escapeHtml(analysis.text || analysis.message || JSON.stringify(analysis))}</p>`;
} }
content.innerHTML = html; content.innerHTML = html;
@@ -681,7 +821,7 @@
const btn = document.getElementById('btn-generate-ncr'); const btn = document.getElementById('btn-generate-ncr');
btn.disabled = true; btn.disabled = true;
btn.innerHTML = '<div class="spinner" style="width:16px;height:16px;border-width:2px;margin:0;"></div> Generazione in corso...'; btn.innerHTML = '<span class="spinner"></span> Generazione in corso...';
try { try {
const result = await api.createNCRsFromAssessment(currentAssessmentId); const result = await api.createNCRsFromAssessment(currentAssessmentId);
@@ -694,29 +834,31 @@
const majorCount = (result.data.created || []).filter(n => n.severity === 'major').length; const majorCount = (result.data.created || []).filter(n => n.severity === 'major').length;
const minorCount = total - majorCount; const minorCount = total - majorCount;
resultDiv.innerHTML = ` resultDiv.innerHTML = `
<div style="background:var(--secondary-bg); border:1px solid #bbf7d0; border-radius:var(--border-radius); padding:16px; text-align:left;"> <div style="background:#E6F4EC; border:1px solid #bbf7d0; border-radius:10px; padding:16px;">
<strong style="color:#15803d;">${total} non conformita' generate con successo</strong> <strong style="color:#15803d;">${total} non conformità generate con successo</strong>
<div style="margin-top:8px; font-size:0.8125rem; color:var(--gray-600);"> <div style="margin-top:8px; font-size:12.5px; color:var(--muted);">
${majorCount > 0 ? `<span style="color:var(--danger); font-weight:600;">${majorCount} major</span> ` : ''} ${majorCount > 0 ? `<span style="color:var(--c-red); font-weight:600;">${majorCount} major</span> ` : ''}
${minorCount > 0 ? `<span style="color:var(--warning); font-weight:600;">${minorCount} minor</span>` : ''} ${minorCount > 0 ? `<span style="color:var(--c-amber); font-weight:600;">${minorCount} minor</span>` : ''}
</div> </div>
</div>`; </div>`;
showNotification(`${total} non conformita' generate.`, 'success'); showNotification(`${total} non conformità generate.`, 'success');
} else { } else {
resultDiv.innerHTML = '<p style="color:var(--gray-500);">Nessun gap trovato — tutti i controlli sono implementati.</p>'; resultDiv.innerHTML = '<p class="text-muted">Nessun gap trovato — tutti i controlli sono implementati.</p>';
showNotification('Nessun gap trovato.', 'info'); showNotification('Nessun gap trovato.', 'info');
} }
btn.style.display = 'none'; btn.style.display = 'none';
} else { } else {
resultDiv.innerHTML = `<p style="color:var(--danger);">${escapeHtml(result.message || 'Errore nella generazione.')}</p>`; resultDiv.innerHTML = `<p style="color:var(--c-red);">${escapeHtml(result.message || 'Errore nella generazione.')}</p>`;
showNotification(result.message || 'Errore.', 'error'); showNotification(result.message || 'Errore.', 'error');
btn.disabled = false; btn.disabled = false;
btn.innerHTML = '<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm5 6a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V8z" clip-rule="evenodd"/></svg> Genera Non Conformita\' dai Gap'; btn.innerHTML = '<i data-lucide="file-plus" class="ic"></i> Genera Non Conformità dai Gap';
refreshIcons();
} }
} catch (e) { } catch (e) {
showNotification('Errore di connessione.', 'error'); showNotification('Errore di connessione.', 'error');
btn.disabled = false; btn.disabled = false;
btn.innerHTML = '<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm5 6a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V8z" clip-rule="evenodd"/></svg> Genera Non Conformita\' dai Gap'; btn.innerHTML = '<i data-lucide="file-plus" class="ic"></i> Genera Non Conformità dai Gap';
refreshIcons();
} }
} }
</script> </script>
+31 -31
View File
@@ -3,12 +3,12 @@
<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>Inventario - NIS2 Agile</title> <title>Inventario · 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 (self-host only, nessun CDN esterno). -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (tabelle/modali/form) -> 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"> <!-- style.css resta caricato DOPO v3.css: veste i widget renderizzati via JS
<link rel="stylesheet" href="/css/style.css?v=20260618"> (tabelle/modali/form/badge). È self-hostato (nessun CDN). v3.css governa chrome + shell. -->
<style> <style>
/* ── Stats Cards ────────────────────────────────────────── */ /* ── Stats Cards ────────────────────────────────────────── */
.asset-stats { .asset-stats {
@@ -280,20 +280,26 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2" data-i18n="assets.title">Inventario</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span data-i18n="assets.title">Inventario</span>
<span class="text-muted" style="font-size:0.8rem; margin-right:8px;">Art. 21.2.i NIS2</span> </nav>
<button class="btn btn-secondary" onclick="showDependencyMap()" title="Mappa delle dipendenze tra i beni">Mappa Dipendenze</button> <div class="page-head">
<button class="btn btn-secondary" onclick="openImportModal()" title="Importa beni da CSV/CMDB">Importa</button> <div>
<button class="btn btn-secondary" onclick="exportAssets()" title="Esporta l'inventario in CSV (stesso formato dell'importazione)">Esporta</button> <h1 data-i18n="assets.title">Inventario</h1>
<button class="btn btn-primary" onclick="showCreateAssetModal()">+ Nuovo bene</button> <p class="sub">Beni, perimetro tecnico e rilevanza NIS2 · Art. 21.2.i NIS2</p>
</div>
<div class="content-header-actions" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<button class="btn-ghost" onclick="showDependencyMap()" title="Mappa delle dipendenze tra i beni"><i data-lucide="git-fork" class="ic"></i>Mappa Dipendenze</button>
<button class="btn-ghost" onclick="openImportModal()" title="Importa beni da CSV/CMDB"><i data-lucide="upload" class="ic"></i>Importa</button>
<button class="btn-ghost" onclick="exportAssets()" title="Esporta l'inventario in CSV (stesso formato dell'importazione)"><i data-lucide="download" class="ic"></i>Esporta</button>
<button class="btn-primary" onclick="showCreateAssetModal()"><i data-lucide="plus" class="ic"></i>Nuovo bene</button>
</div>
</div> </div>
</header>
<div class="content-body"> <div class="content-body">
<!-- Stats Cards --> <!-- Stats Cards -->
@@ -368,28 +374,22 @@
</div> </div>
</div> </div>
</main> </main>
</div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → pagina -->
<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> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
I18n.init(); I18n.init();
HelpSystem.init(); HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
loadSubclasses(); // C4 — voci+sottoclassi per i selettori (fire-and-forget, pronto al primo "Nuovo") loadSubclasses(); // C4 — voci+sottoclassi per i selettori (fire-and-forget, pronto al primo "Nuovo")
// ── Labels ────────────────────────────────────────────── // ── Labels ──────────────────────────────────────────────
+192 -153
View File
@@ -3,65 +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>Calendario scadenze - NIS2 Agile</title> <title>Calendario · NIS2 Agile</title>
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <!-- UI V3: Inter self-host + design system v3.css. Self-host only: no external font/script hosts. -->
<link rel="stylesheet" href="/css/style.css?v=20260618"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<style> <link rel="stylesheet" href="/css/v3.css?v=20260627">
.cal-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
.cal-note { font-size:.82rem; color:#3730a3; background:#eef2ff; border:1px solid #c7d2fe; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
.cal-bar { display:flex; flex-wrap:wrap; gap:12px; align-items:center; justify-content:space-between; margin-bottom:14px; }
.cal-nav { display:flex; align-items:center; gap:8px; }
.cal-nav h3 { margin:0; font-size:1.05rem; min-width:170px; text-align:center; text-transform:capitalize; }
.cal-views { display:flex; gap:6px; }
.cal-view-btn { padding:7px 14px; font-weight:600; font-size:.85rem; cursor:pointer; border:1px solid var(--gray-200,#e5e7eb); background:#fff; color:var(--gray-600,#4b5563); border-radius:8px; }
.cal-view-btn.active { background:var(--primary,#2563eb); color:#fff; border-color:var(--primary,#2563eb); }
.cal-summary { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:14px; }
.cal-kpi { flex:1 1 0; min-width:120px; background:#fff; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:10px 14px; }
.cal-kpi .n { font-size:1.5rem; font-weight:800; line-height:1; }
.cal-kpi .l { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); margin-top:4px; }
.cal-kpi.k-overdue .n { color:#b91c1c; } .cal-kpi.k-due_soon .n { color:#b45309; }
.cal-kpi.k-upcoming .n { color:#1d4ed8; } .cal-kpi.k-done .n { color:#15803d; }
.cal-filters { background:#f8fafc; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:12px 14px; margin-bottom:14px; }
.cal-filters h4 { margin:0 0 8px; font-size:.78rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
.cal-chips { display:flex; flex-wrap:wrap; gap:8px; }
.cal-chip { display:inline-flex; align-items:center; gap:6px; font-size:.82rem; padding:5px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:20px; background:#fff; cursor:pointer; user-select:none; }
.cal-chip input { margin:0; cursor:pointer; }
.cal-chip .dot { width:10px; height:10px; border-radius:50%; display:inline-block; }
.cal-filter-row { margin-top:10px; }
table.cal-grid { width:100%; border-collapse:collapse; table-layout:fixed; }
table.cal-grid th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); padding:6px 4px; text-align:center; }
table.cal-grid td { border:1px solid var(--gray-100,#f3f4f6); vertical-align:top; height:96px; padding:4px; background:#fff; }
table.cal-grid td.cal-out { background:#fafafa; }
table.cal-grid td.cal-today { background:#fffbeb; outline:2px solid #fbbf24; outline-offset:-2px; }
.cal-daynum { font-size:.78rem; font-weight:600; color:var(--gray-600,#4b5563); }
.cal-day-dots { display:flex; flex-wrap:wrap; gap:3px; margin-top:4px; }
.cal-dot { width:9px; height:9px; border-radius:50%; cursor:pointer; }
.cal-more { font-size:.7rem; color:var(--gray-500,#6b7280); cursor:pointer; margin-top:3px; }
.st-overdue { background:#dc2626; } .st-due_soon { background:#d97706; }
.st-upcoming { background:#2563eb; } .st-done { background:#16a34a; }
.cal-list { width:100%; border-collapse:collapse; font-size:.88rem; }
.cal-list th, .cal-list td { text-align:left; padding:9px 12px; border-top:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
.cal-list th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
.cal-list tr.row-overdue td { background:#fef2f2; }
.cal-list a.cal-link { color:var(--primary,#2563eb); text-decoration:none; font-weight:600; }
.cal-list a.cal-link:hover { text-decoration:underline; }
.st-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; white-space:nowrap; color:#fff; }
.st-badge.st-overdue { background:#dc2626; } .st-badge.st-due_soon { background:#d97706; }
.st-badge.st-upcoming { background:#2563eb; } .st-badge.st-done { background:#16a34a; }
.cal-empty { text-align:center; padding:22px 16px; color:var(--gray-500,#6b7280); font-size:.86rem; }
.hidden { display:none !important; }
.type-tag { display:inline-block; font-size:.7rem; padding:2px 7px; border-radius:6px; background:#eef2ff; color:#3730a3; }
/* popover giorno */
.cal-pop { position:fixed; inset:0; background:rgba(15,23,42,.4); z-index:1050; display:none; align-items:flex-start; justify-content:center; padding:40px 16px; overflow:auto; }
.cal-pop.open { display:flex; }
.cal-pop-card { background:#fff; border-radius:12px; width:100%; max-width:520px; box-shadow:0 20px 50px rgba(0,0,0,.25); }
.cal-pop-head { display:flex; justify-content:space-between; align-items:center; padding:14px 18px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
.cal-pop-head h3 { margin:0; font-size:1rem; }
.cal-pop-body { padding:14px 18px; }
.cal-pop-item { display:flex; gap:8px; align-items:flex-start; padding:8px 0; border-top:1px solid var(--gray-100,#f3f4f6); }
.cal-pop-item:first-child { border-top:none; }
.cal-pop-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); }
</style>
<!-- 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">
@@ -75,94 +20,189 @@
<meta name="application-name" content="NIS2 Agile"> <meta name="application-name" content="NIS2 Agile">
<script src="/js/pwa.js?v=20260614" defer></script> <script src="/js/pwa.js?v=20260614" defer></script>
<!-- PWA:end --> <!-- PWA:end -->
<style>
/* ── Calendario V3 — shell app full-page (fonte: docs/mockup Ui/calendario.html).
Solo classi NON coperte da v3.css; token condivisi via v3.css. Il calendario reale
è un AGGREGATORE READ-ONLY (api.calendarEvents/calendarSummary): niente create/edit
eventi → dal mockup riusiamo .cal-app/.cal-side/.cal-main/.mv-grid/.view-switch/.legend,
wiring sui dati reali. Le due viste reali sono Griglia (mese) e Lista. */
.cal-app{display:grid;grid-template-columns:250px 1fr;gap:18px;align-items:start}
.cal-side{display:flex;flex-direction:column;gap:16px}
/* KPI sintetici (by_status) */
.cal-kpis{display:grid;gap:10px}
.cal-kpi{background:#fff;border:1px solid var(--line);border-radius:12px;padding:12px 14px;box-shadow:var(--shadow);display:flex;align-items:center;justify-content:space-between;gap:10px}
.cal-kpi .n{font-size:1.5rem;font-weight:800;line-height:1}
.cal-kpi .l{font-size:.74rem;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}
.cal-kpi.k-overdue .n{color:var(--c-red)} .cal-kpi.k-due_soon .n{color:#b45309}
.cal-kpi.k-upcoming .n{color:var(--c-blue)} .cal-kpi.k-done .n{color:#15803d}
/* legenda categorie + filtri */
.legend{background:#fff;border:1px solid var(--line);border-radius:12px;padding:14px;box-shadow:var(--shadow)}
.legend p{margin:0 0 9px;font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#8093A8}
.cal-chips{display:flex;flex-direction:column;gap:4px}
.cal-chip{display:inline-flex;align-items:center;gap:9px;font-size:12.5px;padding:5px 4px;border-radius:7px;cursor:pointer;user-select:none}
.cal-chip:hover{background:#F3F7FC}
.cal-chip input{margin:0;cursor:pointer;accent-color:var(--c-blue);flex:0 0 auto}
.cal-chip .dot{width:11px;height:11px;border-radius:3px;display:inline-block;flex:0 0 auto}
.cal-chip .lbl{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* main */
.cal-main{min-width:0}
.cal-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px;flex-wrap:wrap}
.tb-left{display:flex;align-items:center;gap:10px}
.tb-nav{display:flex;gap:2px}
.tb-title{font-size:19px;font-weight:800;text-transform:capitalize;letter-spacing:-.3px;margin-left:4px}
.view-switch{display:inline-flex;background:#E7EDF4;border-radius:9px;padding:3px}
.view-switch button{border:0;background:transparent;font:inherit;font-weight:600;font-size:12.5px;color:var(--muted);padding:6px 14px;border-radius:7px;cursor:pointer}
.view-switch button.on{background:#fff;color:var(--ink);box-shadow:0 1px 2px rgba(16,40,80,.12)}
/* vista mese (griglia) */
.mv{background:#fff;border:1px solid var(--line);border-radius:12px;overflow:hidden;box-shadow:var(--shadow)}
.mv-dow{display:grid;grid-template-columns:repeat(7,1fr);background:#F6F9FC;border-bottom:1px solid var(--line)}
.mv-dow div{padding:9px 8px;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:#7a8aa0}
.mv-grid{display:grid;grid-template-columns:repeat(7,1fr)}
.mc{min-height:108px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);padding:6px 7px;position:relative;overflow:hidden}
.mc:nth-child(7n){border-right:0}
.mc.has{cursor:pointer}
.mc.has:hover{background:#FAFCFE}
.mc .dnum{font-size:12.5px;font-weight:600;width:25px;height:25px;display:flex;align-items:center;justify-content:center;border-radius:50%;color:var(--ink)}
.mc.other{background:#FAFBFD}.mc.other .dnum{color:#AEBAC8}
.mc.today .dnum{background:var(--c-blue);color:#fff}
.mc .chip{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:600;color:var(--ink);border-radius:5px;padding:2px 6px;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
.mc .chip:hover{background:#F0F4F9}
.mc .chip .cdot{width:8px;height:8px;border-radius:50%;flex:0 0 auto}
.mc .more{font-size:10.5px;color:var(--muted);margin-top:3px;font-weight:600;cursor:pointer}
.mc-empty{grid-column:1 / -1;text-align:center;padding:30px 16px;color:var(--muted);font-size:13px}
/* vista lista */
.cal-list-card{background:#fff;border:1px solid var(--line);border-radius:12px;overflow:hidden;box-shadow:var(--shadow)}
.cal-list{width:100%;border-collapse:collapse;font-size:13px}
.cal-list th,.cal-list td{text-align:left;padding:10px 14px;border-top:1px solid var(--line);vertical-align:middle}
.cal-list thead th{border-top:0;font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:#8093A8;font-weight:700;background:#F6F9FC}
.cal-list tr.row-overdue td{background:#FEF2F2}
.cal-list a.cal-link{color:var(--c-blue);font-weight:600}
.cal-list a.cal-link:hover{text-decoration:underline}
.type-tag{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600;padding:3px 9px;border-radius:6px}
.type-tag .cdot{width:8px;height:8px;border-radius:50%}
.st-badge{display:inline-block;font-size:10px;font-weight:700;padding:3px 9px;border-radius:6px;white-space:nowrap;color:#fff;text-transform:uppercase;letter-spacing:.03em}
.st-badge.st-overdue{background:#dc2626}.st-badge.st-due_soon{background:#d97706}
.st-badge.st-upcoming{background:#2563eb}.st-badge.st-done{background:#16a34a}
.cal-empty{text-align:center;padding:30px 16px;color:var(--muted);font-size:13px}
.hidden{display:none !important}
/* popover giorno (modale) */
.cal-modal{position:fixed;inset:0;background:rgba(13,28,52,.45);display:none;align-items:flex-start;justify-content:center;z-index:90;padding:40px 16px;overflow:auto}
.cal-modal.open{display:flex}
.cal-modal-card{background:#fff;border-radius:14px;width:100%;max-width:520px;box-shadow:0 24px 60px rgba(11,30,60,.35)}
.cal-modal-head{display:flex;align-items:center;justify-content:space-between;padding:15px 18px;border-bottom:1px solid var(--line)}
.cal-modal-head h3{margin:0;font-size:15px;font-weight:700}
.cal-modal-x{border:0;background:transparent;cursor:pointer;color:var(--muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1.4rem;line-height:1}
.cal-modal-x:hover{background:#F0F4F9}
.cal-modal-body{padding:14px 18px}
.cal-pop-item{display:flex;gap:10px;align-items:flex-start;padding:9px 0;border-top:1px solid var(--line)}
.cal-pop-item:first-child{border-top:none}
.cal-pop-item .cdot{width:10px;height:10px;border-radius:50%;margin-top:4px;flex:0 0 auto}
@media (max-width:900px){
.cal-app{grid-template-columns:1fr}
.cal-side{flex-direction:row;flex-wrap:wrap}
.cal-kpis{grid-template-columns:repeat(2,1fr);flex:1;min-width:220px}
.legend{flex:1;min-width:220px}
.mc{min-height:84px}
}
</style>
</head> </head>
<body> <body>
<div class="app-layout"> <!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="cal.title">Calendario scadenze</h2>
</header>
<div class="content-body">
<div class="cal-intro">
<strong>Tutte le scadenze in un solo posto.</strong>
Il calendario aggrega <em>in tempo reale</em> ogni scadenza del sistema — incidenti, revisioni policy e controlli,
trattamenti di rischio, non conformita e azioni correttive, formazione, attivita stakeholder, scadenziario revisioni,
audit interni e decisioni del riesame — con stato e link diretto all'elemento.
</div>
<div class="cal-note">
Ancoraggio: buona prassi di monitoraggio scadenze e revisioni periodiche (<strong>ISO 9.1/9.3</strong>,
NIS2 <strong>GV.PO-02 / GV.SC-07 / DE.CM</strong>). Strumento di supporto operativo, non un parere legale.
</div>
<!-- KPI sintetici --> <main class="wrap">
<div class="cal-summary" id="cal-summary" aria-live="polite"></div> <nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<!-- Barra: navigazione mese + selettore vista --> <i data-lucide="chevron-right" class="ic"></i><span data-i18n="cal.title">Calendario</span>
<div class="cal-bar"> </nav>
<div class="cal-nav"> <div class="page-head">
<button class="btn btn-outline-primary btn-sm" type="button" onclick="calPrevMonth()" aria-label="Mese precedente">&lsaquo;</button> <div>
<h3 id="cal-month-label">—</h3> <h1 data-i18n="cal.title">Calendario scadenze</h1>
<button class="btn btn-outline-primary btn-sm" type="button" onclick="calNextMonth()" aria-label="Mese successivo">&rsaquo;</button> <p class="sub">Vista temporale aggregata di scadenze, revisioni e controlli del sistema</p>
<button class="btn btn-outline-secondary btn-sm" type="button" onclick="calToday()">Oggi</button>
</div>
<div class="cal-views" role="tablist" aria-label="Vista calendario">
<button class="cal-view-btn active" id="view-grid" role="tab" aria-selected="true" aria-controls="pane-grid" onclick="calSetView('grid')">Griglia</button>
<button class="cal-view-btn" id="view-list" role="tab" aria-selected="false" aria-controls="pane-list" onclick="calSetView('list')">Lista</button>
</div> </div>
</div> </div>
<!-- Filtri --> <div class="cal-app">
<div class="cal-filters"> <!-- Sidebar: KPI + legenda/filtri categoria + stato -->
<h4>Filtra per tipo</h4> <aside class="cal-side">
<div class="cal-kpis" id="cal-summary" aria-live="polite"></div>
<div class="legend">
<p>Filtra per tipo</p>
<div class="cal-chips" id="cal-type-chips"></div> <div class="cal-chips" id="cal-type-chips"></div>
<div class="cal-filter-row"> </div>
<h4>Filtra per stato</h4>
<div class="legend">
<p>Filtra per stato</p>
<div class="cal-chips" id="cal-status-chips"></div> <div class="cal-chips" id="cal-status-chips"></div>
</div> </div>
</aside>
<!-- Main: toolbar + viste -->
<section class="cal-main">
<div class="cal-toolbar">
<div class="tb-left">
<button class="ctrl-today" id="tb-today" onclick="calToday()"><i data-lucide="calendar-check" class="ic"></i>Oggi</button>
<div class="tb-nav">
<button class="ctrl-btn" id="tb-prev" title="Mese precedente" aria-label="Mese precedente" onclick="calPrevMonth()"><i data-lucide="chevron-left" class="ic"></i></button>
<button class="ctrl-btn" id="tb-next" title="Mese successivo" aria-label="Mese successivo" onclick="calNextMonth()"><i data-lucide="chevron-right" class="ic"></i></button>
</div>
<span class="tb-title" id="cal-month-label">—</span>
</div>
<div class="view-switch" role="tablist" aria-label="Vista calendario">
<button data-v="grid" class="on" id="view-grid" role="tab" aria-selected="true" aria-controls="pane-grid" onclick="calSetView('grid')">Griglia</button>
<button data-v="list" id="view-list" role="tab" aria-selected="false" aria-controls="pane-list" onclick="calSetView('list')">Lista</button>
</div>
</div> </div>
<!-- Vista griglia mensile --> <!-- Vista griglia mensile -->
<section id="pane-grid" role="tabpanel" aria-labelledby="view-grid" aria-live="polite"> <section id="pane-grid" class="mv" role="tabpanel" aria-labelledby="view-grid" aria-live="polite">
<table class="cal-grid"> <div class="mv-dow"><div>Lun</div><div>Mar</div><div>Mer</div><div>Gio</div><div>Ven</div><div>Sab</div><div>Dom</div></div>
<thead> <div class="mv-grid" id="mv-grid"></div>
<tr id="cal-weekhead"></tr>
</thead>
<tbody id="cal-grid-body"></tbody>
</table>
</section> </section>
<!-- Vista lista --> <!-- Vista lista -->
<section id="pane-list" class="hidden" role="tabpanel" aria-labelledby="view-list" aria-live="polite"> <section id="pane-list" class="hidden" role="tabpanel" aria-labelledby="view-list" aria-live="polite">
<div id="cal-list-wrap"></div> <div class="cal-list-card" id="cal-list-wrap"></div>
</section>
</section> </section>
</div> </div>
</main> </main>
</div>
<!-- Popover eventi di un giorno --> <!-- Popover eventi di un giorno (modale) -->
<div class="cal-pop" id="cal-pop" role="dialog" aria-modal="true" aria-labelledby="cal-pop-title"> <div class="cal-modal" id="cal-pop" role="dialog" aria-modal="true" aria-labelledby="cal-pop-title">
<div class="cal-pop-card"> <div class="cal-modal-card">
<div class="cal-pop-head"> <div class="cal-modal-head">
<h3 id="cal-pop-title">Eventi</h3> <h3 id="cal-pop-title">Eventi</h3>
<button class="cal-pop-close" type="button" aria-label="Chiudi" onclick="calPopClose()">&times;</button> <button class="cal-modal-x" type="button" aria-label="Chiudi" onclick="calPopClose()">&times;</button>
</div> </div>
<div class="cal-pop-body" id="cal-pop-body"></div> <div class="cal-modal-body" id="cal-pop-body"></div>
</div> </div>
</div> </div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help. La logica del
calendario (aggregatore read-only) resta inline e usa gli stessi id/funzioni. -->
<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>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></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> <script>
'use strict'; 'use strict';
/* /*
* NIS2 Agile - Calendario unico delle scadenze (Modulo C) * NIS2 Agile - Calendario unico delle scadenze (Modulo C) — UI V3
* Aggregatore READ-ONLY: griglia mensile + lista + filtri (tipo/stato) + deep-link. * Aggregatore READ-ONLY: griglia mensile + lista + filtri (tipo/stato) + deep-link.
* Dependency-free: nessuna libreria calendario, render in JS puro. * Dati reali via api.calendarEvents / api.calendarSummary. Dependency-free.
* Veste fedele a docs/mockup Ui/calendario.html (.cal-app/.mv-grid/.view-switch/.legend).
*/ */
var CAL = { var CAL = {
view: 'grid', view: 'grid',
@@ -206,12 +246,14 @@
var day = String(d.getDate()).padStart(2, '0'); var day = String(d.getDate()).padStart(2, '0');
return d.getFullYear() + '-' + m + '-' + day; return d.getFullYear() + '-' + m + '-' + day;
} }
function refreshIcons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
document.addEventListener('DOMContentLoaded', async function () { document.addEventListener('DOMContentLoaded', async function () {
if (typeof checkAuth === 'function' && !checkAuth()) return; if (typeof checkAuth === 'function' && !checkAuth()) return;
if (window.I18n && I18n.init) I18n.init('it'); if (typeof loadSidebar === 'function') loadSidebar(); // = loadTopnavV3()
if (typeof loadSidebar === 'function') loadSidebar(); if (window.I18n && I18n.init) I18n.init();
if (window.HelpSystem && HelpSystem.init) HelpSystem.init(); if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
refreshIcons();
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') calPopClose(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') calPopClose(); });
var pop = el('cal-pop'); if (pop) pop.addEventListener('click', function (e) { if (e.target === pop) calPopClose(); }); var pop = el('cal-pop'); if (pop) pop.addEventListener('click', function (e) { if (e.target === pop) calPopClose(); });
CAL.cursor = new Date(); CAL.cursor.setDate(1); CAL.cursor = new Date(); CAL.cursor.setDate(1);
@@ -224,13 +266,13 @@
var from = new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() - 2, 1); var from = new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() - 2, 1);
var to = new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() + 3, 0); var to = new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() + 3, 0);
CAL.rangeFrom = ymd(from); CAL.rangeTo = ymd(to); CAL.rangeFrom = ymd(from); CAL.rangeTo = ymd(to);
el('cal-grid-body').innerHTML = '<tr><td colspan="7" class="cal-empty">Caricamento…</td></tr>'; el('mv-grid').innerHTML = '<div class="mc-empty"><span class="v3-loading"><span class="v3-spin"></span>Caricamento…</span></div>';
try { try {
var data = await api.calendarEvents({ from: CAL.rangeFrom, to: CAL.rangeTo }); var data = await api.calendarEvents({ from: CAL.rangeFrom, to: CAL.rangeTo });
CAL.events = (data && data.events) ? data.events : []; CAL.events = (data && data.events) ? data.events : [];
} catch (e) { } catch (e) {
CAL.events = []; CAL.events = [];
el('cal-grid-body').innerHTML = '<tr><td colspan="7" class="cal-empty">Errore nel caricamento: ' + esc(e.message || '') + '</td></tr>'; el('mv-grid').innerHTML = '<div class="mc-empty">Errore nel caricamento: ' + esc(e.message || '') + '</div>';
} }
await loadSummary(); await loadSummary();
render(); render();
@@ -242,7 +284,7 @@
var bs = (s && s.by_status) || {}; var bs = (s && s.by_status) || {};
var defs = [['overdue', 'In ritardo'], ['due_soon', 'In scadenza'], ['upcoming', 'Future'], ['done', 'Assolte']]; var defs = [['overdue', 'In ritardo'], ['due_soon', 'In scadenza'], ['upcoming', 'Future'], ['done', 'Assolte']];
el('cal-summary').innerHTML = defs.map(function (d) { el('cal-summary').innerHTML = defs.map(function (d) {
return '<div class="cal-kpi k-' + d[0] + '"><div class="n">' + (bs[d[0]] || 0) + '</div><div class="l">' + d[1] + '</div></div>'; return '<div class="cal-kpi k-' + d[0] + '"><div class="l">' + d[1] + '</div><div class="n">' + (bs[d[0]] || 0) + '</div></div>';
}).join(''); }).join('');
} catch (e) { el('cal-summary').innerHTML = ''; } } catch (e) { el('cal-summary').innerHTML = ''; }
} }
@@ -252,12 +294,12 @@
el('cal-type-chips').innerHTML = Object.keys(TYPE_META).map(function (t) { el('cal-type-chips').innerHTML = Object.keys(TYPE_META).map(function (t) {
var m = TYPE_META[t]; var m = TYPE_META[t];
return '<label class="cal-chip"><input type="checkbox" class="f-type" value="' + escAttr(t) + '" checked onchange="onTypeFilter()">' + return '<label class="cal-chip"><input type="checkbox" class="f-type" value="' + escAttr(t) + '" checked onchange="onTypeFilter()">' +
'<span class="dot" style="background:' + m.color + '"></span>' + esc(m.label) + '</label>'; '<span class="dot" style="background:' + m.color + '"></span><span class="lbl">' + esc(m.label) + '</span></label>';
}).join(''); }).join('');
el('cal-status-chips').innerHTML = Object.keys(STATUS_META).map(function (s) { el('cal-status-chips').innerHTML = Object.keys(STATUS_META).map(function (s) {
var m = STATUS_META[s]; var m = STATUS_META[s];
return '<label class="cal-chip"><input type="checkbox" class="f-status" value="' + escAttr(s) + '" checked onchange="onStatusFilter()">' + return '<label class="cal-chip"><input type="checkbox" class="f-status" value="' + escAttr(s) + '" checked onchange="onStatusFilter()">' +
'<span class="dot" style="background:' + m.color + '"></span>' + esc(m.label) + '</label>'; '<span class="dot" style="background:' + m.color + '"></span><span class="lbl">' + esc(m.label) + '</span></label>';
}).join(''); }).join('');
} }
function onTypeFilter() { function onTypeFilter() {
@@ -294,8 +336,8 @@
} }
function calSetView(v) { function calSetView(v) {
CAL.view = v; CAL.view = v;
el('view-grid').classList.toggle('active', v === 'grid'); el('view-grid').classList.toggle('on', v === 'grid');
el('view-list').classList.toggle('active', v === 'list'); el('view-list').classList.toggle('on', v === 'list');
el('view-grid').setAttribute('aria-selected', v === 'grid' ? 'true' : 'false'); el('view-grid').setAttribute('aria-selected', v === 'grid' ? 'true' : 'false');
el('view-list').setAttribute('aria-selected', v === 'list' ? 'true' : 'false'); el('view-list').setAttribute('aria-selected', v === 'list' ? 'true' : 'false');
el('pane-grid').classList.toggle('hidden', v !== 'grid'); el('pane-grid').classList.toggle('hidden', v !== 'grid');
@@ -308,10 +350,8 @@
if (CAL.view === 'grid') renderGrid(); else renderList(); if (CAL.view === 'grid') renderGrid(); else renderList();
} }
// ── Vista griglia mensile ── // ── Vista griglia mensile (.mv-grid del mockup) ──
function renderGrid() { function renderGrid() {
el('cal-weekhead').innerHTML = WEEKDAYS.map(function (w) { return '<th>' + w + '</th>'; }).join('');
var year = CAL.cursor.getFullYear(), month = CAL.cursor.getMonth(); var year = CAL.cursor.getFullYear(), month = CAL.cursor.getMonth();
var firstOfMonth = new Date(year, month, 1); var firstOfMonth = new Date(year, month, 1);
// settimana inizia lunedi (getDay: 0=dom) // settimana inizia lunedi (getDay: 0=dom)
@@ -324,32 +364,31 @@
visibleEvents().forEach(function (e) { (byDay[e.date] = byDay[e.date] || []).push(e); }); visibleEvents().forEach(function (e) { (byDay[e.date] = byDay[e.date] || []).push(e); });
var html = ''; var html = '';
for (var w = 0; w < 6; w++) { for (var i = 0; i < 42; i++) {
html += '<tr>'; var cur = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i);
for (var d = 0; d < 7; d++) {
var cur = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + (w * 7 + d));
var key = ymd(cur); var key = ymd(cur);
var inMonth = (cur.getMonth() === month); var inMonth = (cur.getMonth() === month);
var cls = inMonth ? '' : ' cal-out';
if (key === todayStr) cls += ' cal-today';
var evs = byDay[key] || []; var evs = byDay[key] || [];
var dots = ''; var cls = 'mc' + (inMonth ? '' : ' other') + (key === todayStr ? ' today' : '') + (evs.length ? ' has' : '');
var shown = evs.slice(0, 6); var chips = '';
shown.forEach(function (e) { evs.slice(0, 3).forEach(function (e) {
var c = (TYPE_META[e.type] || {}).color || '#64748b'; var c = (TYPE_META[e.type] || {}).color || '#64748b';
var tip = escAttr((TYPE_META[e.type] || { label: e.type }).label + ' — ' + e.title); var tip = escAttr((TYPE_META[e.type] || { label: e.type }).label + ' — ' + e.title);
dots += '<span class="cal-dot st-' + esc(e.status) + '" style="background:' + c + '" title="' + tip + '" onclick="calPopDay(\'' + key + '\')"></span>'; chips += '<div class="chip" title="' + tip + '"><span class="cdot" style="background:' + c + '"></span>' + esc(e.title) + '</div>';
}); });
var more = evs.length > shown.length ? '<div class="cal-more" onclick="calPopDay(\'' + key + '\')">+' + (evs.length - shown.length) + ' altri</div>' : ''; var more = evs.length > 3 ? '<div class="more">+' + (evs.length - 3) + ' altri</div>' : '';
var clickDay = evs.length ? ' style="cursor:pointer" role="button" tabindex="0" aria-label="' + escAttr(cur.getDate() + ': ' + evs.length + ' scadenze') + '" onclick="calPopDay(\'' + key + '\')" onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();calPopDay(\'' + key + '\');}"' : ''; var click = evs.length ? ' role="button" tabindex="0" aria-label="' + escAttr(cur.getDate() + ': ' + evs.length + ' scadenze') + '" data-day="' + key + '"' : '';
html += '<td class="' + cls + '">' + html += '<div class="' + cls + '"' + click + '>' +
'<div class="cal-daynum"' + clickDay + '>' + cur.getDate() + '</div>' + '<div class="dnum">' + cur.getDate() + '</div>' + chips + more + '</div>';
(evs.length ? '<div class="cal-day-dots">' + dots + '</div>' + more : '') +
'</td>';
} }
html += '</tr>'; var grid = el('mv-grid');
} grid.innerHTML = html;
el('cal-grid-body').innerHTML = html; // wiring click/keyboard sui giorni con eventi
grid.querySelectorAll('.mc.has').forEach(function (cell) {
var key = cell.getAttribute('data-day');
cell.addEventListener('click', function () { calPopDay(key); });
cell.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); calPopDay(key); } });
});
} }
// ── Vista lista (overdue in cima, poi per data) ── // ── Vista lista (overdue in cima, poi per data) ──
@@ -364,8 +403,8 @@
var m = TYPE_META[e.type] || { label: e.type, color: '#64748b' }; var m = TYPE_META[e.type] || { label: e.type, color: '#64748b' };
var sm = STATUS_META[e.status] || { label: e.status }; var sm = STATUS_META[e.status] || { label: e.status };
return '<tr class="row-' + esc(e.status) + '">' + return '<tr class="row-' + esc(e.status) + '">' +
'<td>' + esc(formatDate(e.date)) + '</td>' + '<td>' + esc(calFmtDate(e.date)) + '</td>' +
'<td><span class="type-tag" style="background:' + m.color + '22;color:' + m.color + '">' + esc(m.label) + '</span></td>' + '<td><span class="type-tag" style="background:' + m.color + '22;color:' + m.color + '"><span class="cdot" style="background:' + m.color + '"></span>' + esc(m.label) + '</span></td>' +
'<td><a class="cal-link" href="' + escAttr(e.link || '#') + '">' + esc(e.title) + '</a></td>' + '<td><a class="cal-link" href="' + escAttr(e.link || '#') + '">' + esc(e.title) + '</a></td>' +
'<td><span class="st-badge st-' + esc(e.status) + '">' + esc(sm.label) + '</span></td>' + '<td><span class="st-badge st-' + esc(e.status) + '">' + esc(sm.label) + '</span></td>' +
'</tr>'; '</tr>';
@@ -374,7 +413,7 @@
'<table class="cal-list"><thead><tr><th>Data</th><th>Tipo</th><th>Elemento</th><th>Stato</th></tr></thead><tbody>' + rows + '</tbody></table>'; '<table class="cal-list"><thead><tr><th>Data</th><th>Tipo</th><th>Elemento</th><th>Stato</th></tr></thead><tbody>' + rows + '</tbody></table>';
} }
function formatDate(s) { function calFmtDate(s) {
var p = String(s).split('-'); var p = String(s).split('-');
if (p.length !== 3) return s; if (p.length !== 3) return s;
return p[2] + '/' + p[1] + '/' + p[0]; return p[2] + '/' + p[1] + '/' + p[0];
@@ -384,15 +423,15 @@
function calPopDay(dateKey) { function calPopDay(dateKey) {
var evs = visibleEvents().filter(function (e) { return e.date === dateKey; }); var evs = visibleEvents().filter(function (e) { return e.date === dateKey; });
if (!evs.length) return; if (!evs.length) return;
el('cal-pop-title').textContent = 'Scadenze del ' + formatDate(dateKey); el('cal-pop-title').textContent = 'Scadenze del ' + calFmtDate(dateKey);
el('cal-pop-body').innerHTML = evs.map(function (e) { el('cal-pop-body').innerHTML = evs.map(function (e) {
var m = TYPE_META[e.type] || { label: e.type, color: '#64748b' }; var m = TYPE_META[e.type] || { label: e.type, color: '#64748b' };
var sm = STATUS_META[e.status] || { label: e.status }; var sm = STATUS_META[e.status] || { label: e.status };
return '<div class="cal-pop-item">' + return '<div class="cal-pop-item">' +
'<span class="cal-dot st-' + esc(e.status) + '" style="background:' + m.color + ';margin-top:4px;flex:0 0 auto"></span>' + '<span class="cdot" style="background:' + m.color + '"></span>' +
'<div style="flex:1 1 auto">' + '<div style="flex:1 1 auto;min-width:0">' +
'<div><a class="cal-link" href="' + escAttr(e.link || '#') + '">' + esc(e.title) + '</a></div>' + '<div><a class="cal-link" href="' + escAttr(e.link || '#') + '">' + esc(e.title) + '</a></div>' +
'<div style="font-size:.78rem;color:#6b7280;margin-top:2px">' + esc(m.label) + '<div style="font-size:.8rem;color:var(--muted);margin-top:3px">' + esc(m.label) +
' · <span class="st-badge st-' + esc(e.status) + '">' + esc(sm.label) + '</span></div>' + ' · <span class="st-badge st-' + esc(e.status) + '">' + esc(sm.label) + '</span></div>' +
'</div></div>'; '</div></div>';
}).join(''); }).join('');
+94 -303
View File
@@ -3,270 +3,62 @@
<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>Le Mie Aziende - NIS2 Agile</title> <title>Le Mie Aziende · NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (MAI CDN). Caricato PRIMA di style.css: BI veste la chrome/sidebar, style.css resta autorevole per i widget. --> <!-- UI V3: Inter self-host + design system v3.css (NO Bootstrap Italia / NO CDN). -->
<link rel="stylesheet" href="vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="css/style.css?v=20260618"> <link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
.companies-header { /* ── Card aziende (non coperte da v3.css; nessun CDN) ───────────── */
display: flex; .companies-toolbar { display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:18px; }
align-items: center; .search-box { position:relative; min-width:240px; flex:1; max-width:340px; }
justify-content: space-between; .search-box svg { position:absolute; left:12px; top:50%; transform:translateY(-50%); color:var(--muted); pointer-events:none; }
gap: 16px; .search-box input { width:100%; padding:9px 12px 9px 38px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13px; background:#fff; color:var(--ink); outline:none; }
margin-bottom: 28px; .search-box input:focus { border-color:#9FBEDD; }
flex-wrap: wrap;
}
.companies-header h1 { .companies-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(300px, 1fr)); gap:18px; }
font-size: 1.5rem; .company-card { background:var(--card); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow); padding:20px; transition:box-shadow .12s, transform .12s; display:flex; flex-direction:column; gap:16px; }
font-weight: 700; .company-card:hover { box-shadow:0 8px 20px rgba(16,40,80,.13); border-color:#9FBEDD; transform:translateY(-1px); }
color: var(--gray-900); .company-card-header { display:flex; align-items:flex-start; gap:14px; }
letter-spacing: -0.01em; .company-card-avatar { width:48px; height:48px; border-radius:11px; background:linear-gradient(135deg,#143A5C,#1F5FA8); color:#fff; display:flex; align-items:center; justify-content:center; font-weight:800; font-size:1.05rem; flex-shrink:0; }
} .company-card-info { flex:1; min-width:0; }
.company-card-name { font-size:1rem; font-weight:800; color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-bottom:4px; }
.company-card-vat { font-size:.8125rem; color:var(--muted); font-family:'Inter',monospace; }
.companies-header-actions { .entity-badge { display:inline-flex; align-items:center; gap:5px; padding:3px 9px; border-radius:6px; font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.03em; white-space:nowrap; }
display: flex; .entity-badge.essential { background:#FBE9E9; color:#B23636; }
align-items: center; .entity-badge.important { background:#FBF1E2; color:#9A6418; }
gap: 12px; .entity-badge.not_applicable, .entity-badge.voluntary { background:#EEF2F7; color:#5b6b80; }
flex-wrap: wrap;
}
.search-box { .company-score-row { display:flex; align-items:center; gap:12px; }
position: relative; .company-score-label { font-size:.8125rem; color:var(--muted); white-space:nowrap; }
min-width: 240px; .company-score-bar { flex:1; height:7px; background:#EAF0F6; border-radius:99px; overflow:hidden; }
} .company-score-bar-fill { height:100%; border-radius:99px; transition:width .6s ease; }
.company-score-value { font-size:.875rem; font-weight:700; min-width:36px; text-align:right; }
.search-box svg { .company-stats { display:flex; gap:16px; }
position: absolute; .company-stat { display:flex; flex-direction:column; gap:2px; }
left: 12px; .company-stat-value { font-size:1.125rem; font-weight:800; color:var(--ink); }
top: 50%; .company-stat-label { font-size:.75rem; color:var(--muted); }
transform: translateY(-50%);
color: var(--gray-400);
pointer-events: none;
}
.search-box input { .company-card-footer { border-top:1px solid var(--line); padding-top:16px; display:flex; align-items:center; justify-content:space-between; gap:8px; }
padding-left: 38px; .company-card-sector { font-size:.8125rem; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex:1; }
} .btn-sm { padding:6px 11px; font-size:11.5px; }
/* ── Company Cards Grid ─────────────────────────────────────── */ .companies-empty { text-align:center; padding:60px 24px; color:var(--muted); grid-column:1 / -1; }
.companies-grid { .companies-empty svg { width:56px; height:56px; color:var(--line); margin:0 auto 18px; display:block; }
display: grid; .companies-empty h3 { font-size:1.05rem; font-weight:700; color:var(--ink); margin-bottom:8px; }
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); .companies-empty p { font-size:.9375rem; margin-bottom:22px; line-height:1.6; }
gap: 20px;
}
.company-card { /* Skeleton loader */
background: var(--card-bg); .skeleton { background:linear-gradient(90deg,#EEF2F7 25%,#F6F9FC 50%,#EEF2F7 75%); background-size:200% 100%; animation:shimmer 1.5s infinite; border-radius:6px; }
border: 1px solid var(--gray-200); @keyframes shimmer { 0%{background-position:200% 0;} 100%{background-position:-200% 0;} }
border-radius: var(--border-radius-lg); @media (prefers-reduced-motion:reduce) { .skeleton{animation:none;} }
padding: 24px; .skeleton-card { background:var(--card); border:1px solid var(--line); border-radius:var(--radius); padding:20px; display:flex; flex-direction:column; gap:16px; }
transition: all var(--transition);
display: flex;
flex-direction: column;
gap: 16px;
}
.company-card:hover { /* Modale connettori (reskin sobrio, nessun CDN) */
box-shadow: var(--card-shadow-hover); .conn-row { border:1px solid var(--line); border-radius:8px; padding:12px; margin-bottom:10px; }
border-color: var(--primary-light); .conn-row input { padding:7px 9px; border:1px solid var(--line); border-radius:6px; font:inherit; font-size:13px; outline:none; }
transform: translateY(-2px); .conn-row input:focus { border-color:#9FBEDD; }
}
.company-card-header {
display: flex;
align-items: flex-start;
gap: 14px;
}
.company-card-avatar {
width: 48px;
height: 48px;
border-radius: var(--border-radius);
background: var(--primary-bg);
color: var(--primary);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.1rem;
flex-shrink: 0;
}
.company-card-info {
flex: 1;
min-width: 0;
}
.company-card-name {
font-size: 1rem;
font-weight: 700;
color: var(--gray-900);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
}
.company-card-vat {
font-size: 0.8125rem;
color: var(--gray-500);
font-family: monospace;
}
.entity-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 99px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.entity-badge.essential {
background: rgba(234, 67, 53, 0.1);
color: #c5221f;
}
.entity-badge.important {
background: rgba(251, 188, 4, 0.15);
color: #b06000;
}
.entity-badge.not_applicable,
.entity-badge.voluntary {
background: var(--gray-100);
color: var(--gray-500);
}
/* ── Score Bar ───────────────────────────────────────────────── */
.company-score-row {
display: flex;
align-items: center;
gap: 12px;
}
.company-score-label {
font-size: 0.8125rem;
color: var(--gray-500);
white-space: nowrap;
}
.company-score-bar {
flex: 1;
height: 6px;
background: var(--gray-100);
border-radius: 99px;
overflow: hidden;
}
.company-score-bar-fill {
height: 100%;
border-radius: 99px;
transition: width 0.6s ease;
}
.company-score-value {
font-size: 0.875rem;
font-weight: 700;
min-width: 36px;
text-align: right;
}
/* ── Stats Row ───────────────────────────────────────────────── */
.company-stats {
display: flex;
gap: 16px;
}
.company-stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.company-stat-value {
font-size: 1.125rem;
font-weight: 700;
color: var(--gray-900);
}
.company-stat-label {
font-size: 0.75rem;
color: var(--gray-500);
}
/* ── Card Footer ─────────────────────────────────────────────── */
.company-card-footer {
border-top: 1px solid var(--gray-100);
padding-top: 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.company-card-sector {
font-size: 0.8125rem;
color: var(--gray-500);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── Empty State ─────────────────────────────────────────────── */
.companies-empty {
text-align: center;
padding: 80px 24px;
color: var(--gray-500);
}
.companies-empty svg {
width: 64px;
height: 64px;
color: var(--gray-300);
margin: 0 auto 20px;
display: block;
}
.companies-empty h3 {
font-size: 1.1rem;
font-weight: 600;
color: var(--gray-700);
margin-bottom: 8px;
}
.companies-empty p {
font-size: 0.9375rem;
margin-bottom: 24px;
line-height: 1.6;
}
/* ── Loading skeleton ────────────────────────────────────────── */
.skeleton {
background: linear-gradient(90deg, var(--gray-100) 25%, var(--gray-50) 50%, var(--gray-100) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 6px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.skeleton-card {
background: var(--card-bg);
border: 1px solid var(--gray-200);
border-radius: var(--border-radius-lg);
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -283,29 +75,29 @@
<!-- 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 id="sidebar" class="sidebar"></aside>
<!-- Main Content --> <main class="wrap">
<main class="main-content"> <nav class="crumb" aria-label="breadcrumb">
<div class="page-container"> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>Le Mie Aziende</span>
<!-- Page Header --> </nav>
<div class="companies-header" id="nis2-companies-header"> <div class="page-head" id="nis2-companies-header">
<div> <div>
<h1>Le Mie Aziende</h1> <h1>Le Mie Aziende</h1>
<p style="color:var(--gray-500);font-size:.9rem;margin-top:2px;">Gestisci il portafoglio clienti e monitora la compliance NIS2</p> <p class="sub">Gestisci il portafoglio clienti e monitora la compliance NIS2</p>
</div> </div>
<div class="companies-header-actions"> <a href="onboarding.html" class="btn-primary" id="nis2-companies-add-btn">
<i data-lucide="plus" class="ic"></i>Aggiungi Cliente
</a>
</div>
<section class="card card-pad">
<div class="companies-toolbar">
<div class="search-box"> <div class="search-box">
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg> <svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"/></svg>
<input type="text" class="form-input" id="search-input" placeholder="Cerca azienda..." oninput="filterCompanies(this.value)"> <input type="text" id="search-input" placeholder="Cerca azienda..." oninput="filterCompanies(this.value)" aria-label="Cerca azienda">
</div>
<a href="onboarding.html" class="btn btn-primary" id="nis2-companies-add-btn">
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
Aggiungi Cliente
</a>
</div> </div>
</div> </div>
@@ -314,19 +106,19 @@
<!-- Skeleton loaders --> <!-- Skeleton loaders -->
<div class="skeleton-card"> <div class="skeleton-card">
<div style="display:flex;gap:12px;align-items:center"> <div style="display:flex;gap:12px;align-items:center">
<div class="skeleton" style="width:48px;height:48px;border-radius:8px;flex-shrink:0"></div> <div class="skeleton" style="width:48px;height:48px;border-radius:11px;flex-shrink:0"></div>
<div style="flex:1"><div class="skeleton" style="height:16px;margin-bottom:8px"></div><div class="skeleton" style="height:12px;width:60%"></div></div> <div style="flex:1"><div class="skeleton" style="height:16px;margin-bottom:8px"></div><div class="skeleton" style="height:12px;width:60%"></div></div>
</div> </div>
<div class="skeleton" style="height:6px"></div> <div class="skeleton" style="height:7px"></div>
<div style="display:flex;gap:16px"><div class="skeleton" style="height:40px;flex:1"></div><div class="skeleton" style="height:40px;flex:1"></div></div> <div style="display:flex;gap:16px"><div class="skeleton" style="height:40px;flex:1"></div><div class="skeleton" style="height:40px;flex:1"></div></div>
<div class="skeleton" style="height:36px"></div> <div class="skeleton" style="height:36px"></div>
</div> </div>
<div class="skeleton-card"> <div class="skeleton-card">
<div style="display:flex;gap:12px;align-items:center"> <div style="display:flex;gap:12px;align-items:center">
<div class="skeleton" style="width:48px;height:48px;border-radius:8px;flex-shrink:0"></div> <div class="skeleton" style="width:48px;height:48px;border-radius:11px;flex-shrink:0"></div>
<div style="flex:1"><div class="skeleton" style="height:16px;margin-bottom:8px"></div><div class="skeleton" style="height:12px;width:60%"></div></div> <div style="flex:1"><div class="skeleton" style="height:16px;margin-bottom:8px"></div><div class="skeleton" style="height:12px;width:60%"></div></div>
</div> </div>
<div class="skeleton" style="height:6px"></div> <div class="skeleton" style="height:7px"></div>
<div style="display:flex;gap:16px"><div class="skeleton" style="height:40px;flex:1"></div><div class="skeleton" style="height:40px;flex:1"></div></div> <div style="display:flex;gap:16px"><div class="skeleton" style="height:40px;flex:1"></div><div class="skeleton" style="height:40px;flex:1"></div></div>
<div class="skeleton" style="height:36px"></div> <div class="skeleton" style="height:36px"></div>
</div> </div>
@@ -339,24 +131,20 @@
</svg> </svg>
<h3>Nessuna azienda ancora</h3> <h3>Nessuna azienda ancora</h3>
<p>Aggiungi il tuo primo cliente per iniziare a monitorare la compliance NIS2.</p> <p>Aggiungi il tuo primo cliente per iniziare a monitorare la compliance NIS2.</p>
<a href="onboarding.html" class="btn btn-primary" id="nis2-companies-add-first-btn"> <a href="onboarding.html" class="btn-primary" id="nis2-companies-add-first-btn" style="display:inline-flex">
<svg width="18" height="18" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg> <i data-lucide="plus" class="ic"></i>Aggiungi Primo Cliente
Aggiungi Primo Cliente
</a> </a>
</div> </div>
</section>
</div>
</main> </main>
</div>
<script src="js/api.js?v=20260631"></script> <!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide → topnav-v3.js (chrome) → i18n → help → init pagina -->
<script src="js/common.js?v=20260624g"></script> <script src="/js/api.js?v=20260625d"></script>
<!-- Bootstrap Italia bundle + override sidebar BI (common-bi.js dopo common.js: ridefinisce solo loadSidebar) --> <script src="/js/common.js?v=20260627v3"></script>
<script src="vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script>if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('vendor/bootstrap-italia/dist/fonts'); }</script> <script src="/js/topnav-v3.js?v=20260627"></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> <script>
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
@@ -367,7 +155,9 @@
} }
loadSidebar(); loadSidebar();
if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init(); if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
// ── Data ──────────────────────────────────────────────────────── // ── Data ────────────────────────────────────────────────────────
let allCompanies = []; let allCompanies = [];
@@ -405,6 +195,7 @@
function renderCompanies(orgs) { function renderCompanies(orgs) {
const grid = document.getElementById('companies-grid'); const grid = document.getElementById('companies-grid');
grid.innerHTML = orgs.map(org => renderCompanyCard(org)).join(''); grid.innerHTML = orgs.map(org => renderCompanyCard(org)).join('');
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
} }
function renderCompanyCard(org) { function renderCompanyCard(org) {
@@ -454,12 +245,12 @@
<div class="company-card-footer"> <div class="company-card-footer">
<span class="company-card-sector">${escapeHtml(sectorLabel)}</span> <span class="company-card-sector">${escapeHtml(sectorLabel)}</span>
<button class="btn btn-ghost btn-sm" onclick="openConnectors(${orgId}, '${escapeHtml(name).replace(/'/g, "\\'")}')" title="Connettori Evidence Automation"> <button class="btn-ghost btn-sm" onclick="openConnectors(${orgId}, '${escapeHtml(name).replace(/'/g, "\\'")}')" title="Connettori Evidence Automation">
Connettori Connettori
</button> </button>
<button class="btn btn-primary btn-sm" onclick="manageCompany(${orgId})"> <button class="btn-primary btn-sm" onclick="manageCompany(${orgId})">
Gestisci Gestisci
<svg width="14" height="14" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"/></svg> <i data-lucide="arrow-right" class="ic"></i>
</button> </button>
</div> </div>
</div>`; </div>`;
@@ -480,45 +271,45 @@
headers: { 'Authorization': `Bearer ${api.token}`, 'X-Organization-Id': orgId } headers: { 'Authorization': `Bearer ${api.token}`, 'X-Organization-Id': orgId }
}); });
const data = await res.json(); const data = await res.json();
if (!data.success) { showConnModal(orgName, `<p style="color:#ef4444;">${escapeHtml(data.message || 'Errore')}</p>`); return; } if (!data.success) { showConnModal(orgName, `<p style="color:var(--c-red);">${escapeHtml(data.message || 'Errore')}</p>`); return; }
const existing = {}; const existing = {};
(data.data.connectors || []).forEach(c => existing[c.connector_type] = c); (data.data.connectors || []).forEach(c => existing[c.connector_type] = c);
const rows = (data.data.available_types || []).map(t => { const rows = (data.data.available_types || []).map(t => {
const c = existing[t] || {}; const c = existing[t] || {};
const cfg = (c.config && typeof c.config === 'object') ? c.config : {}; const cfg = (c.config && typeof c.config === 'object') ? c.config : {};
const ss = c.secret_status || 'not_set'; const ss = c.secret_status || 'not_set';
const ssBadge = { not_set: ['Segreto non impostato', '#9ca3af'], pending: ['Segreto in attesa', '#eab308'], configured: ['Segreto configurato', '#22c55e'] }[ss]; const ssBadge = { not_set: ['Segreto non impostato', '#9ca3af'], pending: ['Segreto in attesa', '#D98324'], configured: ['Segreto configurato', '#1E8A5A'] }[ss];
return `<div class="conn-row" data-type="${t}" style="border:1px solid var(--gray-200);border-radius:8px;padding:12px;margin-bottom:10px;"> return `<div class="conn-row" data-type="${t}">
<div style="display:flex;justify-content:space-between;align-items:center;"> <div style="display:flex;justify-content:space-between;align-items:center;">
<strong>${escapeHtml(CONNECTOR_LABELS[t] || t)}</strong> <strong>${escapeHtml(CONNECTOR_LABELS[t] || t)}</strong>
<label style="font-size:.8rem;"><input type="checkbox" class="c-en" ${c.enabled ? 'checked' : ''}> Attivo</label> <label style="font-size:.8rem;"><input type="checkbox" class="c-en" ${c.enabled ? 'checked' : ''}> Attivo</label>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:8px;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:8px;">
<input class="c-tenant" placeholder="tenant_id / account_id" value="${escapeHtml(cfg.tenant_id || cfg.account_id || '')}" style="padding:6px;border:1px solid var(--gray-300);border-radius:6px;"> <input class="c-tenant" placeholder="tenant_id / account_id" value="${escapeHtml(cfg.tenant_id || cfg.account_id || '')}">
<input class="c-client" placeholder="client_id / app_id" value="${escapeHtml(cfg.client_id || cfg.app_id || '')}" style="padding:6px;border:1px solid var(--gray-300);border-radius:6px;"> <input class="c-client" placeholder="client_id / app_id" value="${escapeHtml(cfg.client_id || cfg.app_id || '')}">
</div> </div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;"> <div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px;">
<span style="font-size:.72rem;color:${ssBadge[1]};">● ${ssBadge[0]}</span> <span style="font-size:.72rem;color:${ssBadge[1]};">● ${ssBadge[0]}</span>
<button class="btn btn-sm btn-primary" onclick="saveConn('${t}', this)">Salva</button> <button class="btn-primary btn-sm" onclick="saveConn('${t}', this)">Salva</button>
</div> </div>
</div>`; </div>`;
}).join(''); }).join('');
showConnModal(orgName, ` showConnModal(orgName, `
<p style="font-size:.82rem;color:var(--gray-600);margin-bottom:12px;"> <p style="font-size:.82rem;color:var(--muted);margin-bottom:12px;">
Configura i connettori di <strong>Evidence Automation</strong>. Qui si salvano solo i parametri Configura i connettori di <strong>Evidence Automation</strong>. Qui si salvano solo i parametri
NON segreti (tenant/client id). Il <strong>client secret</strong> va caricato nel vault dall'amministratore NON segreti (tenant/client id). Il <strong>client secret</strong> va caricato nel vault dall'amministratore
(il comando viene mostrato dopo il salvataggio).</p>${rows}`); (il comando viene mostrato dopo il salvataggio).</p>${rows}`);
} catch (e) { showConnModal(orgName, '<p style="color:#ef4444;">Errore di connessione</p>'); } } catch (e) { showConnModal(orgName, '<p style="color:var(--c-red);">Errore di connessione</p>'); }
} }
function showConnModal(orgName, body) { function showConnModal(orgName, body) {
const old = document.getElementById('conn-modal'); if (old) old.remove(); const old = document.getElementById('conn-modal'); if (old) old.remove();
document.body.insertAdjacentHTML('beforeend', ` document.body.insertAdjacentHTML('beforeend', `
<div id="conn-modal" onclick="if(event.target===this)closeConn()" style="position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px;"> <div id="conn-modal" onclick="if(event.target===this)closeConn()" style="position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1000;display:flex;align-items:center;justify-content:center;padding:20px;">
<div style="background:var(--card-bg,#fff);border-radius:12px;max-width:640px;width:100%;max-height:85vh;overflow-y:auto;padding:22px;"> <div style="background:var(--card);border-radius:12px;max-width:640px;width:100%;max-height:85vh;overflow-y:auto;padding:22px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;">
<h3 style="margin:0;">Connettori — ${escapeHtml(orgName)}</h3> <h3 style="margin:0;">Connettori — ${escapeHtml(orgName)}</h3>
<button class="btn btn-ghost btn-sm" onclick="closeConn()">✕</button> <button class="btn-ghost btn-sm" onclick="closeConn()">✕</button>
</div>${body} </div>${body}
</div></div>`); </div></div>`);
} }
+82 -61
View File
@@ -3,61 +3,62 @@
<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>Competenze - NIS2 Agile</title> <title>Competenze · NIS2 Agile</title>
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <!-- UI V3: Inter self-host + design system v3.css. No external CDN, no Bootstrap Italia. -->
<link rel="stylesheet" href="/css/style.css?v=20260618"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
.cmp-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:16px; font-size:.92rem; line-height:1.6; } /* V3+ page-specific (componenti non coperti da v3.css): tab interni, tabella, gap card, modali. Token V3. */
.cmp-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 18px; } .cmp-intro { background:#EFF4FB; border-left:4px solid var(--c-blue); padding:14px 18px; border-radius:10px; margin-bottom:14px; font-size:13.5px; line-height:1.6; color:var(--ink); }
.cmp-note { font-size:12.5px; color:#9A6418; background:#FBF1E2; border:1px solid #f3d9a8; border-radius:10px; padding:10px 12px; margin:0 0 18px; }
.cmp-tabs { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:18px; } .cmp-tabs { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:18px; }
.cmp-tab { flex:1 1 160px; min-width:140px; padding:10px 12px; border:1.5px solid var(--gray-200,#e5e7eb); border-radius:10px; background:#fff; cursor:pointer; font-size:.85rem; font-weight:600; text-align:center; } .cmp-tab { flex:1 1 160px; min-width:140px; padding:11px 12px; border:1.5px solid var(--line); border-radius:10px; background:#fff; cursor:pointer; font:inherit; font-size:13px; font-weight:600; text-align:center; color:var(--ink); }
.cmp-tab.active { border-color:var(--primary,#2563eb); background:#eff6ff; color:var(--primary,#2563eb); } .cmp-tab.active { border-color:var(--c-blue); background:#EFF4FB; color:var(--c-blue); }
.cmp-panel { display:none; } .cmp-panel { display:none; }
.cmp-panel.active { display:block; } .cmp-panel.active { display:block; }
.cmp-table { width:100%; border-collapse:collapse; font-size:.88rem; } .cmp-table { width:100%; border-collapse:collapse; font-size:13.5px; }
.cmp-table th, .cmp-table td { text-align:left; padding:9px 10px; border-bottom:1px solid var(--gray-100,#f3f4f6); vertical-align:top; } .cmp-table th, .cmp-table td { text-align:left; padding:10px 11px; border-bottom:1px solid var(--line); vertical-align:top; }
.cmp-table th { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); } .cmp-table th { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); font-weight:700; }
.cmp-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; } .cmp-badge { display:inline-block; font-size:10.5px; font-weight:700; padding:2px 8px; border-radius:6px; }
.cmp-badge.global { background:#ede9fe; color:#5b21b6; } .cmp-badge.global { background:#EFE7FB; color:#5b21b6; }
.cmp-badge.area { background:#f1f5f9; color:#475569; } .cmp-badge.area { background:#EEF2F7; color:#5b6b80; }
.cmp-badge.lvl { background:#e0f2fe; color:#075985; } .cmp-badge.lvl { background:#E7F0FB; color:#1F5FA8; }
.cmp-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; } .cmp-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
.cmp-toolbar select, .cmp-inline select, .cmp-inline input { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; } .cmp-toolbar select, .cmp-inline select, .cmp-inline input { padding:8px 10px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
.cmp-inline { display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end; background:#f9fafb; border:1px solid var(--gray-100,#f3f4f6); border-radius:10px; padding:12px; margin-bottom:14px; } .cmp-inline { display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end; background:#FAFCFE; border:1px solid var(--line); border-radius:10px; padding:12px; margin-bottom:14px; }
.cmp-inline .fld { display:flex; flex-direction:column; gap:3px; } .cmp-inline .fld { display:flex; flex-direction:column; gap:3px; }
.cmp-inline .fld label { font-size:.72rem; font-weight:600; color:var(--gray-500,#6b7280); } .cmp-inline .fld label { font-size:11px; font-weight:700; color:var(--muted); }
.cmp-actions-cell { display:flex; gap:6px; flex-wrap:wrap; } .cmp-actions-cell { display:flex; gap:6px; flex-wrap:wrap; }
.cmp-actions-cell .btn, .cmp-toolbar .btn { padding:4px 9px; font-size:.76rem; }
/* Gap cards */ /* Gap cards */
.gap-role { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; } .gap-role { border:1px solid var(--line); border-radius:11px; margin-bottom:14px; overflow:hidden; }
.gap-role-head { background:var(--gray-50,#f9fafb); padding:11px 14px; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:6px; } .gap-role-head { background:#FAFCFE; padding:12px 14px; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:6px; }
.gap-role-title { font-weight:700; } .gap-role-title { font-weight:700; }
.gap-role-sub { font-size:.82rem; color:var(--gray-500,#6b7280); } .gap-role-sub { font-size:12px; color:var(--muted); }
.gap-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; padding:10px 14px; border-top:1px solid var(--gray-100,#f3f4f6); } .gap-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; padding:11px 14px; border-top:1px solid var(--line); }
.gap-row .skill { flex:1 1 220px; font-weight:600; font-size:.9rem; } .gap-row .skill { flex:1 1 220px; font-weight:600; font-size:13.5px; }
.gap-meter { font-size:.82rem; } .gap-meter { font-size:12.5px; color:var(--muted); }
.gap-pill { display:inline-block; font-size:.72rem; font-weight:800; padding:2px 9px; border-radius:20px; } .gap-pill { display:inline-block; font-size:10.5px; font-weight:800; padding:2px 9px; border-radius:20px; }
.gap-pill.ok { background:#dcfce7; color:#166534; } .gap-pill.ok { background:#E6F4EC; color:#1E7A4E; }
.gap-pill.bad { background:#fee2e2; color:#991b1b; } .gap-pill.bad { background:#FBE9E9; color:#B23636; }
.gap-row .ctas { display:flex; gap:6px; flex-wrap:wrap; } .gap-row .ctas { display:flex; gap:6px; flex-wrap:wrap; }
.gap-row .ctas .btn { padding:4px 9px; font-size:.76rem; } .cmp-empty { text-align:center; padding:34px 16px; color:var(--muted); }
.cmp-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); }
/* Modale */ /* Modale */
.cm-overlay { display:none; position:fixed; inset:0; background:rgba(15,23,42,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; } .cm-overlay { display:none; position:fixed; inset:0; background:rgba(15,38,60,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
.cm-overlay.open { display:flex; } .cm-overlay.open { display:flex; }
.cm-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(0,0,0,.25); } .cm-dialog { background:#fff; border-radius:14px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(11,40,80,.25); }
.cm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); } .cm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--line); }
.cm-head h3 { margin:0; font-size:1.05rem; } .cm-head h3 { margin:0; font-size:17px; font-weight:800; }
.cm-body { padding:18px 20px; } .cm-body { padding:18px 20px; }
.cm-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; } .cm-foot { padding:14px 20px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px; }
.cm-field { margin-bottom:14px; } .cm-field { margin-bottom:14px; }
.cm-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; } .cm-field label { display:block; font-weight:600; font-size:12.5px; margin-bottom:5px; }
.cm-field input[type=text], .cm-field select, .cm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; } .cm-field input[type=text], .cm-field select, .cm-field textarea { width:100%; padding:9px 11px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
.cm-field textarea { min-height:64px; resize:vertical; } .cm-field textarea { min-height:64px; resize:vertical; }
.cm-err { color:#b91c1c; font-size:.82rem; min-height:18px; } .cm-err { color:var(--c-red); font-size:12.5px; min-height:18px; }
.cm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); } .cm-close { background:none; border:none; font-size:22px; line-height:1; cursor:pointer; color:#9aa8ba; }
.cm-maplist { margin-top:10px; } .cm-maplist { margin-top:10px; }
.cm-maprow { display:flex; justify-content:space-between; align-items:center; padding:7px 0; border-top:1px solid var(--gray-100,#f3f4f6); font-size:.86rem; } .cm-maprow { display:flex; justify-content:space-between; align-items:center; padding:8px 0; border-top:1px solid var(--line); font-size:13px; }
.savehint { font-size:12px; color:var(--muted); }
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -74,17 +75,24 @@
<!-- PWA:end --> <!-- PWA:end -->
</head> </head>
<body> <body>
<div class="app-layout"> <div id="topnav"></div>
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h2 data-i18n="comp.title">Competenze</h2> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span>Struttura interna</span>
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="comp.title">Competenze</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="comp.title">Competenze</h1>
<p class="sub">Skill matrix, requisiti per ruolo e copertura dei gap</p>
</div>
<span class="savehint" id="cmp-hint" role="status" aria-live="polite"></span> <span class="savehint" id="cmp-hint" role="status" aria-live="polite"></span>
</div> </div>
</header>
<div class="content-body"> <div class="stack">
<section class="card card-pad">
<div class="cmp-intro"> <div class="cmp-intro">
<strong>Competenze, requisiti e gap.</strong> <strong>Competenze, requisiti e gap.</strong>
Definisci le competenze necessarie, assegnale ai ruoli dell'organigramma, registra quelle possedute dalle persone e individua i divari (gap) da colmare con formazione o azioni correttive. Definisci le competenze necessarie, assegnale ai ruoli dell'organigramma, registra quelle possedute dalle persone e individua i divari (gap) da colmare con formazione o azioni correttive.
@@ -103,7 +111,7 @@
<!-- TAB 1 — Catalogo competenze --> <!-- TAB 1 — Catalogo competenze -->
<div class="cmp-panel active" id="panel-catalog"> <div class="cmp-panel active" id="panel-catalog">
<div class="cmp-toolbar"> <div class="cmp-toolbar">
<button class="btn btn-primary btn-sm" onclick="cmpOpenSkill()">+ Nuova competenza</button> <button class="btn-primary" onclick="cmpOpenSkill()"><i data-lucide="plus" class="ic"></i>Nuova competenza</button>
<span class="gap-role-sub" id="cmp-catalog-count"></span> <span class="gap-role-sub" id="cmp-catalog-count"></span>
</div> </div>
<div id="cmp-catalog"></div> <div id="cmp-catalog"></div>
@@ -112,13 +120,13 @@
<!-- TAB 2 — Requisiti per ruolo --> <!-- TAB 2 — Requisiti per ruolo -->
<div class="cmp-panel" id="panel-roles"> <div class="cmp-panel" id="panel-roles">
<div class="cmp-toolbar"> <div class="cmp-toolbar">
<label for="cmp-role-sel" style="font-weight:600;font-size:.86rem;">Ruolo:</label> <label for="cmp-role-sel" style="font-weight:600;font-size:13px;">Ruolo:</label>
<select id="cmp-role-sel" onchange="cmpLoadRoleSkills()"></select> <select id="cmp-role-sel" onchange="cmpLoadRoleSkills()"></select>
</div> </div>
<div class="cmp-inline" id="cmp-role-add" style="display:none;"> <div class="cmp-inline" id="cmp-role-add" style="display:none;">
<div class="fld"><label>Competenza</label><select id="cmp-rs-skill"></select></div> <div class="fld"><label>Competenza</label><select id="cmp-rs-skill"></select></div>
<div class="fld"><label>Livello richiesto</label><select id="cmp-rs-level"></select></div> <div class="fld"><label>Livello richiesto</label><select id="cmp-rs-level"></select></div>
<button class="btn btn-primary btn-sm" onclick="cmpAddRoleSkill()">Aggiungi requisito</button> <button class="btn-primary" onclick="cmpAddRoleSkill()">Aggiungi requisito</button>
</div> </div>
<div id="cmp-role-skills"></div> <div id="cmp-role-skills"></div>
</div> </div>
@@ -126,7 +134,7 @@
<!-- TAB 3 — Competenze persone --> <!-- TAB 3 — Competenze persone -->
<div class="cmp-panel" id="panel-users"> <div class="cmp-panel" id="panel-users">
<div class="cmp-toolbar"> <div class="cmp-toolbar">
<label for="cmp-user-sel" style="font-weight:600;font-size:.86rem;">Persona:</label> <label for="cmp-user-sel" style="font-weight:600;font-size:13px;">Persona:</label>
<select id="cmp-user-sel" onchange="cmpLoadUserSkills()"></select> <select id="cmp-user-sel" onchange="cmpLoadUserSkills()"></select>
</div> </div>
<div class="cmp-inline" id="cmp-user-add" style="display:none;"> <div class="cmp-inline" id="cmp-user-add" style="display:none;">
@@ -134,7 +142,7 @@
<div class="fld"><label>Livello posseduto</label><select id="cmp-us-level"></select></div> <div class="fld"><label>Livello posseduto</label><select id="cmp-us-level"></select></div>
<div class="fld"><label>Corso (opz.)</label><select id="cmp-us-course"></select></div> <div class="fld"><label>Corso (opz.)</label><select id="cmp-us-course"></select></div>
<div class="fld"><label>Evidenza (opz.)</label><input type="text" id="cmp-us-evidence" maxlength="500" placeholder="es. attestato, data"></div> <div class="fld"><label>Evidenza (opz.)</label><input type="text" id="cmp-us-evidence" maxlength="500" placeholder="es. attestato, data"></div>
<button class="btn btn-primary btn-sm" onclick="cmpAddUserSkill()">Registra</button> <button class="btn-primary" onclick="cmpAddUserSkill()">Registra</button>
</div> </div>
<div id="cmp-user-skills"></div> <div id="cmp-user-skills"></div>
</div> </div>
@@ -142,14 +150,14 @@
<!-- TAB 4 — Gap & azioni --> <!-- TAB 4 — Gap & azioni -->
<div class="cmp-panel" id="panel-gap"> <div class="cmp-panel" id="panel-gap">
<div class="cmp-toolbar"> <div class="cmp-toolbar">
<button class="btn btn-outline btn-sm" onclick="cmpLoadGap()">Ricalcola gap</button> <button class="btn-ghost" onclick="cmpLoadGap()"><i data-lucide="refresh-cw" class="ic"></i>Ricalcola gap</button>
<span class="gap-role-sub" id="cmp-gap-summary"></span> <span class="gap-role-sub" id="cmp-gap-summary"></span>
</div> </div>
<div id="cmp-gap"></div> <div id="cmp-gap"></div>
</div> </div>
</section>
</div> </div>
</main> </main>
</div>
<!-- Modale competenza (crea/modifica) --> <!-- Modale competenza (crea/modifica) -->
<div class="cm-overlay" id="cmp-skill-modal" role="dialog" aria-modal="true" aria-labelledby="cmp-skill-title"> <div class="cm-overlay" id="cmp-skill-modal" role="dialog" aria-modal="true" aria-labelledby="cmp-skill-title">
@@ -162,7 +170,7 @@
<div class="cm-field"><label for="cmp-skill-desc">Descrizione</label><textarea id="cmp-skill-desc" maxlength="2000"></textarea></div> <div class="cm-field"><label for="cmp-skill-desc">Descrizione</label><textarea id="cmp-skill-desc" maxlength="2000"></textarea></div>
<div class="cm-err" id="cmp-skill-err" role="alert"></div> <div class="cm-err" id="cmp-skill-err" role="alert"></div>
</div> </div>
<div class="cm-foot"><button class="btn btn-outline" type="button" onclick="cmpClose('cmp-skill-modal')">Annulla</button><button class="btn btn-primary" type="button" id="cmp-skill-save" onclick="cmpSaveSkill()">Salva</button></div> <div class="cm-foot"><button class="btn-ghost" type="button" onclick="cmpClose('cmp-skill-modal')">Annulla</button><button class="btn-primary" type="button" id="cmp-skill-save" onclick="cmpSaveSkill()">Salva</button></div>
</div> </div>
</div> </div>
@@ -174,21 +182,34 @@
<input type="hidden" id="cmp-courses-skill-id"> <input type="hidden" id="cmp-courses-skill-id">
<div class="cm-field" style="display:flex;gap:8px;align-items:flex-end;"> <div class="cm-field" style="display:flex;gap:8px;align-items:flex-end;">
<div style="flex:1;"><label for="cmp-courses-sel">Aggiungi corso</label><select id="cmp-courses-sel"></select></div> <div style="flex:1;"><label for="cmp-courses-sel">Aggiungi corso</label><select id="cmp-courses-sel"></select></div>
<button class="btn btn-primary" type="button" onclick="cmpMapCourse()">Aggiungi</button> <button class="btn-primary" type="button" onclick="cmpMapCourse()">Aggiungi</button>
</div> </div>
<div class="cm-maplist" id="cmp-courses-list"></div> <div class="cm-maplist" id="cmp-courses-list"></div>
<div class="cm-err" id="cmp-courses-err" role="alert"></div> <div class="cm-err" id="cmp-courses-err" role="alert"></div>
</div> </div>
<div class="cm-foot"><button class="btn btn-outline" type="button" onclick="cmpClose('cmp-courses-modal')">Chiudi</button></div> <div class="cm-foot"><button class="btn-ghost" type="button" onclick="cmpClose('cmp-courses-modal')">Chiudi</button></div>
</div> </div>
</div> </div>
<!-- Scripts V3. competenze.js si auto-inizializza su DOMContentLoaded (checkAuth/loadSidebar/I18n/Help). -->
<script src="/js/api.js?v=20260625d"></script> <script src="/js/api.js?v=20260625d"></script>
<script src="/js/common.js?v=20260624g"></script> <script src="/js/common.js?v=20260627v3"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></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/competenze.js?v=20260617"></script> <script src="/js/competenze.js?v=20260627"></script>
<script>
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
v3Icons();
document.addEventListener('DOMContentLoaded', function () {
v3Icons();
var root = document.querySelector('main.wrap');
if (root && window.MutationObserver) {
var mo = new MutationObserver(function () { v3Icons(); });
mo.observe(root, { childList: true, subtree: true });
}
});
</script>
</body> </body>
</html> </html>
+42 -31
View File
@@ -2,10 +2,13 @@
<html lang="it"> <html lang="it">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Connettori Discovery - NIS2 Agile</title> <title>Connettori Discovery · NIS2 Agile</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <!-- UI V3: Inter self-host + design system v3.css (self-host only, nessun CDN esterno). -->
<link rel="stylesheet" href="/css/style.css?v=20260618"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<!-- style.css resta caricato DOPO v3.css: veste i widget renderizzati via JS
(tabelle/form/modali). Self-hostato (nessun CDN). v3.css governa chrome + shell. -->
<meta name="theme-color" content="#0066CC"> <meta name="theme-color" content="#0066CC">
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
@@ -14,15 +17,24 @@
<script src="/js/pwa.js?v=20260614" defer></script> <script src="/js/pwa.js?v=20260614" defer></script>
</head> </head>
<body> <body>
<div class="app-layout"> <!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header"> <main class="wrap">
<h1 class="content-title">Connettori Discovery</h1> <nav class="crumb" aria-label="breadcrumb">
<div class="content-header-actions"> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<button class="btn btn-primary" id="btn-dc-new">+ Nuovo connettore</button> <i data-lucide="chevron-right" class="ic"></i><span>Connettori Discovery</span>
</nav>
<div class="page-head">
<div>
<h1>Connettori Discovery</h1>
<p class="sub">Scansione rete/cloud → auto-popola l'Inventario (asset, dipendenze, flussi ID.AM-03)</p>
</div> </div>
</header> <div class="content-header-actions">
<button class="btn-primary" id="btn-dc-new"><i data-lucide="plus" class="ic"></i>Nuovo connettore</button>
</div>
</div>
<div class="content-body"> <div class="content-body">
<div class="card" style="margin-bottom:16px"> <div class="card" style="margin-bottom:16px">
<div class="card-body"> <div class="card-body">
@@ -40,12 +52,12 @@
<div class="card-body"> <div class="card-body">
<div class="form-row" style="display:flex;gap:12px;flex-wrap:wrap"> <div class="form-row" style="display:flex;gap:12px;flex-wrap:wrap">
<div class="form-group" style="flex:1;min-width:200px"> <div class="form-group" style="flex:1;min-width:200px">
<label>Nome</label> <label class="form-label" for="dc-name">Nome</label>
<input id="dc-name" class="form-control" placeholder="Es. Rete sede Modena" maxlength="120"> <input id="dc-name" class="form-input" placeholder="Es. Rete sede Modena" maxlength="120">
</div> </div>
<div class="form-group" style="min-width:160px"> <div class="form-group" style="min-width:160px">
<label>Tipo</label> <label class="form-label" for="dc-type">Tipo</label>
<select id="dc-type" class="form-control"> <select id="dc-type" class="form-select">
<option value="network">Rete (scan IP)</option> <option value="network">Rete (scan IP)</option>
<option value="aws">AWS</option> <option value="aws">AWS</option>
<option value="azure">Azure</option> <option value="azure">Azure</option>
@@ -56,8 +68,8 @@
</select> </select>
</div> </div>
<div class="form-group" style="min-width:160px"> <div class="form-group" style="min-width:160px">
<label>Schedulazione</label> <label class="form-label" for="dc-schedule">Schedulazione</label>
<select id="dc-schedule" class="form-control"> <select id="dc-schedule" class="form-select">
<option value="manual">Manuale</option> <option value="manual">Manuale</option>
<option value="daily">Giornaliera</option> <option value="daily">Giornaliera</option>
<option value="hourly">Oraria</option> <option value="hourly">Oraria</option>
@@ -66,12 +78,12 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Target (CIDR/IP, separati da virgola) — descrittivo / usato dall'agente</label> <label class="form-label" for="dc-targets">Target (CIDR/IP, separati da virgola) — descrittivo / usato dall'agente</label>
<input id="dc-targets" class="form-control" placeholder="192.168.1.0/24, 10.0.0.5"> <input id="dc-targets" class="form-input" placeholder="192.168.1.0/24, 10.0.0.5">
</div> </div>
<div style="display:flex;gap:8px;margin-top:8px"> <div style="display:flex;gap:8px;margin-top:8px">
<button class="btn btn-primary" id="btn-dc-create">Crea connettore</button> <button class="btn-primary" id="btn-dc-create"><i data-lucide="check" class="ic"></i>Crea connettore</button>
<button class="btn btn-outline-primary" id="btn-dc-cancel">Annulla</button> <button class="btn-ghost" id="btn-dc-cancel">Annulla</button>
</div> </div>
</div> </div>
</div> </div>
@@ -83,25 +95,24 @@
</div> </div>
</div> </div>
</main> </main>
</div>
<script src="/js/common.js?v=20260624g"></script> <!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → pagina -->
<script src="/js/api.js?v=20260625d"></script> <script src="/js/api.js?v=20260625d"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/js/common.js?v=20260627v3"></script>
<script> <script src="/vendor/lucide/lucide.min.js"></script>
if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); } <script src="/js/topnav-v3.js?v=20260627"></script>
</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/connettori-discovery.js?v=20260625"></script> <script src="/js/connettori-discovery.js?v=20260627"></script>
<script> <script>
if (typeof checkAuth === 'function' && !checkAuth()) { if (typeof checkAuth === 'function' && !checkAuth()) {
// checkAuth reindirizza a login // checkAuth reindirizza a login
} else { } else {
if (typeof loadSidebar === 'function') loadSidebar(); if (typeof loadSidebar === 'function') loadSidebar(); // = loadTopnavV3()
if (typeof I18n !== 'undefined' && I18n.init) I18n.init(); if (typeof I18n !== 'undefined' && I18n.init) I18n.init();
if (typeof HelpSystem !== 'undefined' && HelpSystem.init) HelpSystem.init(); if (typeof HelpSystem !== 'undefined' && HelpSystem.init) HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
if (window.DiscoveryUI) DiscoveryUI.init(); if (window.DiscoveryUI) DiscoveryUI.init();
} }
</script> </script>
+38 -18
View File
@@ -3,10 +3,21 @@
<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>Controlli periodici - NIS2 Agile</title> <title>Controlli periodici · NIS2 Agile</title>
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia CDN). -->
<link rel="stylesheet" href="/css/style.css?v=20260618"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
/* ── Compat V3: token + bottoni emessi dal JS (ex style.css, niente CDN) ── */
:root{
--primary:#1F5FA8;--gray-100:#F1F5F9;--gray-200:#E3E9F0;--gray-400:#94A3B8;--gray-500:#64748B;
}
.btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:8px;padding:8px 13px;font:inherit;font-weight:600;font-size:12.5px;cursor:pointer;line-height:1.2}
.btn:disabled{opacity:.55;cursor:not-allowed}
.btn-primary{background:var(--c-blue);color:#fff}.btn-primary:hover:not(:disabled){background:#1A5494}
.btn-outline{background:#fff;color:var(--ink);border-color:var(--line)}.btn-outline:hover:not(:disabled){background:#F5F8FC;border-color:#c7d4e4}
.btn-sm{padding:5px 11px;font-size:11.5px}
a.btn{text-decoration:none}
.pc-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; } .pc-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
.pc-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; } .pc-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
.pc-toolbar { display:flex; justify-content:flex-end; margin-bottom:12px; } .pc-toolbar { display:flex; justify-content:flex-end; margin-bottom:12px; }
@@ -50,16 +61,23 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header"> <main class="wrap">
<h2 data-i18n="pctl.title">Controlli periodici</h2> <nav class="crumb" aria-label="breadcrumb">
<div class="content-header-actions"> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<button class="btn btn-primary btn-sm" onclick="pcOpen()">+ Nuovo controllo</button> <i data-lucide="chevron-right" class="ic"></i><span>Monitoraggio</span>
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="pctl.title">Controlli periodici</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="pctl.title">Controlli periodici</h1>
<p class="sub">Controlli ricorrenti, esecuzioni con evidenza ed esiti (ISO/IEC 27001 §9.1 / A.8.16)</p>
</div> </div>
</header> <button class="btn-primary" onclick="pcOpen()"><i data-lucide="plus" class="ic"></i>Nuovo controllo</button>
<div class="content-body"> </div>
<section class="card card-pad">
<div class="pc-intro"> <div class="pc-intro">
<strong>Controlli periodici (monitoraggio).</strong> <strong>Controlli periodici (monitoraggio).</strong>
Definisci i controlli ricorrenti (con responsabile, frequenza e metodo) e registra ogni esecuzione con esito ed evidenze: Definisci i controlli ricorrenti (con responsabile, frequenza e metodo) e registra ogni esecuzione con esito ed evidenze:
@@ -69,9 +87,8 @@
Ancoraggio: <strong>ISO/IEC 27001 §9.1</strong> (monitoraggio e misurazione) e <strong>A.8.16</strong> (attività di monitoraggio) — <strong>buona prassi</strong>, non un obbligo di legge. Per NIS2 supporta GV.PO-02/DE.CM. Strumento di supporto, non un parere legale. Ancoraggio: <strong>ISO/IEC 27001 §9.1</strong> (monitoraggio e misurazione) e <strong>A.8.16</strong> (attività di monitoraggio) — <strong>buona prassi</strong>, non un obbligo di legge. Per NIS2 supporta GV.PO-02/DE.CM. Strumento di supporto, non un parere legale.
</div> </div>
<div id="pc-list" aria-live="polite"></div> <div id="pc-list" aria-live="polite"></div>
</div> </section>
</main> </main>
</div>
<!-- Modale controllo (crea/modifica) --> <!-- Modale controllo (crea/modifica) -->
<div class="sm-overlay" id="pc-modal" role="dialog" aria-modal="true" aria-labelledby="pc-modal-title"> <div class="sm-overlay" id="pc-modal" role="dialog" aria-modal="true" aria-labelledby="pc-modal-title">
@@ -136,10 +153,12 @@
</div> </div>
</div> </div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<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>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></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> <script>
@@ -155,9 +174,10 @@
document.addEventListener('DOMContentLoaded', async function () { document.addEventListener('DOMContentLoaded', async function () {
if (typeof checkAuth === 'function' && !checkAuth()) return; if (typeof checkAuth === 'function' && !checkAuth()) return;
if (window.I18n && I18n.init) I18n.init('it');
if (typeof loadSidebar === 'function') loadSidebar(); if (typeof loadSidebar === 'function') loadSidebar();
if (window.I18n && I18n.init) I18n.init('it');
if (window.HelpSystem && HelpSystem.init) HelpSystem.init(); if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
document.addEventListener('keydown', e => { if (e.key === 'Escape') { pcClose(); detClose(); execClose(); } }); document.addEventListener('keydown', e => { if (e.key === 'Escape') { pcClose(); detClose(); execClose(); } });
['pc-modal','det-modal','exec-modal'].forEach(m => { const o = el(m); if (o) o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); }); }); ['pc-modal','det-modal','exec-modal'].forEach(m => { const o = el(m); if (o) o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); }); });
try { const r = await api.orgRolesList(); PC.roles = (r && (r.roles || r)) || []; } catch (e) { PC.roles = []; } try { const r = await api.orgRolesList(); PC.roles = (r && (r.roles || r)) || []; } catch (e) { PC.roles = []; }
+185 -334
View File
@@ -3,274 +3,114 @@
<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>AI Cross-Analysis — NIS2 Agile</title> <title>AI Cross-Analysis · 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 (self-host only, nessun CDN esterno). -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (chat/portfolio/storico) -> 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">
<style> <style>
:root { :root {
--nis2-cyan: #06B6D4; --nis2-cyan: #06B6D4;
--nis2-cyan-light: #ecfeff; --nis2-cyan-light: #ecfeff;
--l4-purple: #7c3aed; --l4-purple: #7c3aed;
--l4-purple-bg: #f5f3ff; --l4-purple-bg: #f5f3ff;
/* compat token (ex style.css, niente CDN) */
--gray-100:#F1F5F9;--gray-200:#E3E9F0;--gray-300:#CBD5E1;--gray-400:#94A3B8;
--gray-500:#64748B;--gray-600:#475569;--gray-700:#334155;--gray-800:#1E293B;
--card-bg:#fff;--danger:#B42318;--danger-bg:#FEECEB;--warning:#B45309;--secondary:#16794A;
--border-radius:10px;--border-radius-sm:7px;--border-radius-lg:14px;
--transition:.18s ease;--font-family:inherit;
} }
/* Icone sprite BI: dimensione coerente inline */ /* Icone inline (Lucide stroke) renderizzate via stringa: stile coerente inline */
.ico { width: 1em; height: 1em; vertical-align: -0.125em; fill: currentColor; } .ico { width:1em; height:1em; vertical-align:-0.125em; fill:none; stroke:currentColor; stroke-width:2; stroke-linecap:round; stroke-linejoin:round; }
.ico-lg { width: 2.5rem; height: 2.5rem; } .ico-lg { width:2.5rem; height:2.5rem; }
/* badge testuali pagina */
.privacy-badge { display:inline-flex; align-items:center; gap:5px; background:#d1fae5; color:#065f46; border-radius:20px; padding:3px 10px; font-size:.72rem; font-weight:700; }
.role-badge { display:inline-flex; align-items:center; gap:5px; background:var(--l4-purple-bg); color:var(--l4-purple); font-size:.75rem; font-weight:600; padding:4px 10px; border-radius:12px; }
/* ── Chat container ─────────────────────────── */ /* ── Chat container ─────────────────────────── */
.chat-wrap { .chat-wrap { display:flex; flex-direction:column; height:calc(100vh - 320px); min-height:460px; }
display: flex; .chat-messages { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:14px; }
flex-direction: column; .chat-input-area { border-top:1px solid var(--gray-200); padding:16px; background:var(--card-bg); border-radius:0 0 var(--border-radius-lg) var(--border-radius-lg); }
height: calc(100vh - 200px); .chat-input-row { display:flex; gap:10px; align-items:flex-end; }
min-height: 500px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 14px;
}
.chat-input-area {
border-top: 1px solid var(--gray-200);
padding: 16px;
background: var(--card-bg);
border-radius: 0 0 var(--border-radius-lg) var(--border-radius-lg);
}
.chat-input-row {
display: flex;
gap: 10px;
align-items: flex-end;
}
.chat-input-row textarea { .chat-input-row textarea {
flex: 1; flex:1; min-height:56px; max-height:140px; resize:vertical; padding:10px 14px;
min-height: 56px; border:1.5px solid var(--gray-300); border-radius:var(--border-radius); font:inherit; font-size:.9rem; line-height:1.5; transition:border-color var(--transition);
max-height: 140px;
resize: vertical;
padding: 10px 14px;
border: 1.5px solid var(--gray-300);
border-radius: var(--border-radius);
font-family: var(--font-family);
font-size: 0.9rem;
line-height: 1.5;
transition: border-color var(--transition);
} }
.chat-input-row textarea:focus { .chat-input-row textarea:focus { outline:none; border-color:var(--l4-purple); }
outline: none; .btn-send { background:var(--l4-purple); color:#fff; border:none; border-radius:var(--border-radius); padding:14px 18px; cursor:pointer; font-size:1rem; transition:background .2s; flex-shrink:0; }
border-color: var(--l4-purple); .btn-send:hover:not(:disabled) { background:#6d28d9; }
} .btn-send:disabled { opacity:.5; cursor:not-allowed; }
.btn-send { .btn-send:focus-visible { outline:3px solid #fff; outline-offset:2px; box-shadow:0 0 0 5px var(--l4-purple); }
background: var(--l4-purple);
color: white;
border: none;
border-radius: var(--border-radius);
padding: 14px 18px;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
flex-shrink: 0;
}
.btn-send:hover:not(:disabled) { background: #6d28d9; }
.btn-send:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-send:focus-visible { outline: 3px solid #fff; outline-offset: 2px; box-shadow: 0 0 0 5px var(--l4-purple); }
/* ── Message bubbles ────────────────────────── */ /* ── Message bubbles ────────────────────────── */
.msg { .msg { display:flex; gap:10px; align-items:flex-start; }
display: flex; .msg.user { flex-direction:row-reverse; }
gap: 10px; .msg-avatar { width:34px; height:34px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:.85rem; flex-shrink:0; }
align-items: flex-start; .msg.user .msg-avatar { background:var(--l4-purple); color:#fff; }
} .msg.ai .msg-avatar { background:var(--gray-800); color:#fff; }
.msg.user { flex-direction: row-reverse; } .msg-body { max-width:78%; flex:1; }
.msg-avatar { .msg.user .msg-body { display:flex; justify-content:flex-end; }
width: 34px; .msg-bubble { padding:10px 14px; border-radius:var(--border-radius); font-size:.88rem; line-height:1.55; }
height: 34px; .msg.user .msg-bubble { background:var(--l4-purple); color:#fff; border-radius:var(--border-radius) 0 var(--border-radius) var(--border-radius); }
border-radius: 50%; .msg.ai .msg-bubble { background:var(--gray-100); color:var(--gray-800); border-radius:0 var(--border-radius) var(--border-radius) var(--border-radius); width:100%; }
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
flex-shrink: 0;
}
.msg.user .msg-avatar { background: var(--l4-purple); color: white; }
.msg.ai .msg-avatar { background: var(--gray-800); color: white; }
.msg-body {
max-width: 78%;
flex: 1;
}
.msg.user .msg-body { display: flex; justify-content: flex-end; }
.msg-bubble {
padding: 10px 14px;
border-radius: var(--border-radius);
font-size: 0.88rem;
line-height: 1.55;
}
.msg.user .msg-bubble {
background: var(--l4-purple);
color: white;
border-radius: var(--border-radius) 0 var(--border-radius) var(--border-radius);
}
.msg.ai .msg-bubble {
background: var(--gray-100);
color: var(--gray-800);
border-radius: 0 var(--border-radius) var(--border-radius) var(--border-radius);
width: 100%;
}
/* ── AI response structured ─────────────────── */ /* ── AI response structured ─────────────────── */
.ai-answer { margin-bottom: 12px; font-size: 0.88rem; line-height: 1.6; } .ai-answer { margin-bottom:12px; font-size:.88rem; line-height:1.6; }
.ai-section { margin-top: 12px; } .ai-section { margin-top:12px; }
.ai-section-title { .ai-section-title { display:flex; align-items:center; gap:6px; font-size:.72rem; font-weight:700; text-transform:uppercase; letter-spacing:.05em; color:var(--gray-500); margin-bottom:5px; }
font-size: 0.72rem; .ai-section ul { margin:0; padding:0 0 0 16px; font-size:.84rem; color:var(--gray-700); }
font-weight: 700; .ai-section ul li { margin-bottom:3px; }
text-transform: uppercase; .ai-benchmark { background:var(--l4-purple-bg); border-left:3px solid var(--l4-purple); padding:8px 12px; border-radius:0 var(--border-radius-sm) var(--border-radius-sm) 0; font-size:.82rem; color:var(--gray-700); margin-top:10px; }
letter-spacing: 0.05em; .ai-privacy-note { background:#fef3c7; border-left:3px solid var(--warning); padding:6px 10px; border-radius:0 var(--border-radius-sm) var(--border-radius-sm) 0; font-size:.78rem; color:#92400e; margin-top:8px; }
color: var(--gray-500); .msg-time { font-size:.68rem; color:var(--gray-400); margin-top:3px; text-align:right; }
margin-bottom: 5px; .msg.ai .msg-time { text-align:left; }
}
.ai-section ul {
margin: 0; padding: 0 0 0 16px;
font-size: 0.84rem;
color: var(--gray-700);
}
.ai-section ul li { margin-bottom: 3px; }
.ai-benchmark {
background: var(--l4-purple-bg);
border-left: 3px solid var(--l4-purple);
padding: 8px 12px;
border-radius: 0 var(--border-radius-sm) var(--border-radius-sm) 0;
font-size: 0.82rem;
color: var(--gray-700);
margin-top: 10px;
}
.ai-privacy-note {
background: #fef3c7;
border-left: 3px solid var(--warning);
padding: 6px 10px;
border-radius: 0 var(--border-radius-sm) var(--border-radius-sm) 0;
font-size: 0.78rem;
color: #92400e;
margin-top: 8px;
}
.msg-time {
font-size: 0.68rem;
color: var(--gray-400);
margin-top: 3px;
text-align: right;
}
.msg.ai .msg-time { text-align: left; }
/* ── Thinking bubble ────────────────────────── */ /* ── Thinking bubble ────────────────────────── */
.thinking-bubble { .thinking-bubble { display:flex; gap:5px; padding:12px 16px; background:var(--gray-100); border-radius:0 var(--border-radius) var(--border-radius) var(--border-radius); width:fit-content; }
display: flex; .thinking-dot { width:7px; height:7px; background:var(--gray-400); border-radius:50%; animation:thinkBounce 1.2s infinite; }
gap: 5px; .thinking-dot:nth-child(2) { animation-delay:.2s; }
padding: 12px 16px; .thinking-dot:nth-child(3) { animation-delay:.4s; }
background: var(--gray-100); @keyframes thinkBounce { 0%,60%,100% { transform:translateY(0); } 30% { transform:translateY(-8px); } }
border-radius: 0 var(--border-radius) var(--border-radius) var(--border-radius);
width: fit-content;
}
.thinking-dot {
width: 7px; height: 7px;
background: var(--gray-400);
border-radius: 50%;
animation: thinkBounce 1.2s infinite;
}
.thinking-dot:nth-child(2) { animation-delay: 0.2s; }
.thinking-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes thinkBounce {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-8px); }
}
/* ── Portfolio summary card ─────────────────── */ /* ── Portfolio summary card ─────────────────── */
.portfolio-header { .portfolio-header { background:linear-gradient(135deg, #4c1d95, #6d28d9); border-radius:var(--border-radius-lg); padding:18px 22px; color:#fff; margin-bottom:16px; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; }
background: linear-gradient(135deg, #4c1d95, #6d28d9); .portfolio-stat { text-align:center; }
border-radius: var(--border-radius-lg); .portfolio-stat .val { font-size:1.8rem; font-weight:800; color:#c4b5fd; }
padding: 18px 22px; .portfolio-stat .lbl { font-size:.68rem; opacity:.7; text-transform:uppercase; letter-spacing:.04em; }
color: white;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
}
.portfolio-stat {
text-align: center;
}
.portfolio-stat .val { font-size: 1.8rem; font-weight: 800; color: #c4b5fd; }
.portfolio-stat .lbl { font-size: 0.68rem; opacity: 0.7; text-transform: uppercase; letter-spacing: 0.04em; }
/* ── Quick questions ────────────────────────── */ /* ── Quick questions ────────────────────────── */
.quick-qs { .quick-qs { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px; }
display: flex; .quick-q { background:var(--l4-purple-bg); color:var(--l4-purple); border:1px solid #ddd6fe; border-radius:20px; padding:5px 12px; font:inherit; font-size:.77rem; cursor:pointer; transition:background .2s; }
gap: 8px; .quick-q:hover { background:#ede9fe; }
flex-wrap: wrap; .quick-q:focus-visible { outline:2px solid var(--l4-purple); outline-offset:2px; }
margin-bottom: 12px;
}
.quick-q {
background: var(--l4-purple-bg);
color: var(--l4-purple);
border: 1px solid #ddd6fe;
border-radius: 20px;
padding: 5px 12px;
font-size: 0.77rem;
cursor: pointer;
transition: background 0.2s;
}
.quick-q:hover { background: #ede9fe; }
.quick-q:focus-visible { outline: 2px solid var(--l4-purple); outline-offset: 2px; }
/* ── Privacy badge ──────────────────────────── */
.privacy-badge {
display: inline-flex;
align-items: center;
gap: 5px;
background: #d1fae5;
color: #065f46;
border-radius: 20px;
padding: 3px 10px;
font-size: 0.72rem;
font-weight: 600;
}
/* ── Empty state ────────────────────────────── */ /* ── Empty state ────────────────────────────── */
.chat-empty { .chat-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:var(--gray-400); text-align:center; gap:10px; }
display: flex; .chat-empty .ico { color:#c4b5fd; }
flex-direction: column; .chat-empty h4 { color:var(--gray-600); font-size:.95rem; margin:0; }
align-items: center; .chat-empty p { font-size:.82rem; max-width:360px; margin:0; }
justify-content: center;
height: 100%;
color: var(--gray-400);
text-align: center;
gap: 10px;
}
.chat-empty .ico { color: #c4b5fd; }
.chat-empty h4 { color: var(--gray-600); font-size: 0.95rem; }
.chat-empty p { font-size: 0.82rem; max-width: 360px; }
/* ── Sidebar tab ────────────────────────────── */ /* ── Sidebar tab ────────────────────────────── */
.tab-bar { display: flex; border-bottom: 1px solid var(--gray-200); margin-bottom: 0; } .tab-bar { display:flex; border-bottom:1px solid var(--gray-200); margin-bottom:0; flex-wrap:wrap; }
.tab-btn { .tab-btn { display:inline-flex; align-items:center; gap:6px; padding:10px 18px; border:none; background:none; font:inherit; font-size:.84rem; font-weight:600; color:var(--gray-500); cursor:pointer; border-bottom:2px solid transparent; margin-bottom:-1px; transition:color .2s, border-color .2s; }
padding: 10px 18px; .tab-btn.active { color:var(--l4-purple); border-bottom-color:var(--l4-purple); }
border: none; .tab-btn:focus-visible { outline:2px solid var(--l4-purple); outline-offset:-2px; }
background: none; .tab-pane { display:none; }
font-size: 0.84rem; .tab-pane.active { display:block; }
font-weight: 600;
color: var(--gray-500); /* ── portfolio tab helper grids (ex style.css) ── */
cursor: pointer; .ca-grid2 { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:16px; }
border-bottom: 2px solid transparent; .ca-sub { background:var(--card-bg); border:1px solid var(--gray-200); border-radius:var(--border-radius); overflow:hidden; }
margin-bottom: -1px; .ca-sub-head { padding:12px 16px; border-bottom:1px solid var(--gray-100); }
transition: color 0.2s, border-color 0.2s; .ca-sub-head h4 { margin:0; font-size:.9rem; }
} .ca-sub-body { padding:14px 16px; }
.tab-btn.active { color: var(--l4-purple); border-bottom-color: var(--l4-purple); } .ca-cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:12px; }
.tab-btn:focus-visible { outline: 2px solid var(--l4-purple); outline-offset: -2px; } .ca-card { background:var(--card-bg); border:1px solid var(--gray-200); border-radius:var(--border-radius); padding:12px 14px; }
.tab-pane { display: none; } .ca-spinner { width:30px; height:30px; border:3px solid var(--gray-200); border-top-color:var(--l4-purple); border-radius:50%; animation:ca-spin .8s linear infinite; margin:30px auto; }
.tab-pane.active { display: block; } @keyframes ca-spin { to { transform:rotate(360deg); } }
.text-muted { color:var(--gray-500); }
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -287,25 +127,25 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header">
<h2>
<svg class="ico" aria-hidden="true" style="color:var(--l4-purple);margin-right:8px;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-software"></use></svg>
AI Cross-Analysis
<span class="privacy-badge" style="margin-left:10px;">
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-lock"></use></svg> Dati aggregati — privacy garantita
</span>
</h2>
<div class="content-header-actions">
<span class="badge" style="background:var(--l4-purple-bg);color:var(--l4-purple);font-size:0.75rem;padding:4px 10px;border-radius:12px;">
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg> Solo Consulenti &amp; Admin
</span>
</div>
</header>
<div class="content-body"> <main class="wrap">
<nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>AI Cross-Analysis</span>
</nav>
<div class="page-head">
<div>
<h1 style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<i data-lucide="cpu" class="ic" style="width:24px;height:24px;color:var(--l4-purple);"></i>
AI Cross-Analysis
<span class="privacy-badge"><i data-lucide="lock" class="ic"></i> Dati aggregati — privacy garantita</span>
</h1>
<p class="sub">Analisi AI aggregata sul portfolio clienti — dati anonimizzati</p>
</div>
<span class="role-badge"><i data-lucide="user" class="ic"></i> Solo Consulenti &amp; Admin</span>
</div>
<!-- Portfolio header --> <!-- Portfolio header -->
<div class="portfolio-header" id="portfolio-header"> <div class="portfolio-header" id="portfolio-header">
@@ -319,13 +159,13 @@
<div class="card" style="padding:0;overflow:hidden;"> <div class="card" style="padding:0;overflow:hidden;">
<div class="tab-bar" role="tablist" aria-label="Sezioni AI Cross-Analysis"> <div class="tab-bar" role="tablist" aria-label="Sezioni AI Cross-Analysis">
<button class="tab-btn active" onclick="switchTab('chat')" role="tab" aria-selected="true" aria-controls="tab-chat"> <button class="tab-btn active" onclick="switchTab('chat')" role="tab" aria-selected="true" aria-controls="tab-chat">
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-comment"></use></svg> Chat AI <i data-lucide="message-circle" class="ic"></i> Chat AI
</button> </button>
<button class="tab-btn" onclick="switchTab('history')" role="tab" aria-selected="false" aria-controls="tab-history"> <button class="tab-btn" onclick="switchTab('history')" role="tab" aria-selected="false" aria-controls="tab-history">
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-clock"></use></svg> Storico <i data-lucide="clock" class="ic"></i> Storico
</button> </button>
<button class="tab-btn" onclick="switchTab('portfolio')" role="tab" aria-selected="false" aria-controls="tab-portfolio"> <button class="tab-btn" onclick="switchTab('portfolio')" role="tab" aria-selected="false" aria-controls="tab-portfolio">
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-chart-line"></use></svg> Portfolio dati <i data-lucide="line-chart" class="ic"></i> Portfolio dati
</button> </button>
</div> </div>
@@ -334,7 +174,7 @@
<div class="chat-wrap"> <div class="chat-wrap">
<div class="chat-messages" id="chat-messages" aria-live="polite"> <div class="chat-messages" id="chat-messages" aria-live="polite">
<div class="chat-empty" id="chat-empty"> <div class="chat-empty" id="chat-empty">
<svg class="ico ico-lg" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-software"></use></svg> <i data-lucide="cpu" class="ico ico-lg"></i>
<h4>Analisi AI del tuo portfolio clienti</h4> <h4>Analisi AI del tuo portfolio clienti</h4>
<p>Fai una domanda sui dati aggregati delle tue organizzazioni. I dati vengono trasmessi in forma anonima e aggregata.</p> <p>Fai una domanda sui dati aggregati delle tue organizzazioni. I dati vengono trasmessi in forma anonima e aggregata.</p>
</div> </div>
@@ -349,15 +189,15 @@
<button class="quick-q" onclick="setQuestion('Benchmark: dove si posiziona il mio portfolio rispetto agli standard NIS2?')">Benchmark NIS2</button> <button class="quick-q" onclick="setQuestion('Benchmark: dove si posiziona il mio portfolio rispetto agli standard NIS2?')">Benchmark NIS2</button>
</div> </div>
<div class="chat-input-row"> <div class="chat-input-row">
<label for="chat-input" class="visually-hidden">Domanda all'AI sul portfolio</label> <label for="chat-input" class="visually-hidden" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);">Domanda all'AI sul portfolio</label>
<textarea id="chat-input" placeholder="Es: Quali sono le aree più critiche nel mio portfolio clienti?" rows="2" <textarea id="chat-input" placeholder="Es: Quali sono le aree più critiche nel mio portfolio clienti?" rows="2"
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage();}"></textarea> onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage();}"></textarea>
<button class="btn-send" onclick="sendMessage()" id="btn-send" aria-label="Invia domanda"> <button class="btn-send" onclick="sendMessage()" id="btn-send" aria-label="Invia domanda">
<svg class="ico" aria-hidden="true" style="color:#fff;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-mail"></use></svg> <i data-lucide="send" class="ic" style="color:#fff;"></i>
</button> </button>
</div> </div>
<div style="font-size:0.7rem;color:var(--gray-400);margin-top:6px;text-align:center;"> <div style="font-size:0.7rem;color:var(--gray-400);margin-top:6px;text-align:center;display:flex;align-items:center;gap:5px;justify-content:center;">
<svg class="ico" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-locked"></use></svg> <i data-lucide="lock" class="ico"></i>
Dati anonimizzati e aggregati — nessun nome organizzazione inviato all'AI Dati anonimizzati e aggregati — nessun nome organizzazione inviato all'AI
</div> </div>
</div> </div>
@@ -367,48 +207,59 @@
<!-- TAB: HISTORY --> <!-- TAB: HISTORY -->
<div class="tab-pane" id="tab-history" role="tabpanel"> <div class="tab-pane" id="tab-history" role="tabpanel">
<div style="padding:16px;" id="history-content"> <div style="padding:16px;" id="history-content">
<div class="spinner" style="margin:30px auto;display:block;"></div> <div class="ca-spinner"></div>
</div> </div>
</div> </div>
<!-- TAB: PORTFOLIO DATI --> <!-- TAB: PORTFOLIO DATI -->
<div class="tab-pane" id="tab-portfolio" role="tabpanel"> <div class="tab-pane" id="tab-portfolio" role="tabpanel">
<div style="padding:16px;" id="portfolio-content"> <div style="padding:16px;" id="portfolio-content">
<div class="spinner" style="margin:30px auto;display:block;"></div> <div class="ca-spinner"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
</main> </main>
</div>
<script src="/js/api.js?v=20260625d"></script> <!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
<script src="/js/common.js?v=20260624g"></script> topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <script src="/js/api.js?v=20260625d"></script>
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, <script src="/js/common.js?v=20260627v3"></script>
riusando tutti gli helper di common.js (zero backend). --> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/js/topnav-v3.js?v=20260627"></script>
<script> <script src="/js/i18n.js?v=20260631"></script>
if (window.bootstrap && bootstrap.loadFonts) { <script src="/js/help.js?v=20260625d"></script>
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); <script>
// ── Icone inline (Lucide stroke) usate nelle stringhe HTML generate dal JS.
// Renderizzate immediatamente (no createIcons re-run), self-host, niente sprite BI. ──
const ICO_PATHS = {
'it-software': '<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 2v2M15 2v2M9 20v2M15 20v2M20 9h2M20 14h2M2 9h2M2 14h2"/>',
'it-search': '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
'it-note': '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M16 13H8M16 17H8M10 9H8"/>',
'it-warning-circle': '<circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/>',
'it-chart-line': '<path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/>',
'it-lock': '<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
'it-close': '<path d="M18 6 6 18M6 6l12 12"/>',
'it-user': '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>'
};
// Compat con il vecchio helper ico(name, cls): emette SVG inline (stroke), dimensione via .ico/.ico-lg.
function ico(name, cls) {
const p = ICO_PATHS[name] || ICO_PATHS['it-software'];
return `<svg class="ico ${cls||''}" viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;
} }
</script>
<script src="/js/common-bi.js?v=20260625d"></script>
<script src="/js/i18n.js?v=20260631"></script>
<script>
const SPRITE = '/vendor/bootstrap-italia/dist/svg/sprites.svg';
function ico(name, cls) { return `<svg class="ico ${cls||''}" aria-hidden="true"><use href="${SPRITE}#${name}"></use></svg>`; }
if (!checkAuth()) throw new Error('Not authenticated'); // ── Auth & Init ───────────────────────────────────────────────────
loadSidebar(); if (!checkAuth()) throw new Error('Not authenticated');
I18n.init(); loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
let PORTFOLIO = null; // dati aggregati portfolio let PORTFOLIO = null; // dati aggregati portfolio
// ── Init ────────────────────────────────────────────────────────── // ── Init ──────────────────────────────────────────────────────────
loadPortfolio(); loadPortfolio();
async function loadPortfolio() { async function loadPortfolio() {
try { try {
const r = await api.get('/cross-analysis/portfolio'); const r = await api.get('/cross-analysis/portfolio');
if (!r.success) { if (!r.success) {
@@ -435,9 +286,9 @@ async function loadPortfolio() {
document.getElementById('portfolio-header').innerHTML = ` document.getElementById('portfolio-header').innerHTML = `
<div style="color:white;opacity:0.7;">Errore caricamento portfolio</div>`; <div style="color:white;opacity:0.7;">Errore caricamento portfolio</div>`;
} }
} }
function renderPortfolioHeader(d) { function renderPortfolioHeader(d) {
const agg = d.aggregated || {}; const agg = d.aggregated || {};
const score = agg.avg_compliance_score ?? '—'; const score = agg.avg_compliance_score ?? '—';
const totalRisks = agg.risks?.total_open ?? '—'; const totalRisks = agg.risks?.total_open ?? '—';
@@ -451,20 +302,20 @@ function renderPortfolioHeader(d) {
<div class="portfolio-stat"><div class="val">${totalRisks}</div><div class="lbl">Rischi aperti</div></div> <div class="portfolio-stat"><div class="val">${totalRisks}</div><div class="lbl">Rischi aperti</div></div>
<div class="portfolio-stat"><div class="val">${trainPct}${trainPct!=='—'?'%':''}</div><div class="lbl">Formazione media</div></div> <div class="portfolio-stat"><div class="val">${trainPct}${trainPct!=='—'?'%':''}</div><div class="lbl">Formazione media</div></div>
<div class="portfolio-stat"><div class="val">${d.org_count}</div><div class="lbl">Organizzazioni</div></div>`; <div class="portfolio-stat"><div class="val">${d.org_count}</div><div class="lbl">Organizzazioni</div></div>`;
} }
function renderPortfolioTab(d) { function renderPortfolioTab(d) {
const agg = d.aggregated; const agg = d.aggregated;
if (!agg) { if (!agg) {
document.getElementById('portfolio-content').innerHTML = '<p class="text-muted">Nessun dato disponibile.</p>'; document.getElementById('portfolio-content').innerHTML = '<p class="text-muted">Nessun dato disponibile.</p>';
return; return;
} }
let html = '<div class="grid-2" style="gap:16px;">'; let html = '<div class="ca-grid2">';
// Settori // Settori
if (agg.by_sector && Object.keys(agg.by_sector).length) { if (agg.by_sector && Object.keys(agg.by_sector).length) {
html += `<div class="card" style="margin:0;"><div class="card-header"><h4>Distribuzione Settoriale</h4></div><div class="card-body">`; html += `<div class="ca-sub"><div class="ca-sub-head"><h4>Distribuzione Settoriale</h4></div><div class="ca-sub-body">`;
for (const [sec, cnt] of Object.entries(agg.by_sector)) { for (const [sec, cnt] of Object.entries(agg.by_sector)) {
const pct = Math.round(cnt / d.org_count * 100); const pct = Math.round(cnt / d.org_count * 100);
html += `<div style="margin-bottom:8px;"> html += `<div style="margin-bottom:8px;">
@@ -481,7 +332,7 @@ function renderPortfolioTab(d) {
// Compliance score // Compliance score
if (agg.score_distribution) { if (agg.score_distribution) {
html += `<div class="card" style="margin:0;"><div class="card-header"><h4>Distribuzione Score Compliance</h4></div><div class="card-body">`; html += `<div class="ca-sub"><div class="ca-sub-head"><h4>Distribuzione Score Compliance</h4></div><div class="ca-sub-body">`;
const colors = {'0-20%':'var(--danger)','21-40%':'#f97316','41-60%':'var(--warning)','61-80%':'#84cc16','81-100%':'var(--secondary)'}; const colors = {'0-20%':'var(--danger)','21-40%':'#f97316','41-60%':'var(--warning)','61-80%':'#84cc16','81-100%':'var(--secondary)'};
for (const [range, cnt] of Object.entries(agg.score_distribution)) { for (const [range, cnt] of Object.entries(agg.score_distribution)) {
if (cnt === 0) continue; if (cnt === 0) continue;
@@ -497,11 +348,11 @@ function renderPortfolioTab(d) {
// Categorie NIS2 deboli // Categorie NIS2 deboli
if (agg.assessments?.avg_by_category && Object.keys(agg.assessments.avg_by_category).length) { if (agg.assessments?.avg_by_category && Object.keys(agg.assessments.avg_by_category).length) {
const cats = Object.entries(agg.assessments.avg_by_category).slice(0, 5); const cats = Object.entries(agg.assessments.avg_by_category).slice(0, 5);
html += `<div class="card" style="margin:0;grid-column:1/-1;"><div class="card-header"><h4>Score Medio per Categoria NIS2 (dal più basso)</h4></div><div class="card-body">`; html += `<div class="ca-sub" style="grid-column:1/-1;"><div class="ca-sub-head"><h4>Score Medio per Categoria NIS2 (dal più basso)</h4></div><div class="ca-sub-body">`;
html += '<div class="step-cards" style="grid-template-columns:repeat(auto-fill,minmax(180px,1fr));">'; html += '<div class="ca-cards">';
for (const [cat, score] of cats) { for (const [cat, score] of cats) {
const color = score < 30 ? 'var(--danger)' : score < 60 ? 'var(--warning)' : 'var(--secondary)'; const color = score < 30 ? 'var(--danger)' : score < 60 ? 'var(--warning)' : 'var(--secondary)';
html += `<div class="step-card" style="cursor:default;"> html += `<div class="ca-card">
<div style="font-size:0.75rem;color:var(--gray-500);margin-bottom:4px;">${cat}</div> <div style="font-size:0.75rem;color:var(--gray-500);margin-bottom:4px;">${cat}</div>
<div style="font-size:1.4rem;font-weight:800;color:${color};">${score}%</div> <div style="font-size:1.4rem;font-weight:800;color:${color};">${score}%</div>
<div style="height:3px;background:var(--gray-200);border-radius:2px;margin-top:6px;"> <div style="height:3px;background:var(--gray-200);border-radius:2px;margin-top:6px;">
@@ -514,10 +365,10 @@ function renderPortfolioTab(d) {
html += '</div>'; html += '</div>';
document.getElementById('portfolio-content').innerHTML = html; document.getElementById('portfolio-content').innerHTML = html;
} }
// ── Tab switching ───────────────────────────────────────────────── // ── Tab switching ─────────────────────────────────────────────────
function switchTab(tab) { function switchTab(tab) {
const tabs = ['chat','history','portfolio']; const tabs = ['chat','history','portfolio'];
document.querySelectorAll('.tab-btn').forEach((b, i) => { document.querySelectorAll('.tab-btn').forEach((b, i) => {
const active = tabs[i] === tab; const active = tabs[i] === tab;
@@ -527,12 +378,12 @@ function switchTab(tab) {
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
document.getElementById('tab-' + tab).classList.add('active'); document.getElementById('tab-' + tab).classList.add('active');
if (tab === 'history') loadHistory(); if (tab === 'history') loadHistory();
} }
// ── Send message ────────────────────────────────────────────────── // ── Send message ──────────────────────────────────────────────────
let isThinking = false; let isThinking = false;
async function sendMessage() { async function sendMessage() {
if (isThinking) return; if (isThinking) return;
const input = document.getElementById('chat-input'); const input = document.getElementById('chat-input');
const question = input.value.trim(); const question = input.value.trim();
@@ -569,9 +420,9 @@ async function sendMessage() {
document.getElementById('btn-send').disabled = false; document.getElementById('btn-send').disabled = false;
input.focus(); input.focus();
} }
} }
function appendMessage(role, text) { function appendMessage(role, text) {
const msgs = document.getElementById('chat-messages'); const msgs = document.getElementById('chat-messages');
const time = new Date().toLocaleTimeString('it-IT', {hour:'2-digit',minute:'2-digit'}); const time = new Date().toLocaleTimeString('it-IT', {hour:'2-digit',minute:'2-digit'});
const iconName = role === 'user' ? 'it-user' : 'it-software'; const iconName = role === 'user' ? 'it-user' : 'it-software';
@@ -584,9 +435,9 @@ function appendMessage(role, text) {
</div> </div>
</div>`; </div>`;
msgs.scrollTop = msgs.scrollHeight; msgs.scrollTop = msgs.scrollHeight;
} }
function appendAIResponse(result, orgCount) { function appendAIResponse(result, orgCount) {
const msgs = document.getElementById('chat-messages'); const msgs = document.getElementById('chat-messages');
const time = new Date().toLocaleTimeString('it-IT', {hour:'2-digit',minute:'2-digit'}); const time = new Date().toLocaleTimeString('it-IT', {hour:'2-digit',minute:'2-digit'});
@@ -628,9 +479,9 @@ function appendAIResponse(result, orgCount) {
</div> </div>
</div>`; </div>`;
msgs.scrollTop = msgs.scrollHeight; msgs.scrollTop = msgs.scrollHeight;
} }
function appendError(msg) { function appendError(msg) {
const msgs = document.getElementById('chat-messages'); const msgs = document.getElementById('chat-messages');
msgs.innerHTML += ` msgs.innerHTML += `
<div class="msg ai"> <div class="msg ai">
@@ -642,9 +493,9 @@ function appendError(msg) {
</div> </div>
</div>`; </div>`;
msgs.scrollTop = msgs.scrollHeight; msgs.scrollTop = msgs.scrollHeight;
} }
function appendThinking(id) { function appendThinking(id) {
const msgs = document.getElementById('chat-messages'); const msgs = document.getElementById('chat-messages');
msgs.innerHTML += ` msgs.innerHTML += `
<div class="msg ai" id="${id}"> <div class="msg ai" id="${id}">
@@ -658,16 +509,16 @@ function appendThinking(id) {
</div> </div>
</div>`; </div>`;
msgs.scrollTop = msgs.scrollHeight; msgs.scrollTop = msgs.scrollHeight;
} }
function removeThinking(id) { function removeThinking(id) {
document.getElementById(id)?.remove(); document.getElementById(id)?.remove();
} }
// ── History ─────────────────────────────────────────────────────── // ── History ───────────────────────────────────────────────────────
async function loadHistory() { async function loadHistory() {
const el = document.getElementById('history-content'); const el = document.getElementById('history-content');
el.innerHTML = '<div class="spinner" style="margin:30px auto;display:block;"></div>'; el.innerHTML = '<div class="ca-spinner"></div>';
try { try {
const r = await api.get('/cross-analysis/history?limit=20'); const r = await api.get('/cross-analysis/history?limit=20');
if (!r.success) { el.innerHTML = '<p class="text-muted">Errore caricamento storico.</p>'; return; } if (!r.success) { el.innerHTML = '<p class="text-muted">Errore caricamento storico.</p>'; return; }
@@ -689,23 +540,23 @@ async function loadHistory() {
} catch(e) { } catch(e) {
el.innerHTML = '<p class="text-muted">Errore: ' + escapeHtml(e.message) + '</p>'; el.innerHTML = '<p class="text-muted">Errore: ' + escapeHtml(e.message) + '</p>';
} }
} }
// ── Helpers ─────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────
function setQuestion(q) { function setQuestion(q) {
document.getElementById('chat-input').value = q; document.getElementById('chat-input').value = q;
document.getElementById('chat-input').focus(); document.getElementById('chat-input').focus();
autoResizeTextarea(document.getElementById('chat-input')); autoResizeTextarea(document.getElementById('chat-input'));
} }
function autoResizeTextarea(el) { function autoResizeTextarea(el) {
el.style.height = 'auto'; el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 140) + 'px'; el.style.height = Math.min(el.scrollHeight, 140) + 'px';
} }
document.getElementById('chat-input').addEventListener('input', function() { document.getElementById('chat-input').addEventListener('input', function() {
autoResizeTextarea(this); autoResizeTextarea(this);
}); });
</script> </script>
</body> </body>
</html> </html>
+122
View File
@@ -263,3 +263,125 @@ a.stat:hover{box-shadow:0 8px 20px rgba(16,40,80,.13);transform:translateY(-1px)
.ctrl-btn{min-width:44px;min-height:44px} .ctrl-btn{min-width:44px;min-height:44px}
.fab{width:52px;height:52px} .fab{width:52px;height:52px}
} }
/* ═══════════════════════════════════════════════════════════════════════════
* V3 — WIDGET CONDIVISI (compat) — modali/toast/form/bottoni/spinner/progress.
* Generati da common.js (showModal/showNotification) e dal markup di varie
* pagine: vivevano in style.css (rimosso dalle pagine V3). Qui sono ri-portati
* UNA volta sola e ritematizzati sui colori V3. NON ridefinisce .btn-primary /
* .btn-ghost / .badge-t (già in v3.css sopra): nessun conflitto.
* ═════════════════════════════════════════════════════════════════════════ */
:root{
/* alias V2→V3 usati dai widget sottostanti */
--primary:#1F5FA8; --primary-dark:#16487f; --primary-light:#3b7bc4; --primary-bg:#eaf1f9;
--secondary:#1E8A5A; --secondary-light:#27a06b; --secondary-bg:#e3f3ea;
--warning:#D98324; --warning-light:#e89a45; --warning-bg:#fbeede;
--danger:#C62828; --danger-light:#e05656; --danger-bg:#fbe5e5;
--info:#1F5FA8; --info-bg:#eaf1f9;
--gray-50:#f8fafc; --gray-100:#f1f5f9; --gray-200:#e2e8f0; --gray-300:#cbd5e1;
--gray-400:#94a3b8; --gray-500:#64748b; --gray-600:#475569; --gray-700:#334155;
--gray-800:#1e293b; --gray-900:#0f172a;
--card-bg:var(--card); --border-radius:10px; --border-radius-lg:var(--radius); --border-radius-sm:6px;
--transition:250ms ease; --transition-fast:150ms ease;
}
/* Bottoni (varianti non presenti in v3.css; self-sufficient, non toccano primary/ghost) */
.btn-secondary,.btn-success,.btn-danger,.btn-warning,.btn-outline{
display:inline-flex;align-items:center;justify-content:center;gap:8px;padding:10px 18px;
font-size:.875rem;font-weight:600;font-family:inherit;border:1px solid transparent;
border-radius:var(--radius);cursor:pointer;text-decoration:none;line-height:1.4;white-space:nowrap;
transition:background .15s ease,border-color .15s ease,color .15s ease}
.btn-secondary{background:var(--gray-100);color:var(--gray-700);border-color:var(--gray-300)}
.btn-secondary:hover:not(:disabled){background:var(--gray-200);border-color:var(--gray-400)}
.btn-success{background:var(--secondary);color:#fff;border-color:var(--secondary)}
.btn-success:hover:not(:disabled){background:var(--secondary-light)}
.btn-danger{background:var(--danger);color:#fff;border-color:var(--danger)}
.btn-danger:hover:not(:disabled){background:var(--danger-light)}
.btn-warning{background:var(--warning);color:#1a1a1a;border-color:var(--warning)}
.btn-outline{background:transparent;color:var(--navy-nav);border-color:var(--navy-nav)}
.btn-outline:hover:not(:disabled){background:var(--navy-nav);color:#fff}
.btn-secondary:disabled,.btn-success:disabled,.btn-danger:disabled,.btn-warning:disabled,.btn-outline:disabled{opacity:.5;cursor:not-allowed}
.btn-sm{padding:6px 14px;font-size:.8rem}
.btn-lg{padding:14px 28px;font-size:1rem}
.btn-icon{padding:8px;width:36px;height:36px}
.btn-group{display:flex;gap:8px}
/* Form */
.form-group{margin-bottom:20px}
.form-label{display:block;font-size:.8125rem;font-weight:600;color:var(--gray-700);margin-bottom:6px}
.form-label .required{color:var(--danger);margin-left:2px}
.form-group .form-label{position:static;line-height:1.4;padding:0;width:auto;max-width:none;z-index:auto;background-color:transparent;cursor:default}
.form-input,.form-select,.form-textarea{width:100%;padding:10px 14px;font-size:.875rem;font-family:inherit;color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:var(--border-radius);transition:border-color var(--transition-fast),box-shadow var(--transition-fast);outline:none}
.form-input:focus,.form-select:focus,.form-textarea:focus{border-color:var(--navy-nav);box-shadow:0 0 0 3px rgba(31,95,168,.15)}
.form-input::placeholder,.form-textarea::placeholder{color:var(--gray-400)}
.form-input.error,.form-select.error,.form-textarea.error{border-color:var(--danger);box-shadow:0 0 0 3px rgba(198,40,40,.12)}
.form-textarea{resize:vertical;min-height:100px}
.form-select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%2364748b' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:36px}
.form-help{font-size:.75rem;color:var(--gray-500);margin-top:6px}
/* Badge generici (style.css legacy) — coesistono con .badge-t di v3.css */
.badge{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;font-size:.7rem;font-weight:600;border-radius:100px;text-transform:uppercase;letter-spacing:.03em;white-space:nowrap}
.badge-primary{background:var(--primary-bg);color:var(--primary)}
.badge-success{background:var(--secondary-bg);color:#15803d}
.badge-warning{background:var(--warning-bg);color:#a16207}
.badge-danger{background:var(--danger-bg);color:var(--danger)}
.badge-info{background:var(--info-bg);color:var(--primary)}
.badge-neutral{background:var(--gray-100);color:var(--gray-600)}
.badge-lg{padding:6px 16px;font-size:.8rem}
.tag{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;font-size:.75rem;font-weight:500;background:var(--gray-100);color:var(--gray-600);border-radius:var(--border-radius-sm);border:1px solid var(--gray-200)}
/* Progress */
.progress{width:100%;height:8px;background:var(--gray-200);border-radius:100px;overflow:hidden}
.progress-bar{height:100%;border-radius:100px;transition:width .8s ease;background:var(--navy-nav)}
.progress-bar.success{background:var(--secondary)}
.progress-bar.warning{background:var(--warning)}
.progress-bar.danger{background:var(--danger)}
.progress-lg{height:12px}
.progress-label{display:flex;justify-content:space-between;margin-bottom:6px;font-size:.8rem}
.progress-label span:first-child{font-weight:600;color:var(--gray-700)}
.progress-label span:last-child{color:var(--gray-500)}
/* Modal (showModal di common.js) */
.modal-overlay{position:fixed;inset:0;width:100%;height:100%;background:rgba(15,23,42,.6);backdrop-filter:blur(4px);z-index:1000;display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:all var(--transition)}
.modal-overlay.active{opacity:1;visibility:visible}
.modal{background:var(--card);border-radius:var(--border-radius-lg);box-shadow:0 20px 60px rgba(0,0,0,.2);width:90%;max-width:560px;max-height:85vh;overflow-y:auto;transform:translateY(20px) scale(.95);transition:transform var(--transition)}
.modal-overlay.active .modal{transform:translateY(0) scale(1)}
.modal-overlay .modal{display:block;position:relative;inset:auto;margin:0;z-index:auto;pointer-events:auto}
.modal-header{padding:20px 24px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between}
.modal-header h3{font-size:1.125rem;font-weight:700;color:var(--ink)}
.modal-close{background:none;border:none;color:var(--gray-400);cursor:pointer;padding:4px;border-radius:var(--border-radius-sm);transition:color var(--transition-fast);line-height:1;font-size:1.4rem}
.modal-close:hover{color:var(--gray-700)}
.modal-body{padding:24px}
.modal-footer{padding:16px 24px;border-top:1px solid var(--line);display:flex;justify-content:flex-end;gap:8px}
/* Notifiche / toast (showNotification di common.js) */
.notification-container{position:fixed;top:20px;right:20px;z-index:2000;display:flex;flex-direction:column;gap:8px;pointer-events:none}
.notification{background:var(--card);border-radius:var(--border-radius);box-shadow:0 8px 24px rgba(0,0,0,.15);padding:14px 20px;display:flex;align-items:center;gap:12px;min-width:320px;max-width:480px;pointer-events:auto;animation:slideIn .3s ease;border-left:4px solid var(--gray-400)}
.notification.success{border-left-color:var(--secondary)}
.notification.error{border-left-color:var(--danger)}
.notification.warning{border-left-color:var(--warning)}
.notification.info{border-left-color:var(--primary)}
.notification-icon{width:20px;height:20px;flex-shrink:0}
.notification.success .notification-icon{color:var(--secondary)}
.notification.error .notification-icon{color:var(--danger)}
.notification.warning .notification-icon{color:var(--warning)}
.notification.info .notification-icon{color:var(--primary)}
.notification-message{flex:1;font-size:.875rem;color:var(--gray-700);font-weight:500}
.notification-close{background:none;border:none;color:var(--gray-400);cursor:pointer;padding:2px;font-size:1.1rem;line-height:1}
.notification.fade-out{animation:slideOut .3s ease forwards}
@keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
@keyframes slideOut{from{transform:translateX(0);opacity:1}to{transform:translateX(100%);opacity:0}}
/* Spinner */
.spinner{width:24px;height:24px;border:3px solid var(--gray-200);border-top-color:var(--navy-nav);border-radius:50%;animation:spin .7s linear infinite;margin:0 auto}
.spinner-lg{width:40px;height:40px;border-width:4px}
@keyframes spin{to{transform:rotate(360deg)}}
/* Utility generiche (ex Bootstrap Italia/style.css) usate da markup di varie pagine.
* .btn base SENZA padding → non altera il padding di .btn-primary/.btn-ghost (v3) ; i
* variant (sm/secondary/outline/...) restano self-sufficient. */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;font-family:inherit;font-weight:600;border:1px solid transparent;border-radius:var(--radius);cursor:pointer;text-decoration:none;line-height:1.4;white-space:nowrap;transition:background .15s ease,border-color .15s ease,color .15s ease}
.btn:disabled{opacity:.5;cursor:not-allowed}
.text-muted{color:var(--muted)}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
:root{ --success:var(--c-green); --error:var(--c-red); }
+127 -84
View File
@@ -3,12 +3,42 @@
<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>Guida all'uso - NIS2 Agile</title> <title>Guida all'uso · 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 / NO Bootstrap Italia). -->
cosi' style.css resta autorevole per la chrome (sidebar/widget). BI veste la guida. --> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <link rel="stylesheet" href="/css/v3.css?v=20260627">
<link rel="stylesheet" href="/css/style.css?v=20260618">
<style> <style>
/* ── V3: card di base + accordion self-contained (ex Bootstrap Italia, ZERO CDN) ── */
.content-body .card,.card-bg{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}
.content-body .card .card-body{padding:20px}
.mb-4{margin-bottom:1.5rem} .mb-0{margin-bottom:0} .mt-3{margin-top:1rem} .mt-4{margin-top:1.5rem}
.text-center{text-align:center} .text-muted{color:var(--muted)} .text-uppercase{text-transform:uppercase}
.h6{font-size:.85rem;font-weight:700} .shadow-sm{box-shadow:var(--shadow)}
.alert{padding:14px 18px;border-radius:8px;font-size:.92rem}
.alert-warning{background:#FBF1E2;border:1px solid #EBD9B4;color:#7a5310}
/* Accordion */
.accordion{display:flex;flex-direction:column;gap:10px}
.accordion-item{border:1px solid var(--line);border-radius:10px;background:var(--card);overflow:hidden;box-shadow:var(--shadow)}
.accordion-header{margin:0}
.accordion-button{display:flex;align-items:center;width:100%;border:0;background:#fff;
padding:16px 18px;font:inherit;font-size:15px;font-weight:700;color:var(--ink);cursor:pointer;text-align:left}
.accordion-button:hover{background:#F5F8FC}
.accordion-button::after{content:"";margin-left:auto;width:10px;height:10px;border-right:2px solid var(--muted);
border-bottom:2px solid var(--muted);transform:rotate(-45deg);transition:transform .18s;flex:0 0 auto}
.accordion-button:not(.collapsed)::after{transform:rotate(45deg)}
.accordion-collapse{display:none}
.accordion-collapse.show{display:block}
.accordion-body{padding:8px 20px 22px}
.accordion-body h3{font-size:1.05rem;margin:18px 0 8px;color:var(--ink)}
.accordion-body ul,.accordion-body ol{padding-left:20px} .accordion-body li{margin:4px 0}
:root{ --c-num:#06c; }
/* Lucide inline (ex sprite BI box-icon): dimensione coerente al testo */
.box-icon{width:1.05em;height:1.05em;vertical-align:-3px;margin-right:6px;display:inline-block}
.plain-box .box-icon,.callout-tip .box-icon{color:#06c}
.example-box .box-icon{color:#a66300}
.norm-box .box-icon{color:#5a6772}
.alert-warning .box-icon{color:#a16207}
/* ── Stili specifici guida (callout normativi/esempi) ──────── */ /* ── Stili specifici guida (callout normativi/esempi) ──────── */
.guide-wrap { max-width: 940px; margin: 0 auto; } .guide-wrap { max-width: 940px; margin: 0 auto; }
@@ -103,16 +133,20 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2">Guida all'uso di NIS2 Agile</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span>Guida all'uso</span>
<span class="text-muted">Per chi inizia da zero</span> </nav>
<div class="page-head">
<div>
<h1>Guida all'uso di NIS2 Agile</h1>
<p class="sub">Per chi inizia da zero — parole semplici, esempi pratici, norma</p>
</div>
</div> </div>
</header>
<div class="content-body"> <div class="content-body">
<div class="guide-wrap"> <div class="guide-wrap">
@@ -127,7 +161,7 @@
un <strong style="color:#7a4a00;">esempio pratico</strong> e <strong style="color:#5a6772;">cosa dice la norma</strong>. un <strong style="color:#7a4a00;">esempio pratico</strong> e <strong style="color:#5a6772;">cosa dice la norma</strong>.
</p> </p>
<div class="alert alert-warning mt-3 mb-0" role="note"> <div class="alert alert-warning mt-3 mb-0" role="note">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-warning-circle"></use></svg> <i data-lucide="alert-triangle" class="box-icon" aria-hidden="true"></i>
<strong>Avvertenza.</strong> Questa guida ha finalità operative e divulgative: spiega come usare la <strong>Avvertenza.</strong> Questa guida ha finalità operative e divulgative: spiega come usare la
piattaforma e riassume in modo semplificato la normativa. <strong>Non costituisce un parere legale</strong> piattaforma e riassume in modo semplificato la normativa. <strong>Non costituisce un parere legale</strong>
e non sostituisce la consulenza di un professionista né le indicazioni ufficiali dell'ACN (Agenzia per la e non sostituisce la consulenza di un professionista né le indicazioni ufficiali dell'ACN (Agenzia per la
@@ -143,7 +177,7 @@
<nav class="card card-bg shadow-sm mb-4 guide-toc-card" aria-label="Indice"> <nav class="card card-bg shadow-sm mb-4 guide-toc-card" aria-label="Indice">
<div class="card-body"> <div class="card-body">
<h2 class="h6 text-uppercase mb-3" style="letter-spacing:.05em;"> <h2 class="h6 text-uppercase mb-3" style="letter-spacing:.05em;">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-list"></use></svg> <i data-lucide="list" class="box-icon" aria-hidden="true"></i>
Indice Indice
</h2> </h2>
<ul class="guide-toc-list"> <ul class="guide-toc-list">
@@ -180,7 +214,7 @@
<div id="body-1" class="accordion-collapse collapse show" role="region" aria-labelledby="head-1" data-bs-parent="#guide-accordion"> <div id="body-1" class="accordion-collapse collapse show" role="region" aria-labelledby="head-1" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> NIS2 è una legge europea (Direttiva UE 2022/2555, recepita in Italia <strong>In parole semplici.</strong> NIS2 è una legge europea (Direttiva UE 2022/2555, recepita in Italia
con il D.Lgs. 138/2024) che obbliga molte aziende a proteggere bene i propri sistemi informatici. con il D.Lgs. 138/2024) che obbliga molte aziende a proteggere bene i propri sistemi informatici.
Lo scopo è evitare che attacchi hacker, fughe di dati o blocchi dei sistemi danneggino cittadini, Lo scopo è evitare che attacchi hacker, fughe di dati o blocchi dei sistemi danneggino cittadini,
@@ -194,7 +228,7 @@
<li><strong>Controllare i fornitori</strong> che hanno accesso ai propri sistemi (Art. 21, lettera d).</li> <li><strong>Controllare i fornitori</strong> che hanno accesso ai propri sistemi (Art. 21, lettera d).</li>
</ol> </ol>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio reale.</strong> Una clinica privata con 250 dipendenti è "in scope" NIS2 perché <strong>Esempio reale.</strong> Una clinica privata con 250 dipendenti è "in scope" NIS2 perché
opera nel settore sanitario (settore essenziale). Se subisce un ransomware che blocca le cartelle cliniche opera nel settore sanitario (settore essenziale). Se subisce un ransomware che blocca le cartelle cliniche
per 8 ore, deve notificarlo al CSIRT Italia entro 24 ore con una prima segnalazione e poi entro 72 ore per 8 ore, deve notificarlo al CSIRT Italia entro 24 ore con una prima segnalazione e poi entro 72 ore
@@ -220,7 +254,7 @@
<div id="body-2" class="accordion-collapse collapse" role="region" aria-labelledby="head-2" data-bs-parent="#guide-accordion"> <div id="body-2" class="accordion-collapse collapse" role="region" aria-labelledby="head-2" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> "In scope" vuol dire che la legge si applica a te. Dipende da <strong>In parole semplici.</strong> "In scope" vuol dire che la legge si applica a te. Dipende da
<em>cosa fai</em> (settore) e <em>quanto sei grande</em> (dipendenti e fatturato). <em>cosa fai</em> (settore) e <em>quanto sei grande</em> (dipendenti e fatturato).
</div> </div>
@@ -265,7 +299,7 @@
<li>la <strong>Pubblica Amministrazione</strong> nei casi previsti.</li> <li>la <strong>Pubblica Amministrazione</strong> nei casi previsti.</li>
</ul> </ul>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-help-circle"></use></svg> <i data-lucide="help-circle" class="box-icon" aria-hidden="true"></i>
<strong>Caso reale: Agile Technology.</strong> Una software house che eroga a terzi una piattaforma SaaS <strong>Caso reale: Agile Technology.</strong> Una software house che eroga a terzi una piattaforma SaaS
e servizi ICT gestiti (come NIS2 Agile stessa) rientra tipicamente tra i <strong>fornitori di servizi e servizi ICT gestiti (come NIS2 Agile stessa) rientra tipicamente tra i <strong>fornitori di servizi
gestiti B2B</strong>: in questo caso è in ambito <em>a prescindere dal numero di dipendenti o dal gestiti B2B</strong>: in questo caso è in ambito <em>a prescindere dal numero di dipendenti o dal
@@ -273,7 +307,7 @@
l'esenzione "piccola impresa" qui non si applica. l'esenzione "piccola impresa" qui non si applica.
</div> </div>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio pratico.</strong> "Aurora Sanità S.p.A." con 480 dipendenti e 92 milioni € di fatturato, <strong>Esempio pratico.</strong> "Aurora Sanità S.p.A." con 480 dipendenti e 92 milioni € di fatturato,
settore sanitario → <strong>essenziale + grande</strong> → pienamente in scope. Deve registrarsi sul portale settore sanitario → <strong>essenziale + grande</strong> → pienamente in scope. Deve registrarsi sul portale
ACN (Agenzia per la Cybersicurezza Nazionale) entro le scadenze. ACN (Agenzia per la Cybersicurezza Nazionale) entro le scadenze.
@@ -313,7 +347,7 @@
esterni (Microsoft 365, Google Workspace, AWS, Azure, Identity Provider, EDR, SIEM, ticketing): sono le esterni (Microsoft 365, Google Workspace, AWS, Azure, Identity Provider, EDR, SIEM, ticketing): sono le
fonti da cui la piattaforma raccoglie automaticamente le evidenze di conformità.</p> fonti da cui la piattaforma raccoglie automaticamente le evidenze di conformità.</p>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-lock"></use></svg> <i data-lucide="lock" class="box-icon" aria-hidden="true"></i>
<strong>Sicurezza dei segreti.</strong> Nella scheda inserisci solo i parametri non sensibili (es. <strong>Sicurezza dei segreti.</strong> Nella scheda inserisci solo i parametri non sensibili (es.
tenant id, client id). Le <strong>credenziali segrete</strong> non vengono mai salvate nel prodotto: tenant id, client id). Le <strong>credenziali segrete</strong> non vengono mai salvate nel prodotto:
sono custodite separatamente nel vault cifrato. Dopo il salvataggio la piattaforma indica il sono custodite separatamente nel vault cifrato. Dopo il salvataggio la piattaforma indica il
@@ -345,7 +379,7 @@
<li><strong>consultant</strong> — gestisce <em>più aziende clienti</em> (sezioni "Aziende" e "Connettori").</li> <li><strong>consultant</strong> — gestisce <em>più aziende clienti</em> (sezioni "Aziende" e "Connettori").</li>
</ul> </ul>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>Nota.</strong> Le sezioni <strong>Aziende</strong> e <strong>Connettori</strong> sono pensate per <strong>Nota.</strong> Le sezioni <strong>Aziende</strong> e <strong>Connettori</strong> sono pensate per
consulenti/studi che gestiscono più clienti: se sei un utente singolo potresti non vederle. consulenti/studi che gestiscono più clienti: se sei un utente singolo potresti non vederle.
</div> </div>
@@ -366,7 +400,7 @@
<li><strong>Genera il report esecutivo</strong> dalla sezione Report — utile per il board.</li> <li><strong>Genera il report esecutivo</strong> dalla sezione Report — utile per il board.</li>
</ol> </ol>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-clock"></use></svg> <i data-lucide="clock" class="box-icon" aria-hidden="true"></i>
<strong>Tempi indicativi.</strong> Un primo ciclo "decente" si fa in 6–8 settimane di lavoro <strong>Tempi indicativi.</strong> Un primo ciclo "decente" si fa in 6–8 settimane di lavoro
spalmate, con 2 persone (un IT e un compliance). Non puntare alla perfezione subito: meglio spalmate, con 2 persone (un IT e un compliance). Non puntare alla perfezione subito: meglio
coprire <em>tutte</em> le 10 categorie al 50% che 3 al 100% e 7 a zero. coprire <em>tutte</em> le 10 categorie al 50% che 3 al 100% e 7 a zero.
@@ -377,7 +411,7 @@
(top 25%, sopra/sotto la mediana) e lo scarto rispetto alla media. È un dato che i tool tradizionali (top 25%, sopra/sotto la mediana) e lo scarto rispetto alla media. È un dato che i tool tradizionali
non possono offrire, perché serve una rete multi-azienda.</p> non possono offrire, perché serve una rete multi-azienda.</p>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-lock"></use></svg> <i data-lucide="lock" class="box-icon" aria-hidden="true"></i>
<strong>Privacy.</strong> Il confronto è <strong>anonimo e aggregato</strong>: appare solo se nel <strong>Privacy.</strong> Il confronto è <strong>anonimo e aggregato</strong>: appare solo se nel
tuo settore ci sono almeno 3 organizzazioni, e non mostra mai i dati di una singola azienda. tuo settore ci sono almeno 3 organizzazioni, e non mostra mai i dati di una singola azienda.
</div> </div>
@@ -395,7 +429,7 @@
<div id="body-5" class="accordion-collapse collapse" role="region" aria-labelledby="head-5" data-bs-parent="#guide-accordion"> <div id="body-5" class="accordion-collapse collapse" role="region" aria-labelledby="head-5" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> L'Art. 21 elenca 10 famiglie di "cose che devi avere". <strong>In parole semplici.</strong> L'Art. 21 elenca 10 famiglie di "cose che devi avere".
La Gap Analysis ti fa domande per ognuna e calcola quanto sei attrezzato. Non serve essere La Gap Analysis ti fa domande per ognuna e calcola quanto sei attrezzato. Non serve essere
perfetti — serve <strong>sapere dove sei messo male</strong> e <strong>aver iniziato a migliorare</strong>. perfetti — serve <strong>sapere dove sei messo male</strong> e <strong>aver iniziato a migliorare</strong>.
@@ -433,7 +467,7 @@
critici. Anche SMS è meglio di nulla, app authenticator è meglio di SMS, hardware token (FIDO2) critici. Anche SMS è meglio di nulla, app authenticator è meglio di SMS, hardware token (FIDO2)
è il top.</p></div> è il top.</p></div>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio Aurora Sanità.</strong> Punteggio iniziale 58%. I gap principali sono in <strong>Esempio Aurora Sanità.</strong> Punteggio iniziale 58%. I gap principali sono in
Supply Chain (40%) e Crittografia (50%). Piano di azione: assessment dei 12 fornitori critici Supply Chain (40%) e Crittografia (50%). Piano di azione: assessment dei 12 fornitori critici
entro 6 mesi, attivare TLS 1.3 ovunque e cifratura at-rest sui DB cartelle cliniche entro 9 mesi. entro 6 mesi, attivare TLS 1.3 ovunque e cifratura at-rest sui DB cartelle cliniche entro 9 mesi.
@@ -462,7 +496,7 @@
<strong>+6 misure e +29 requisiti</strong> rispetto agli importanti.</li> <strong>+6 misure e +29 requisiti</strong> rispetto agli importanti.</li>
</ul> </ul>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio (requisiti di governance).</strong> <code>GV.RR-04</code> richiede che la cybersicurezza <strong>Esempio (requisiti di governance).</strong> <code>GV.RR-04</code> richiede che la cybersicurezza
sia integrata nella gestione delle risorse umane: il <strong>personale autorizzato ad accedere</strong> sia integrata nella gestione delle risorse umane: il <strong>personale autorizzato ad accedere</strong>
ai sistemi rilevanti e gli <strong>amministratori di sistema</strong> devono essere individuati previa ai sistemi rilevanti e gli <strong>amministratori di sistema</strong> devono essere individuati previa
@@ -472,7 +506,7 @@
comunicata e applicata. comunicata e applicata.
</div> </div>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-help-circle"></use></svg> <i data-lucide="help-circle" class="box-icon" aria-hidden="true"></i>
<strong>Come si collega alla piattaforma.</strong> Le <strong>80 domande</strong> della Gap Analysis <strong>Come si collega alla piattaforma.</strong> Le <strong>80 domande</strong> della Gap Analysis
(8 × 10 categorie Art. 21) danno il quadro di maturità di primo livello; le <strong>specifiche di base (8 × 10 categorie Art. 21) danno il quadro di maturità di primo livello; le <strong>specifiche di base
ACN</strong> (Allegati 1–2) sono il dettaglio operativo verso cui portare l'implementazione. Il modulo ACN</strong> (Allegati 1–2) sono il dettaglio operativo verso cui portare l'implementazione. Il modulo
@@ -499,7 +533,7 @@
<div id="body-5b" class="accordion-collapse collapse" role="region" aria-labelledby="head-5b" data-bs-parent="#guide-accordion"> <div id="body-5b" class="accordion-collapse collapse" role="region" aria-labelledby="head-5b" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> La Gap Analysis dell'Art. 21 (cap. 5) ti dice a grandi linee <strong>In parole semplici.</strong> La Gap Analysis dell'Art. 21 (cap. 5) ti dice a grandi linee
dove sei messo sulle 10 famiglie di misure. Ma in Italia il metro di conformità vero è la dove sei messo sulle 10 famiglie di misure. Ma in Italia il metro di conformità vero è la
<strong>Determinazione ACN n. 164179 del 14/04/2025</strong>, che declina quelle famiglie in <strong>Determinazione ACN n. 164179 del 14/04/2025</strong>, che declina quelle famiglie in
@@ -532,7 +566,7 @@
(cita solo requisiti effettivamente esistenti).</li> (cita solo requisiti effettivamente esistenti).</li>
</ol> </ol>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-help-circle"></use></svg> <i data-lucide="help-circle" class="box-icon" aria-hidden="true"></i>
<strong>Quale uso quando.</strong> La Gap Analysis Art. 21 (cap. 5) è un primo orientamento. La Gap Analysis <strong>Quale uso quando.</strong> La Gap Analysis Art. 21 (cap. 5) è un primo orientamento. La Gap Analysis
ACN è la verifica di conformità puntuale richiesta in concreto: usala per prepararti all'autovalutazione ACN. ACN è la verifica di conformità puntuale richiesta in concreto: usala per prepararti all'autovalutazione ACN.
Fonte: Agenzia per la Cybersicurezza Nazionale, Determinazione n. 164179 del 14/04/2025 (Allegati 1 e 2). Fonte: Agenzia per la Cybersicurezza Nazionale, Determinazione n. 164179 del 14/04/2025 (Allegati 1 e 2).
@@ -551,7 +585,7 @@
<div id="body-6" class="accordion-collapse collapse" role="region" aria-labelledby="head-6" data-bs-parent="#guide-accordion"> <div id="body-6" class="accordion-collapse collapse" role="region" aria-labelledby="head-6" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> Il "registro dei rischi" è un elenco di cose brutte che <strong>In parole semplici.</strong> Il "registro dei rischi" è un elenco di cose brutte che
<em>potrebbero</em> accadere, con quanto è probabile e quanto farebbe male. Per ognuna decidi <em>potrebbero</em> accadere, con quanto è probabile e quanto farebbe male. Per ognuna decidi
cosa fare: ridurla, accettarla, assicurarla, o eliminarla. cosa fare: ridurla, accettarla, assicurarla, o eliminarla.
@@ -572,7 +606,7 @@
<li><strong>Evitare</strong>: smetto di fare l'attività che genera il rischio.</li> <li><strong>Evitare</strong>: smetto di fare l'attività che genera il rischio.</li>
</ul> </ul>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio.</strong> Rischio "Ransomware su sistema PACS" (Aurora Sanità) → probabilità 4, <strong>Esempio.</strong> Rischio "Ransomware su sistema PACS" (Aurora Sanità) → probabilità 4,
impatto 5 → punteggio inerente 20 (critico). Trattamento: mitigazione = MFA su RDP + backup impatto 5 → punteggio inerente 20 (critico). Trattamento: mitigazione = MFA su RDP + backup
immutabili + patch mensili. Dopo le misure: probabilità 2, impatto 4 → punteggio residuo 8 (medio). immutabili + patch mensili. Dopo le misure: probabilità 2, impatto 4 → punteggio residuo 8 (medio).
@@ -601,7 +635,7 @@
piattaforma esegue una <strong>simulazione Monte Carlo</strong> e restituisce la piattaforma esegue una <strong>simulazione Monte Carlo</strong> e restituisce la
<strong>Perdita Annua Attesa (ALE)</strong> con i percentili P10 / P50 / P90 e un istogramma.</p> <strong>Perdita Annua Attesa (ALE)</strong> con i percentili P10 / P50 / P90 e un istogramma.</p>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio.</strong> Ransomware: 0,5–2 attacchi/anno (probabile 1), vulnerabilità 0,3, <strong>Esempio.</strong> Ransomware: 0,5–2 attacchi/anno (probabile 1), vulnerabilità 0,3,
perdita 50k–2M € (probabile 300k). Risultato: ALE media ≈ 175.000 € l'anno. Un numero così parla perdita 50k–2M € (probabile 300k). Risultato: ALE media ≈ 175.000 € l'anno. Un numero così parla
alla Direzione meglio di "rischio alto" e aiuta a giustificare gli investimenti di sicurezza. alla Direzione meglio di "rischio alto" e aiuta a giustificare gli investimenti di sicurezza.
@@ -609,7 +643,7 @@
<p>Il <strong>Registro Quantitativo</strong> somma l'ALE di tutti i rischi quantificati, dando <p>Il <strong>Registro Quantitativo</strong> somma l'ALE di tutti i rischi quantificati, dando
l'esposizione economica complessiva del portafoglio.</p> l'esposizione economica complessiva del portafoglio.</p>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-chart-line"></use></svg> <i data-lucide="trending-up" class="box-icon" aria-hidden="true"></i>
<strong>Perché serve quantificare in euro.</strong> L'ALE va sempre confrontata con il <em>costo</em> <strong>Perché serve quantificare in euro.</strong> L'ALE va sempre confrontata con il <em>costo</em>
della non-conformità, non solo con il costo delle misure. Le sanzioni NIS2 (art. 34 Direttiva (UE) della non-conformità, non solo con il costo delle misure. Le sanzioni NIS2 (art. 34 Direttiva (UE)
2022/2555, recepito dal D.Lgs. 138/2024) arrivano fino a <strong>10 milioni € o il 2% del fatturato 2022/2555, recepito dal D.Lgs. 138/2024) arrivano fino a <strong>10 milioni € o il 2% del fatturato
@@ -624,7 +658,7 @@
due soglie (attenzione e critica): la piattaforma calcola da sola lo stato a <strong>semaforo</strong> due soglie (attenzione e critica): la piattaforma calcola da sola lo stato a <strong>semaforo</strong>
– verde, ambra, rosso – in base al valore e alla direzione (se valori alti o bassi sono peggio).</p> – verde, ambra, rosso – in base al valore e alla direzione (se valori alti o bassi sono peggio).</p>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio.</strong> KRI "% endpoint senza MFA": soglia attenzione 10%, critica 25%. Valore <strong>Esempio.</strong> KRI "% endpoint senza MFA": soglia attenzione 10%, critica 25%. Valore
attuale 18% → semaforo <strong>ambra</strong>. Sale a 30% → <strong>rosso</strong>: intervento prioritario. attuale 18% → semaforo <strong>ambra</strong>. Sale a 30% → <strong>rosso</strong>: intervento prioritario.
</div> </div>
@@ -642,7 +676,7 @@
<div id="body-7" class="accordion-collapse collapse" role="region" aria-labelledby="head-7" data-bs-parent="#guide-accordion"> <div id="body-7" class="accordion-collapse collapse" role="region" aria-labelledby="head-7" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> Se hai un incidente "significativo" devi avvisare il CSIRT <strong>In parole semplici.</strong> Se hai un incidente "significativo" devi avvisare il CSIRT
(la squadra nazionale di risposta cyber) in tre tappe: un preallarme entro 24 ore, una (la squadra nazionale di risposta cyber) in tre tappe: un preallarme entro 24 ore, una
notifica entro 72 ore, e una relazione finale entro un mese. notifica entro 72 ore, e una relazione finale entro un mese.
@@ -660,7 +694,7 @@
e 4 (essenziali) della Determina ACN 164179/2025</strong> (e, per i fornitori digitali, dal Reg. (UE) e 4 (essenziali) della Determina ACN 164179/2025</strong> (e, per i fornitori digitali, dal Reg. (UE)
2024/2690) e variano per tipologia di soggetto.</p> 2024/2690) e variano per tipologia di soggetto.</p>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-warning-circle"></use></svg> <i data-lucide="alert-triangle" class="box-icon" aria-hidden="true"></i>
<strong>Indicatori usati dalla piattaforma per il triage</strong> (non sono soglie di legge, ma <strong>Indicatori usati dalla piattaforma per il triage</strong> (non sono soglie di legge, ma
aiutano a capire se sei vicino alla significatività): ≥ 500 utenti colpiti · blocco servizi &gt; 4 ore · aiutano a capire se sei vicino alla significatività): ≥ 500 utenti colpiti · blocco servizi &gt; 4 ore ·
impatto transfrontaliero · attacco intenzionale (ransomware, DDoS, intrusione) · danni diretti impatto transfrontaliero · attacco intenzionale (ransomware, DDoS, intrusione) · danni diretti
@@ -677,7 +711,7 @@
<p>Analisi completa: causa radice, azioni correttive, lezioni apprese, raccomandazioni per il futuro. <p>Analisi completa: causa radice, azioni correttive, lezioni apprese, raccomandazioni per il futuro.
<em>Termine normativo: relazione finale entro <strong>un mese dalla notifica</strong> (cioè dal momento delle 72h), non dall'incidente — art. 23 D.Lgs. 138/2024.</em></p></div> <em>Termine normativo: relazione finale entro <strong>un mese dalla notifica</strong> (cioè dal momento delle 72h), non dall'incidente — art. 23 D.Lgs. 138/2024.</em></p></div>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio.</strong> Aurora Sanità subisce un DDoS sul portale prenotazioni alle 09:00 di lunedì. <strong>Esempio.</strong> Aurora Sanità subisce un DDoS sul portale prenotazioni alle 09:00 di lunedì.
Sono colpiti 8.500 utenti per 4 ore → significativo. Workflow: preallarme entro martedì 09:00 → Sono colpiti 8.500 utenti per 4 ore → significativo. Workflow: preallarme entro martedì 09:00 →
notifica entro giovedì 09:00 → relazione finale entro un mese <em>dalla notifica</em> (giovedì + 1 mese). notifica entro giovedì 09:00 → relazione finale entro un mese <em>dalla notifica</em> (giovedì + 1 mese).
@@ -705,7 +739,7 @@
IS-1…IS-4, gravità) e fa partire i tempi Art. 23. Gli alert duplicati vengono riconosciuti e non IS-1…IS-4, gravità) e fa partire i tempi Art. 23. Gli alert duplicati vengono riconosciuti e non
creano doppioni.</p> creano doppioni.</p>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-help-circle"></use></svg> <i data-lucide="help-circle" class="box-icon" aria-hidden="true"></i>
<strong>Per chi integra.</strong> L'endpoint è <code>POST /api/services/incidents-ingest</code> <strong>Per chi integra.</strong> L'endpoint è <code>POST /api/services/incidents-ingest</code>
con una API key dedicata (scope <code>ingest:incidents</code>). Trasforma il modulo incidenti da con una API key dedicata (scope <code>ingest:incidents</code>). Trasforma il modulo incidenti da
reattivo a proattivo. reattivo a proattivo.
@@ -724,7 +758,7 @@
<div id="body-8" class="accordion-collapse collapse" role="region" aria-labelledby="head-8" data-bs-parent="#guide-accordion"> <div id="body-8" class="accordion-collapse collapse" role="region" aria-labelledby="head-8" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> Una "policy" è un documento scritto che dice <em>come</em> <strong>In parole semplici.</strong> Una "policy" è un documento scritto che dice <em>come</em>
si fa una cosa in azienda. Serve sia per fare bene, sia per dimostrare alle autorità che ci hai pensato. si fa una cosa in azienda. Serve sia per fare bene, sia per dimostrare alle autorità che ci hai pensato.
</div> </div>
@@ -740,7 +774,7 @@
<li>Acceptable Use Policy (per i dipendenti).</li> <li>Acceptable Use Policy (per i dipendenti).</li>
</ol> </ol>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio.</strong> Genera con l'AI la bozza della "Politica di Crittografia": clicchi <strong>Esempio.</strong> Genera con l'AI la bozza della "Politica di Crittografia": clicchi
"Genera con AI", indichi il settore (sanità), l'AI ti propone un documento di 4–5 pagine con "Genera con AI", indichi il settore (sanità), l'AI ti propone un documento di 4–5 pagine con
sezioni standard (algoritmi accettati, gestione chiavi, KMS, audit). Tu rivedi, adatti e fai sezioni standard (algoritmi accettati, gestione chiavi, KMS, audit). Tu rivedi, adatti e fai
@@ -753,7 +787,7 @@
Per ogni policy approvata, i dipendenti registrano la <strong>presa visione</strong> con un clic; la Per ogni policy approvata, i dipendenti registrano la <strong>presa visione</strong> con un clic; la
piattaforma tiene la copertura (es. "32 su 40 hanno preso visione") e mostra chi manca.</p> piattaforma tiene la copertura (es. "32 su 40 hanno preso visione") e mostra chi manca.</p>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check-circle"></use></svg> <i data-lucide="check-circle" class="box-icon" aria-hidden="true"></i>
<strong>Importante.</strong> L'attestation è <strong>legata alla versione</strong>: se aggiorni la <strong>Importante.</strong> L'attestation è <strong>legata alla versione</strong>: se aggiorni la
policy e la riapprovi con una nuova versione, chi aveva firmato la versione precedente deve policy e la riapprovi con una nuova versione, chi aveva firmato la versione precedente deve
prendere visione di nuovo. Così la prova di lettura è sempre sulla versione corrente. prendere visione di nuovo. Così la prova di lettura è sempre sulla versione corrente.
@@ -776,7 +810,7 @@
<div id="body-9" class="accordion-collapse collapse" role="region" aria-labelledby="head-9" data-bs-parent="#guide-accordion"> <div id="body-9" class="accordion-collapse collapse" role="region" aria-labelledby="head-9" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> Se un tuo fornitore IT viene bucato, l'attaccante può entrare <strong>In parole semplici.</strong> Se un tuo fornitore IT viene bucato, l'attaccante può entrare
in casa tua. NIS2 ti obbliga a valutare i fornitori "critici" (quelli con accesso ai tuoi sistemi in casa tua. NIS2 ti obbliga a valutare i fornitori "critici" (quelli con accesso ai tuoi sistemi
o dati sensibili). o dati sensibili).
@@ -791,7 +825,7 @@
<li>Inserire clausole NIS2 nel contratto (right to audit, notifica incidenti, etc.).</li> <li>Inserire clausole NIS2 nel contratto (right to audit, notifica incidenti, etc.).</li>
</ol> </ol>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio.</strong> "MedTech Manutenzione PACS S.r.l." ha accesso remoto al PACS di <strong>Esempio.</strong> "MedTech Manutenzione PACS S.r.l." ha accesso remoto al PACS di
Aurora Sanità → criticità "critical". Risk score iniziale 6/10 (ha ISO 27001, ma sub-appalto Aurora Sanità → criticità "critical". Risk score iniziale 6/10 (ha ISO 27001, ma sub-appalto
non chiaro). Azione: clausola contrattuale che vieta sub-appalto senza autorizzazione e non chiaro). Azione: clausola contrattuale che vieta sub-appalto senza autorizzazione e
@@ -805,7 +839,7 @@
<p>Alla consegna, il sistema calcola in automatico un <strong>punteggio</strong> e aggiorna la scheda <p>Alla consegna, il sistema calcola in automatico un <strong>punteggio</strong> e aggiorna la scheda
del fornitore, così il suo livello di rischio si allinea da solo alle risposte ricevute.</p> del fornitore, così il suo livello di rischio si allinea da solo alle risposte ricevute.</p>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio.</strong> Invii il questionario al provider cloud. Compila il link entro 30 giorni: <strong>Esempio.</strong> Invii il questionario al provider cloud. Compila il link entro 30 giorni:
7 "sì" e 1 "parziale" → punteggio 94/100, requisiti di sicurezza soddisfatti. 7 "sì" e 1 "parziale" → punteggio 94/100, requisiti di sicurezza soddisfatti.
</div> </div>
@@ -826,7 +860,7 @@
duplicarlo: utile per sincronizzazioni periodiche da un CMDB o da un gestionale aziendale. È disponibile anche duplicarlo: utile per sincronizzazioni periodiche da un CMDB o da un gestionale aziendale. È disponibile anche
un'<strong>API key</strong> per l'integrazione automatica dal sistema dell'azienda cliente.</p> un'<strong>API key</strong> per l'integrazione automatica dal sistema dell'azienda cliente.</p>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>Nota interpretativa sul perimetro.</strong> NIS2 non fornisce una lista chiusa di "fornitori <strong>Nota interpretativa sul perimetro.</strong> NIS2 non fornisce una lista chiusa di "fornitori
da censire": l'obbligo (Direttiva (UE) 2022/2555, art. 21.2(d) — sicurezza della catena di da censire": l'obbligo (Direttiva (UE) 2022/2555, art. 21.2(d) — sicurezza della catena di
approvvigionamento) richiede di valutare i fornitori in funzione del rischio che introducono, dando approvvigionamento) richiede di valutare i fornitori in funzione del rischio che introducono, dando
@@ -850,7 +884,7 @@
<div id="body-10" class="accordion-collapse collapse" role="region" aria-labelledby="head-10" data-bs-parent="#guide-accordion"> <div id="body-10" class="accordion-collapse collapse" role="region" aria-labelledby="head-10" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> NIS2 è chiara: <em>gli amministratori (CDA, direttori) <strong>In parole semplici.</strong> NIS2 è chiara: <em>gli amministratori (CDA, direttori)
devono essere formati sulla cybersecurity</em>. Non è un nice-to-have, è obbligatorio. E sono devono essere formati sulla cybersecurity</em>. Non è un nice-to-have, è obbligatorio. E sono
loro che rispondono in prima persona se l'azienda non è conforme. loro che rispondono in prima persona se l'azienda non è conforme.
@@ -863,7 +897,7 @@
almeno annuale.</li> almeno annuale.</li>
</ol> </ol>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio.</strong> Aurora Sanità organizza: 1 corso da 4h per CDA, 1 corso da 16h per il team <strong>Esempio.</strong> Aurora Sanità organizza: 1 corso da 4h per CDA, 1 corso da 16h per il team
IT, una sessione obbligatoria di 1h per tutti i dipendenti più simulazioni di phishing trimestrali. IT, una sessione obbligatoria di 1h per tutti i dipendenti più simulazioni di phishing trimestrali.
Tutto loggato nel modulo Formazione. Tutto loggato nel modulo Formazione.
@@ -896,7 +930,7 @@
<div id="body-11" class="accordion-collapse collapse" role="region" aria-labelledby="head-11" data-bs-parent="#guide-accordion"> <div id="body-11" class="accordion-collapse collapse" role="region" aria-labelledby="head-11" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> Non puoi proteggere quello che non sai di avere. <strong>In parole semplici.</strong> Non puoi proteggere quello che non sai di avere.
L'inventario asset elenca tutti i sistemi, applicazioni, database e infrastrutture critiche. L'inventario asset elenca tutti i sistemi, applicazioni, database e infrastrutture critiche.
</div> </div>
@@ -911,7 +945,7 @@
<li><strong>Facility</strong>: data center, sale server.</li> <li><strong>Facility</strong>: data center, sale server.</li>
</ul> </ul>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio.</strong> Aurora Sanità inserisce: il PACS, il database cartelle cliniche, <strong>Esempio.</strong> Aurora Sanità inserisce: il PACS, il database cartelle cliniche,
il portale prenotazioni. Per ognuno: criticità, vendor, location, owner. Quando arriva un incidente il portale prenotazioni. Per ognuno: criticità, vendor, location, owner. Quando arriva un incidente
o un rischio, può essere associato a uno o più asset → tracciabilità totale. o un rischio, può essere associato a uno o più asset → tracciabilità totale.
@@ -923,7 +957,7 @@
carichi un CSV (o un export dal CMDB/cloud) e la piattaforma crea gli asset calcolando carichi un CSV (o un export dal CMDB/cloud) e la piattaforma crea gli asset calcolando
<strong>automaticamente</strong> il punteggio per ciascuno.</p> <strong>automaticamente</strong> il punteggio per ciascuno.</p>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-files"></use></svg> <i data-lucide="files" class="box-icon" aria-hidden="true"></i>
<strong>Niente duplicati.</strong> Se indichi un identificativo esterno (<code>external_ref</code>), <strong>Niente duplicati.</strong> Se indichi un identificativo esterno (<code>external_ref</code>),
re-importare lo stesso asset lo <strong>aggiorna</strong> invece di duplicarlo: ideale per re-importare lo stesso asset lo <strong>aggiorna</strong> invece di duplicarlo: ideale per
sincronizzazioni periodiche dal CMDB. sincronizzazioni periodiche dal CMDB.
@@ -942,7 +976,7 @@
<div id="body-12" class="accordion-collapse collapse" role="region" aria-labelledby="head-12" data-bs-parent="#guide-accordion"> <div id="body-12" class="accordion-collapse collapse" role="region" aria-labelledby="head-12" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> Quando le autorità (ACN) verranno a chiederti conto della tua <strong>In parole semplici.</strong> Quando le autorità (ACN) verranno a chiederti conto della tua
compliance, devi poter mostrare documenti, evidenze, registri. Qui li trovi tutti. compliance, devi poter mostrare documenti, evidenze, registri. Qui li trovi tutti.
</div> </div>
@@ -964,17 +998,17 @@
<strong>arancio</strong> (evidenza scaduta), <strong>rosso</strong> (non conforme), grigio (non <strong>arancio</strong> (evidenza scaduta), <strong>rosso</strong> (non conforme), grigio (non
monitorato). La copertura indica quanti controlli sono alimentati da evidenze automatiche.</p> monitorato). La copertura indica quanti controlli sono alimentati da evidenze automatiche.</p>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-help-circle"></use></svg> <i data-lucide="help-circle" class="box-icon" aria-hidden="true"></i>
<strong>Perché conta.</strong> Avvicina l'azienda a una compliance <em>continua</em> anziché <strong>Perché conta.</strong> Avvicina l'azienda a una compliance <em>continua</em> anziché
fotografata una volta l'anno: è il modello dei migliori strumenti internazionali. fotografata una volta l'anno: è il modello dei migliori strumenti internazionali.
</div> </div>
<div class="callout-tip"> <div class="callout-tip">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>Prerequisito.</strong> Questa vista si popola man mano che configuri i <strong>Connettori</strong> <strong>Prerequisito.</strong> Questa vista si popola man mano che configuri i <strong>Connettori</strong>
(vedi cap. 3): senza connettori i controlli restano "non monitorati" (grigio) e la copertura è 0%. (vedi cap. 3): senza connettori i controlli restano "non monitorati" (grigio) e la copertura è 0%.
</div> </div>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio.</strong> Pre-audit ISO 27001: genera il report esecutivo, esporta in CSV <strong>Esempio.</strong> Pre-audit ISO 27001: genera il report esecutivo, esporta in CSV
rischi e controlli, scarica l'audit log certificato. L'auditor ha tutto in 5 minuti. rischi e controlli, scarica l'audit log certificato. L'auditor ha tutto in 5 minuti.
</div> </div>
@@ -992,7 +1026,7 @@
<div id="body-13" class="accordion-collapse collapse" role="region" aria-labelledby="head-13" data-bs-parent="#guide-accordion"> <div id="body-13" class="accordion-collapse collapse" role="region" aria-labelledby="head-13" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> Due cose distinte gestite in un unico modulo: la <strong>In parole semplici.</strong> Due cose distinte gestite in un unico modulo: la
<strong>segnalazione anonima di vulnerabilità</strong> (prevista da NIS2, art. 12, divulgazione <strong>segnalazione anonima di vulnerabilità</strong> (prevista da NIS2, art. 12, divulgazione
coordinata) e il <strong>whistleblowing</strong> dei dipendenti (segnalazione di illeciti), coordinata) e il <strong>whistleblowing</strong> dei dipendenti (segnalazione di illeciti),
@@ -1006,7 +1040,7 @@
classificate dall'AI e (opzionalmente) risolte automaticamente.</li> classificate dall'AI e (opzionalmente) risolte automaticamente.</li>
</ul> </ul>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Esempio.</strong> Un dipendente nota che le credenziali admin del CRM sono salvate in <strong>Esempio.</strong> Un dipendente nota che le credenziali admin del CRM sono salvate in
un foglio Excel condiviso. Invia una segnalazione anonima → il responsabile compliance la riceve un foglio Excel condiviso. Invia una segnalazione anonima → il responsabile compliance la riceve
con codice di tracciamento → il dipendente può seguire lo stato senza rivelare la propria identità. con codice di tracciamento → il dipendente può seguire lo stato senza rivelare la propria identità.
@@ -1025,7 +1059,7 @@
<div id="body-14" class="accordion-collapse collapse" role="region" aria-labelledby="head-14" data-bs-parent="#guide-accordion"> <div id="body-14" class="accordion-collapse collapse" role="region" aria-labelledby="head-14" data-bs-parent="#guide-accordion">
<div class="accordion-body"> <div class="accordion-body">
<div class="plain-box"> <div class="plain-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-info-circle"></use></svg> <i data-lucide="info" class="box-icon" aria-hidden="true"></i>
<strong>In parole semplici.</strong> La piattaforma usa Claude (Anthropic) per assisterti. L'AI <strong>In parole semplici.</strong> La piattaforma usa Claude (Anthropic) per assisterti. L'AI
è bravissima a partire dal foglio bianco, ma <strong>non sostituisce</strong> il tuo giudizio: è bravissima a partire dal foglio bianco, ma <strong>non sostituisce</strong> il tuo giudizio:
rivedi sempre quello che produce prima di approvarlo. rivedi sempre quello che produce prima di approvarlo.
@@ -1042,7 +1076,7 @@
<li><strong>Classificazione Feedback</strong>: tag automatico per le segnalazioni di bug/UX.</li> <li><strong>Classificazione Feedback</strong>: tag automatico per le segnalazioni di bug/UX.</li>
</ul> </ul>
<div class="example-box"> <div class="example-box">
<svg class="icon icon-sm box-icon" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-note"></use></svg> <i data-lucide="sticky-note" class="box-icon" aria-hidden="true"></i>
<strong>Buona pratica.</strong> Quando l'AI suggerisce 8 rischi, non importarli tutti ciecamente. <strong>Buona pratica.</strong> Quando l'AI suggerisce 8 rischi, non importarli tutti ciecamente.
Scegli i 4 più rilevanti per il tuo contesto, scartane uno che è duplicato di un asset interno, Scegli i 4 più rilevanti per il tuo contesto, scartane uno che è duplicato di un asset interno,
e personalizza descrizione/probabilità per la tua realtà. e personalizza descrizione/probabilità per la tua realtà.
@@ -1140,46 +1174,55 @@
</div><!-- /guide-wrap --> </div><!-- /guide-wrap -->
</div><!-- /content-body --> </div><!-- /content-body -->
</main> </main>
</div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help -->
<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 (accordion) + 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>
<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> <script>
// La guida è accessibile anche da non autenticati (utile per onboarding) // La guida è accessibile anche da non autenticati (utile per onboarding)
// ma se sei loggato carichi sidebar + i18n normalmente. Stessa logica // ma se sei loggato carichi chrome + i18n normalmente. ZERO modifiche backend.
// della guida.html originale: ZERO modifiche backend. if (typeof loadSidebar === 'function') loadSidebar(); // = loadTopnavV3()
if (typeof loadSidebar === 'function') loadSidebar();
if (typeof I18n !== 'undefined' && I18n.init) I18n.init(); if (typeof I18n !== 'undefined' && I18n.init) I18n.init();
if (typeof HelpSystem !== 'undefined' && HelpSystem.init) HelpSystem.init(); if (typeof HelpSystem !== 'undefined' && HelpSystem.init) HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
// ── Accordion self-contained (sostituisce bootstrap.Collapse, ZERO CDN) ──
function openChapter(num, doScroll) {
var body = document.getElementById('body-' + num);
var btn = document.querySelector('[data-bs-target="#body-' + num + '"]');
if (!body) return;
body.classList.add('show');
if (btn) { btn.classList.remove('collapsed'); btn.setAttribute('aria-expanded', 'true'); }
if (doScroll) {
var sec = document.getElementById('cap-' + num);
if (sec) sec.scrollIntoView({ behavior: 'smooth' });
}
}
document.querySelectorAll('.accordion-button[data-bs-target]').forEach(function (btn) {
btn.addEventListener('click', function () {
var sel = btn.getAttribute('data-bs-target'); // es. #body-5
var body = document.querySelector(sel);
if (!body) return;
var willOpen = !body.classList.contains('show');
body.classList.toggle('show', willOpen);
btn.classList.toggle('collapsed', !willOpen);
btn.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
});
});
// Apri automaticamente il capitolo se si arriva con un'ancora (#cap-N). // Apri automaticamente il capitolo se si arriva con un'ancora (#cap-N).
(function openFromHash() { (function openFromHash() {
var h = window.location.hash; var h = window.location.hash;
if (!h) return; if (!h) return;
var num = h.replace('#cap-', ''); openChapter(h.replace('#cap-', ''), true);
var body = document.getElementById('body-' + num);
if (body && window.bootstrap && bootstrap.Collapse) {
bootstrap.Collapse.getOrCreateInstance(body).show();
document.getElementById('cap-' + num).scrollIntoView({ behavior: 'smooth' });
}
})(); })();
window.addEventListener('hashchange', function () { window.addEventListener('hashchange', function () {
var num = window.location.hash.replace('#cap-', ''); openChapter(window.location.hash.replace('#cap-', ''), false);
var body = document.getElementById('body-' + num);
if (body && window.bootstrap && bootstrap.Collapse) {
bootstrap.Collapse.getOrCreateInstance(body).show();
}
}); });
</script> </script>
</body> </body>
+28 -27
View File
@@ -3,12 +3,12 @@
<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>Gestione Incidenti - NIS2 Agile</title> <title>Incidenti · 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 (self-host only, nessun CDN esterno). -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (tabelle/decision-tree/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"> <!-- style.css resta caricato DOPO v3.css: veste i widget renderizzati via JS
<link rel="stylesheet" href="/css/style.css?v=20260618"> (tabelle/decision-tree/timeline/modali/badge). Self-hostato (nessun CDN). v3.css governa chrome + shell. -->
<style> <style>
/* ── Filters Bar ─────────────────────────────────────── */ /* ── Filters Bar ─────────────────────────────────────── */
.filters-bar { .filters-bar {
@@ -274,16 +274,23 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2" data-i18n="incidents.title">Gestione Incidenti</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span data-i18n="incidents.title">Gestione Incidenti</span>
<button class="btn btn-primary" onclick="showCreateIncidentModal()">+ Nuovo Incidente</button> </nav>
<div class="page-head">
<div>
<h1 data-i18n="incidents.title">Gestione Incidenti</h1>
<p class="sub">Registro incidenti e notifiche CSIRT · Art. 23 NIS2 (24h / 72h / 30g)</p>
</div>
<div class="content-header-actions">
<button class="btn-primary" onclick="showCreateIncidentModal()"><i data-lucide="plus" class="ic"></i>Nuovo Incidente</button>
</div>
</div> </div>
</header>
<div class="content-body"> <div class="content-body">
<!-- List View --> <!-- List View -->
@@ -349,28 +356,22 @@
<div id="view-detail" class="hidden"></div> <div id="view-detail" class="hidden"></div>
</div> </div>
</main> </main>
</div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → pagina -->
<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> <script>
// ── Auth & Init ────────────────────────────────────────────── // ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
I18n.init(); I18n.init();
HelpSystem.init(); HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
// ── Labels ─────────────────────────────────────────────────── // ── Labels ───────────────────────────────────────────────────
const CLASSIFICATION_LABELS = { const CLASSIFICATION_LABELS = {
+46 -17
View File
@@ -3,11 +3,17 @@
<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>NIS2 Agile — Integrazioni Esterne</title> <title>Integrazioni Esterne · NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN), PRIMA di style.css cosi' il design system esistente resta autorevole --> <!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia). -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="css/style.css?v=20260618"> <link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
/* ── V3 compat bridge: token usati dalla page-CSS qui sotto (palette CHIARA V3) ── */
:root{
--primary:#1F5FA8;
--text-primary:var(--ink); --text-secondary:var(--muted);
--bg-card:#fff; --border:var(--line);
}
/* ── Page-specific overrides ── */ /* ── Page-specific overrides ── */
.integ-hero { .integ-hero {
background: linear-gradient(135deg, rgba(6,182,212,.08) 0%, rgba(99,102,241,.06) 100%); background: linear-gradient(135deg, rgba(6,182,212,.08) 0%, rgba(99,102,241,.06) 100%);
@@ -55,7 +61,7 @@
.api-table { width: 100%; border-collapse: collapse; font-size: .83rem; margin-bottom: 1.5rem; } .api-table { width: 100%; border-collapse: collapse; font-size: .83rem; margin-bottom: 1.5rem; }
.api-table th { .api-table th {
background: rgba(255,255,255,.04); background: #F5F8FC;
color: var(--text-secondary); color: var(--text-secondary);
font-weight: 600; text-align: left; font-weight: 600; text-align: left;
padding: .6rem .75rem; padding: .6rem .75rem;
@@ -63,10 +69,14 @@
} }
.api-table td { .api-table td {
padding: .55rem .75rem; padding: .55rem .75rem;
border-bottom: 1px solid rgba(255,255,255,.04); border-bottom: 1px solid var(--line);
vertical-align: top; vertical-align: top;
} }
.api-table tr:hover td { background: rgba(255,255,255,.02); } .api-table tr:hover td { background: #F8FAFD; }
/* Badge stato (ex style.css) usati nei tab Inviti/Licenze */
.badge-status{display:inline-block;font-size:.7rem;font-weight:700;padding:.15rem .5rem;border-radius:6px;background:#EEF2F7;color:#5b6b80}
.badge-status.s-pending{background:#FBF1E2;color:#9A6418}
.badge-status.s-used{background:#E6F4EC;color:#1E7A4E}
.api-table code { .api-table code {
background: rgba(6,182,212,.1); background: rgba(6,182,212,.1);
color: var(--primary); color: var(--primary);
@@ -123,7 +133,7 @@
counter-increment: steps; counter-increment: steps;
display: flex; align-items: flex-start; gap: .75rem; display: flex; align-items: flex-start; gap: .75rem;
padding: .65rem 0; padding: .65rem 0;
border-bottom: 1px solid rgba(255,255,255,.04); border-bottom: 1px solid var(--line);
font-size: .85rem; font-size: .85rem;
color: var(--text-secondary); color: var(--text-secondary);
line-height: 1.5; line-height: 1.5;
@@ -169,7 +179,7 @@
.event-list { display: flex; flex-direction: column; gap: .5rem; margin-bottom: 1.5rem; } .event-list { display: flex; flex-direction: column; gap: .5rem; margin-bottom: 1.5rem; }
.event-row { .event-row {
display: flex; align-items: center; gap: .75rem; display: flex; align-items: center; gap: .75rem;
background: rgba(255,255,255,.03); background: #F8FAFD;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
padding: .6rem .9rem; padding: .6rem .9rem;
@@ -221,12 +231,19 @@
</head> </head>
<body> <body>
<div id="sidebar-placeholder"></div> <!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
<div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<div class="page-header"> <nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>Integrazioni Esterne</span>
</nav>
<div class="page-head">
<div>
<h1>Integrazioni Esterne</h1> <h1>Integrazioni Esterne</h1>
<p>API, webhook e connettori per sistemi GRC di terze parti</p> <p class="sub">API, webhook e connettori per sistemi GRC di terze parti</p>
</div>
</div> </div>
<!-- Hero --> <!-- Hero -->
@@ -912,10 +929,22 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
</main> </main>
<script src="js/common.js?v=20260624g"></script> <!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
<script src="js/i18n.js?v=20260631"></script> topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help -->
<script src="/js/api.js?v=20260625d"></script>
<script src="/js/common.js?v=20260627v3"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627"></script>
<script src="/js/i18n.js?v=20260631"></script>
<script src="/js/help.js?v=20260625d"></script>
<script> <script>
function showTab(id) { if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); // = loadTopnavV3()
if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
function showTab(id) {
document.querySelectorAll('.tab-btn').forEach((b, i) => { document.querySelectorAll('.tab-btn').forEach((b, i) => {
const ids = ['api', 'invites', 'lg231', 'webhook', 'quickstart']; const ids = ['api', 'invites', 'lg231', 'webhook', 'quickstart'];
b.classList.toggle('active', ids[i] === id); b.classList.toggle('active', ids[i] === id);
@@ -925,7 +954,7 @@ function showTab(id) {
}); });
// scroll to tabs // scroll to tabs
document.querySelector('.tab-nav').scrollIntoView({ behavior: 'smooth', block: 'start' }); document.querySelector('.tab-nav').scrollIntoView({ behavior: 'smooth', block: 'start' });
} }
</script> </script>
</body> </body>
</html> </html>
+39 -17
View File
@@ -3,10 +3,21 @@
<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>Audit interni - NIS2 Agile</title> <title>Audit interni · NIS2 Agile</title>
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia CDN). -->
<link rel="stylesheet" href="/css/style.css?v=20260618"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
/* ── Compat V3: token + bottoni emessi dal JS (ex style.css, niente CDN) ── */
:root{
--primary:#1F5FA8;--gray-100:#F1F5F9;--gray-200:#E3E9F0;--gray-400:#94A3B8;--gray-500:#64748B;
}
.btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:8px;padding:8px 13px;font:inherit;font-weight:600;font-size:12.5px;cursor:pointer;line-height:1.2}
.btn:disabled{opacity:.55;cursor:not-allowed}
.btn-primary{background:var(--c-blue);color:#fff}.btn-primary:hover:not(:disabled){background:#1A5494}
.btn-outline{background:#fff;color:var(--ink);border-color:var(--line)}.btn-outline:hover:not(:disabled){background:#F5F8FC;border-color:#c7d4e4}
.btn-sm{padding:5px 11px;font-size:11.5px}
a.btn{text-decoration:none}
.ia-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; } .ia-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
.ia-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; } .ia-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
.ia-toolbar { display:flex; justify-content:flex-end; margin-bottom:12px; } .ia-toolbar { display:flex; justify-content:flex-end; margin-bottom:12px; }
@@ -68,13 +79,23 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header"> <main class="wrap">
<h2 data-i18n="iaud.title">Audit interni</h2> <nav class="crumb" aria-label="breadcrumb">
</header> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-body"> <i data-lucide="chevron-right" class="ic"></i><span>Monitoraggio</span>
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="iaud.title">Audit interni</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="iaud.title">Audit interni</h1>
<p class="sub">Programma, checklist ed esiti degli audit interni del SGSI (ISO/IEC 27001 §9.2)</p>
</div>
<button class="btn-primary" onclick="auditOpen()"><i data-lucide="plus" class="ic"></i>Nuovo audit</button>
</div>
<section class="card card-pad">
<div class="ia-intro"> <div class="ia-intro">
<strong>Audit interni del SGSI (ISO/IEC 27001 §9.2).</strong> <strong>Audit interni del SGSI (ISO/IEC 27001 §9.2).</strong>
Programma gli audit, conduci la <em>checklist</em> (clausole 4-10 ISO 27001 + controlli Annex A applicabili dal tuo SoA), Programma gli audit, conduci la <em>checklist</em> (clausole 4-10 ISO 27001 + controlli Annex A applicabili dal tuo SoA),
@@ -85,11 +106,9 @@
(<strong>GV.PO-02</strong>, monitoraggio del programma di sicurezza). Strumento di supporto organizzativo, non un parere legale. (<strong>GV.PO-02</strong>, monitoraggio del programma di sicurezza). Strumento di supporto organizzativo, non un parere legale.
</div> </div>
<div class="ia-toolbar"><button class="btn btn-primary btn-sm" onclick="auditOpen()">+ Nuovo audit</button></div>
<div id="aud-list" aria-live="polite"></div> <div id="aud-list" aria-live="polite"></div>
</div> </section>
</main> </main>
</div>
<!-- Modale crea/modifica audit --> <!-- Modale crea/modifica audit -->
<div class="sm-overlay" id="aud-modal" role="dialog" aria-modal="true" aria-labelledby="aud-modal-title"> <div class="sm-overlay" id="aud-modal" role="dialog" aria-modal="true" aria-labelledby="aud-modal-title">
@@ -139,10 +158,12 @@
</div> </div>
</div> </div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<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>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></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> <script>
@@ -172,9 +193,10 @@
document.addEventListener('DOMContentLoaded', async function () { document.addEventListener('DOMContentLoaded', async function () {
if (typeof checkAuth === 'function' && !checkAuth()) return; if (typeof checkAuth === 'function' && !checkAuth()) return;
if (window.I18n && I18n.init) I18n.init('it');
if (typeof loadSidebar === 'function') loadSidebar(); if (typeof loadSidebar === 'function') loadSidebar();
if (window.I18n && I18n.init) I18n.init('it');
if (window.HelpSystem && HelpSystem.init) HelpSystem.init(); if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
document.addEventListener('keydown', e => { if (e.key === 'Escape') { auditClose(); detClose(); } }); document.addEventListener('keydown', e => { if (e.key === 'Escape') { auditClose(); detClose(); } });
['aud-modal', 'det-modal'].forEach(m => { const o = el(m); if (o) o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); }); }); ['aud-modal', 'det-modal'].forEach(m => { const o = el(m); if (o) o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); }); });
await iaLoadAll(); await iaLoadAll();
+92 -78
View File
@@ -3,49 +3,54 @@
<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>Modello Organizzativo SGSI (ISO 27001) - NIS2 Agile</title> <title>Modello Organizzativo SGSI (ISO 27001) · 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. Self-host only, nessun CDN esterno. -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (wizard/SoA/tabelle) -> 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">
<style> <style>
.isms-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:20px; font-size:.92rem; line-height:1.6; } /* Stili specifici della pagina SGSI non coperti da v3.css (nessun CDN). */
.isms-steps { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:20px; } .text-muted { color: var(--muted); }
.isms-step { flex:1 1 140px; min-width:120px; padding:10px 12px; border:1.5px solid var(--gray-200,#e5e7eb); border-radius:10px; background:#fff; cursor:pointer; font-size:.82rem; text-align:center; } .sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); }
.isms-step.active { border-color:var(--primary,#2563eb); background:#eff6ff; font-weight:700; } /* Base .btn usata dai bottoni generati via JS (es. "btn btn-sm btn-outline") */
.isms-step.done { border-color:#16a34a; } .btn { display:inline-flex; align-items:center; justify-content:center; gap:7px; font:inherit; font-weight:600; font-size:12.5px; border-radius:8px; padding:8px 13px; border:1px solid transparent; cursor:pointer; line-height:1.4; text-decoration:none; }
.isms-step .num { display:inline-block; width:22px; height:22px; line-height:22px; border-radius:50%; background:var(--gray-100,#f3f4f6); font-weight:700; margin-bottom:4px; } .btn .icon, .btn .ic { width:15px; height:15px; }
.isms-step.done .num { background:#16a34a; color:#fff; } .isms-intro { background:#EAF1FA; border-left:4px solid var(--c-blue); padding:14px 18px; border-radius:10px; margin-bottom:18px; font-size:.92rem; line-height:1.6; color:var(--ink); }
.isms-steps { display:flex; flex-wrap:wrap; gap:8px; margin-bottom:20px; }
.isms-step { flex:1 1 140px; min-width:120px; padding:11px 12px; border:1.5px solid var(--line); border-radius:11px; background:#fff; cursor:pointer; font-size:.82rem; text-align:center; transition:border-color .12s,background .12s; }
.isms-step:hover { border-color:#9FBEDD; }
.isms-step.active { border-color:var(--c-blue); background:#EAF1FA; font-weight:700; }
.isms-step.done { border-color:var(--c-green); }
.isms-step .num { display:inline-block; width:22px; height:22px; line-height:22px; border-radius:50%; background:#EEF2F7; font-weight:700; margin-bottom:4px; }
.isms-step.done .num { background:var(--c-green); color:#fff; }
.isms-panel { display:none; } .isms-panel { display:none; }
.isms-panel.active { display:block; } .isms-panel.active { display:block; }
.isms-disclaimer { font-size:.8rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:12px 0; } .isms-panel h3 { margin:0 0 12px; font-size:17px; font-weight:800; }
.raci-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); } .isms-panel h4 { margin:16px 0 8px; font-size:14px; font-weight:700; }
.raci-row input, .raci-row select { padding:6px 8px; border:1px solid var(--gray-200,#e5e7eb); border-radius:6px; font-size:.85rem; } .isms-disclaimer { font-size:.8rem; color:#92400e; background:#FBF1E2; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:12px 0; }
.soa-std { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; } .raci-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; padding:8px 0; border-bottom:1px solid var(--line); }
.soa-std-head { background:var(--gray-50,#f9fafb); padding:12px 16px; cursor:pointer; display:flex; justify-content:space-between; align-items:center; } .raci-row input, .raci-row select { padding:8px 10px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:.85rem; color:var(--ink); }
.soa-std { border:1px solid var(--line); border-radius:11px; margin-bottom:14px; overflow:hidden; }
.soa-std-head { background:#F5F8FC; padding:12px 16px; cursor:pointer; display:flex; justify-content:space-between; align-items:center; }
.soa-std-body { padding:6px 14px 14px; display:none; } .soa-std-body { padding:6px 14px 14px; display:none; }
.soa-std.open .soa-std-body { display:block; } .soa-std.open .soa-std-body { display:block; }
.soa-theme { font-size:.78rem; font-weight:700; text-transform:uppercase; color:var(--gray-500,#6b7280); margin:14px 0 6px; letter-spacing:.04em; } .soa-theme { font-size:.78rem; font-weight:700; text-transform:uppercase; color:var(--muted); margin:14px 0 6px; letter-spacing:.04em; }
.soa-ctrl { padding:12px 0; border-top:1px solid var(--gray-100,#f3f4f6); } .soa-ctrl { padding:12px 0; border-top:1px solid var(--line); }
.soa-ctrl-head { display:flex; gap:10px; align-items:flex-start; justify-content:space-between; flex-wrap:wrap; } .soa-ctrl-head { display:flex; gap:10px; align-items:flex-start; justify-content:space-between; flex-wrap:wrap; }
.soa-code { display:inline-block; background:#eef2ff; color:#3730a3; font-weight:700; font-size:.72rem; padding:2px 8px; border-radius:6px; margin-right:6px; } .soa-code { display:inline-block; background:#E7F0FB; color:#1F5FA8; font-weight:700; font-size:.72rem; padding:2px 8px; border-radius:6px; margin-right:6px; }
.soa-derived { background:#ecfdf5; color:#065f46; font-size:.68rem; padding:1px 6px; border-radius:5px; } .soa-derived { background:#E6F4EC; color:#1E7A4E; font-size:.68rem; padding:1px 6px; border-radius:5px; }
.soa-opts { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; } .soa-opts { display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
.soa-opt { min-height:34px; padding:5px 10px; border:1.5px solid var(--gray-200,#e5e7eb); border-radius:18px; background:#fff; cursor:pointer; font-size:.8rem; font-weight:600; } .soa-opt { min-height:34px; padding:5px 10px; border:1.5px solid var(--line); border-radius:18px; background:#fff; cursor:pointer; font:inherit; font-size:.8rem; font-weight:600; color:var(--ink); }
.soa-opt.sel-implemented,.soa-opt.sel-verified { background:#16a34a; color:#fff; border-color:#16a34a; } .soa-opt.sel-implemented,.soa-opt.sel-verified { background:var(--c-green); color:#fff; border-color:var(--c-green); }
.soa-opt.sel-in_progress { background:#f59e0b; color:#fff; border-color:#f59e0b; } .soa-opt.sel-in_progress { background:var(--c-amber); color:#fff; border-color:var(--c-amber); }
.soa-opt.sel-not_started { background:#9ca3af; color:#fff; border-color:#9ca3af; } .soa-opt.sel-not_started { background:#9ca3af; color:#fff; border-color:#9ca3af; }
.soa-justif { width:100%; margin-top:8px; padding:8px; border:1px solid var(--gray-200,#e5e7eb); border-radius:6px; font-size:.84rem; min-height:46px; } .soa-justif { width:100%; margin-top:8px; padding:8px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:.84rem; min-height:46px; color:var(--ink); }
.isms-field { margin-bottom:14px; } .isms-field { margin-bottom:14px; }
.isms-field label { display:block; font-weight:600; font-size:.88rem; margin-bottom:4px; } .isms-field label { display:block; font-weight:600; font-size:.88rem; margin-bottom:4px; }
.isms-field textarea, .isms-field input { width:100%; padding:9px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; } .isms-field textarea, .isms-field input { width:100%; padding:9px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:.9rem; color:var(--ink); }
.isms-flags label { display:block; padding:8px 0; font-size:.9rem; } .isms-flags label { display:block; padding:8px 0; font-size:.9rem; }
.ck-item { display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); font-size:.9rem; } .ck-item { display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--line); font-size:.9rem; }
.ck-yes { color:#16a34a; font-weight:700; } .ck-no { color:#9ca3af; } .ck-yes { color:var(--c-green); font-weight:700; } .ck-no { color:#9ca3af; }
.savehint { font-size:.78rem; color:var(--gray-400,#9ca3af); min-height:16px; } .savehint { font-size:.78rem; color:var(--muted); min-height:16px; }
/* Allinea l'icona sprite BI nel bottone Esporta col testo */
.content-header-actions .btn .icon { vertical-align:text-bottom; margin-right:4px; }
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -62,21 +67,29 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header"> <main class="wrap">
<h2 data-i18n="isms.title">Modello Organizzativo SGSI (ISO 27001)</h2> <nav class="crumb" aria-label="breadcrumb">
<div class="content-header-actions"> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>SGSI (ISO 27001)</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="isms.title">Modello Organizzativo SGSI (ISO 27001)</h1>
<p class="sub">Procedura guidata ISO/IEC 27001:2022 — clausole 4–10 + Statement of Applicability</p>
</div>
<div class="content-header-actions" style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
<span class="savehint" id="isms-savehint" role="status" aria-live="polite"></span> <span class="savehint" id="isms-savehint" role="status" aria-live="polite"></span>
<button class="btn btn-outline btn-sm" id="nis2-isms-btn-export" onclick="ismsExportView()" data-i18n="isms.export"> <button class="btn-ghost" id="nis2-isms-btn-export" onclick="ismsExportView()" data-i18n="isms.export">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-file"></use></svg> <i data-lucide="file-text" class="ic"></i>
<span>Esporta SGSI</span> <span>Esporta SGSI</span>
</button> </button>
</div> </div>
</header> </div>
<div class="content-body"> <section class="card card-pad">
<div class="isms-intro" id="nis2-isms-intro"> <div class="isms-intro" id="nis2-isms-intro">
<strong data-i18n="isms.intro.title">Costruisci il tuo Sistema di Gestione della Sicurezza delle Informazioni.</strong> <strong data-i18n="isms.intro.title">Costruisci il tuo Sistema di Gestione della Sicurezza delle Informazioni.</strong>
<span data-i18n="isms.intro.body">Procedura guidata ISO/IEC 27001:2022 (clausole 4-10 + Statement of Applicability). Lo Statement of Applicability viene pre-popolato dalle tue risposte alla Gap Analysis NIS2. Attiva le estensioni cloud ISO 27017/27018 dove pertinenti.</span> <span data-i18n="isms.intro.body">Procedura guidata ISO/IEC 27001:2022 (clausole 4-10 + Statement of Applicability). Lo Statement of Applicability viene pre-popolato dalle tue risposte alla Gap Analysis NIS2. Attiva le estensioni cloud ISO 27017/27018 dove pertinenti.</span>
@@ -87,7 +100,7 @@
<!-- STEP 1 - Contesto & Ambito (cl.4) --> <!-- STEP 1 - Contesto & Ambito (cl.4) -->
<div class="isms-panel" id="panel-0"> <div class="isms-panel" id="panel-0">
<div class="card"><div class="card-body"> <div class="card card-pad">
<h3 data-i18n="isms.s1.title">1. Contesto e Ambito (cl. 4)</h3> <h3 data-i18n="isms.s1.title">1. Contesto e Ambito (cl. 4)</h3>
<div class="isms-field"><label for="f-scope" data-i18n="isms.s1.scope">Ambito del SGSI (scope statement)</label><textarea id="f-scope" rows="3" placeholder="Es. Tutti i sistemi e processi a supporto dei servizi ICT erogati dalla sede di..."></textarea></div> <div class="isms-field"><label for="f-scope" data-i18n="isms.s1.scope">Ambito del SGSI (scope statement)</label><textarea id="f-scope" rows="3" placeholder="Es. Tutti i sistemi e processi a supporto dei servizi ICT erogati dalla sede di..."></textarea></div>
<div class="isms-field"><label for="f-ctx-int" data-i18n="isms.s1.ctx_int">Contesto interno</label><textarea id="f-ctx-int" rows="2"></textarea></div> <div class="isms-field"><label for="f-ctx-int" data-i18n="isms.s1.ctx_int">Contesto interno</label><textarea id="f-ctx-int" rows="2"></textarea></div>
@@ -101,57 +114,57 @@
<label><input type="checkbox" id="f-cloud-provider"> <span data-i18n="isms.s1.cloud_provider">Siamo fornitori di servizi cloud → ISO 27017 (lato provider)</span></label> <label><input type="checkbox" id="f-cloud-provider"> <span data-i18n="isms.s1.cloud_provider">Siamo fornitori di servizi cloud → ISO 27017 (lato provider)</span></label>
<label><input type="checkbox" id="f-pii-cloud"> <span data-i18n="isms.s1.pii_cloud">Trattiamo dati personali (PII) in cloud pubblico come responsabili → ISO 27018</span></label> <label><input type="checkbox" id="f-pii-cloud"> <span data-i18n="isms.s1.pii_cloud">Trattiamo dati personali (PII) in cloud pubblico come responsabili → ISO 27018</span></label>
</div> </div>
<button class="btn btn-primary" id="nis2-isms-btn-save-step1" onclick="saveStep1()" data-i18n="isms.save_next">Salva e continua</button> <button class="btn-primary" id="nis2-isms-btn-save-step1" onclick="saveStep1()" data-i18n="isms.save_next">Salva e continua</button>
</div></div> </div>
</div> </div>
<!-- STEP 2 - Leadership (cl.5) --> <!-- STEP 2 - Leadership (cl.5) -->
<div class="isms-panel" id="panel-1"> <div class="isms-panel" id="panel-1">
<div class="card"><div class="card-body"> <div class="card card-pad">
<h3 data-i18n="isms.s2.title">2. Leadership: ruoli e responsabilità (cl. 5)</h3> <h3 data-i18n="isms.s2.title">2. Leadership: ruoli e responsabilità (cl. 5)</h3>
<p class="text-muted" data-i18n="isms.s2.hint">Definisci i ruoli del SGSI con la matrice RACI (Responsible, Accountable, Consulted, Informed).</p> <p class="text-muted" data-i18n="isms.s2.hint">Definisci i ruoli del SGSI con la matrice RACI (Responsible, Accountable, Consulted, Informed).</p>
<div id="roles-list"></div> <div id="roles-list"></div>
<div class="raci-row" style="border-top:2px solid var(--gray-200,#e5e7eb); margin-top:10px;"> <div class="raci-row" style="border-top:2px solid var(--line); margin-top:10px;">
<input id="r-name" placeholder="Ruolo (es. CISO)" aria-label="Ruolo" style="flex:1 1 160px;"> <input id="r-name" placeholder="Ruolo (es. CISO)" aria-label="Ruolo" style="flex:1 1 160px;">
<input id="r-resp" placeholder="Responsabilità" aria-label="Responsabilità" style="flex:2 1 220px;"> <input id="r-resp" placeholder="Responsabilità" aria-label="Responsabilità" style="flex:2 1 220px;">
<select id="r-raci" aria-label="RACI"><option value="">RACI</option><option>R</option><option>A</option><option>C</option><option>I</option></select> <select id="r-raci" aria-label="RACI"><option value="">RACI</option><option>R</option><option>A</option><option>C</option><option>I</option></select>
<button class="btn btn-sm btn-primary" id="nis2-isms-btn-add-role" onclick="addRole()" data-i18n="isms.s2.add">Aggiungi ruolo</button> <button class="btn-primary btn-sm" id="nis2-isms-btn-add-role" onclick="addRole()" data-i18n="isms.s2.add">Aggiungi ruolo</button>
</div>
<button class="btn-ghost" id="nis2-isms-btn-next-step2" style="margin-top:14px;" onclick="goStep(2)" data-i18n="isms.next">Continua</button>
</div> </div>
<button class="btn btn-outline" id="nis2-isms-btn-next-step2" style="margin-top:14px;" onclick="goStep(2)" data-i18n="isms.next">Continua</button>
</div></div>
</div> </div>
<!-- STEP 3 - Risk (cl.6) --> <!-- STEP 3 - Risk (cl.6) -->
<div class="isms-panel" id="panel-2"> <div class="isms-panel" id="panel-2">
<div class="card"><div class="card-body"> <div class="card card-pad">
<h3 data-i18n="isms.s3.title">3. Risk: metodologia e obiettivi (cl. 6)</h3> <h3 data-i18n="isms.s3.title">3. Risk: metodologia e obiettivi (cl. 6)</h3>
<div class="isms-field"><label for="f-method" data-i18n="isms.s3.method">Metodologia di risk assessment</label><textarea id="f-method" rows="3" placeholder="Es. ISO 27005 / NIST SP 800-30; scala probabilità×impatto 5×5 (già usata nel modulo Rischi)."></textarea></div> <div class="isms-field"><label for="f-method" data-i18n="isms.s3.method">Metodologia di risk assessment</label><textarea id="f-method" rows="3" placeholder="Es. ISO 27005 / NIST SP 800-30; scala probabilità×impatto 5×5 (già usata nel modulo Rischi)."></textarea></div>
<div class="isms-field"><label for="f-objectives" data-i18n="isms.s3.objectives">Obiettivi del SGSI (uno per riga)</label><textarea id="f-objectives" rows="3"></textarea></div> <div class="isms-field"><label for="f-objectives" data-i18n="isms.s3.objectives">Obiettivi del SGSI (uno per riga)</label><textarea id="f-objectives" rows="3"></textarea></div>
<p class="text-muted" data-i18n="isms.s3.link">Il registro dei rischi vive nel modulo <a href="risks.html">Rischi</a>: questo passo ne dichiara la metodologia.</p> <p class="text-muted" data-i18n="isms.s3.link">Il registro dei rischi vive nel modulo <a href="risks.html">Rischi</a>: questo passo ne dichiara la metodologia.</p>
<button class="btn btn-primary" id="nis2-isms-btn-save-step3" onclick="saveStep3()" data-i18n="isms.save_next">Salva e continua</button> <button class="btn-primary" id="nis2-isms-btn-save-step3" onclick="saveStep3()" data-i18n="isms.save_next">Salva e continua</button>
</div></div> </div>
</div> </div>
<!-- STEP 4 - SoA --> <!-- STEP 4 - SoA -->
<div class="isms-panel" id="panel-3"> <div class="isms-panel" id="panel-3">
<div class="card"><div class="card-body"> <div class="card card-pad">
<h3 data-i18n="isms.s4.title">4. Statement of Applicability</h3> <h3 data-i18n="isms.s4.title">4. Statement of Applicability</h3>
<p class="text-muted" data-i18n="isms.s4.hint">I controlli sono pre-popolati dalle risposte NIS2 dove esiste corrispondenza (badge "da NIS2"). Conferma applicabilità, stato e motivazione per ciascuno.</p> <p class="text-muted" data-i18n="isms.s4.hint">I controlli sono pre-popolati dalle risposte NIS2 dove esiste corrispondenza (badge "da NIS2"). Conferma applicabilità, stato e motivazione per ciascuno.</p>
<div style="margin:10px 0;"> <div style="margin:10px 0;">
<button class="btn btn-sm btn-outline" id="nis2-isms-btn-derive-soa" onclick="deriveSoa()" data-i18n="isms.s4.derive">Ri-deriva dal NIS2</button> <button class="btn-ghost btn-sm" id="nis2-isms-btn-derive-soa" onclick="deriveSoa()" data-i18n="isms.s4.derive">Ri-deriva dal NIS2</button>
<span id="soa-stats" class="text-muted" style="margin-left:12px; font-size:.85rem;"></span> <span id="soa-stats" class="text-muted" style="margin-left:12px; font-size:.85rem;"></span>
</div> </div>
<div id="soa-groups"></div> <div id="soa-groups"></div>
<button class="btn btn-outline" id="nis2-isms-btn-next-step4" style="margin-top:14px;" onclick="goStep(4)" data-i18n="isms.next">Continua</button> <button class="btn-ghost" id="nis2-isms-btn-next-step4" style="margin-top:14px;" onclick="goStep(4)" data-i18n="isms.next">Continua</button>
</div></div> </div>
</div> </div>
<!-- STEP 5 - Documented information (cl.7-8) --> <!-- STEP 5 - Documented information (cl.7-8) -->
<div class="isms-panel" id="panel-4"> <div class="isms-panel" id="panel-4">
<div class="card"><div class="card-body"> <div class="card card-pad">
<h3 data-i18n="isms.s5.title">5. Documented Information (cl. 7-8)</h3> <h3 data-i18n="isms.s5.title">5. Documented Information (cl. 7-8)</h3>
<div class="raci-row" style="border:none;"> <div class="raci-row" style="border:none;">
<label for="d-type" class="sr-only" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);">Tipo di documento</label> <label for="d-type" class="sr-only">Tipo di documento</label>
<select id="d-type" style="flex:1 1 200px;" aria-label="Tipo di documento"> <select id="d-type" style="flex:1 1 200px;" aria-label="Tipo di documento">
<option value="policy_sgsi">Policy SGSI</option> <option value="policy_sgsi">Policy SGSI</option>
<option value="dichiarazione_applicabilita">Dichiarazione di Applicabilità</option> <option value="dichiarazione_applicabilita">Dichiarazione di Applicabilità</option>
@@ -160,44 +173,45 @@
<option value="procedura_continuita">Procedura continuità operativa</option> <option value="procedura_continuita">Procedura continuità operativa</option>
<option value="politica_fornitori">Politica sicurezza fornitori</option> <option value="politica_fornitori">Politica sicurezza fornitori</option>
</select> </select>
<button class="btn btn-sm btn-primary" onclick="aiGenDoc()" id="btn-aigen" data-i18n="isms.s5.aigen">Genera bozza con AI</button> <button class="btn-primary btn-sm" onclick="aiGenDoc()" id="btn-aigen" data-i18n="isms.s5.aigen">Genera bozza con AI</button>
</div> </div>
<div class="savehint" id="aigen-hint" role="status" aria-live="polite"></div> <div class="savehint" id="aigen-hint" role="status" aria-live="polite"></div>
<div id="docs-list" style="margin-top:14px;"></div> <div id="docs-list" style="margin-top:14px;"></div>
<button class="btn btn-outline" id="nis2-isms-btn-next-step5" style="margin-top:14px;" onclick="goStep(5)" data-i18n="isms.next">Continua</button> <button class="btn-ghost" id="nis2-isms-btn-next-step5" style="margin-top:14px;" onclick="goStep(5)" data-i18n="isms.next">Continua</button>
</div></div> </div>
</div> </div>
<!-- STEP 6 - Monitoring & Readiness (cl.9-10) --> <!-- STEP 6 - Monitoring & Readiness (cl.9-10) -->
<div class="isms-panel" id="panel-5"> <div class="isms-panel" id="panel-5">
<div class="card"><div class="card-body"> <div class="card card-pad">
<h3 data-i18n="isms.s6.title">6. Monitoraggio e Miglioramento (cl. 9-10)</h3> <h3 data-i18n="isms.s6.title">6. Monitoraggio e Miglioramento (cl. 9-10)</h3>
<p class="text-muted" data-i18n="isms.s6.hint">Gli audit interni e le non conformità si gestiscono nei moduli esistenti: <a href="reports.html">Audit & Report</a> e NCR/CAPA.</p> <p class="text-muted" data-i18n="isms.s6.hint">Gli audit interni e le non conformità si gestiscono nei moduli esistenti: <a href="reports.html">Audit & Report</a> e NCR/CAPA.</p>
<h4 data-i18n="isms.s6.readiness">Completamento del SGSI</h4> <h4 data-i18n="isms.s6.readiness">Completamento del SGSI</h4>
<div style="text-align:center; margin:14px 0;"> <div style="text-align:center; margin:14px 0;">
<div style="font-size:2.6rem; font-weight:800;" id="readiness-pct">0%</div> <div style="font-size:2.6rem; font-weight:800; color:var(--c-blue);" id="readiness-pct">0%</div>
</div> </div>
<div id="readiness-checklist"></div> <div id="readiness-checklist"></div>
</div></div>
</div> </div>
</div> </div>
</section>
</main> </main>
</div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → isms.js.
isms.js si auto-inizializza su DOMContentLoaded (checkAuth/I18n/loadSidebar/Help/render). -->
<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/isms.js?v=20260620b"></script> <script src="/js/isms.js?v=20260627v3"></script>
<script>
// Icone Lucide della chrome + crumb/page-head statici. L'init dati e il wiring
// (renderSteps/loadModel/goStep) restano in isms.js (suo DOMContentLoaded).
document.addEventListener('DOMContentLoaded', function(){
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
});
</script>
</body> </body>
</html> </html>
+58 -40
View File
@@ -3,15 +3,36 @@
<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>Knowledge Base - NIS2 Agile</title> <title>Knowledge Base · 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 / NO Bootstrap Italia). -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (lista doc, search) -> 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">
<style> <style>
/* ── V3 compat bridge: token + componenti (card/form/btn/badge) ex style.css ── */
:root{
--primary:#1F5FA8; --gray-100:#f1f5f9; --gray-300:#cbd5e1; --gray-500:#64748b;
--gray-600:#475569; --gray-700:#334155;
}
.card-pad .card-header{padding:0 0 14px;border-bottom:1px solid var(--line);margin-bottom:16px}
.card-pad .card-header h3{margin:0;font-size:15px;font-weight:700}
.card-pad .card-body{padding:0}
.text-muted{color:var(--muted)} .text-danger{color:var(--c-red)}
.form-group{margin-bottom:0} .form-group + .form-group{margin-top:12px}
.form-control{width:100%;padding:9px 12px;border:1px solid var(--line);border-radius:8px;
font:inherit;font-size:13px;color:var(--ink);background:#fff}
.form-control:focus{outline:none;border-color:#9FBEDD;box-shadow:0 0 0 3px rgba(31,95,168,.12)}
label{font-size:12.5px;font-weight:600;color:var(--ink);display:block;margin-bottom:4px}
.btn{display:inline-flex;align-items:center;gap:7px;border-radius:8px;padding:8px 13px;
font:inherit;font-weight:600;font-size:12.5px;cursor:pointer;border:1px solid var(--line);background:#fff;color:var(--ink)}
.btn:hover{background:#F5F8FC}
.btn-primary{background:var(--c-blue);color:#fff;border-color:var(--c-blue)} .btn-primary:hover{background:#1A5494}
.btn-secondary{background:#fff;color:var(--ink)}
.btn-danger{background:var(--c-red);color:#fff;border-color:var(--c-red)} .btn-danger:hover{filter:brightness(.95)}
.btn-sm{padding:5px 10px;font-size:11.5px}
.btn .icon{width:15px;height:15px}
.kb-scope-radios { display: flex; gap: 18px; flex-wrap: wrap; font-size: .9rem; } .kb-scope-radios { display: flex; gap: 18px; flex-wrap: wrap; font-size: .9rem; }
.kb-scope-radios label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; } .kb-scope-radios label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; margin-bottom:0; }
.kb-share-block { margin-top: 12px; } .kb-share-block { margin-top: 12px; }
.kb-doc-list { margin-top: 24px; } .kb-doc-list { margin-top: 24px; }
.kb-doc-row { padding: 10px 12px; border-bottom: 1px solid var(--gray-100); display: flex; justify-content: space-between; align-items: center; } .kb-doc-row { padding: 10px 12px; border-bottom: 1px solid var(--gray-100); display: flex; justify-content: space-between; align-items: center; }
@@ -19,8 +40,6 @@
.kb-scope-badge.SYSTEM { background: #DBEAFE; color: #1E40AF; } .kb-scope-badge.SYSTEM { background: #DBEAFE; color: #1E40AF; }
.kb-scope-badge.FIRM { background: #FEF3C7; color: #92400E; } .kb-scope-badge.FIRM { background: #FEF3C7; color: #92400E; }
.kb-scope-badge.ORG { background: #DCFCE7; color: #166534; } .kb-scope-badge.ORG { background: #DCFCE7; color: #166534; }
/* Allinea l'icona sprite BI dentro i bottoni del design system esistente */
.btn .icon { fill: currentColor; vertical-align: middle; }
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -37,19 +56,23 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2">Knowledge Base</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span>Knowledge Base</span>
<button class="btn btn-primary" id="btn-kb-upload"> </nav>
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-upload"></use></svg> <div class="page-head">
Carica documento <div>
<h1>Knowledge Base</h1>
<p class="sub">Documenti che l'AI usa per rispondere con citazioni precise</p>
</div>
<button class="btn-primary" id="btn-kb-upload">
<i data-lucide="upload" class="ic"></i>Carica documento
</button> </button>
</div> </div>
</header>
<div class="content-body"> <div class="content-body">
<p class="text-muted" style="margin-top:0;"> <p class="text-muted" style="margin-top:0;">
@@ -60,7 +83,7 @@
</p> </p>
<!-- Form upload (hidden by default) --> <!-- Form upload (hidden by default) -->
<div class="card" id="kb-form-card" style="display:none;margin-top:16px;"> <div class="card card-pad" id="kb-form-card" style="display:none;margin-top:16px;">
<div class="card-body"> <div class="card-body">
<div class="form-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;"> <div class="form-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div class="form-group"> <div class="form-group">
@@ -120,7 +143,7 @@
</div> </div>
<!-- Lista documenti --> <!-- Lista documenti -->
<div class="card kb-doc-list"> <div class="card card-pad kb-doc-list">
<div class="card-header"> <div class="card-header">
<h3>Documenti caricati visibili</h3> <h3>Documenti caricati visibili</h3>
</div> </div>
@@ -132,15 +155,16 @@
</div> </div>
<!-- Search preview --> <!-- Search preview -->
<div class="card" style="margin-top:24px;"> <div class="card card-pad" style="margin-top:24px;">
<div class="card-header"><h3>Test search semantica</h3></div> <div class="card-header"><h3>Test search semantica</h3></div>
<div class="card-body"> <div class="card-body">
<div style="display:flex;gap:8px;"> <div style="display:flex;gap:8px;align-items:flex-end;">
<label for="kb-search-query" class="sr-only visually-hidden">Query di ricerca semantica</label> <div style="flex:1;">
<label for="kb-search-query">Query di ricerca semantica</label>
<input type="text" id="kb-search-query" class="form-control" placeholder="Es: come gestire un incidente NIS2?" aria-label="Query di ricerca semantica"> <input type="text" id="kb-search-query" class="form-control" placeholder="Es: come gestire un incidente NIS2?" aria-label="Query di ricerca semantica">
</div>
<button class="btn btn-secondary" id="btn-kb-search"> <button class="btn btn-secondary" id="btn-kb-search">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-search"></use></svg> <i data-lucide="search" class="icon"></i>Cerca
Cerca
</button> </button>
</div> </div>
<div id="kb-search-results" style="margin-top:12px;"></div> <div id="kb-search-results" style="margin-top:12px;"></div>
@@ -148,31 +172,25 @@
</div> </div>
</div> </div>
</main> </main>
</div>
<!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). --> <!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
<script src="/js/common.js?v=20260624g"></script> topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → kb.js -->
<script src="/js/api.js?v=20260625d"></script> <script src="/js/api.js?v=20260625d"></script>
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <script src="/js/common.js?v=20260627v3"></script>
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. --> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/js/topnav-v3.js?v=20260627"></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/kb.js"></script> <script src="/js/kb.js"></script>
<script> <script>
// Gate auth + chrome come le altre pagine -bi. // Gate auth + chrome come le altre pagine V3.
if (typeof checkAuth === 'function' && !checkAuth()) { if (typeof checkAuth === 'function' && !checkAuth()) {
// checkAuth reindirizza a login se non autenticato // checkAuth reindirizza a login se non autenticato
} else { } else {
if (typeof loadSidebar === 'function') loadSidebar(); if (typeof loadSidebar === 'function') loadSidebar(); // = loadTopnavV3()
if (typeof I18n !== 'undefined' && I18n.init) I18n.init(); if (typeof I18n !== 'undefined' && I18n.init) I18n.init();
if (typeof HelpSystem !== 'undefined' && HelpSystem.init) HelpSystem.init(); if (typeof HelpSystem !== 'undefined' && HelpSystem.init) HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
} }
</script> </script>
</body> </body>
+1 -1
View File
@@ -26,7 +26,7 @@
<h1>Manutenzione in corso</h1> <h1>Manutenzione in corso</h1>
<p>Stiamo aggiornando l'<strong>interfaccia (UI)</strong> di NIS2 Agile su richiesta di modifica. <p>Stiamo aggiornando l'<strong>interfaccia (UI)</strong> di NIS2 Agile su richiesta di modifica.
Il servizio non è momentaneamente accessibile.</p> Il servizio non è momentaneamente accessibile.</p>
<div class="win">Finestra di manutenzione: <b>oggi dalle 10:39 alle ~11:45</b> (ora italiana)<br>durata stimata <b>circa 1 ora</b></div> <div class="win">Finestra di manutenzione: <b>oggi dalle 10:39 alle ~12:15</b> (ora italiana)<br>durata stimata <b>circa 1–1,5 ore</b></div>
<p class="small">Grazie per la pazienza. Non occorre fare nulla: l'accesso tornerà disponibile automaticamente al termine.</p> <p class="small">Grazie per la pazienza. Non occorre fare nulla: l'accesso tornerà disponibile automaticamente al termine.</p>
</div> </div>
</body> </body>
+39 -17
View File
@@ -3,10 +3,21 @@
<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>Riesame di Direzione - NIS2 Agile</title> <title>Riesame di Direzione · NIS2 Agile</title>
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia CDN). -->
<link rel="stylesheet" href="/css/style.css?v=20260618"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
/* ── Compat V3: token + bottoni emessi dal JS (ex style.css, niente CDN) ── */
:root{
--primary:#1F5FA8;--gray-100:#F1F5F9;--gray-200:#E3E9F0;--gray-400:#94A3B8;--gray-500:#64748B;
}
.btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:8px;padding:8px 13px;font:inherit;font-weight:600;font-size:12.5px;cursor:pointer;line-height:1.2}
.btn:disabled{opacity:.55;cursor:not-allowed}
.btn-primary{background:var(--c-blue);color:#fff}.btn-primary:hover:not(:disabled){background:#1A5494}
.btn-outline{background:#fff;color:var(--ink);border-color:var(--line)}.btn-outline:hover:not(:disabled){background:#F5F8FC;border-color:#c7d4e4}
.btn-sm{padding:5px 11px;font-size:11.5px}
a.btn{text-decoration:none}
.mr-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; } .mr-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
.mr-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; } .mr-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
.mr-toolbar { display:flex; justify-content:flex-end; margin-bottom:12px; } .mr-toolbar { display:flex; justify-content:flex-end; margin-bottom:12px; }
@@ -62,13 +73,23 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header"> <main class="wrap">
<h2 data-i18n="mreview.title">Riesame di Direzione</h2> <nav class="crumb" aria-label="breadcrumb">
</header> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-body"> <i data-lucide="chevron-right" class="ic"></i><span>Monitoraggio</span>
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="mreview.title">Riesame di Direzione</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="mreview.title">Riesame di Direzione</h1>
<p class="sub">Verbale del riesame periodico del SGSI: input aggregati, decisioni e approvazione (ISO/IEC 27001 §9.3)</p>
</div>
<button class="btn-primary" onclick="mrOpen()"><i data-lucide="plus" class="ic"></i>Nuovo riesame</button>
</div>
<section class="card card-pad">
<div class="mr-intro"> <div class="mr-intro">
<strong>Riesame periodico del SGSI da parte della direzione.</strong> <strong>Riesame periodico del SGSI da parte della direzione.</strong>
Produce il <em>verbale</em> con gli elementi in ingresso (aggregati automaticamente dai moduli esistenti) e in uscita Produce il <em>verbale</em> con gli elementi in ingresso (aggregati automaticamente dai moduli esistenti) e in uscita
@@ -79,11 +100,9 @@
art. 23 D.Lgs. 138/2024). Strumento di supporto e pre-audit: non sostituisce l'auditor. art. 23 D.Lgs. 138/2024). Strumento di supporto e pre-audit: non sostituisce l'auditor.
</div> </div>
<div class="mr-toolbar"><button class="btn btn-primary btn-sm" onclick="mrOpen()">+ Nuovo riesame</button></div>
<div id="mr-list" aria-live="polite"></div> <div id="mr-list" aria-live="polite"></div>
</div> </section>
</main> </main>
</div>
<!-- Modale dettaglio / verbale --> <!-- Modale dettaglio / verbale -->
<div class="sm-overlay" id="mr-modal" role="dialog" aria-modal="true" aria-labelledby="mr-modal-title"> <div class="sm-overlay" id="mr-modal" role="dialog" aria-modal="true" aria-labelledby="mr-modal-title">
@@ -126,10 +145,12 @@
</div> </div>
</div> </div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<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>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></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> <script>
@@ -160,9 +181,10 @@
document.addEventListener('DOMContentLoaded', async function () { document.addEventListener('DOMContentLoaded', async function () {
if (typeof checkAuth === 'function' && !checkAuth()) return; if (typeof checkAuth === 'function' && !checkAuth()) return;
if (window.I18n && I18n.init) I18n.init('it');
if (typeof loadSidebar === 'function') loadSidebar(); if (typeof loadSidebar === 'function') loadSidebar();
if (window.I18n && I18n.init) I18n.init('it');
if (window.HelpSystem && HelpSystem.init) HelpSystem.init(); if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
document.addEventListener('keydown', e => { if (e.key === 'Escape') mrClose(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') mrClose(); });
const ov = el('mr-modal'); if (ov) ov.addEventListener('click', e => { if (e.target === ov) mrClose(); }); const ov = el('mr-modal'); if (ov) ov.addEventListener('click', e => { if (e.target === ov) mrClose(); });
await mrLoadAll(); await mrLoadAll();
+74 -76
View File
@@ -3,35 +3,35 @@
<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>Misure e Requisiti - NIS2 Agile</title> <title>Misure e Requisiti · 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 / NO Bootstrap Italia). -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/style.css?v=20260618"> <link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
.mr-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:14px; font-size:.92rem; line-height:1.6; } /* V3 — specifico pagina Misure/Requisiti (non coperto da v3.css) */
.mr-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; } .mr-intro { background:#EAF1FA; border-left:4px solid var(--c-blue); padding:14px 18px; border-radius:10px; margin-bottom:14px; font-size:13.5px; line-height:1.6; color:var(--ink); }
.mr-ro { font-size:.8rem; color:#3730a3; background:#eef2ff; border:1px solid #c7d2fe; border-radius:8px; padding:8px 12px; margin:0 0 16px; display:flex; gap:8px; align-items:center; } .mr-note { font-size:12.5px; color:#92400e; background:#FBF1E2; border:1px solid #fde68a; border-radius:10px; padding:10px 12px; margin:0 0 16px; }
.mr-stats { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:16px; } .mr-ro { font-size:12.5px; color:#3730a3; background:#eef2ff; border:1px solid #c7d2fe; border-radius:10px; padding:8px 12px; margin:0 0 16px; display:flex; gap:8px; align-items:center; }
.mr-stat { flex:1 1 140px; min-width:120px; background:#fff; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:12px 14px; } .mr-stats { display:flex; flex-wrap:wrap; gap:12px; margin-bottom:16px; }
.mr-stat .n { font-size:1.5rem; font-weight:800; line-height:1; } .mr-stat { flex:1 1 140px; min-width:120px; background:#fff; border:1px solid var(--line); border-radius:11px; padding:14px 16px; box-shadow:var(--shadow); }
.mr-stat .l { font-size:.76rem; color:var(--gray-500,#6b7280); margin-top:4px; } .mr-stat .n { font-size:27px; font-weight:800; line-height:1; color:var(--c-blue); }
.mr-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; } .mr-stat .l { font-size:11px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#7a8aa0; margin-top:6px; }
.mr-toolbar label { font-weight:600; font-size:.82rem; } .mr-toolbar { display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
.mr-toolbar select, .mr-toolbar input[type=search] { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; } .mr-toolbar label { font-weight:600; font-size:13px; color:var(--ink); }
.mr-toolbar select, .mr-toolbar input[type=search] { padding:8px 11px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
.mr-toolbar .grow { flex:1 1 auto; } .mr-toolbar .grow { flex:1 1 auto; }
.mr-group { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; } .mr-group { border:1px solid var(--line); border-radius:11px; margin-bottom:14px; overflow:hidden; }
.mr-group-head { background:#f8fafc; padding:11px 14px; border-bottom:1px solid var(--gray-100,#f3f4f6); } .mr-group-head { background:#FAFCFE; padding:12px 16px; border-bottom:1px solid var(--line); }
.mr-group-head .code { font-weight:800; color:#1e3a8a; font-size:.95rem; } .mr-group-head .code { font-weight:800; color:#1e3a8a; font-size:14px; }
.mr-group-head .area { font-size:.78rem; color:var(--gray-500,#6b7280); } .mr-group-head .area { font-size:12px; color:var(--muted); }
.mr-group-head .descr { font-size:.86rem; color:#334155; margin-top:3px; line-height:1.5; } .mr-group-head .descr { font-size:13px; color:#334155; margin-top:3px; line-height:1.5; }
.mr-tag { display:inline-block; font-size:.66rem; font-weight:800; padding:2px 8px; border-radius:20px; margin-left:6px; vertical-align:middle; } .mr-tag { display:inline-block; font-size:10px; font-weight:800; padding:2px 8px; border-radius:20px; margin-left:6px; vertical-align:middle; }
.mr-tag.ess { background:#fef3c7; color:#92400e; } .mr-tag.ess { background:#FBF1E2; color:#92400e; }
.mr-table { width:100%; border-collapse:collapse; font-size:.85rem; } .mr-table { width:100%; border-collapse:collapse; font-size:13px; }
.mr-table th, .mr-table td { text-align:left; padding:9px 11px; border-bottom:1px solid var(--gray-100,#f3f4f6); vertical-align:top; } .mr-table th, .mr-table td { text-align:left; padding:9px 12px; border-bottom:1px solid var(--line); vertical-align:top; }
.mr-table th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); background:#fff; position:sticky; top:0; } .mr-table th { font-size:10.5px; text-transform:uppercase; letter-spacing:.04em; color:#7a8aa0; background:#fff; position:sticky; top:0; }
.mr-req-code { white-space:nowrap; font-weight:700; color:#475569; } .mr-req-code { white-space:nowrap; font-weight:700; color:#475569; }
.mr-code-pill { display:inline-block; font-size:.7rem; font-weight:700; color:#3730a3; background:#eef2ff; border-radius:6px; padding:1px 6px; } .mr-code-pill { display:inline-block; font-size:11px; font-weight:700; color:#3730a3; background:#eef2ff; border-radius:6px; padding:1px 6px; }
/* Ticket #384 p.3 — codici Policy come link attivi verso il menu POLICY */
a.mr-code-link { text-decoration:none; cursor:pointer; margin-right:3px; } a.mr-code-link { text-decoration:none; cursor:pointer; margin-right:3px; }
a.mr-code-link:hover { background:#e0e7ff; text-decoration:underline; } a.mr-code-link:hover { background:#e0e7ff; text-decoration:underline; }
a.mr-code-link:focus-visible { outline:2px solid #3730a3; outline-offset:1px; } a.mr-code-link:focus-visible { outline:2px solid #3730a3; outline-offset:1px; }
@@ -41,31 +41,32 @@
.mr-yn.n { color:#cbd5e1; } .mr-yn.n { color:#cbd5e1; }
.mr-row.spento { opacity:.4; } .mr-row.spento { opacity:.4; }
.mr-row.spento td { font-style:italic; } .mr-row.spento td { font-style:italic; }
.mr-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); } .mr-empty { text-align:center; padding:34px 16px; color:var(--muted); }
.sv-badge { display:inline-block; font-size:.72rem; font-weight:800; padding:3px 10px; border-radius:20px; border:none; cursor:pointer; } .savehint { font-size:12.5px; color:var(--muted); min-height:16px; }
.sv-badge.sv-todo { background:#f1f5f9; color:#475569; } .sv-badge { display:inline-block; font-size:11px; font-weight:800; padding:3px 10px; border-radius:20px; border:none; cursor:pointer; }
.sv-badge.sv-todo { background:#EEF2F7; color:#475569; }
.sv-badge.sv-na { background:#e5e7eb; color:#6b7280; } .sv-badge.sv-na { background:#e5e7eb; color:#6b7280; }
.sv-badge.sv-ko { background:#fee2e2; color:#991b1b; } .sv-badge.sv-ko { background:#FBE9E9; color:#991b1b; }
.sv-badge.sv-mid { background:#fef3c7; color:#92400e; } .sv-badge.sv-mid { background:#FBF1E2; color:#92400e; }
.sv-badge.sv-ok { background:#dcfce7; color:#166534; } .sv-badge.sv-ok { background:#E6F4EC; color:#166534; }
.sv-val { font-size:.76rem; color:#64748b; margin-top:4px; max-width:300px; } .sv-val { font-size:11.5px; color:#64748b; margin-top:4px; max-width:300px; }
/* Modale valutazione */ /* Modale valutazione (page-owned) */
.rm-overlay { display:none; position:fixed; inset:0; background:rgba(15,23,42,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; } .rm-overlay { display:none; position:fixed; inset:0; background:rgba(15,27,46,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
.rm-overlay.open { display:flex; } .rm-overlay.open { display:flex; }
.rm-dialog { background:#fff; border-radius:12px; width:100%; max-width:600px; box-shadow:0 20px 50px rgba(0,0,0,.25); } .rm-dialog { background:#fff; border-radius:12px; width:100%; max-width:600px; box-shadow:0 20px 50px rgba(11,40,80,.25); }
.rm-head { display:flex; justify-content:space-between; align-items:flex-start; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); } .rm-head { display:flex; justify-content:space-between; align-items:flex-start; padding:16px 20px; border-bottom:1px solid var(--line); }
.rm-head h3 { margin:0; font-size:1.02rem; } .rm-head h3 { margin:0; font-size:16px; }
.rm-head .sub { font-size:.8rem; color:var(--gray-500,#6b7280); margin-top:4px; } .rm-head .sub { font-size:12.5px; color:var(--muted); margin-top:4px; }
.rm-body { padding:18px 20px; } .rm-body { padding:18px 20px; }
.rm-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; } .rm-foot { padding:14px 20px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px; }
.rm-field { margin-bottom:14px; } .rm-field { margin-bottom:14px; }
.rm-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; } .rm-field label { display:block; font-weight:600; font-size:13px; margin-bottom:4px; }
.rm-field select, .rm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; } .rm-field select, .rm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:14px; }
.rm-field textarea { min-height:80px; resize:vertical; } .rm-field textarea { min-height:80px; resize:vertical; }
.rm-ctx { background:#f8fafc; border:1px solid var(--gray-100,#f3f4f6); border-radius:8px; padding:10px 12px; font-size:.83rem; margin-bottom:14px; line-height:1.5; } .rm-ctx { background:#FAFCFE; border:1px solid var(--line); border-radius:8px; padding:10px 12px; font-size:13px; margin-bottom:14px; line-height:1.5; }
.rm-ctx .pill { display:inline-block; font-size:.7rem; font-weight:700; color:#3730a3; background:#eef2ff; border-radius:6px; padding:1px 6px; margin-right:4px; } .rm-ctx .pill { display:inline-block; font-size:11px; font-weight:700; color:#3730a3; background:#eef2ff; border-radius:6px; padding:1px 6px; margin-right:4px; }
.rm-err { color:#b91c1c; font-size:.82rem; min-height:18px; } .rm-err { color:#b91c1c; font-size:12.5px; min-height:18px; }
.rm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); } .rm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--muted); }
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -82,17 +83,24 @@
<!-- PWA:end --> <!-- PWA:end -->
</head> </head>
<body> <body>
<div class="app-layout"> <!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header"> <main class="wrap">
<h2 data-i18n="mr.title">Misure e Requisiti</h2> <nav class="crumb" aria-label="breadcrumb">
<div class="content-header-actions"> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>Lex e ISO</span>
<i data-lucide="chevron-right" class="ic"></i><span>Misure e requisiti</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="mr.title">Misure e Requisiti</h1>
<p class="sub">Elenco canonico delle misure di base ACN e dei relativi requisiti</p>
</div>
<span class="savehint" id="mr-hint" role="status" aria-live="polite"></span> <span class="savehint" id="mr-hint" role="status" aria-live="polite"></span>
</div> </div>
</header>
<div class="content-body"> <section class="card card-pad">
<div class="mr-intro"> <div class="mr-intro">
<strong>L'elenco canonico delle misure di sicurezza di base e dei relativi requisiti.</strong> <strong>L'elenco canonico delle misure di sicurezza di base e dei relativi requisiti.</strong>
Ogni requisito riporta la <strong>policy di default</strong> che lo soddisfa e il <strong>rischio di default</strong> associato. È la base su cui si costruiscono policy, rischi e attività: le righe si "spengono" quando non sono applicabili alla classe del tuo soggetto. Ogni requisito riporta la <strong>policy di default</strong> che lo soddisfa e il <strong>rischio di default</strong> associato. È la base su cui si costruiscono policy, rischi e attività: le righe si "spengono" quando non sono applicabili alla classe del tuo soggetto.
@@ -129,9 +137,8 @@
</div> </div>
<div id="mr-list" aria-live="polite"><div class="mr-empty">Caricamento…</div></div> <div id="mr-list" aria-live="polite"><div class="mr-empty">Caricamento…</div></div>
</div> </section>
</main> </main>
</div>
<!-- Modale valutazione del requisito (org-scoped) --> <!-- Modale valutazione del requisito (org-scoped) -->
<div class="rm-overlay" id="mr-modal" role="dialog" aria-modal="true" aria-labelledby="mr-modal-title"> <div class="rm-overlay" id="mr-modal" role="dialog" aria-modal="true" aria-labelledby="mr-modal-title">
@@ -161,16 +168,17 @@
<div class="rm-err" id="mr-eval-err" role="alert"></div> <div class="rm-err" id="mr-eval-err" role="alert"></div>
</div> </div>
<div class="rm-foot"> <div class="rm-foot">
<button class="btn btn-outline" type="button" onclick="mrCloseEval()">Annulla</button> <button class="btn-ghost" type="button" onclick="mrCloseEval()">Annulla</button>
<button class="btn btn-primary" type="button" id="mr-eval-save" onclick="mrSaveEval()">Salva</button> <button class="btn-primary" type="button" id="mr-eval-save" onclick="mrSaveEval()">Salva</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Scripts V3 (ordine ESATTO come dashboard.html) -->
<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>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></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> <script>
@@ -185,12 +193,9 @@
function el(id) { return document.getElementById(id); } function el(id) { return document.getElementById(id); }
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; } function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
// Ticket #384 p.2.1 — i codici procedura/policy si presentano con prefisso "Policy." (Proc.NN -> Policy.NN). // Ticket #384 p.2.1 — i codici procedura/policy si presentano con prefisso "Policy." (Proc.NN -> Policy.NN).
// Il codice canonico nel catalogo resta proc_code (chiave di join requisiti<->procedure): qui si rietichetta solo per la UI.
function policyCode(code) { return code ? String(code).replace(/^Proc\./i, 'Policy.') : code; } function policyCode(code) { return code ? String(code).replace(/^Proc\./i, 'Policy.') : code; }
// Ticket #384 p.2.2/p.3 — colonna "Policy (default)" multi-valore con link attivi al menu POLICY. // Ticket #384 p.2.2/p.3 — colonna "Policy (default)" multi-valore con link attivi al menu POLICY.
// Usa le policy d'organizzazione collegate (req.policies dal backend); in assenza degrada
// al codice di default del catalogo (proc_code), com'era prima.
function renderPolicyLinks(req) { function renderPolicyLinks(req) {
const pols = req.policies || []; const pols = req.policies || [];
if (pols.length) { if (pols.length) {
@@ -220,6 +225,7 @@
if (window.I18n && I18n.init) I18n.init('it'); if (window.I18n && I18n.init) I18n.init('it');
if (typeof loadSidebar === 'function') loadSidebar(); if (typeof loadSidebar === 'function') loadSidebar();
if (window.HelpSystem && HelpSystem.init) HelpSystem.init(); if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
await mrLoad(); await mrLoad();
}); });
@@ -230,17 +236,14 @@
MR.org_class = (data && data.org_class) || null; MR.org_class = (data && data.org_class) || null;
MR.has_org = !!(data && data.has_org); MR.has_org = !!(data && data.has_org);
MR.stati = (data && data.stati) || Object.keys(STATO_META); MR.stati = (data && data.stati) || Object.keys(STATO_META);
// indicizza i requisiti per id (per la modale)
MR.byId = {}; MR.byId = {};
MR.measures.forEach(function (m) { (m.requisiti || []).forEach(function (q) { MR.byId[q.id] = q; }); }); MR.measures.forEach(function (m) { (m.requisiti || []).forEach(function (q) { MR.byId[q.id] = q; }); });
// se conosco la classe dell'org, parto in modalità "auto"; altrimenti mostro tutto
if (!MR.org_class) el('mr-class').value = 'all'; if (!MR.org_class) el('mr-class').value = 'all';
el('mr-stat-mis').textContent = (data && data.totals && data.totals.misure) || MR.measures.length; el('mr-stat-mis').textContent = (data && data.totals && data.totals.misure) || MR.measures.length;
el('mr-stat-req').textContent = (data && data.totals && data.totals.requisiti) || 0; el('mr-stat-req').textContent = (data && data.totals && data.totals.requisiti) || 0;
el('mr-stats').style.display = 'flex'; el('mr-stats').style.display = 'flex';
// Ticket #384 p.3 — deep-link dal menu POLICY: ?requisito=ID → mostra ed evidenzia il requisito.
const focusReq = parseInt(new URLSearchParams(location.search).get('requisito') || '', 10); const focusReq = parseInt(new URLSearchParams(location.search).get('requisito') || '', 10);
if (focusReq) el('mr-class').value = 'all'; // evita che la riga sia filtrata via if (focusReq) el('mr-class').value = 'all';
mrRender(); mrRender();
if (focusReq) mrFocusRequisito(focusReq); if (focusReq) mrFocusRequisito(focusReq);
} catch (e) { } catch (e) {
@@ -248,7 +251,6 @@
} }
} }
// Ticket #384 p.3 — scorre alla riga del requisito e la evidenzia (arrivo da link Policy).
function mrFocusRequisito(reqId) { function mrFocusRequisito(reqId) {
const row = document.getElementById('req-' + reqId); const row = document.getElementById('req-' + reqId);
if (!row) return; if (!row) return;
@@ -257,14 +259,12 @@
setTimeout(function () { row.classList.remove('mr-row-focus'); }, 4000); setTimeout(function () { row.classList.remove('mr-row-focus'); }, 4000);
} }
// classe effettiva selezionata (risolve "auto" sulla classe dell'org)
function mrEffectiveClass() { function mrEffectiveClass() {
const sel = el('mr-class').value; const sel = el('mr-class').value;
if (sel === 'auto') return MR.org_class; // può essere null => tutto attivo if (sel === 'auto') return MR.org_class;
if (sel === 'all') return null; if (sel === 'all') return null;
return sel; // important | essential return sel;
} }
// un requisito è "attivo" per la classe effettiva?
function reqActive(req, cls) { function reqActive(req, cls) {
if (!cls) return true; if (!cls) return true;
return cls === 'important' ? !!req.applies_important : !!req.applies_essential; return cls === 'important' ? !!req.applies_important : !!req.applies_essential;
@@ -283,9 +283,8 @@
const q = (el('mr-q').value || '').trim(); const q = (el('mr-q').value || '').trim();
const view = el('mr-view').value; const view = el('mr-view').value;
// costruisco le righe filtrate (misura -> requisiti visibili)
let activeCount = 0; let activeCount = 0;
const groupsByKey = {}; // key -> {label, descr, area, code, essOnly, rows:[]} const groupsByKey = {};
MR.measures.forEach(function (m) { MR.measures.forEach(function (m) {
const essOnly = !m.applies_important; const essOnly = !m.applies_important;
@@ -398,7 +397,6 @@
(function () { (function () {
const ov = el('mr-modal'); if (!ov) return; const ov = el('mr-modal'); if (!ov) return;
ov.addEventListener('click', function (e) { if (e.target === ov) mrCloseEval(); }); ov.addEventListener('click', function (e) { if (e.target === ov) mrCloseEval(); });
// focus-trap: Tab/Shift+Tab restano dentro la modale aperta
ov.addEventListener('keydown', function (e) { ov.addEventListener('keydown', function (e) {
if (e.key !== 'Tab' || !ov.classList.contains('open')) return; if (e.key !== 'Tab' || !ov.classList.contains('open')) return;
const nodes = ov.querySelectorAll('select, textarea, button'); const nodes = ov.querySelectorAll('select, textarea, button');
+147 -128
View File
@@ -3,29 +3,63 @@
<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>Aggiornamenti Normativi - NIS2 Agile</title> <title>Aggiornamenti Normativi · 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 / NO Bootstrap Italia). -->
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 aggiornamenti/badge/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">
<style> <style>
.impact-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; } /* V3 — specifico pagina Normative (non coperto da v3.css) */
.impact-critical { background: var(--danger-bg); color: var(--danger); } .nrm-bars{display:grid;gap:18px}
.impact-high { background: #fff7ed; color: #c2410c; } .nrm-progress-track{height:8px;background:#EAF0F6;border-radius:99px;overflow:hidden}
.impact-medium { background: var(--warning-bg); color: #a16207; } .nrm-progress-fill{height:100%;background:var(--c-blue);border-radius:99px;transition:width .5s ease}
.impact-low { background: var(--gray-100); color: var(--gray-600); } .nrm-filters{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
.impact-informational { background: rgba(6,182,212,0.1); color: #0891b2; } .nrm-select{padding:8px 12px;border:1px solid var(--line);border-radius:8px;background:#fff;font:inherit;font-size:13px;color:var(--ink)}
.ack-badge { display: inline-flex; align-items: center; gap: 5px; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 600; } .nrm-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;color:var(--ink)}
.ack-done { background: var(--success-bg); color: var(--success); } .nrm-check input{accent-color:var(--c-blue)}
.ack-pending { background: var(--warning-bg); color: #a16207; } .update-card{border:1px solid var(--line);border-radius:11px;padding:18px;margin-bottom:14px;transition:border-color .12s,box-shadow .12s;background:#fff}
.source-chip { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: 700; background: var(--gray-100); color: var(--gray-600); font-family: monospace; } .update-card:hover{border-color:#9FBEDD;box-shadow:0 4px 12px rgba(16,40,80,.08)}
.update-card { border: 1px solid var(--gray-200); border-radius: var(--border-radius-lg); padding: 20px; margin-bottom: 16px; transition: all var(--transition-fast); } .update-card.action-required{border-left:3px solid var(--c-amber)}
.update-card:hover { border-color: var(--primary); box-shadow: 0 2px 8px rgba(6,182,212,0.1); } .update-card.acked{opacity:.72}
.update-card.action-required { border-left: 3px solid var(--warning); } .source-chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:6px;font-size:10.5px;font-weight:700;background:#EEF2F7;color:#5b6b80;font-family:'Inter',monospace}
.update-card.acked { opacity: 0.7; } .impact-badge{display:inline-flex;align-items:center;padding:2px 9px;border-radius:6px;font-size:10px;font-weight:700;letter-spacing:.03em;text-transform:uppercase}
.ack-progress { height: 6px; background: var(--gray-200); border-radius: 3px; overflow: hidden; } .impact-critical{background:#FBE9E9;color:#B23636}
.ack-progress-bar { height: 100%; background: var(--primary); border-radius: 3px; transition: width 0.5s ease; } .impact-high{background:#FBF1E2;color:#C2410C}
.impact-medium{background:#FBF1E2;color:#9A6418}
.impact-low{background:#EEF2F7;color:#5b6b80}
.impact-informational{background:#E7F0FB;color:#1F5FA8}
.ack-badge{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:6px;font-size:10.5px;font-weight:700}
.ack-done{background:#E6F4EC;color:#1E7A4E}
.ack-pending{background:#FBF1E2;color:#9A6418}
.nrm-uupd-h3{font-size:15px;font-weight:700;color:var(--ink);margin:0 0 6px}
.nrm-ref{font-size:11.5px;color:var(--muted);margin:0 0 8px}
.nrm-ref code{background:#EEF2F7;border-radius:5px;padding:1px 6px}
.nrm-summary{font-size:13px;color:var(--muted);line-height:1.6;margin:0 0 10px}
.nrm-card-grid{display:flex;justify-content:space-between;gap:14px;flex-wrap:wrap}
.nrm-card-side{display:flex;flex-direction:column;gap:8px;align-items:flex-end;flex-shrink:0}
.nrm-eff{font-size:11.5px;color:var(--muted)}
.nrm-action-tag{font-size:10px;font-weight:700;color:#9A6418;background:#FBF1E2;padding:2px 8px;border-radius:6px;text-transform:uppercase;letter-spacing:.03em}
/* Modal + toast prodotti da common.js (style.css rimosso): stile minimo coerente V3 */
.modal-overlay{position:fixed;inset:0;background:rgba(15,27,46,.5);display:flex;align-items:flex-start;justify-content:center;z-index:1050;opacity:0;transition:opacity .2s;padding:48px 16px;overflow:auto}
.modal-overlay.active{opacity:1}
.modal-overlay .modal{background:#fff;border-radius:12px;width:100%;max-width:560px;box-shadow:0 20px 50px rgba(11,40,80,.25);transform:translateY(-8px);transition:transform .2s}
.modal-overlay.active .modal{transform:none}
.modal-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--line)}
.modal-header h3{margin:0;font-size:16px;font-weight:700}
.modal-close{background:none;border:0;font-size:1.5rem;line-height:1;cursor:pointer;color:var(--muted)}
.modal-body{padding:18px 20px}
.modal-footer{padding:14px 20px;border-top:1px solid var(--line);display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap}
.form-group{margin-bottom:14px}
.form-label{display:block;font-weight:600;font-size:13px;margin-bottom:5px}
.form-control{width:100%;padding:9px 10px;border:1px solid var(--line);border-radius:8px;font:inherit;font-size:14px}
textarea.form-control{min-height:64px;resize:vertical}
.notification-container{position:fixed;top:18px;right:18px;z-index:1100;display:flex;flex-direction:column;gap:10px}
.notification{display:flex;align-items:center;gap:10px;background:#fff;border:1px solid var(--line);border-left:4px solid var(--c-blue);border-radius:10px;box-shadow:0 8px 24px rgba(16,40,80,.14);padding:12px 14px;min-width:240px;max-width:360px;font-size:13px}
.notification.success{border-left-color:var(--c-green)}.notification.error{border-left-color:var(--c-red)}.notification.warning{border-left-color:var(--c-amber)}
.notification-icon{width:18px;height:18px;flex:0 0 auto}.notification-icon svg{width:18px;height:18px}
.notification.success .notification-icon{color:var(--c-green)}.notification.error .notification-icon{color:var(--c-red)}.notification.warning .notification-icon{color:var(--c-amber)}.notification.info .notification-icon{color:var(--c-blue)}
.notification-message{flex:1;min-width:0}
.notification-close{background:none;border:0;font-size:1.2rem;line-height:1;cursor:pointer;color:var(--muted)}
.notification.fade-out{opacity:0;transition:opacity .3s}
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -42,47 +76,41 @@
<!-- PWA:end --> <!-- PWA:end -->
</head> </head>
<body> <body>
<div class="app-layout"> <!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<div class="page-header"> <main class="wrap">
<div class="page-header-content"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="page-title">Aggiornamenti Normativi</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<p class="page-subtitle">Feed NIS2 / ACN / DORA — Presa visione obbligatoria per audit compliance</p> <i data-lucide="chevron-right" class="ic"></i><span>Lex e ISO</span>
</div> <i data-lucide="chevron-right" class="ic"></i><span>Normative</span>
<div class="page-header-actions"> </nav>
<button class="btn btn-secondary" onclick="loadPending()"> <div class="page-head">
<svg class="icon icon-sm" aria-hidden="true" style="margin-right:6px;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-clock"></use></svg> <div>
Solo Pendenti <h1 data-i18n="normative.title">Aggiornamenti Normativi</h1>
</button> <p class="sub">Feed NIS2 / ACN / DORA — Presa visione obbligatoria per audit compliance</p>
</div> </div>
<button class="btn-ghost" onclick="loadPending()"><i data-lucide="clock" class="ic"></i>Solo pendenti</button>
</div> </div>
<!-- Stats --> <div class="stack">
<div class="stats-grid mb-24" id="stats-grid"> <!-- Stats (dati reali da /normative/stats) -->
<div class="stat-card"><div class="spinner"></div></div> <div class="grid3" id="stats-grid">
<section class="card card-pad stat blue"><div style="flex:1;min-width:0"><div class="lbl">Caricamento…</div><div class="val">…</div></div></section>
</div> </div>
<!-- Ack Progress bar --> <!-- Avanzamento presa visione -->
<div class="card mb-24"> <section class="card card-pad">
<div class="card-body" style="padding:16px 20px;"> <div class="sec-head"><h3>Avanzamento presa visione</h3><span class="hint" id="ack-pct">—</span></div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;"> <div class="nrm-progress-track"><div class="nrm-progress-fill" id="ack-bar" style="width:0%"></div></div>
<span style="font-size:0.875rem; font-weight:600; color:var(--gray-700);">Avanzamento presa visione</span> <p class="sub" style="margin:8px 0 0;font-size:12px;color:var(--muted)">Documentare la presa visione degli aggiornamenti normativi è richiesto per la compliance NIS2 Art. 21.</p>
<span id="ack-pct" style="font-size:0.875rem; font-weight:700; color:var(--primary);">—</span> </section>
</div>
<div class="ack-progress">
<div class="ack-progress-bar" id="ack-bar" style="width:0%"></div>
</div>
<p style="font-size:0.75rem; color:var(--gray-500); margin-top:6px;">Documentare la presa visione degli aggiornamenti normativi è richiesto per la compliance NIS2 Art.21.</p>
</div>
</div>
<!-- Filters --> <!-- Filtri -->
<div class="card mb-16"> <section class="card card-pad">
<div class="card-body" style="padding:14px 20px;"> <div class="nrm-filters">
<div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;"> <label for="filter-source" class="visually-hidden" style="position:absolute;width:1px;height:1px;overflow:hidden">Filtra per fonte normativa</label>
<label for="filter-source" class="visually-hidden">Filtra per fonte normativa</label> <select id="filter-source" class="nrm-select" onchange="loadUpdates()" aria-label="Filtra per fonte normativa">
<select id="filter-source" class="form-control" style="width:auto;" onchange="loadUpdates()" aria-label="Filtra per fonte normativa">
<option value="">Tutte le fonti</option> <option value="">Tutte le fonti</option>
<option value="dlgs_138_2024">D.Lgs. 138/2024</option> <option value="dlgs_138_2024">D.Lgs. 138/2024</option>
<option value="nis2_directive">Direttiva NIS2</option> <option value="nis2_directive">Direttiva NIS2</option>
@@ -91,8 +119,8 @@
<option value="enisa">ENISA</option> <option value="enisa">ENISA</option>
<option value="iso27001">ISO 27001</option> <option value="iso27001">ISO 27001</option>
</select> </select>
<label for="filter-impact" class="visually-hidden">Filtra per livello di impatto</label> <label for="filter-impact" class="visually-hidden" style="position:absolute;width:1px;height:1px;overflow:hidden">Filtra per livello di impatto</label>
<select id="filter-impact" class="form-control" style="width:auto;" onchange="loadUpdates()" aria-label="Filtra per livello di impatto"> <select id="filter-impact" class="nrm-select" onchange="loadUpdates()" aria-label="Filtra per livello di impatto">
<option value="">Tutti i livelli</option> <option value="">Tutti i livelli</option>
<option value="critical">Critico</option> <option value="critical">Critico</option>
<option value="high">Alto</option> <option value="high">Alto</option>
@@ -100,37 +128,30 @@
<option value="low">Basso</option> <option value="low">Basso</option>
<option value="informational">Informativo</option> <option value="informational">Informativo</option>
</select> </select>
<label style="display:flex; align-items:center; gap:8px; font-size:0.875rem; cursor:pointer;"> <label class="nrm-check"><input type="checkbox" id="filter-action" onchange="loadUpdates()"> Solo azione richiesta</label>
<input type="checkbox" id="filter-action" onchange="loadUpdates()" style="accent-color:var(--primary);">
Solo azione richiesta
</label>
</div>
</div>
</div> </div>
</section>
<div id="updates-container"><div class="spinner" style="margin:60px auto;"></div></div> <div id="updates-container"><div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div></div>
</div>
</main> </main>
</div>
<!-- Scripts V3 (ordine ESATTO come dashboard.html) -->
<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> <script>
// ── Auth + chrome + i18n + help ──────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar(); // = loadTopnavV3()
I18n.init(); if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init(); if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
function refreshIcons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
const sourceLabels = { const sourceLabels = {
nis2_directive: 'NIS2 Directive', dlgs_138_2024: 'D.Lgs. 138/2024', nis2_directive: 'NIS2 Directive', dlgs_138_2024: 'D.Lgs. 138/2024',
@@ -149,30 +170,27 @@
function renderStats(d) { function renderStats(d) {
document.getElementById('stats-grid').innerHTML = ` document.getElementById('stats-grid').innerHTML = `
<div class="stat-card"> <a class="card card-pad stat blue" style="cursor:default">
<div class="stat-label">Aggiornamenti Totali</div> <div class="ibox"><i data-lucide="layers" class="ic"></i></div>
<div class="stat-value">${d.total_updates || 0}</div> <div style="flex:1;min-width:0"><div class="lbl">Aggiornamenti totali</div><div class="val blue">${d.total_updates || 0}</div></div>
</div> </a>
<div class="stat-card"> <a class="card card-pad stat green" style="cursor:default">
<div class="stat-label">Prese Visione</div> <div class="ibox"><i data-lucide="check-check" class="ic"></i></div>
<div class="stat-value" style="color:var(--success);">${d.acknowledged || 0}</div> <div style="flex:1;min-width:0"><div class="lbl">Prese visione</div><div class="val green">${d.acknowledged || 0}</div></div>
</div> </a>
<div class="stat-card"> <a class="card card-pad stat amber" style="cursor:default">
<div class="stat-label">Pendenti</div> <div class="ibox"><i data-lucide="clock" class="ic"></i></div>
<div class="stat-value" style="color:var(--warning);">${d.pending || 0}</div> <div style="flex:1;min-width:0"><div class="lbl">Pendenti</div><div class="val amber">${d.pending || 0}</div><div class="sub">Compliance ACK ${d.ack_rate || 0}%</div></div>
</div> </a>`;
<div class="stat-card">
<div class="stat-label">Compliance ACK</div>
<div class="stat-value" style="color:var(--primary);">${d.ack_rate || 0}%</div>
</div>`;
document.getElementById('ack-pct').textContent = (d.ack_rate || 0) + '%'; document.getElementById('ack-pct').textContent = (d.ack_rate || 0) + '%';
document.getElementById('ack-bar').style.width = (d.ack_rate || 0) + '%'; document.getElementById('ack-bar').style.width = (d.ack_rate || 0) + '%';
refreshIcons();
} }
async function loadUpdates() { async function loadUpdates() {
const container = document.getElementById('updates-container'); const container = document.getElementById('updates-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;"></div>'; container.innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>';
try { try {
const params = new URLSearchParams(); const params = new URLSearchParams();
const source = document.getElementById('filter-source').value; const source = document.getElementById('filter-source').value;
@@ -185,30 +203,36 @@
const result = await api.request('GET', '/normative/list?' + params); const result = await api.request('GET', '/normative/list?' + params);
if (result.success) renderUpdates(result.data.updates || []); if (result.success) renderUpdates(result.data.updates || []);
} catch (e) { } catch (e) {
container.innerHTML = '<div class="empty-state"><h4>Errore caricamento aggiornamenti</h4></div>'; container.innerHTML = '<div class="v3-error"><i data-lucide="alert-triangle" class="ic"></i><b>Errore di caricamento</b><span>Riprova tra poco.</span></div>';
refreshIcons();
} }
} }
async function loadPending() { async function loadPending() {
const container = document.getElementById('updates-container'); const container = document.getElementById('updates-container');
container.innerHTML = '<div class="spinner" style="margin:60px auto;"></div>'; container.innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>';
try { try {
const result = await api.request('GET', '/normative/pending'); const result = await api.request('GET', '/normative/pending');
if (result.success) renderUpdates(result.data.pending || [], true); if (result.success) renderUpdates(result.data.pending || [], true);
} catch (e) { container.innerHTML = '<div class="empty-state"><h4>Errore.</h4></div>'; } } catch (e) {
container.innerHTML = '<div class="v3-error"><i data-lucide="alert-triangle" class="ic"></i><b>Errore di caricamento</b><span>Riprova tra poco.</span></div>';
refreshIcons();
}
} }
function renderUpdates(updates, pendingOnly) { function renderUpdates(updates, pendingOnly) {
const container = document.getElementById('updates-container'); const container = document.getElementById('updates-container');
if (!updates.length) { if (!updates.length) {
container.innerHTML = `<div class="empty-state"> container.innerHTML = `<section class="card card-pad"><div class="v3-empty">
<svg class="icon" aria-hidden="true" style="width:40px;height:40px;color:var(--gray-300)"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-file"></use></svg> <i data-lucide="file-text" class="ic"></i>
<h4>${pendingOnly ? 'Nessun aggiornamento pendente — tutto preso in visione!' : 'Nessun aggiornamento trovato'}</h4> <b>${pendingOnly ? 'Nessun aggiornamento pendente — tutto preso in visione!' : 'Nessun aggiornamento trovato'}</b>
</div>`; <span>I prossimi aggiornamenti normativi appariranno qui.</span>
</div></section>`;
refreshIcons();
return; return;
} }
let html = ''; let html = '<div class="stack">';
updates.forEach(u => { updates.forEach(u => {
const acked = u.is_acknowledged || !!(u.acknowledged_at); const acked = u.is_acknowledged || !!(u.acknowledged_at);
const actionClass = u.action_required ? 'action-required' : ''; const actionClass = u.action_required ? 'action-required' : '';
@@ -218,51 +242,46 @@
).join(' '); ).join(' ');
html += `<div class="update-card ${actionClass} ${ackedClass}" id="update-${u.id}"> html += `<div class="update-card ${actionClass} ${ackedClass}" id="update-${u.id}">
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:12px; flex-wrap:wrap;"> <div class="nrm-card-grid">
<div style="flex:1;"> <div style="flex:1;min-width:0;">
<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:8px;"> <div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:10px;">
<span class="source-chip">${escapeHtml(sourceLabels[u.source] || u.source)}</span> <span class="source-chip">${escapeHtml(sourceLabels[u.source] || u.source)}</span>
<span class="impact-badge impact-${u.impact_level}">${escapeHtml(impactLabels[u.impact_level] || u.impact_level)}</span> <span class="impact-badge impact-${u.impact_level}">${escapeHtml(impactLabels[u.impact_level] || u.impact_level)}</span>
${u.action_required ? '<span style="font-size:0.7rem; font-weight:700; color:var(--warning); background:var(--warning-bg); padding:2px 8px; border-radius:4px;">Azione richiesta</span>' : ''} ${u.action_required ? '<span class="nrm-action-tag">Azione richiesta</span>' : ''}
${acked ${acked
? `<span class="ack-badge ack-done"><svg class="icon" aria-hidden="true" style="width:11px;height:11px;fill:currentColor;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>Preso in visione ${u.ack_by_name ? 'da ' + escapeHtml(u.ack_by_name) : ''}</span>` ? `<span class="ack-badge ack-done"><i data-lucide="check" style="width:12px;height:12px"></i>Preso in visione ${u.ack_by_name ? 'da ' + escapeHtml(u.ack_by_name) : ''}</span>`
: '<span class="ack-badge ack-pending">Da prendere in visione</span>' : '<span class="ack-badge ack-pending">Da prendere in visione</span>'
} }
</div> </div>
<h3 style="font-size:1rem; font-weight:700; color:var(--gray-900); margin-bottom:6px;">${escapeHtml(u.title)}</h3> <h3 class="nrm-uupd-h3">${escapeHtml(u.title)}</h3>
${u.reference ? `<p style="font-size:0.75rem; color:var(--gray-500); margin-bottom:8px;"><code>${escapeHtml(u.reference)}</code></p>` : ''} ${u.reference ? `<p class="nrm-ref"><code>${escapeHtml(u.reference)}</code></p>` : ''}
<p style="font-size:0.8125rem; color:var(--gray-600); line-height:1.6; margin-bottom:10px;">${escapeHtml(u.summary)}</p> <p class="nrm-summary">${escapeHtml(u.summary)}</p>
${domains ? `<div style="display:flex; gap:6px; flex-wrap:wrap;">${domains}</div>` : ''} ${domains ? `<div style="display:flex; gap:6px; flex-wrap:wrap;">${domains}</div>` : ''}
</div> </div>
<div style="display:flex; flex-direction:column; gap:8px; align-items:flex-end; flex-shrink:0;"> <div class="nrm-card-side">
${u.effective_date ? `<p style="font-size:0.75rem; color:var(--gray-500);">In vigore: <strong>${formatDate(u.effective_date)}</strong></p>` : ''} ${u.effective_date ? `<p class="nrm-eff">In vigore: <strong>${formatDate(u.effective_date)}</strong></p>` : ''}
${u.url ? `<a href="${escapeHtml(u.url)}" target="_blank" rel="noopener" class="btn btn-sm btn-secondary" style="font-size:0.75rem;"> ${u.url ? `<a href="${escapeHtml(u.url)}" target="_blank" rel="noopener" class="btn-ghost"><i data-lucide="external-link" class="ic"></i>Fonte</a>` : ''}
<svg class="icon" aria-hidden="true" style="width:12px;height:12px;fill:currentColor;margin-right:4px;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-external-link"></use></svg> ${!acked ? `<button class="btn-primary" onclick="acknowledgeUpdate(${u.id})"><i data-lucide="check" class="ic"></i>Prendi in visione</button>` : ''}
Fonte
</a>` : ''}
${!acked ? `<button class="btn btn-sm btn-primary" onclick="acknowledgeUpdate(${u.id})">
<svg class="icon" aria-hidden="true" style="width:14px;height:14px;fill:currentColor;margin-right:4px;"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>
Prendi in visione
</button>` : ''}
</div> </div>
</div> </div>
</div>`; </div>`;
}); });
html += '</div>';
container.innerHTML = html; container.innerHTML = html;
refreshIcons();
} }
async function acknowledgeUpdate(id) { async function acknowledgeUpdate(id) {
showModal('Presa Visione', ` showModal('Presa Visione', `
<p style="font-size:0.875rem; color:var(--gray-600); margin-bottom:16px;"> <p style="font-size:13px; color:var(--muted); margin-bottom:16px;">
Documentare la presa visione dell'aggiornamento normativo. Opzionalmente aggiungere note sull'impatto per la tua organizzazione. Documentare la presa visione dell'aggiornamento normativo. Opzionalmente aggiungere note sull'impatto per la tua organizzazione.
</p> </p>
<div class="form-group"> <div class="form-group">
<label for="ack-notes" class="form-label">Note (opzionale)</label> <label for="ack-notes" class="form-label">Note (opzionale)</label>
<textarea id="ack-notes" class="form-control" rows="3" placeholder="Es. Analizzato con il team di compliance. Nessun impatto diretto rilevato..."></textarea> <textarea id="ack-notes" class="form-control" rows="3" placeholder="Es. Analizzato con il team di compliance. Nessun impatto diretto rilevato..."></textarea>
</div> </div>
`, `<button class="btn btn-primary" onclick="confirmAck(${id})">Conferma presa visione</button> `, { footer: `<button class="btn-primary" onclick="confirmAck(${id})">Conferma presa visione</button>
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>`); <button class="btn-ghost" onclick="closeModal()">Annulla</button>` });
} }
async function confirmAck(id) { async function confirmAck(id) {
@@ -278,7 +297,7 @@
} catch (e) { showNotification('Errore di connessione.', 'error'); } } catch (e) { showNotification('Errore di connessione.', 'error'); }
} }
// Init // ── Init dati reali ──────────────────────────────────────
loadStats(); loadStats();
loadUpdates(); loadUpdates();
</script> </script>
+78 -51
View File
@@ -3,50 +3,50 @@
<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>Organigramma - NIS2 Agile</title> <title>Organigramma · 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 external CDN, no Bootstrap Italia. -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/style.css?v=20260618"> <link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
.org-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:18px; font-size:.92rem; line-height:1.6; } /* V3+ page-specific (componenti non coperti da v3.css): albero + modale ruolo. Token V3. */
.org-gov-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:12px 0 18px; } .org-intro { background:#EFF4FB; border-left:4px solid var(--c-blue); padding:14px 18px; border-radius:10px; margin-bottom:14px; font-size:13.5px; line-height:1.6; color:var(--ink); }
.org-stats { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:18px; } .org-gov-note { font-size:12.5px; color:#9A6418; background:#FBF1E2; border:1px solid #f3d9a8; border-radius:10px; padding:10px 12px; margin:0 0 18px; }
.org-stat { flex:1 1 150px; min-width:130px; background:#fff; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:12px 14px; } .org-stats { display:flex; flex-wrap:wrap; gap:12px; margin-bottom:18px; }
.org-stat .n { font-size:1.5rem; font-weight:800; color:var(--primary,#2563eb); line-height:1; } .org-stat { flex:1 1 150px; min-width:130px; background:#fff; border:1px solid var(--line); border-radius:11px; padding:13px 15px; }
.org-stat .l { font-size:.78rem; color:var(--gray-500,#6b7280); margin-top:4px; } .org-stat .n { font-size:26px; font-weight:800; color:var(--c-blue); line-height:1; }
.org-stat .l { font-size:11.5px; color:var(--muted); margin-top:5px; }
/* Albero */ /* Albero */
.org-tree { margin-top:6px; } .org-tree { margin-top:6px; }
.org-children { margin-left:26px; border-left:2px solid var(--gray-100,#eef0f3); padding-left:16px; } .org-children { margin-left:26px; border-left:2px solid var(--line); padding-left:16px; }
.org-node { margin:8px 0; } .org-node { margin:8px 0; }
.org-card { display:flex; align-items:flex-start; gap:12px; background:#fff; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:12px 14px; } .org-card { display:flex; align-items:flex-start; gap:12px; background:#fff; border:1px solid var(--line); border-radius:11px; padding:13px 15px; }
.org-card.gov { border-color:#b45309; background:#fffdf7; } .org-card.gov { border-color:#e0b25a; background:#FFFDF7; }
.org-card-main { flex:1 1 auto; min-width:0; } .org-card-main { flex:1 1 auto; min-width:0; }
.org-role-name { font-weight:700; font-size:.98rem; } .org-role-name { font-weight:700; font-size:14px; }
.org-holder { font-size:.85rem; color:var(--gray-600,#4b5563); margin-top:2px; } .org-holder { font-size:12.5px; color:var(--muted); margin-top:3px; }
.org-holder.vacant { color:#9ca3af; font-style:italic; } .org-holder.vacant { color:#9aa8ba; font-style:italic; }
.org-desc { font-size:.82rem; color:var(--gray-500,#6b7280); margin-top:6px; line-height:1.5; } .org-desc { font-size:12.5px; color:var(--muted); margin-top:6px; line-height:1.5; }
.org-badge { display:inline-block; font-size:.68rem; font-weight:700; padding:2px 8px; border-radius:6px; margin-left:6px; vertical-align:middle; } .org-badge { display:inline-block; font-size:10px; font-weight:700; padding:2px 8px; border-radius:6px; margin-left:6px; vertical-align:middle; }
.org-badge.gov { background:#fef3c7; color:#92400e; } .org-badge.gov { background:#FBF1E2; color:#9A6418; }
.org-actions { display:flex; gap:6px; flex-wrap:wrap; flex-shrink:0; } .org-actions { display:flex; gap:6px; flex-wrap:wrap; flex-shrink:0; }
.org-actions .btn { padding:4px 8px; font-size:.74rem; } .org-empty { text-align:center; padding:48px 16px; color:var(--muted); }
.org-empty { text-align:center; padding:48px 16px; color:var(--gray-500,#6b7280); } .org-empty .big { font-size:16px; font-weight:700; color:var(--ink); margin-bottom:6px; }
.org-empty .big { font-size:1.05rem; font-weight:700; color:var(--gray-700,#374151); margin-bottom:6px; }
/* Modale */ /* Modale */
.om-overlay { display:none; position:fixed; inset:0; background:rgba(15,23,42,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; } .om-overlay { display:none; position:fixed; inset:0; background:rgba(15,38,60,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
.om-overlay.open { display:flex; } .om-overlay.open { display:flex; }
.om-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(0,0,0,.25); } .om-dialog { background:#fff; border-radius:14px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(11,40,80,.25); }
.om-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); } .om-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--line); }
.om-head h3 { margin:0; font-size:1.05rem; } .om-head h3 { margin:0; font-size:17px; font-weight:800; }
.om-body { padding:18px 20px; } .om-body { padding:18px 20px; }
.om-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; } .om-foot { padding:14px 20px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px; }
.om-field { margin-bottom:14px; } .om-field { margin-bottom:14px; }
.om-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; } .om-field label { display:block; font-weight:600; font-size:12.5px; margin-bottom:5px; }
.om-field input[type=text], .om-field select, .om-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; } .om-field input[type=text], .om-field select, .om-field textarea { width:100%; padding:9px 11px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
.om-field textarea { min-height:70px; resize:vertical; } .om-field textarea { min-height:70px; resize:vertical; }
.om-check { display:flex; align-items:flex-start; gap:8px; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; font-size:.85rem; } .om-check { display:flex; align-items:flex-start; gap:8px; background:#FBF1E2; border:1px solid #f3d9a8; border-radius:8px; padding:10px 12px; font-size:12.5px; color:var(--ink); }
.om-check input { margin-top:3px; } .om-check input { margin-top:3px; }
.om-err { color:#b91c1c; font-size:.82rem; min-height:18px; margin-top:2px; } .om-err { color:var(--c-red); font-size:12.5px; min-height:18px; margin-top:2px; }
.om-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); } .om-close { background:none; border:none; font-size:22px; line-height:1; cursor:pointer; color:#9aa8ba; }
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -63,17 +63,27 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="org.title">Organigramma</h2>
<div class="content-header-actions">
<button class="btn btn-primary btn-sm" id="org-btn-new" onclick="orgOpenModal()" data-i18n="org.new_role">Nuovo ruolo</button>
</div>
</header>
<div class="content-body"> <main class="wrap">
<nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>Struttura interna</span>
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="org.title">Organigramma</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="org.title">Organigramma</h1>
<p class="sub">Ruoli, gerarchia e titolari — base del modello relazionale NIS2</p>
</div>
<button class="btn-primary" id="org-btn-new" onclick="orgOpenModal()">
<i data-lucide="plus" class="ic"></i><span data-i18n="org.new_role">Nuovo ruolo</span>
</button>
</div>
<div class="stack">
<section class="card card-pad">
<div class="org-intro"> <div class="org-intro">
<strong>Chi è responsabile di cosa.</strong> <strong>Chi è responsabile di cosa.</strong>
L'organigramma mappa i ruoli dell'organizzazione, la loro gerarchia e il titolare di ciascuno. È la base del modello relazionale NIS2: nelle fasi successive i ruoli si collegheranno a competenze, procedure, inventario e rischi. L'organigramma mappa i ruoli dell'organizzazione, la loro gerarchia e il titolare di ciascuno. È la base del modello relazionale NIS2: nelle fasi successive i ruoli si collegheranno a competenze, procedure, inventario e rischi.
@@ -93,12 +103,12 @@
<div id="org-empty" class="org-empty" style="display:none;"> <div id="org-empty" class="org-empty" style="display:none;">
<div class="big">Nessun ruolo definito</div> <div class="big">Nessun ruolo definito</div>
<div style="margin-bottom:16px;">Inizia a costruire l'organigramma della tua organizzazione.</div> <div style="margin-bottom:16px;">Inizia a costruire l'organigramma della tua organizzazione.</div>
<button class="btn btn-primary" onclick="orgOpenModal()">Crea il primo ruolo</button> <button class="btn-primary" onclick="orgOpenModal()"><i data-lucide="plus" class="ic"></i>Crea il primo ruolo</button>
<button class="btn btn-outline" onclick="orgScaffold()" style="margin-left:8px;">Crea struttura di base</button> <button class="btn-ghost" onclick="orgScaffold()" style="margin-left:8px;"><i data-lucide="network" class="ic"></i>Crea struttura di base</button>
</div> </div>
</section>
</div> </div>
</main> </main>
</div>
<!-- Modale crea/modifica ruolo --> <!-- Modale crea/modifica ruolo -->
<div class="om-overlay" id="org-modal" role="dialog" aria-modal="true" aria-labelledby="org-modal-title"> <div class="om-overlay" id="org-modal" role="dialog" aria-modal="true" aria-labelledby="org-modal-title">
@@ -132,18 +142,35 @@
<div class="om-err" id="org-f-err" role="alert"></div> <div class="om-err" id="org-f-err" role="alert"></div>
</div> </div>
<div class="om-foot"> <div class="om-foot">
<button class="btn btn-outline" type="button" onclick="orgCloseModal()">Annulla</button> <button class="btn-ghost" type="button" onclick="orgCloseModal()">Annulla</button>
<button class="btn btn-primary" type="button" id="org-f-save" onclick="orgSaveRole()">Salva</button> <button class="btn-primary" type="button" id="org-f-save" onclick="orgSaveRole()">Salva</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → organigramma.js.
organigramma.js si auto-inizializza su DOMContentLoaded (checkAuth/loadSidebar/I18n/Help). -->
<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>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></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/organigramma.js?v=20260617"></script> <script src="/js/organigramma.js?v=20260627"></script>
<script>
// Render icone Lucide (chrome + crumb + bottoni). organigramma.js fa render albero
// dopo il fetch dati: re-render icone su mutazioni del DOM dell'app.
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
v3Icons();
document.addEventListener('DOMContentLoaded', function () {
v3Icons();
var root = document.querySelector('main.wrap');
if (root && window.MutationObserver) {
var mo = new MutationObserver(function () { v3Icons(); });
mo.observe(root, { childList: true, subtree: true });
}
});
</script>
</body> </body>
</html> </html>
+31 -31
View File
@@ -3,12 +3,12 @@
<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>Policy - NIS2 Agile</title> <title>Policy · 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 (self-host only, nessun CDN esterno). -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (tabelle/editor/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"> <!-- style.css resta caricato DOPO v3.css: veste i widget renderizzati via JS
<link rel="stylesheet" href="/css/style.css?v=20260618"> (tabelle/editor/modali/badge/tab). Self-hostato (nessun CDN). v3.css governa chrome + shell. -->
<style> <style>
/* ── Policy Page Styles ──────────────────────────────────── */ /* ── Policy Page Styles ──────────────────────────────────── */
.filters-bar { .filters-bar {
@@ -254,20 +254,26 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2" data-i18n="policies.title">Gestione Policy</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span data-i18n="policies.title">Gestione Policy</span>
<button id="nis2-policies-btn-ai-generate" class="btn btn-outline" onclick="openAIGenerateModal()"> </nav>
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><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> <div class="page-head">
Genera con AI <div>
</button> <h1 data-i18n="policies.title">Gestione Policy</h1>
<button id="nis2-policies-btn-new" class="btn btn-primary" onclick="openCreateModal()">+ Nuova Policy</button> <p class="sub">Politiche di sicurezza: bozze, revisione, approvazione e template NIS2</p>
</div>
<div class="content-header-actions" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<button id="nis2-policies-btn-ai-generate" class="btn-ghost" onclick="openAIGenerateModal()">
<i data-lucide="sparkles" class="ic"></i>Genera con AI
</button>
<button id="nis2-policies-btn-new" class="btn-primary" onclick="openCreateModal()"><i data-lucide="plus" class="ic"></i>Nuova Policy</button>
</div>
</div> </div>
</header>
<div class="content-body" id="page-content"> <div class="content-body" id="page-content">
<!-- Tabs --> <!-- Tabs -->
@@ -331,28 +337,22 @@
<div id="policy-detail-view" style="display:none;"></div> <div id="policy-detail-view" style="display:none;"></div>
</div> </div>
</main> </main>
</div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → pagina -->
<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> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
I18n.init(); I18n.init();
HelpSystem.init(); HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
// ── Labels ────────────────────────────────────────────── // ── Labels ──────────────────────────────────────────────
const CATEGORY_LABELS = { const CATEGORY_LABELS = {
+71 -49
View File
@@ -3,52 +3,54 @@
<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>Matrice RACI - NIS2 Agile</title> <title>Matrice RACI · NIS2 Agile</title>
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <!-- UI V3: Inter self-host + design system v3.css. No external CDN, no Bootstrap Italia. -->
<link rel="stylesheet" href="/css/style.css?v=20260618"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
.raci-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:16px; font-size:.92rem; line-height:1.6; } /* V3+ page-specific (componenti non coperti da v3.css): tab interni, matrice RACI, link m2m. Token V3. */
.raci-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 18px; } .raci-intro { background:#EFF4FB; border-left:4px solid var(--c-blue); padding:14px 18px; border-radius:10px; margin-bottom:14px; font-size:13.5px; line-height:1.6; color:var(--ink); }
.raci-note { font-size:12.5px; color:#9A6418; background:#FBF1E2; border:1px solid #f3d9a8; border-radius:10px; padding:10px 12px; margin:0 0 18px; }
.cmp-tabs { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:18px; } .cmp-tabs { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:18px; }
.cmp-tab { flex:1 1 200px; min-width:160px; padding:10px 12px; border:1.5px solid var(--gray-200,#e5e7eb); border-radius:10px; background:#fff; cursor:pointer; font-size:.85rem; font-weight:600; text-align:center; } .cmp-tab { flex:1 1 200px; min-width:160px; padding:11px 12px; border:1.5px solid var(--line); border-radius:10px; background:#fff; cursor:pointer; font:inherit; font-size:13px; font-weight:600; text-align:center; color:var(--ink); }
.cmp-tab.active { border-color:var(--primary,#2563eb); background:#eff6ff; color:var(--primary,#2563eb); } .cmp-tab.active { border-color:var(--c-blue); background:#EFF4FB; color:var(--c-blue); }
.cmp-panel { display:none; } .cmp-panel { display:none; }
.cmp-panel.active { display:block; } .cmp-panel.active { display:block; }
.cmp-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; } .cmp-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
.cmp-toolbar select { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; } .cmp-toolbar select { padding:8px 10px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
.cmp-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); } .cmp-empty { text-align:center; padding:34px 16px; color:var(--muted); }
.gap-role-sub { font-size:.82rem; color:var(--gray-500,#6b7280); } .gap-role-sub { font-size:12px; color:var(--muted); }
/* Matrice RACI */ /* Matrice RACI */
.raci-matrix-wrap { overflow-x:auto; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; } .raci-matrix-wrap { overflow-x:auto; border:1px solid var(--line); border-radius:11px; }
.raci-matrix { border-collapse:collapse; font-size:.84rem; min-width:100%; } .raci-matrix { border-collapse:collapse; font-size:13px; min-width:100%; }
.raci-matrix th, .raci-matrix td { padding:8px 10px; border-bottom:1px solid var(--gray-100,#f3f4f6); border-right:1px solid var(--gray-100,#f3f4f6); text-align:center; vertical-align:middle; } .raci-matrix th, .raci-matrix td { padding:9px 11px; border-bottom:1px solid var(--line); border-right:1px solid var(--line); text-align:center; vertical-align:middle; }
.raci-matrix thead th { background:var(--gray-50,#f9fafb); font-size:.72rem; text-transform:uppercase; letter-spacing:.03em; color:var(--gray-500,#6b7280); position:sticky; top:0; } .raci-matrix thead th { background:#FAFCFE; font-size:10.5px; text-transform:uppercase; letter-spacing:.03em; color:var(--muted); font-weight:700; position:sticky; top:0; }
.raci-matrix thead th.raci-grp { text-align:left; } .raci-matrix thead th.raci-grp { text-align:left; }
.raci-matrix th.raci-rolehead { text-align:left; font-weight:700; color:var(--gray-700,#374151); background:#fff; position:sticky; left:0; z-index:2; min-width:180px; } .raci-matrix th.raci-rolehead { text-align:left; font-weight:700; color:var(--ink); background:#fff; position:sticky; left:0; z-index:2; min-width:180px; }
.raci-matrix thead th.raci-corner { position:sticky; left:0; z-index:3; background:var(--gray-50,#f9fafb); text-align:left; } .raci-matrix thead th.raci-corner { position:sticky; left:0; z-index:3; background:#FAFCFE; text-align:left; }
.raci-objhead { font-weight:600; min-width:130px; } .raci-objhead { font-weight:600; min-width:130px; }
.raci-objhead .raci-objtype { display:block; font-size:.66rem; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-400,#9ca3af); } .raci-objhead .raci-objtype { display:block; font-size:9.5px; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:#9aa8ba; }
.raci-cell-sel { width:100%; padding:5px 6px; border:1px solid var(--gray-200,#e5e7eb); border-radius:6px; font-size:.82rem; background:#fff; cursor:pointer; } .raci-cell-sel { width:100%; padding:6px 7px; border:1px solid var(--line); border-radius:6px; font:inherit; font-size:12.5px; background:#fff; color:var(--ink); cursor:pointer; }
.raci-cell-sel.has-r { background:#dcfce7; border-color:#86efac; color:#166534; font-weight:700; } .raci-cell-sel.has-r { background:#E6F4EC; border-color:#86efac; color:#166534; font-weight:700; }
.raci-cell-sel.has-a { background:#fee2e2; border-color:#fca5a5; color:#991b1b; font-weight:700; } .raci-cell-sel.has-a { background:#FBE9E9; border-color:#fca5a5; color:#991b1b; font-weight:700; }
.raci-cell-sel.has-c { background:#fef9c3; border-color:#fde047; color:#854d0e; font-weight:700; } .raci-cell-sel.has-c { background:#FEF9C3; border-color:#fde047; color:#854d0e; font-weight:700; }
.raci-cell-sel.has-i { background:#e0f2fe; border-color:#7dd3fc; color:#075985; font-weight:700; } .raci-cell-sel.has-i { background:#E0F2FE; border-color:#7dd3fc; color:#075985; font-weight:700; }
.raci-legend { display:flex; gap:14px; flex-wrap:wrap; font-size:.78rem; margin:12px 2px 0; color:var(--gray-600,#4b5563); } .raci-legend { display:flex; gap:14px; flex-wrap:wrap; font-size:12px; margin:14px 2px 0; color:var(--muted); }
.raci-legend span b { display:inline-block; width:18px; text-align:center; border-radius:4px; margin-right:4px; } .raci-legend span b { display:inline-block; width:18px; text-align:center; border-radius:4px; margin-right:4px; }
.raci-legend .l-r b { background:#dcfce7; color:#166534; } .raci-legend .l-r b { background:#E6F4EC; color:#166534; }
.raci-legend .l-a b { background:#fee2e2; color:#991b1b; } .raci-legend .l-a b { background:#FBE9E9; color:#991b1b; }
.raci-legend .l-c b { background:#fef9c3; color:#854d0e; } .raci-legend .l-c b { background:#FEF9C3; color:#854d0e; }
.raci-legend .l-i b { background:#e0f2fe; color:#075985; } .raci-legend .l-i b { background:#E0F2FE; color:#075985; }
/* Link m2m */ /* Link m2m */
.raci-inline { display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end; background:#f9fafb; border:1px solid var(--gray-100,#f3f4f6); border-radius:10px; padding:12px; margin-bottom:14px; } .raci-inline { display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end; background:#FAFCFE; border:1px solid var(--line); border-radius:10px; padding:12px; margin-bottom:14px; }
.raci-inline .fld { display:flex; flex-direction:column; gap:3px; } .raci-inline .fld { display:flex; flex-direction:column; gap:3px; }
.raci-inline .fld label { font-size:.72rem; font-weight:600; color:var(--gray-500,#6b7280); } .raci-inline .fld label { font-size:11px; font-weight:700; color:var(--muted); }
.raci-inline select { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; min-width:200px; } .raci-inline select { padding:8px 10px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); min-width:200px; }
.raci-linklist { width:100%; border-collapse:collapse; font-size:.88rem; } .raci-linklist { width:100%; border-collapse:collapse; font-size:13.5px; }
.raci-linklist th, .raci-linklist td { text-align:left; padding:9px 10px; border-bottom:1px solid var(--gray-100,#f3f4f6); } .raci-linklist th, .raci-linklist td { text-align:left; padding:10px 11px; border-bottom:1px solid var(--line); }
.raci-linklist th { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); } .raci-linklist th { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); font-weight:700; }
.raci-linklist .btn { padding:4px 9px; font-size:.76rem; } .raci-arrow { color:#9aa8ba; padding:0 6px; }
.raci-arrow { color:var(--gray-400,#9ca3af); padding:0 6px; } .savehint { font-size:12px; color:var(--muted); }
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -65,17 +67,24 @@
<!-- PWA:end --> <!-- PWA:end -->
</head> </head>
<body> <body>
<div class="app-layout"> <div id="topnav"></div>
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h2 data-i18n="raci.title">Matrice RACI</h2> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span>Struttura interna</span>
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="raci.title">Matrice RACI</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="raci.title">Matrice RACI</h1>
<p class="sub">Responsabilità R/A/C/I dei ruoli e collegamenti tra oggetti</p>
</div>
<span class="savehint" id="raci-hint" role="status" aria-live="polite"></span> <span class="savehint" id="raci-hint" role="status" aria-live="polite"></span>
</div> </div>
</header>
<div class="content-body"> <div class="stack">
<section class="card card-pad">
<div class="raci-intro"> <div class="raci-intro">
<strong>Matrice RACI &amp; collegamenti.</strong> <strong>Matrice RACI &amp; collegamenti.</strong>
Collega i ruoli dell'organigramma agli oggetti del sistema (inventario, procedure, rischi, fornitori) assegnando le responsabilità <strong>R</strong>esponsible / <strong>A</strong>ccountable / <strong>C</strong>onsulted / <strong>I</strong>nformed. Gestisci inoltre i collegamenti molti-a-molti fra procedure, inventario, rischi e misure. Collega i ruoli dell'organigramma agli oggetti del sistema (inventario, procedure, rischi, fornitori) assegnando le responsabilità <strong>R</strong>esponsible / <strong>A</strong>ccountable / <strong>C</strong>onsulted / <strong>I</strong>nformed. Gestisci inoltre i collegamenti molti-a-molti fra procedure, inventario, rischi e misure.
@@ -123,20 +132,33 @@
<label id="raci-blabel" for="raci-bside">Lato B</label> <label id="raci-blabel" for="raci-bside">Lato B</label>
<select id="raci-bside"></select> <select id="raci-bside"></select>
</div> </div>
<button class="btn btn-primary btn-sm" onclick="raciAddLink()">Aggiungi collegamento</button> <button class="btn-primary" onclick="raciAddLink()">Aggiungi collegamento</button>
</div> </div>
<div id="raci-links"></div> <div id="raci-links"></div>
</div> </div>
</section>
</div> </div>
</main> </main>
</div>
<!-- Scripts V3. raci.js si auto-inizializza su DOMContentLoaded (checkAuth/loadSidebar/I18n/Help). -->
<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>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></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/raci.js?v=20260619"></script> <script src="/js/raci.js?v=20260627"></script>
<script>
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
v3Icons();
document.addEventListener('DOMContentLoaded', function () {
v3Icons();
var root = document.querySelector('main.wrap');
if (root && window.MutationObserver) {
var mo = new MutationObserver(function () { v3Icons(); });
mo.observe(root, { childList: true, subtree: true });
}
});
</script>
</body> </body>
</html> </html>
+124 -345
View File
@@ -3,344 +3,127 @@
<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>Audit e Report - NIS2 Agile</title> <title>Audit e Report · 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 (self-host only, nessun CDN esterno). -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (tabelle/modali/form) -> 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">
<style> <style>
/* ── Compat V3: token + widget emessi dal JS (ex style.css, niente CDN) ───── */
:root{
--primary:#1F5FA8;--primary-bg:#EAF1FA;
--secondary:#16794A;--secondary-bg:#E6F4EC;
--warning:#B45309;--danger:#B42318;--info:#1F5FA8;--info-bg:#EAF1FA;
--gray-50:#F8FAFC;--gray-100:#F1F5F9;--gray-200:#E3E9F0;--gray-300:#CBD5E1;
--gray-400:#94A3B8;--gray-500:#64748B;--gray-600:#475569;--gray-700:#334155;--gray-800:#1E293B;
--card-bg:#fff;--border-radius:10px;--border-radius-sm:7px;
--card-shadow:0 1px 2px rgba(16,40,80,.06),0 1px 3px rgba(16,40,80,.05);
--transition-fast:.12s ease;--font-mono:ui-monospace,SFMono-Regular,Menlo,monospace;
}
/* Bottoni emessi dal JS (modali) — coerenti con v3.css */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;border:1px solid transparent;border-radius:8px;padding:8px 14px;font:inherit;font-weight:600;font-size:13px;cursor:pointer;line-height:1.2;text-decoration:none}
.btn:disabled{opacity:.55;cursor:not-allowed}
.btn-primary{background:var(--c-blue,#1F5FA8);color:#fff}.btn-primary:hover:not(:disabled){background:#1A5494}
.btn-secondary{background:#fff;color:var(--ink,#1E293B);border-color:var(--line,#E3E9F0)}.btn-secondary:hover:not(:disabled){background:#F5F8FC;border-color:#c7d4e4}
/* ── Tab Navigation ─────────────────────────────────────── */ /* ── Tab Navigation ─────────────────────────────────────── */
.tab-nav { .tab-nav { display:flex; gap:0; border-bottom:2px solid var(--gray-200); margin-bottom:24px; flex-wrap:wrap; }
display: flex;
gap: 0;
border-bottom: 2px solid var(--gray-200);
margin-bottom: 24px;
}
.tab-nav button { .tab-nav button {
padding: 12px 24px; padding:12px 22px; background:none; border:none; border-bottom:2px solid transparent; margin-bottom:-2px;
background: none; color:var(--gray-500); font:inherit; font-size:.9rem; font-weight:600; cursor:pointer; transition:all var(--transition-fast);
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
color: var(--gray-500);
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
} }
.tab-nav button:hover { .tab-nav button:hover { color:var(--gray-700); background:var(--gray-50); }
color: var(--gray-700); .tab-nav button.active { color:var(--primary); border-bottom-color:var(--primary); }
background: var(--gray-50); .tab-nav button:focus-visible { outline:2px solid var(--primary); outline-offset:-2px; border-radius:4px 4px 0 0; }
} .tab-panel { display:none; }
.tab-nav button.active { .tab-panel.active { display:block; }
color: var(--primary);
border-bottom-color: var(--primary);
}
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ── Data Table ─────────────────────────────────────────── */ /* ── Data Table ─────────────────────────────────────────── */
.data-table { .data-table { width:100%; border-collapse:collapse; background:var(--card-bg); border-radius:var(--border-radius); overflow:hidden; box-shadow:var(--card-shadow); }
width: 100%; .data-table thead { background:var(--gray-50); }
border-collapse: collapse; .data-table th { text-align:left; padding:11px 16px; font-size:.72rem; font-weight:700; color:var(--gray-500); text-transform:uppercase; letter-spacing:.05em; border-bottom:1px solid var(--gray-200); }
background: var(--card-bg); .data-table td { padding:11px 16px; font-size:.875rem; color:var(--gray-700); border-bottom:1px solid var(--gray-100); }
border-radius: var(--border-radius); .data-table tr:last-child td { border-bottom:none; }
overflow: hidden; .data-table tbody tr:hover td { background:var(--gray-50); }
box-shadow: var(--card-shadow); .data-table tbody tr.clickable-row { cursor:pointer; }
}
.data-table thead {
background: var(--gray-50);
}
.data-table th {
text-align: left;
padding: 12px 16px;
font-size: 0.8rem;
font-weight: 600;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--gray-200);
}
.data-table td {
padding: 12px 16px;
font-size: 0.875rem;
color: var(--gray-700);
border-bottom: 1px solid var(--gray-100);
}
.data-table tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover td { background: var(--gray-50); }
/* ── Status Badges ──────────────────────────────────────── */ /* ── Status Badges ──────────────────────────────────────── */
.status-badge { .status-badge { display:inline-flex; align-items:center; padding:3px 10px; border-radius:20px; font-size:.78rem; font-weight:600; }
display: inline-flex; .status-badge.not_started { background:var(--gray-100); color:var(--gray-600); }
align-items: center; .status-badge.in_progress { background:var(--info-bg); color:var(--info); }
padding: 3px 10px; .status-badge.implemented { background:var(--secondary-bg); color:var(--secondary); }
border-radius: 20px; .status-badge.verified { background:var(--primary-bg); color:var(--primary); }
font-size: 0.78rem;
font-weight: 500;
}
.status-badge.not_started { background: var(--gray-100); color: var(--gray-600); }
.status-badge.in_progress { background: var(--info-bg); color: var(--info); }
.status-badge.implemented { background: var(--secondary-bg); color: var(--secondary); }
.status-badge.verified { background: var(--primary-bg); color: var(--primary); }
/* ── Progress Bar ───────────────────────────────────────── */ /* ── Progress Bar ───────────────────────────────────────── */
.progress-bar-container { .progress-bar-container { display:flex; align-items:center; gap:8px; }
display: flex; .progress-bar { flex-grow:1; height:6px; background:var(--gray-200); border-radius:3px; overflow:hidden; min-width:80px; }
align-items: center; .progress-bar-fill { height:100%; border-radius:3px; transition:width .3s ease; }
gap: 8px; .progress-pct { font-size:.8rem; font-weight:700; min-width:36px; text-align:right; color:var(--gray-600); }
}
.progress-bar { /* ── stat-box (monitoraggio) ────────────────────────────── */
flex-grow: 1; .stat-box { padding:10px 14px; background:var(--gray-50); border-radius:8px; }
height: 6px; .stat-box .stat-value { font-size:1.4rem; font-weight:800; }
background: var(--gray-200); .stat-box .stat-label { font-size:.72rem; color:var(--gray-500); }
border-radius: 3px;
overflow: hidden;
min-width: 80px;
}
.progress-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-pct {
font-size: 0.8rem;
font-weight: 600;
min-width: 36px;
text-align: right;
color: var(--gray-600);
}
/* ── Report Section ─────────────────────────────────────── */ /* ── Report Section ─────────────────────────────────────── */
.report-container { .report-container { background:var(--card-bg); border:1px solid var(--gray-200); border-radius:var(--border-radius); overflow:hidden; }
background: var(--card-bg); .report-header { background:var(--navy-nav,#142B4D); color:#fff; padding:30px; text-align:center; }
border-radius: var(--border-radius); .report-header h3 { font-size:1.3rem; margin:0 0 4px; }
box-shadow: var(--card-shadow); .report-header p { color:#cbd5e1; font-size:.85rem; margin:0; }
overflow: hidden; .report-body { padding:24px 30px; }
} .report-section { margin-bottom:24px; }
.report-header { .report-section-title { font-size:1rem; font-weight:700; color:var(--gray-800); margin-bottom:12px; padding-bottom:8px; border-bottom:2px solid var(--primary-bg); }
background: var(--gray-800); .report-summary-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:16px; }
color: white; .report-summary-item { text-align:center; padding:16px; background:var(--gray-50); border-radius:var(--border-radius); }
padding: 32px; .report-summary-value { font-size:1.8rem; font-weight:800; color:var(--gray-800); line-height:1.2; }
text-align: center; .report-summary-label { font-size:.8rem; color:var(--gray-500); margin-top:4px; }
} .report-compliance-bar { height:20px; background:var(--gray-200); border-radius:10px; overflow:hidden; margin:12px 0; }
.report-header h3 { .report-compliance-fill { height:100%; border-radius:10px; transition:width .5s ease; display:flex; align-items:center; justify-content:center; color:#fff; font-size:.75rem; font-weight:700; }
font-size: 1.3rem;
margin-bottom: 4px;
}
.report-header p {
color: var(--gray-300);
font-size: 0.85rem;
}
.report-body {
padding: 24px 32px;
}
.report-section {
margin-bottom: 24px;
}
.report-section-title {
font-size: 1rem;
font-weight: 600;
color: var(--gray-800);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid var(--primary-bg);
}
.report-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
}
.report-summary-item {
text-align: center;
padding: 16px;
background: var(--gray-50);
border-radius: var(--border-radius);
}
.report-summary-value {
font-size: 1.8rem;
font-weight: 700;
color: var(--gray-800);
line-height: 1.2;
}
.report-summary-label {
font-size: 0.8rem;
color: var(--gray-500);
margin-top: 4px;
}
.report-compliance-bar {
height: 20px;
background: var(--gray-200);
border-radius: 10px;
overflow: hidden;
margin: 12px 0;
}
.report-compliance-fill {
height: 100%;
border-radius: 10px;
transition: width 0.5s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
font-weight: 600;
}
/* ── ISO Mapping ────────────────────────────────────────── */ /* ── ISO Mapping ────────────────────────────────────────── */
.iso-mapping-row { .iso-mapping-row { display:flex; align-items:center; gap:16px; padding:12px 0; border-bottom:1px solid var(--gray-100); }
display: flex; .iso-mapping-row:last-child { border-bottom:none; }
align-items: center; .iso-nis2-badge { background:var(--primary-bg); color:var(--primary); padding:6px 14px; border-radius:20px; font-size:.82rem; font-weight:700; white-space:nowrap; min-width:90px; text-align:center; }
gap: 16px; .iso-arrow { color:var(--gray-300); flex-shrink:0; }
padding: 12px 0; .iso-arrow svg { width:20px; height:20px; }
border-bottom: 1px solid var(--gray-100); .iso-controls { display:flex; flex-wrap:wrap; gap:6px; flex-grow:1; }
} .iso-control-badge { background:var(--secondary-bg); color:var(--secondary); padding:4px 10px; border-radius:20px; font-size:.78rem; font-weight:600; }
.iso-mapping-row:last-child { border-bottom: none; } .iso-title { font-size:.85rem; color:var(--gray-500); min-width:200px; }
.iso-nis2-badge {
background: var(--primary-bg);
color: var(--primary);
padding: 6px 14px;
border-radius: 20px;
font-size: 0.82rem;
font-weight: 600;
white-space: nowrap;
min-width: 90px;
text-align: center;
}
.iso-arrow {
color: var(--gray-300);
flex-shrink: 0;
}
.iso-arrow svg { width: 20px; height: 20px; }
.iso-controls {
display: flex;
flex-wrap: wrap;
gap: 6px;
flex-grow: 1;
}
.iso-control-badge {
background: var(--secondary-bg);
color: var(--secondary);
padding: 4px 10px;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 500;
}
.iso-title {
font-size: 0.85rem;
color: var(--gray-500);
min-width: 200px;
}
/* ── Pagination ─────────────────────────────────────────── */ /* ── Pagination ─────────────────────────────────────────── */
.pagination { .pagination { display:flex; justify-content:center; align-items:center; gap:12px; margin-top:20px; padding:12px; }
display: flex; .pagination button { padding:8px 16px; border:1px solid var(--gray-200); background:var(--card-bg); border-radius:var(--border-radius-sm); color:var(--gray-600); cursor:pointer; font:inherit; font-size:.85rem; transition:all var(--transition-fast); }
justify-content: center; .pagination button:hover:not(:disabled) { background:var(--primary-bg); border-color:var(--primary); color:var(--primary); }
align-items: center; .pagination button:disabled { opacity:.4; cursor:not-allowed; }
gap: 12px; .pagination .page-info { font-size:.85rem; color:var(--gray-500); }
margin-top: 20px;
padding: 12px;
}
.pagination button {
padding: 8px 16px;
border: 1px solid var(--gray-200);
background: var(--card-bg);
border-radius: var(--border-radius-sm);
color: var(--gray-600);
cursor: pointer;
font-size: 0.85rem;
transition: all var(--transition-fast);
}
.pagination button:hover:not(:disabled) {
background: var(--primary-bg);
border-color: var(--primary);
color: var(--primary);
}
.pagination button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pagination .page-info {
font-size: 0.85rem;
color: var(--gray-500);
}
/* ── Audit Log Action ───────────────────────────────────── */ /* ── Audit Log ──────────────────────────────────────────── */
.log-action { .log-action { display:inline-flex; align-items:center; gap:4px; font-size:.82rem; }
display: inline-flex; .log-entity { background:var(--gray-100); padding:2px 8px; border-radius:var(--border-radius-sm); font-size:.8rem; color:var(--gray-600); font-family:var(--font-mono); }
align-items: center; .log-details { display:inline-block; max-width:300px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:.8rem; color:var(--gray-400); cursor:help; vertical-align:bottom; }
gap: 4px;
font-size: 0.82rem;
}
.log-entity {
background: var(--gray-100);
padding: 2px 8px;
border-radius: var(--border-radius-sm);
font-size: 0.8rem;
color: var(--gray-600);
font-family: var(--font-mono);
}
.log-details {
max-width: 300px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.8rem;
color: var(--gray-400);
cursor: help;
}
/* ── Controls Row Clickable ─────────────────────────────── */ /* ── Loading / Empty ────────────────────────────────────── */
.data-table tbody tr.clickable-row { cursor: pointer; } .loading-state { text-align:center; padding:48px 20px; color:var(--gray-400); }
.loading-state .spinner { width:32px; height:32px; border:3px solid var(--gray-200); border-top-color:var(--primary); border-radius:50%; animation:rpt-spin .8s linear infinite; margin:0 auto 12px; }
/* ── Loading/Empty ──────────────────────────────────────── */ @keyframes rpt-spin { to { transform:rotate(360deg); } }
.loading-state { .empty-state-box { text-align:center; padding:48px 20px; color:var(--gray-400); }
text-align: center; .empty-state-box svg { width:48px; height:48px; margin-bottom:12px; opacity:.5; }
padding: 48px 20px; .empty-state-box h4 { color:var(--gray-600); margin:0 0 4px; }
color: var(--gray-400); .empty-state-box p { margin:0; }
}
.loading-state .spinner {
width: 32px;
height: 32px;
border: 3px solid var(--gray-200);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 12px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.empty-state-box {
text-align: center;
padding: 48px 20px;
color: var(--gray-400);
}
.empty-state-box svg {
width: 48px;
height: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.empty-state-box h4 {
color: var(--gray-600);
margin-bottom: 4px;
}
/* ── Print ──────────────────────────────────────────────── */ /* ── Print ──────────────────────────────────────────────── */
@media print { @media print {
.sidebar, .content-header, .tab-nav, .btn { display: none !important; } #topnav, .crumb, .page-head .btn-primary, .tab-nav, .btn { display:none !important; }
.main-content { margin: 0 !important; padding: 0 !important; } .wrap { max-width:100% !important; padding:0 !important; }
.report-container { box-shadow: none; border: 1px solid #ddd; } .report-container { box-shadow:none; border:1px solid #ddd; }
body { background: white; } body { background:#fff; }
} }
/* ── Responsive ─────────────────────────────────────────── */ /* ── Responsive ─────────────────────────────────────────── */
@media (max-width: 768px) { @media (max-width:768px) {
.tab-nav button { padding: 10px 12px; font-size: 0.8rem; } .tab-nav button { padding:10px 12px; font-size:.8rem; }
.iso-mapping-row { flex-direction: column; align-items: flex-start; } .iso-mapping-row { flex-direction:column; align-items:flex-start; }
.iso-title { min-width: auto; } .iso-title { min-width:auto; }
.report-summary-grid { grid-template-columns: 1fr 1fr; } .report-summary-grid { grid-template-columns:1fr 1fr; }
} }
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
@@ -358,18 +141,23 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2" data-i18n="audit.title">Audit &amp; Report</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span>Audit e Report</span>
<button class="btn btn-primary" onclick="generateReport()">Genera Report</button> </nav>
<div class="page-head">
<div>
<h1 data-i18n="audit.title">Audit &amp; Report</h1>
<p class="sub">Report di compliance, controlli, audit log e mapping ISO 27001</p>
</div>
<button class="btn-primary" onclick="generateReport()"><i data-lucide="file-text" class="ic"></i>Genera Report</button>
</div> </div>
</header>
<div class="content-body"> <section class="card card-pad">
<!-- Tab Navigation --> <!-- Tab Navigation -->
<div class="tab-nav"> <div class="tab-nav">
<button class="active" onclick="switchTab('report', this)">Report Compliance</button> <button class="active" onclick="switchTab('report', this)">Report Compliance</button>
@@ -439,30 +227,24 @@
</div> </div>
</div> </div>
</div> </div>
</div> </section>
</main> </main>
</div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<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> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
I18n.init(); if (typeof I18n !== 'undefined') I18n.init();
HelpSystem.init(); if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
// ── Labels ────────────────────────────────────────────── // ── Labels ──────────────────────────────────────────────
const controlStatusLabels = { const controlStatusLabels = {
@@ -1017,11 +799,8 @@
} }
let html = ` let html = `
<div class="card"> <div>
<div class="card-header"> <h3 style="margin:0 0 4px;font-size:1.05rem;">Mapping NIS2 - ISO 27001:2022</h3>
<h3>Mapping NIS2 - ISO 27001:2022</h3>
</div>
<div class="card-body">
<p style="font-size:0.85rem; color:var(--gray-500); margin-bottom:20px;"> <p style="font-size:0.85rem; color:var(--gray-500); margin-bottom:20px;">
Corrispondenza tra gli articoli della Direttiva NIS2 e i controlli dello standard ISO/IEC 27001:2022. Corrispondenza tra gli articoli della Direttiva NIS2 e i controlli dello standard ISO/IEC 27001:2022.
</p>`; </p>`;
@@ -1041,7 +820,7 @@
</div>`; </div>`;
}); });
html += '</div></div>'; html += '</div>';
container.innerHTML = html; container.innerHTML = html;
} }
+75 -59
View File
@@ -3,51 +3,60 @@
<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>Scadenziario - NIS2 Agile</title> <title>Scadenziario · 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 / NO Bootstrap Italia). -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/style.css?v=20260618"> <link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
.rev-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:16px; font-size:.92rem; line-height:1.6; } /* V3 — specifico pagina Scadenziario (non coperto da v3.css) */
.rev-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 18px; } .rev-intro { background:#EAF1FA; border-left:4px solid var(--c-blue); padding:14px 18px; border-radius:10px; margin-bottom:16px; font-size:13.5px; line-height:1.6; color:var(--ink); }
.rev-stats { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:18px; } .rev-note { font-size:12.5px; color:#92400e; background:#FBF1E2; border:1px solid #fde68a; border-radius:10px; padding:10px 12px; margin:0 0 18px; }
.rev-stat { flex:1 1 150px; min-width:130px; background:#fff; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:12px 14px; } .rev-stats { display:flex; flex-wrap:wrap; gap:12px; margin-bottom:18px; }
.rev-stat .n { font-size:1.5rem; font-weight:800; line-height:1; } .rev-stat { flex:1 1 150px; min-width:130px; background:#fff; border:1px solid var(--line); border-radius:11px; padding:14px 16px; box-shadow:var(--shadow); }
.rev-stat .l { font-size:.78rem; color:var(--gray-500,#6b7280); margin-top:4px; } .rev-stat .n { font-size:27px; font-weight:800; line-height:1; }
.rev-stat .l { font-size:11px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#7a8aa0; margin-top:6px; }
.rev-stat.s-overdue .n { color:#991b1b; } .rev-stat.s-overdue .n { color:#991b1b; }
.rev-stat.s-due .n { color:#b45309; } .rev-stat.s-due .n { color:#b45309; }
.rev-stat.s-ok .n { color:#166534; } .rev-stat.s-ok .n { color:#166534; }
.rev-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; } .rev-toolbar { display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
.rev-toolbar select { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; } .rev-toolbar select { padding:8px 11px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
.rev-toolbar .grow { flex:1 1 auto; } .rev-toolbar .grow { flex:1 1 auto; }
.rev-table { width:100%; border-collapse:collapse; font-size:.88rem; } .rev-table { width:100%; border-collapse:collapse; font-size:13px; }
.rev-table th, .rev-table td { text-align:left; padding:9px 10px; border-bottom:1px solid var(--gray-100,#f3f4f6); vertical-align:top; } .rev-table th, .rev-table td { text-align:left; padding:10px 12px; border-bottom:1px solid var(--line); vertical-align:top; }
.rev-table th { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); } .rev-table th { font-size:10.5px; text-transform:uppercase; letter-spacing:.04em; color:#7a8aa0; }
.rev-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; } .rev-badge { display:inline-block; font-size:10.5px; font-weight:700; padding:2px 8px; border-radius:6px; }
.rev-badge.type { background:#f1f5f9; color:#475569; } .rev-badge.type { background:#EEF2F7; color:#475569; }
.gap-pill { display:inline-block; font-size:.72rem; font-weight:800; padding:2px 9px; border-radius:20px; } .gap-pill { display:inline-block; font-size:11px; font-weight:800; padding:2px 9px; border-radius:20px; }
.gap-pill.ok { background:#dcfce7; color:#166534; } .gap-pill.ok { background:#E6F4EC; color:#166534; }
.gap-pill.due { background:#fef3c7; color:#92400e; } .gap-pill.due { background:#FBF1E2; color:#92400e; }
.gap-pill.bad { background:#fee2e2; color:#991b1b; } .gap-pill.bad { background:#FBE9E9; color:#991b1b; }
.rev-actions-cell { display:flex; gap:6px; flex-wrap:wrap; } .rev-actions-cell { display:flex; gap:6px; flex-wrap:wrap; }
.rev-actions-cell .btn, .rev-toolbar .btn { padding:4px 9px; font-size:.76rem; } .rev-empty { text-align:center; padding:34px 16px; color:var(--muted); }
.rev-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); } /* Modale (page-owned) */
/* Modale */ .rm-overlay { display:none; position:fixed; inset:0; background:rgba(15,27,46,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
.rm-overlay { display:none; position:fixed; inset:0; background:rgba(15,23,42,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
.rm-overlay.open { display:flex; } .rm-overlay.open { display:flex; }
.rm-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(0,0,0,.25); } .rm-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(11,40,80,.25); }
.rm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); } .rm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--line); }
.rm-head h3 { margin:0; font-size:1.05rem; } .rm-head h3 { margin:0; font-size:16px; }
.rm-body { padding:18px 20px; } .rm-body { padding:18px 20px; }
.rm-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; } .rm-foot { padding:14px 20px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px; }
.rm-field { margin-bottom:14px; } .rm-field { margin-bottom:14px; }
.rm-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; } .rm-field label { display:block; font-weight:600; font-size:13px; margin-bottom:4px; }
.rm-field input[type=text], .rm-field input[type=number], .rm-field input[type=date], .rm-field select, .rm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; } .rm-field input[type=text], .rm-field input[type=number], .rm-field input[type=date], .rm-field select, .rm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:14px; }
.rm-field textarea { min-height:64px; resize:vertical; } .rm-field textarea { min-height:64px; resize:vertical; }
.rm-row { display:flex; gap:12px; } .rm-row { display:flex; gap:12px; }
.rm-row > .rm-field { flex:1 1 0; } .rm-row > .rm-field { flex:1 1 0; }
.rm-err { color:#b91c1c; font-size:.82rem; min-height:18px; } .rm-err { color:#b91c1c; font-size:12.5px; min-height:18px; }
.rm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); } .rm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--muted); }
/* Toast prodotti da common.js (style.css rimosso): stile minimo coerente V3 */
.notification-container{position:fixed;top:18px;right:18px;z-index:1100;display:flex;flex-direction:column;gap:10px}
.notification{display:flex;align-items:center;gap:10px;background:#fff;border:1px solid var(--line);border-left:4px solid var(--c-blue);border-radius:10px;box-shadow:0 8px 24px rgba(16,40,80,.14);padding:12px 14px;min-width:240px;max-width:360px;font-size:13px}
.notification.success{border-left-color:var(--c-green)}.notification.error{border-left-color:var(--c-red)}.notification.warning{border-left-color:var(--c-amber)}
.notification-icon{width:18px;height:18px;flex:0 0 auto}.notification-icon svg{width:18px;height:18px}
.notification.success .notification-icon{color:var(--c-green)}.notification.error .notification-icon{color:var(--c-red)}.notification.warning .notification-icon{color:var(--c-amber)}.notification.info .notification-icon{color:var(--c-blue)}
.notification-message{flex:1;min-width:0}
.notification-close{background:none;border:0;font-size:1.2rem;line-height:1;cursor:pointer;color:var(--muted)}
.notification.fade-out{opacity:0;transition:opacity .3s}
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -64,17 +73,24 @@
<!-- PWA:end --> <!-- PWA:end -->
</head> </head>
<body> <body>
<div class="app-layout"> <!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="rev.title">Scadenziario</h2>
<div class="content-header-actions">
<span class="savehint" id="rev-hint" role="status" aria-live="polite"></span>
</div>
</header>
<div class="content-body"> <main class="wrap">
<nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>Monitoraggio</span>
<i data-lucide="chevron-right" class="ic"></i><span>Scadenziario</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="rev.title">Scadenziario</h1>
<p class="sub">Revisioni periodiche di ruoli, competenze, inventario, procedure, rischi, fornitori e misure</p>
</div>
<span class="savehint" id="rev-hint" role="status" aria-live="polite" style="font-size:12.5px;color:var(--muted)"></span>
</div>
<section class="card card-pad">
<div class="rev-intro"> <div class="rev-intro">
<strong>Revisioni e scadenze in un unico posto.</strong> <strong>Revisioni e scadenze in un unico posto.</strong>
Tieni traccia delle revisioni periodiche di ruoli, competenze, inventario, procedure, rischi, fornitori e misure. Ogni voce mostra quando è prevista la prossima revisione e ti avvisa quando è in scadenza o scaduta. Tieni traccia delle revisioni periodiche di ruoli, competenze, inventario, procedure, rischi, fornitori e misure. Ogni voce mostra quando è prevista la prossima revisione e ti avvisa quando è in scadenza o scaduta.
@@ -91,10 +107,10 @@
</div> </div>
<div class="rev-toolbar"> <div class="rev-toolbar">
<button class="btn btn-primary btn-sm" onclick="revOpen()">+ Nuova scadenza</button> <button class="btn-primary" onclick="revOpen()"><i data-lucide="plus" class="ic"></i>Nuova scadenza</button>
<button class="btn btn-outline btn-sm" id="rev-sync-btn" onclick="revDoSync()">Sincronizza scadenze esistenti</button> <button class="btn-ghost" id="rev-sync-btn" onclick="revDoSync()"><i data-lucide="refresh-cw" class="ic"></i>Sincronizza scadenze esistenti</button>
<span class="grow"></span> <span class="grow"></span>
<label for="rev-filter" style="font-weight:600;font-size:.82rem;">Stato:</label> <label for="rev-filter" style="font-weight:600;font-size:13px;">Stato:</label>
<select id="rev-filter" onchange="revRender()"> <select id="rev-filter" onchange="revRender()">
<option value="all">Tutti</option> <option value="all">Tutti</option>
<option value="overdue">Scadute</option> <option value="overdue">Scadute</option>
@@ -104,9 +120,8 @@
</div> </div>
<div id="rev-list" aria-live="polite"></div> <div id="rev-list" aria-live="polite"></div>
</div> </section>
</main> </main>
</div>
<!-- Modale scadenza (crea/modifica) --> <!-- Modale scadenza (crea/modifica) -->
<div class="rm-overlay" id="rev-modal" role="dialog" aria-modal="true" aria-labelledby="rev-modal-title"> <div class="rm-overlay" id="rev-modal" role="dialog" aria-modal="true" aria-labelledby="rev-modal-title">
@@ -137,14 +152,15 @@
<div class="rm-field"><label for="rev-f-notes">Note</label><textarea id="rev-f-notes" maxlength="2000"></textarea></div> <div class="rm-field"><label for="rev-f-notes">Note</label><textarea id="rev-f-notes" maxlength="2000"></textarea></div>
<div class="rm-err" id="rev-err" role="alert"></div> <div class="rm-err" id="rev-err" role="alert"></div>
</div> </div>
<div class="rm-foot"><button class="btn btn-outline" type="button" onclick="revClose()">Annulla</button><button class="btn btn-primary" type="button" id="rev-save" onclick="revSave()">Salva</button></div> <div class="rm-foot"><button class="btn-ghost" type="button" onclick="revClose()">Annulla</button><button class="btn-primary" type="button" id="rev-save" onclick="revSave()">Salva</button></div>
</div> </div>
</div> </div>
<!-- Scripts V3 (ordine ESATTO come dashboard.html) -->
<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>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></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> <script>
@@ -180,6 +196,7 @@
if (window.I18n && I18n.init) I18n.init('it'); if (window.I18n && I18n.init) I18n.init('it');
if (typeof loadSidebar === 'function') loadSidebar(); if (typeof loadSidebar === 'function') loadSidebar();
if (window.HelpSystem && HelpSystem.init) HelpSystem.init(); if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') revClose(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') revClose(); });
const ov = el('rev-modal'); const ov = el('rev-modal');
@@ -195,7 +212,6 @@
REV.roles = (rolesData && rolesData.roles) || []; REV.roles = (rolesData && rolesData.roles) || [];
revFillOwnerSelect(); revFillOwnerSelect();
} catch (e) { } catch (e) {
// l'organigramma può non essere ancora popolato: non bloccare la pagina
REV.roles = []; REV.roles = [];
} }
} }
@@ -210,7 +226,7 @@
} }
async function revLoad() { async function revLoad() {
el('rev-list').innerHTML = '<div class="rev-empty">Caricamento…</div>'; el('rev-list').innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>';
try { try {
const data = await api.revList(); const data = await api.revList();
REV.items = (data && data.items) || []; REV.items = (data && data.items) || [];
@@ -250,16 +266,16 @@
const freq = (i.frequency_months != null && i.frequency_months !== '') ? (esc(i.frequency_months) + ' mesi') : ''; const freq = (i.frequency_months != null && i.frequency_months !== '') ? (esc(i.frequency_months) + ' mesi') : '';
return '<tr>' return '<tr>'
+ '<td><strong>' + esc(i.title) + '</strong>' + '<td><strong>' + esc(i.title) + '</strong>'
+ (i.notes ? '<div class="gap-role-sub" style="font-size:.8rem;color:var(--gray-500,#6b7280);margin-top:3px;">' + esc(i.notes) + '</div>' : '') + '</td>' + (i.notes ? '<div style="font-size:12px;color:var(--muted);margin-top:3px;">' + esc(i.notes) + '</div>' : '') + '</td>'
+ '<td><span class="rev-badge type">' + esc(typeLabel(i.entity_type)) + '</span></td>' + '<td><span class="rev-badge type">' + esc(typeLabel(i.entity_type)) + '</span></td>'
+ '<td>' + owner + '</td>' + '<td>' + owner + '</td>'
+ '<td>' + fmtDate(i.last_reviewed_at) + '</td>' + '<td>' + fmtDate(i.last_reviewed_at) + '</td>'
+ '<td>' + fmtDate(i.next_review_date) + (freq ? '<div style="font-size:.78rem;color:var(--gray-500,#6b7280);">ogni ' + freq + '</div>' : '') + '</td>' + '<td>' + fmtDate(i.next_review_date) + (freq ? '<div style="font-size:12px;color:var(--muted);">ogni ' + freq + '</div>' : '') + '</td>'
+ '<td><span class="gap-pill ' + meta.cls + '">' + meta.label + '</span></td>' + '<td><span class="gap-pill ' + meta.cls + '">' + meta.label + '</span></td>'
+ '<td><div class="rev-actions-cell">' + '<td><div class="rev-actions-cell">'
+ '<button class="btn btn-outline" onclick="revComplete(' + i.id + ')">Segna revisionato</button>' + '<button class="btn-ghost" onclick="revComplete(' + i.id + ')">Segna revisionato</button>'
+ '<button class="btn btn-outline" onclick="revOpen(' + i.id + ')">Modifica</button>' + '<button class="btn-ghost" onclick="revOpen(' + i.id + ')">Modifica</button>'
+ '<button class="btn btn-outline" onclick="revDelete(' + i.id + ')">Elimina</button>' + '<button class="btn-ghost" onclick="revDelete(' + i.id + ')">Elimina</button>'
+ '</div></td>' + '</div></td>'
+ '</tr>'; + '</tr>';
}).join(''); }).join('');
+31 -30
View File
@@ -3,12 +3,13 @@
<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>Gestione Rischi - NIS2 Agile</title> <title>Rischi · 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 (self-host only, nessun CDN esterno). -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (tabelle/matrice/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"> <!-- style.css resta caricato DOPO v3.css: veste i widget renderizzati via JS
<link rel="stylesheet" href="/css/style.css?v=20260618"> (tabelle/matrice/modali/form/badge) che usano le classi del design system legacy.
È self-hostato (nessun CDN). v3.css governa chrome + shell (.wrap/.card-pad/.page-head). -->
<style> <style>
/* ── View Toggle ─────────────────────────────────────── */ /* ── View Toggle ─────────────────────────────────────── */
.view-toggle { .view-toggle {
@@ -313,26 +314,32 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2" data-i18n="risks.title">Gestione Rischi</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span data-i18n="risks.title">Gestione Rischi</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="risks.title">Gestione Rischi</h1>
<p class="sub">Registro rischi, matrice 5×5, analisi quantitativa FAIR e KRI</p>
</div>
<div class="content-header-actions" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<div class="view-toggle" id="view-toggle"> <div class="view-toggle" id="view-toggle">
<button class="active" onclick="switchView('table')">Tabella</button> <button class="active" onclick="switchView('table')">Tabella</button>
<button onclick="switchView('matrix')">Matrice</button> <button onclick="switchView('matrix')">Matrice</button>
<button onclick="switchView('fair')" data-i18n="risks.fair_tab">Quantitativo (FAIR)</button> <button onclick="switchView('fair')" data-i18n="risks.fair_tab">Quantitativo (FAIR)</button>
<button onclick="switchView('kri')" data-i18n="risks.kri_tab">KRI</button> <button onclick="switchView('kri')" data-i18n="risks.kri_tab">KRI</button>
</div> </div>
<button class="btn btn-secondary" id="btn-ai-suggest" onclick="aiSuggest()"> <button class="btn-ghost" id="btn-ai-suggest" onclick="aiSuggest()">
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><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> <i data-lucide="sparkles" class="ic"></i>AI Suggerisci
AI Suggerisci
</button> </button>
<button class="btn btn-primary" onclick="showRiskModal()">+ Nuovo Rischio</button> <button class="btn-primary" onclick="showRiskModal()"><i data-lucide="plus" class="ic"></i>Nuovo Rischio</button>
</div>
</div> </div>
</header>
<div class="content-body"> <div class="content-body">
<!-- Table View --> <!-- Table View -->
@@ -492,28 +499,22 @@
<div id="view-detail" class="hidden"></div> <div id="view-detail" class="hidden"></div>
</div> </div>
</main> </main>
</div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → pagina -->
<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> <script>
// ── Auth & Init ────────────────────────────────────────────── // ── Auth & Init ──────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
I18n.init(); I18n.init();
HelpSystem.init(); HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
// ── Labels ─────────────────────────────────────────────────── // ── Labels ───────────────────────────────────────────────────
const CATEGORY_LABELS = { const CATEGORY_LABELS = {
+156 -116
View File
@@ -3,22 +3,65 @@
<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>Impostazioni - NIS2 Agile</title> <title>Impostazioni · 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 Bootstrap Italia / NO 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 (tabelle/modali/form) -> 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">
<style> <style>
/* ── Helper generici non (ancora) in v3.css, usati dal markup reso via JS.
Self-host, nessun CDN. v3.css governa chrome + shell + widget condivisi. ── */
:root { --success: var(--secondary); }
.text-muted { color: var(--gray-500); }
.mb-24 { margin-bottom: 24px; }
.card-header {
padding: 16px 24px;
border-bottom: 1px solid var(--gray-100);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.card-header h3 { font-size: 1rem; font-weight: 700; color: var(--gray-900); }
.card-body { padding: 24px; }
.form-control {
width: 100%; padding: 10px 14px; font-size: .875rem; font-family: inherit;
color: var(--ink); background: var(--card); border: 1px solid var(--line);
border-radius: var(--border-radius); outline: none;
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
.form-control:focus { border-color: var(--navy-nav); box-shadow: 0 0 0 3px rgba(31,95,168,.15); }
.form-control::placeholder { color: var(--gray-400); }
/* Tabelle rese via JS (membri, API key, webhook, delivery, audit) */
.table-container { overflow-x: auto; border-radius: var(--border-radius-lg); }
.table-container table { width: 100%; border-collapse: collapse; font-size: .875rem; }
.table-container thead { background: var(--gray-50); }
.table-container thead th {
padding: 12px 16px; font-weight: 600; color: var(--gray-600); text-align: left;
font-size: .75rem; text-transform: uppercase; letter-spacing: .04em;
border-bottom: 2px solid var(--gray-200); white-space: nowrap;
}
.table-container tbody td { padding: 12px 16px; border-bottom: 1px solid var(--gray-100); color: var(--gray-700); }
.table-container tbody tr { transition: background var(--transition-fast); }
.table-container tbody tr:hover { background: var(--gray-50); }
.table-container tbody tr:last-child td { border-bottom: none; }
/* Empty state reso via JS */
.empty-state { text-align: center; padding: 48px 24px; color: var(--gray-400); }
.empty-state svg { width: 64px; height: 64px; margin-bottom: 16px; opacity: .4; }
.empty-state h4 { font-size: 1rem; color: var(--gray-600); margin-bottom: 8px; }
.empty-state p { font-size: .875rem; max-width: 320px; margin: 0 auto; }
/* ── Stili specifici della pagina Impostazioni (non coperti da v3.css) ── */
.settings-tabs { .settings-tabs {
display: flex; display: flex;
border-bottom: 2px solid var(--gray-200); border-bottom: 2px solid var(--gray-200);
margin-bottom: 24px; margin-bottom: 0;
gap: 0; gap: 0;
overflow-x: auto; overflow-x: auto;
} }
.settings-tab { .settings-tab {
padding: 12px 24px; padding: 12px 20px;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
color: var(--gray-500); color: var(--gray-500);
@@ -31,22 +74,15 @@
white-space: nowrap; white-space: nowrap;
font-family: inherit; font-family: inherit;
} }
.settings-tab:hover { .settings-tab:hover { color: var(--gray-700); }
color: var(--gray-700);
}
.settings-tab.active { .settings-tab.active {
color: var(--primary); color: var(--navy-nav);
border-bottom-color: var(--primary); border-bottom-color: var(--navy-nav);
}
.tab-panel {
display: none;
}
.tab-panel.active {
display: block;
}
.settings-section {
margin-bottom: 32px;
} }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
.settings-section { margin-bottom: 32px; }
.settings-section:last-child { margin-bottom: 0; }
.settings-section-title { .settings-section-title {
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 700;
@@ -101,11 +137,6 @@
.plan-badge.free { background: var(--gray-100); color: var(--gray-600); } .plan-badge.free { background: var(--gray-100); color: var(--gray-600); }
.plan-badge.professional { background: var(--primary-bg); color: var(--primary); } .plan-badge.professional { background: var(--primary-bg); color: var(--primary); }
.plan-badge.enterprise { background: #f3e8ff; color: #7c3aed; } .plan-badge.enterprise { background: #f3e8ff; color: #7c3aed; }
.password-section {
border-top: 1px solid var(--gray-200);
padding-top: 24px;
margin-top: 24px;
}
.strength-bar { .strength-bar {
display: flex; display: flex;
gap: 4px; gap: 4px;
@@ -147,6 +178,7 @@
border-radius: var(--border-radius); border-radius: var(--border-radius);
margin-bottom: 16px; margin-bottom: 16px;
} }
.security-item:last-child { margin-bottom: 0; }
.security-item-header { .security-item-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -186,7 +218,8 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.btn-icon-action:hover { .btn-icon-action:hover,
.btn-icon-action.btn-danger-hover:hover {
border-color: var(--danger); border-color: var(--danger);
color: var(--danger); color: var(--danger);
background: var(--danger-bg); background: var(--danger-bg);
@@ -195,6 +228,9 @@
width: 16px; width: 16px;
height: 16px; height: 16px;
} }
/* Bottoni reso via JS che combinano btn + variante: assicura l'allineamento icona */
.btn svg { width: 18px; height: 18px; }
.required { color: var(--danger); margin-left: 2px; }
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -211,19 +247,24 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2" data-i18n="settings.title">Impostazioni</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span data-i18n="settings.title">Impostazioni</span>
<span class="text-muted" id="header-org-name"></span> </nav>
<div class="page-head">
<div>
<h1 data-i18n="settings.title">Impostazioni</h1>
<p class="sub">Profilo, organizzazione, membri, sicurezza e integrazioni</p>
</div>
<div class="text-muted" id="header-org-name"></div>
</div> </div>
</header>
<div class="content-body"> <!-- Tab Navigation (gli INDICI di tabMap dipendono dall'ordine: NON modificarlo) -->
<!-- Tab Navigation --> <section class="card card-pad" style="padding-bottom:0;">
<div class="settings-tabs"> <div class="settings-tabs">
<button class="settings-tab active" onclick="switchTab('org')">Organizzazione</button> <button class="settings-tab active" onclick="switchTab('org')">Organizzazione</button>
<button class="settings-tab" onclick="switchTab('profile')">Profilo</button> <button class="settings-tab" onclick="switchTab('profile')">Profilo</button>
@@ -235,11 +276,12 @@
<button class="settings-tab" onclick="switchTab('webhooks')">Webhook</button> <button class="settings-tab" onclick="switchTab('webhooks')">Webhook</button>
<button class="settings-tab" id="tab-btn-branding" onclick="switchTab('branding')" style="display:none;">Branding</button> <button class="settings-tab" id="tab-btn-branding" onclick="switchTab('branding')" style="display:none;">Branding</button>
</div> </div>
</section>
<div class="stack" style="margin-top:18px;">
<!-- ══════════════ TAB: Organizzazione ══════════════ --> <!-- ══════════════ TAB: Organizzazione ══════════════ -->
<div class="tab-panel active" id="tab-org"> <div class="tab-panel active" id="tab-org">
<div class="card"> <section class="card card-pad">
<div class="card-body">
<div class="settings-section"> <div class="settings-section">
<h3 class="settings-section-title">Classificazione NIS2</h3> <h3 class="settings-section-title">Classificazione NIS2</h3>
<p class="settings-section-desc">La classificazione viene calcolata automaticamente in base a settore, numero di dipendenti e fatturato.</p> <p class="settings-section-desc">La classificazione viene calcolata automaticamente in base a settore, numero di dipendenti e fatturato.</p>
@@ -261,7 +303,7 @@
<p class="settings-section-desc">Informazioni generali dell'organizzazione soggetta a compliance NIS2.</p> <p class="settings-section-desc">Informazioni generali dell'organizzazione soggetta a compliance NIS2.</p>
<form id="org-form" onsubmit="saveOrganization(event)"> <form id="org-form" onsubmit="saveOrganization(event)">
<div class="form-row"> <div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:18px;">
<div class="form-group"> <div class="form-group">
<label for="org-name" class="form-label">Nome Organizzazione <span class="required">*</span></label> <label for="org-name" class="form-label">Nome Organizzazione <span class="required">*</span></label>
<input type="text" class="form-input" id="org-name" required> <input type="text" class="form-input" id="org-name" required>
@@ -291,7 +333,7 @@
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:18px;">
<div class="form-group"> <div class="form-group">
<label for="org-vat" class="form-label">Partita IVA</label> <label for="org-vat" class="form-label">Partita IVA</label>
<input type="text" class="form-input" id="org-vat" placeholder="IT12345678901"> <input type="text" class="form-input" id="org-vat" placeholder="IT12345678901">
@@ -302,7 +344,7 @@
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:18px;">
<div class="form-group"> <div class="form-group">
<label for="org-employees" class="form-label">Numero Dipendenti</label> <label for="org-employees" class="form-label">Numero Dipendenti</label>
<input type="number" class="form-input" id="org-employees" min="0" placeholder="0"> <input type="number" class="form-input" id="org-employees" min="0" placeholder="0">
@@ -313,7 +355,7 @@
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:18px;">
<div class="form-group"> <div class="form-group">
<label for="org-country" class="form-label">Paese</label> <label for="org-country" class="form-label">Paese</label>
<input type="text" class="form-input" id="org-country" placeholder="IT"> <input type="text" class="form-input" id="org-country" placeholder="IT">
@@ -329,7 +371,7 @@
<input type="text" class="form-input" id="org-address"> <input type="text" class="form-input" id="org-address">
</div> </div>
<div class="form-row"> <div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:18px;">
<div class="form-group"> <div class="form-group">
<label for="org-website" class="form-label">Sito Web</label> <label for="org-website" class="form-label">Sito Web</label>
<input type="url" class="form-input" id="org-website" placeholder="https://..."> <input type="url" class="form-input" id="org-website" placeholder="https://...">
@@ -345,19 +387,18 @@
<input type="tel" class="form-input" id="org-phone" style="max-width:320px;"> <input type="tel" class="form-input" id="org-phone" style="max-width:320px;">
</div> </div>
<button type="submit" class="btn btn-primary" id="btn-save-org"> <button type="submit" class="btn-primary" id="btn-save-org">
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/></svg> <svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/></svg>
Salva Modifiche Salva Modifiche
</button> </button>
</form> </form>
</div> </div>
</div> </section>
</div>
</div> </div>
<!-- ══════════════ TAB: Profilo ══════════════ --> <!-- ══════════════ TAB: Profilo ══════════════ -->
<div class="tab-panel" id="tab-profile"> <div class="tab-panel" id="tab-profile">
<div class="card mb-24"> <section class="card mb-24">
<div class="card-header"> <div class="card-header">
<h3>Informazioni Personali</h3> <h3>Informazioni Personali</h3>
</div> </div>
@@ -372,7 +413,7 @@
<span class="form-help">L'email non puo' essere modificata.</span> <span class="form-help">L'email non puo' essere modificata.</span>
</div> </div>
<div class="form-row"> <div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:18px;">
<div class="form-group"> <div class="form-group">
<label for="profile-name" class="form-label">Nome Completo <span class="required">*</span></label> <label for="profile-name" class="form-label">Nome Completo <span class="required">*</span></label>
<input type="text" class="form-input" id="profile-name" required> <input type="text" class="form-input" id="profile-name" required>
@@ -391,16 +432,16 @@
</select> </select>
</div> </div>
<button type="submit" class="btn btn-primary" id="btn-save-profile"> <button type="submit" class="btn-primary" id="btn-save-profile">
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/></svg> <svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"/></svg>
Aggiorna Profilo Aggiorna Profilo
</button> </button>
</form> </form>
</div> </div>
</div> </div>
</div> </section>
<div class="card"> <section class="card">
<div class="card-header"> <div class="card-header">
<h3>Cambia Password</h3> <h3>Cambia Password</h3>
</div> </div>
@@ -430,23 +471,24 @@
<input type="password" class="form-input" id="pw-confirm" required autocomplete="new-password"> <input type="password" class="form-input" id="pw-confirm" required autocomplete="new-password">
</div> </div>
<button type="submit" class="btn btn-warning" id="btn-change-pw"> <button type="submit" class="btn-warning" id="btn-change-pw">
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg> <svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>
Cambia Password Cambia Password
</button> </button>
</form> </form>
</div> </div>
</div> </section>
</div> </div>
<!-- ══════════════ TAB: Membri ══════════════ -->
<!-- ══════════════ TAB: Preferenze (Fase 4 / G12) ══════════════ --> <!-- ══════════════ TAB: Preferenze (Fase 4 / G12) ══════════════ -->
<div class="tab-panel" id="tab-preferences"> <div class="tab-panel" id="tab-preferences">
<div class="card"> <section class="card">
<div class="card-header"> <div class="card-header">
<div>
<h3>Preferenze utente</h3> <h3>Preferenze utente</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Personalizzazione interfaccia e notifiche.</p> <p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Personalizzazione interfaccia e notifiche.</p>
</div> </div>
</div>
<div class="card-body"> <div class="card-body">
<form id="preferences-form" style="max-width:560px;"> <form id="preferences-form" style="max-width:560px;">
<div class="form-group"> <div class="form-group">
@@ -497,19 +539,20 @@
</div> </div>
<div class="form-group" style="margin-top:24px;"> <div class="form-group" style="margin-top:24px;">
<button type="submit" class="btn btn-primary" id="pref-save-btn">Salva preferenze</button> <button type="submit" class="btn-primary" id="pref-save-btn">Salva preferenze</button>
<span id="pref-saved-msg" style="margin-left:12px; color:var(--success); display:none; font-size:.85rem;">✓ Salvato</span> <span id="pref-saved-msg" style="margin-left:12px; color:var(--success); display:none; font-size:.85rem;">✓ Salvato</span>
</div> </div>
</form> </form>
</div> </div>
</div> </section>
</div> </div>
<!-- ══════════════ TAB: Membri ══════════════ -->
<div class="tab-panel" id="tab-members"> <div class="tab-panel" id="tab-members">
<div class="card"> <section class="card">
<div class="card-header"> <div class="card-header">
<h3>Membri dell'Organizzazione</h3> <h3>Membri dell'Organizzazione</h3>
<button class="btn btn-primary btn-sm" onclick="showInviteModal()"> <button class="btn-primary btn-sm" onclick="showInviteModal()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><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> <svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><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>
Invita Membro Invita Membro
</button> </button>
@@ -519,27 +562,27 @@
<div class="spinner" style="margin:40px auto;"></div> <div class="spinner" style="margin:40px auto;"></div>
</div> </div>
</div> </div>
</div> </section>
</div> </div>
<!-- ══════════════ TAB: Permessi documentali ══════════════ --> <!-- ══════════════ TAB: Permessi documentali ══════════════ -->
<div class="tab-panel" id="tab-docperms"> <div class="tab-panel" id="tab-docperms">
<div class="card"> <section class="card">
<div class="card-header"><h3>Permessi documentali (ciclo di vita SGSI)</h3></div> <div class="card-header"><h3>Permessi documentali (ciclo di vita SGSI)</h3></div>
<div class="card-body"> <div class="card-body">
<p class="text-muted" style="margin-top:0;">Definisci quali <strong>ruoli</strong> possono compiere ogni azione del ciclo di vita dei documenti del Modello SGSI (ISO/IEC 27001 cl. 7.5). La configurazione vale per questa organizzazione; il super amministratore ha sempre tutti i permessi.</p> <p class="text-muted" style="margin-top:0;">Definisci quali <strong>ruoli</strong> possono compiere ogni azione del ciclo di vita dei documenti del Modello SGSI (ISO/IEC 27001 cl. 7.5). La configurazione vale per questa organizzazione; il super amministratore ha sempre tutti i permessi.</p>
<div id="docperms-matrix"><div class="spinner" style="margin:30px auto;"></div></div> <div id="docperms-matrix"><div class="spinner" style="margin:30px auto;"></div></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:14px;"> <div style="display:flex;justify-content:space-between;align-items:center;margin-top:14px;">
<span id="docperms-hint" class="text-muted" style="font-size:.85rem;"></span> <span id="docperms-hint" class="text-muted" style="font-size:.85rem;"></span>
<button class="btn btn-primary" id="docperms-save" onclick="saveDocPerms()">Salva permessi</button> <button class="btn-primary" id="docperms-save" onclick="saveDocPerms()">Salva permessi</button>
</div>
</div> </div>
</div> </div>
</section>
</div> </div>
<!-- ══════════════ TAB: Sicurezza ══════════════ --> <!-- ══════════════ TAB: Sicurezza ══════════════ -->
<div class="tab-panel" id="tab-security"> <div class="tab-panel" id="tab-security">
<div class="card mb-24"> <section class="card mb-24">
<div class="card-header"> <div class="card-header">
<h3>Sicurezza Account</h3> <h3>Sicurezza Account</h3>
</div> </div>
@@ -560,24 +603,24 @@
<p class="security-item-desc">L'accesso alle API avviene tramite token JWT (JSON Web Token) con scadenza automatica e meccanismo di refresh.</p> <p class="security-item-desc">L'accesso alle API avviene tramite token JWT (JSON Web Token) con scadenza automatica e meccanismo di refresh.</p>
</div> </div>
</div> </div>
</div> </section>
<div class="card mb-24" id="card-sessions"> <section class="card mb-24" id="card-sessions">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;"> <div class="card-header">
<div> <div>
<h3>Sessioni Attive</h3> <h3>Sessioni Attive</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Dispositivi attualmente loggati al tuo account. Puoi disconnetterli singolarmente o tutti tranne questo.</p> <p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Dispositivi attualmente loggati al tuo account. Puoi disconnetterli singolarmente o tutti tranne questo.</p>
</div> </div>
<button class="btn btn-outline btn-danger" onclick="revokeAllOtherSessions()" id="revoke-all-btn" style="display:none;">Disconnetti gli altri</button> <button class="btn-outline btn-danger" onclick="revokeAllOtherSessions()" id="revoke-all-btn" style="display:none;">Disconnetti gli altri</button>
</div> </div>
<div class="card-body" style="padding:0;"> <div class="card-body" style="padding:0;">
<div id="sessions-container"> <div id="sessions-container">
<div class="spinner" style="margin:40px auto;"></div> <div class="spinner" style="margin:40px auto;"></div>
</div> </div>
</div> </div>
</div> </section>
<div class="card"> <section class="card">
<div class="card-header"> <div class="card-header">
<h3>Registro Attivita' Recenti</h3> <h3>Registro Attivita' Recenti</h3>
</div> </div>
@@ -586,20 +629,20 @@
<div class="spinner" style="margin:40px auto;"></div> <div class="spinner" style="margin:40px auto;"></div>
</div> </div>
</div> </div>
</div> </section>
</div> </div>
<!-- ══════════════ TAB: API Keys ══════════════ --> <!-- ══════════════ TAB: API Keys ══════════════ -->
<div class="tab-panel" id="tab-apikeys"> <div class="tab-panel" id="tab-apikeys">
<div class="card mb-24"> <section class="card mb-24">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;"> <div class="card-header">
<div> <div>
<h3>API Keys</h3> <h3>API Keys</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;"> <p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">
Chiavi per accesso esterno alle API NIS2 Agile (SIEM, GRC, dashboard esterne). Chiavi per accesso esterno alle API NIS2 Agile (SIEM, GRC, dashboard esterne).
</p> </p>
</div> </div>
<button class="btn btn-primary" onclick="showCreateApiKeyModal()"> <button class="btn-primary" onclick="showCreateApiKeyModal()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg> <svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
Nuova API Key Nuova API Key
</button> </button>
@@ -607,26 +650,26 @@
<div class="card-body" style="padding:0;"> <div class="card-body" style="padding:0;">
<div id="apikeys-container"><div class="spinner" style="margin:40px auto;"></div></div> <div id="apikeys-container"><div class="spinner" style="margin:40px auto;"></div></div>
</div> </div>
</div> </section>
<div class="card"> <section class="card">
<div class="card-header"><h3>Scope Disponibili</h3></div> <div class="card-header"><h3>Scope Disponibili</h3></div>
<div class="card-body"> <div class="card-body">
<div id="available-scopes-container"></div> <div id="available-scopes-container"></div>
</div> </div>
</div> </section>
</div> </div>
<!-- ══════════════ TAB: Webhook ══════════════ --> <!-- ══════════════ TAB: Webhook ══════════════ -->
<div class="tab-panel" id="tab-webhooks"> <div class="tab-panel" id="tab-webhooks">
<div class="card mb-24"> <section class="card mb-24">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;"> <div class="card-header">
<div> <div>
<h3>Webhook Subscriptions</h3> <h3>Webhook Subscriptions</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;"> <p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">
Notifiche push verso sistemi esterni (SIEM, 231 Agile, SustainAI) su eventi NIS2. Notifiche push verso sistemi esterni (SIEM, 231 Agile, SustainAI) su eventi NIS2.
</p> </p>
</div> </div>
<button class="btn btn-primary" onclick="showCreateWebhookModal()"> <button class="btn-primary" onclick="showCreateWebhookModal()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg> <svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/></svg>
Nuovo Webhook Nuovo Webhook
</button> </button>
@@ -634,25 +677,27 @@
<div class="card-body" style="padding:0;"> <div class="card-body" style="padding:0;">
<div id="webhooks-container"><div class="spinner" style="margin:40px auto;"></div></div> <div id="webhooks-container"><div class="spinner" style="margin:40px auto;"></div></div>
</div> </div>
</div> </section>
<div class="card"> <section class="card">
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;"> <div class="card-header">
<h3>Delivery Log</h3> <h3>Delivery Log</h3>
<button class="btn btn-sm btn-secondary" onclick="loadDeliveries()">Aggiorna</button> <button class="btn-sm btn-secondary" onclick="loadDeliveries()">Aggiorna</button>
</div> </div>
<div class="card-body" style="padding:0;"> <div class="card-body" style="padding:0;">
<div id="deliveries-container"><div class="empty-state" style="padding:32px;"><p>Seleziona un webhook per vedere i delivery.</p></div></div> <div id="deliveries-container"><div class="empty-state" style="padding:32px;"><p>Seleziona un webhook per vedere i delivery.</p></div></div>
</div> </div>
</div> </section>
</div> </div>
<!-- ══════════════ TAB: Branding (Fase 5 / G16) ══════════════ --> <!-- ══════════════ TAB: Branding (Fase 5 / G16) ══════════════ -->
<div class="tab-panel" id="tab-branding"> <div class="tab-panel" id="tab-branding">
<div class="card"> <section class="card">
<div class="card-header"> <div class="card-header">
<div>
<h3>Branding white-label</h3> <h3>Branding white-label</h3>
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Personalizza colori, logo e nome prodotto per il tuo studio. Visibile a tutti i tuoi clienti loggati.</p> <p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Personalizza colori, logo e nome prodotto per il tuo studio. Visibile a tutti i tuoi clienti loggati.</p>
</div> </div>
</div>
<div class="card-body"> <div class="card-body">
<form id="branding-form" style="max-width:560px;"> <form id="branding-form" style="max-width:560px;">
<div class="form-group"> <div class="form-group">
@@ -676,40 +721,35 @@
</div> </div>
<div class="form-group" style="margin-top:24px;"> <div class="form-group" style="margin-top:24px;">
<button type="submit" class="btn btn-primary" id="brand-save-btn">Salva branding</button> <button type="submit" class="btn-primary" id="brand-save-btn">Salva branding</button>
<span id="brand-saved-msg" style="margin-left:12px; color:var(--success); display:none; font-size:.85rem;">✓ Salvato — ricarica per vedere</span> <span id="brand-saved-msg" style="margin-left:12px; color:var(--success); display:none; font-size:.85rem;">✓ Salvato — ricarica per vedere</span>
</div> </div>
</form> </form>
</div> </div>
</div> </section>
</div> </div>
</div> </div>
</main> </main>
</div>
<!-- Scripts V3: api.js (client REST, prima) → common.js (helper+FAB reali) →
Lucide self-host → topnav-v3.js (chrome, ridefinisce loadSidebar) →
i18n → help → init pagina (inline, invariato). -->
<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> <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();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
let currentOrg = null; let currentOrg = null;
let currentUser = null; let currentUser = null;
@@ -898,7 +938,7 @@
: ''; : '';
const actionBtn = isCurrent const actionBtn = isCurrent
? '' ? ''
: '<button class="btn btn-sm btn-outline btn-danger" onclick="revokeSession(\'' + s.id + '\')">Disconnetti</button>'; : '<button class="btn-sm btn-outline btn-danger" onclick="revokeSession(\'' + s.id + '\')">Disconnetti</button>';
return '<div style="display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--gray-100);">' return '<div style="display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--gray-100);">'
+ '<div>' + '<div>'
+ '<div style="font-weight:600;">' + escapeHtml(s.device_label) + badge + '</div>' + '<div style="font-weight:600;">' + escapeHtml(s.device_label) + badge + '</div>'
@@ -1313,7 +1353,7 @@
<td><span class="member-role-badge ${role}">${escapeHtml(roleLabel)}</span></td> <td><span class="member-role-badge ${role}">${escapeHtml(roleLabel)}</span></td>
<td>${formatDate(m.joined_at)}</td> <td>${formatDate(m.joined_at)}</td>
<td> <td>
<button class="btn-icon-action" onclick="removeMember(${m.id}, '${escapeHtml(m.full_name || m.email)}')" title="Rimuovi membro"> <button class="btn-icon-action btn-danger-hover" onclick="removeMember(${m.id}, '${escapeHtml(m.full_name || m.email)}')" title="Rimuovi membro">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg> <svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
</button> </button>
</td> </td>
@@ -1343,8 +1383,8 @@
</div> </div>
`, { `, {
footer: ` footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button> <button class="btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="doInviteMember()">Invita</button> <button class="btn-primary" onclick="doInviteMember()">Invita</button>
` `
}); });
} }
@@ -1387,8 +1427,8 @@
</p> </p>
`, { `, {
footer: ` footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button> <button class="btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-danger" onclick="doRemoveMember(${userId})">Rimuovi</button> <button class="btn-danger" onclick="doRemoveMember(${userId})">Rimuovi</button>
` `
}); });
} }
@@ -1480,7 +1520,7 @@
<label for="apikey-expires" class="form-label">Scadenza (opzionale)</label> <label for="apikey-expires" class="form-label">Scadenza (opzionale)</label>
<input type="date" id="apikey-expires" class="form-control"> <input type="date" id="apikey-expires" class="form-control">
</div> </div>
`, '<button class="btn btn-primary" onclick="createApiKey()">Crea API Key</button>'); `, '<button class="btn-primary" onclick="createApiKey()">Crea API Key</button>');
} }
async function createApiKey() { async function createApiKey() {
@@ -1501,7 +1541,7 @@
${escapeHtml(result.data.key)} ${escapeHtml(result.data.key)}
</div> </div>
<p style="font-size:0.8rem; color:var(--gray-500); margin-top:8px;">Usa come header: <code>X-API-Key: ${escapeHtml(result.data.key)}</code></p> <p style="font-size:0.8rem; color:var(--gray-500); margin-top:8px;">Usa come header: <code>X-API-Key: ${escapeHtml(result.data.key)}</code></p>
`, '<button class="btn btn-primary" onclick="closeModal(); loadApiKeys();">Ho salvato la chiave</button>'); `, '<button class="btn-primary" onclick="closeModal(); loadApiKeys();">Ho salvato la chiave</button>');
} else { showNotification(result.message || 'Errore.', 'error'); } } else { showNotification(result.message || 'Errore.', 'error'); }
} catch (e) { showNotification('Errore di connessione.', 'error'); } } catch (e) { showNotification('Errore di connessione.', 'error'); }
} }
@@ -1590,7 +1630,7 @@
<label class="form-label">Eventi da ascoltare *</label> <label class="form-label">Eventi da ascoltare *</label>
<div style="max-height:200px; overflow-y:auto; padding:8px; border:1px solid var(--gray-200); border-radius:var(--border-radius); margin-top:8px;">${evtCheckboxes}</div> <div style="max-height:200px; overflow-y:auto; padding:8px; border:1px solid var(--gray-200); border-radius:var(--border-radius); margin-top:8px;">${evtCheckboxes}</div>
</div> </div>
`, '<button class="btn btn-primary" onclick="createWebhook()">Crea Webhook</button>'); `, '<button class="btn-primary" onclick="createWebhook()">Crea Webhook</button>');
} }
async function createWebhook() { async function createWebhook() {
@@ -1612,7 +1652,7 @@
${escapeHtml(result.data.secret)} ${escapeHtml(result.data.secret)}
</div> </div>
<p style="font-size:0.75rem; color:var(--gray-500); margin-top:8px;">Firma: <code>sha256=HMAC_SHA256(body, secret)</code></p> <p style="font-size:0.75rem; color:var(--gray-500); margin-top:8px;">Firma: <code>sha256=HMAC_SHA256(body, secret)</code></p>
`, '<button class="btn btn-primary" onclick="closeModal(); loadWebhooks();">Ho salvato il secret</button>'); `, '<button class="btn-primary" onclick="closeModal(); loadWebhooks();">Ho salvato il secret</button>');
} else showNotification(result.message || 'Errore.', 'error'); } else showNotification(result.message || 'Errore.', 'error');
} catch (e) { showNotification('Errore di connessione.', 'error'); } } catch (e) { showNotification('Errore di connessione.', 'error'); }
} }
+37 -15
View File
@@ -3,11 +3,22 @@
<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>Attività stakeholder - NIS2 Agile</title> <title>Attività stakeholder · NIS2 Agile</title>
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg CDN / NO Bootstrap Italia). -->
<link rel="stylesheet" href="/css/style.css?v=20260618"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
.sa-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; } /* ── Mappa variabili legacy (style.css rimosso) → token V3 + widget base ── */
:root {
--primary:#1F5FA8; --gray-50:#F7FAFD; --gray-100:#EEF2F7; --gray-200:#E3E9F0;
--gray-400:#94A3B8; --gray-500:#64748B;
}
.btn { display:inline-flex; align-items:center; justify-content:center; gap:6px; border:1px solid var(--line); background:#fff; color:var(--ink); border-radius:8px; padding:8px 13px; font:inherit; font-weight:600; font-size:12.5px; cursor:pointer; text-decoration:none; }
.btn:hover { background:#F5F8FC; border-color:#c7d4e4; }
.btn-outline { background:#fff; color:var(--ink); border-color:var(--line); }
.btn-outline:hover { background:#F5F8FC; border-color:#c7d4e4; }
.btn-sm { padding:5px 10px; font-size:11.5px; }
.sa-intro { background:#EAF1FA; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
.sa-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; } .sa-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
.sa-tabs { display:flex; gap:6px; border-bottom:2px solid var(--gray-200,#e5e7eb); margin-bottom:16px; } .sa-tabs { display:flex; gap:6px; border-bottom:2px solid var(--gray-200,#e5e7eb); margin-bottom:16px; }
.sa-tab { padding:9px 16px; font-weight:600; font-size:.9rem; cursor:pointer; border:none; background:none; color:var(--gray-500,#6b7280); border-bottom:2px solid transparent; margin-bottom:-2px; } .sa-tab { padding:9px 16px; font-weight:600; font-size:.9rem; cursor:pointer; border:none; background:none; color:var(--gray-500,#6b7280); border-bottom:2px solid transparent; margin-bottom:-2px; }
@@ -72,12 +83,21 @@
<!-- PWA:end --> <!-- PWA:end -->
</head> </head>
<body> <body>
<div class="app-layout"> <!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header"> <main class="wrap">
<h2 data-i18n="sact.title">Attività stakeholder</h2> <nav class="crumb" aria-label="breadcrumb">
</header> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><a href="stakeholders.html">Stakeholder</a>
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="sact.title">Attività</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="sact.title">Attività stakeholder</h1>
<p class="sub">Questionari tipo, attività e magic-link verso terze parti (GV.SC-02/05/07 · art. 24 D.Lgs. 138/2024)</p>
</div>
</div>
<div class="content-body"> <div class="content-body">
<div class="sa-intro"> <div class="sa-intro">
<strong>Attività verso gli stakeholder.</strong> <strong>Attività verso gli stakeholder.</strong>
@@ -107,7 +127,6 @@
</section> </section>
</div> </div>
</main> </main>
</div>
<!-- Modale questionario tipo --> <!-- Modale questionario tipo -->
<div class="sm-overlay" id="tpl-modal" role="dialog" aria-modal="true" aria-labelledby="tpl-modal-title"> <div class="sm-overlay" id="tpl-modal" role="dialog" aria-modal="true" aria-labelledby="tpl-modal-title">
@@ -194,10 +213,12 @@
</div> </div>
</div> </div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<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>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></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> <script>
@@ -217,8 +238,9 @@
document.addEventListener('DOMContentLoaded', async function () { document.addEventListener('DOMContentLoaded', async function () {
if (typeof checkAuth === 'function' && !checkAuth()) return; if (typeof checkAuth === 'function' && !checkAuth()) return;
if (window.I18n && I18n.init) I18n.init('it'); if (window.I18n && I18n.init) I18n.init('it');
if (typeof loadSidebar === 'function') loadSidebar(); if (typeof loadSidebar === 'function') loadSidebar(); // = loadTopnavV3()
if (window.HelpSystem && HelpSystem.init) HelpSystem.init(); if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
document.addEventListener('keydown', e => { if (e.key === 'Escape') { tplClose(); actClose(); detClose(); } }); document.addEventListener('keydown', e => { if (e.key === 'Escape') { tplClose(); actClose(); detClose(); } });
['tpl-modal','act-modal','det-modal'].forEach(m => { const o = el(m); if (o) o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); }); }); ['tpl-modal','act-modal','det-modal'].forEach(m => { const o = el(m); if (o) o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); }); });
await saLoadAll(); await saLoadAll();
+41 -21
View File
@@ -3,12 +3,22 @@
<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>Stakeholder - NIS2 Agile</title> <title>Stakeholder · 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 / NO Bootstrap Italia). -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/style.css?v=20260618"> <link rel="stylesheet" href="/css/v3.css?v=20260627">
<style> <style>
.stk-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; } /* ── Mappa variabili legacy (style.css rimosso) → token V3 + widget base ── */
:root {
--primary:#1F5FA8; --gray-50:#F7FAFD; --gray-100:#EEF2F7; --gray-200:#E3E9F0;
--gray-400:#94A3B8; --gray-500:#64748B;
}
.btn { display:inline-flex; align-items:center; justify-content:center; gap:6px; border:1px solid var(--line); background:#fff; color:var(--ink); border-radius:8px; padding:8px 13px; font:inherit; font-weight:600; font-size:12.5px; cursor:pointer; text-decoration:none; }
.btn:hover { background:#F5F8FC; border-color:#c7d4e4; }
.btn-outline { background:#fff; color:var(--ink); border-color:var(--line); }
.btn-outline:hover { background:#F5F8FC; border-color:#c7d4e4; }
.btn-sm { padding:5px 10px; font-size:11.5px; }
.stk-intro { background:#EAF1FA; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
.stk-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; } .stk-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
.stk-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; } .stk-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
.stk-toolbar .grow { flex:1 1 auto; } .stk-toolbar .grow { flex:1 1 auto; }
@@ -84,18 +94,26 @@
<!-- PWA:end --> <!-- PWA:end -->
</head> </head>
<body> <body>
<div class="app-layout"> <!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="stk.title">Stakeholder</h2>
<div class="content-header-actions">
<button class="btn btn-outline btn-sm" onclick="stkTypeOpen()">+ Aggiungi tipo</button>
<button class="btn btn-primary btn-sm" onclick="stkOpen()">+ Aggiungi stakeholder</button>
</div>
</header>
<div class="content-body"> <main class="wrap">
<nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="stk.title">Stakeholder</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="stk.title">Stakeholder</h1>
<p class="sub">Registro stakeholder e matrice di Mendelow (GV.SC-02 · art. 24 D.Lgs. 138/2024)</p>
</div>
<div class="content-header-actions" style="display:flex;gap:8px;flex-wrap:wrap;">
<button class="btn-ghost" onclick="stkTypeOpen()"><i data-lucide="plus" class="ic"></i>Aggiungi tipo</button>
<button class="btn-primary" onclick="stkOpen()"><i data-lucide="plus" class="ic"></i>Aggiungi stakeholder</button>
</div>
</div>
<div class="stack">
<div class="stk-intro"> <div class="stk-intro">
<strong>Mappatura e gestione degli stakeholder.</strong> <strong>Mappatura e gestione degli stakeholder.</strong>
Registra gli stakeholder interni ed esterni scegliendo il tipo dall'elenco (puoi aggiungerne di nuovi), Registra gli stakeholder interni ed esterni scegliendo il tipo dall'elenco (puoi aggiungerne di nuovi),
@@ -130,7 +148,6 @@
<div id="stk-groups" aria-live="polite"></div> <div id="stk-groups" aria-live="polite"></div>
</div> </div>
</main> </main>
</div>
<!-- Modale stakeholder (crea/modifica) --> <!-- Modale stakeholder (crea/modifica) -->
<div class="sm-overlay" id="stk-modal" role="dialog" aria-modal="true" aria-labelledby="stk-modal-title"> <div class="sm-overlay" id="stk-modal" role="dialog" aria-modal="true" aria-labelledby="stk-modal-title">
@@ -191,10 +208,12 @@
</div> </div>
</div> </div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<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>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></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> <script>
@@ -223,8 +242,9 @@
document.addEventListener('DOMContentLoaded', async function () { document.addEventListener('DOMContentLoaded', async function () {
if (typeof checkAuth === 'function' && !checkAuth()) return; if (typeof checkAuth === 'function' && !checkAuth()) return;
if (window.I18n && I18n.init) I18n.init('it'); if (window.I18n && I18n.init) I18n.init('it');
if (typeof loadSidebar === 'function') loadSidebar(); if (typeof loadSidebar === 'function') loadSidebar(); // = loadTopnavV3()
if (window.HelpSystem && HelpSystem.init) HelpSystem.init(); if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { stkClose(); stkTypeClose(); } }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { stkClose(); stkTypeClose(); } });
['stk-modal','stk-type-modal'].forEach(function (mid) { ['stk-modal','stk-type-modal'].forEach(function (mid) {
+95 -37
View File
@@ -3,13 +3,70 @@
<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>Supply Chain - NIS2 Agile</title> <title>Supply Chain · 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 / NO Bootstrap Italia). -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (tabelle/modali/form) -> 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">
<style> <style>
/* ── Mappa variabili legacy (style.css rimosso) → token V3. Mantiene
intatte le regole CSS della pagina che usavano --gray-*, --danger, ecc. ─ */
:root {
--card-bg:#fff; --border-radius:12px; --transition-fast:.15s;
--primary:#1F5FA8; --secondary:#1E8A5A; --danger:#C62828; --warning:#D98324;
--gray-50:#F7FAFD; --gray-100:#EEF2F7; --gray-200:#E3E9F0; --gray-300:#CBD5E1;
--gray-400:#94A3B8; --gray-500:#64748B; --gray-600:#475569; --gray-700:#334155;
--gray-800:#1E293B; --gray-900:#15263C;
--info-bg:#E7F0FB; --warning-bg:#FBF1E2; --danger-bg:#FBE9E9;
}
/* ── Widget styles V3 (componenti emessi via JS: tabelle/modali/form/badge).
Solo regole essenziali NON coperte da v3.css, agganciate ai token V3.
.btn-primary/.btn-ghost restano definiti da v3.css. ───────────────── */
.btn { display:inline-flex; align-items:center; justify-content:center; gap:6px; border:1px solid var(--line); background:#fff; color:var(--ink); border-radius:8px; padding:8px 13px; font:inherit; font-weight:600; font-size:12.5px; cursor:pointer; text-decoration:none; }
.btn:hover { background:#F5F8FC; border-color:#c7d4e4; }
.btn-secondary { background:#fff; color:var(--ink); border-color:var(--line); }
.btn-secondary:hover { background:#F5F8FC; border-color:#c7d4e4; }
.btn-danger { background:var(--c-red); color:#fff; border-color:var(--c-red); }
.btn-danger:hover { background:#A82020; }
.btn-sm { padding:5px 10px; font-size:11.5px; }
.btn .icon { width:14px; height:14px; }
.badge { display:inline-block; font-size:10px; font-weight:700; letter-spacing:.03em; text-transform:uppercase; padding:2px 8px; border-radius:6px; background:#EEF2F7; color:#5b6b80; white-space:nowrap; }
.badge-success { background:#E6F4EC; color:#1E7A4E; }
.badge-info { background:#E7F0FB; color:#1F5FA8; }
.badge-warning { background:#FBF1E2; color:#9A6418; }
.badge-danger { background:#FBE9E9; color:#B23636; }
.badge-neutral { background:#EEF2F7; color:#5b6b80; }
.form-group { margin-bottom:14px; }
.form-label { display:block; font-weight:600; font-size:.86rem; margin-bottom:5px; color:var(--ink); }
.required { color:var(--c-red); }
.form-input, .form-select, .form-textarea { width:100%; padding:9px 11px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:.9rem; background:#fff; color:var(--ink); }
.form-input:focus, .form-select:focus, .form-textarea:focus { outline:2px solid var(--gold); outline-offset:1px; border-color:#9FBEDD; }
.form-textarea { min-height:60px; resize:vertical; }
.empty-state { text-align:center; padding:40px 20px; color:var(--muted); }
.empty-state h4 { margin:8px 0 4px; color:var(--ink); font-size:1rem; }
.empty-state p { margin:0; font-size:.86rem; }
.table-container { width:100%; overflow-x:auto; }
.table-container table { width:100%; border-collapse:collapse; font-size:.88rem; }
.table-container th, .table-container td { text-align:left; padding:11px 14px; border-top:1px solid var(--line); vertical-align:middle; }
.table-container thead th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); border-top:0; }
.table-container tbody tr:hover { background:#FAFCFE; }
.card-header { padding:16px 20px 0; }
.card-header h3 { margin:0 0 14px; font-size:15px; font-weight:700; }
.card-body { padding:20px; }
.mb-24 { margin-bottom:24px; }
/* Modali (markup di showModal in common.js) */
.modal-overlay { position:fixed; inset:0; background:rgba(15,38,60,.5); display:flex; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; z-index:1050; opacity:0; transition:opacity .2s; }
.modal-overlay.active { opacity:1; }
.modal { background:#fff; border-radius:var(--radius); width:100%; max-width:560px; box-shadow:0 20px 50px rgba(11,40,80,.25); }
.modal-header { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--line); }
.modal-header h3 { margin:0; font-size:1.05rem; font-weight:700; }
.modal-close { background:none; border:none; font-size:1.5rem; line-height:1; cursor:pointer; color:var(--muted); }
.modal-body { padding:20px; }
.modal-footer { padding:14px 20px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px; }
/* Spinner inverso (su pulsante import) */
.progress-spinner { display:inline-block; width:14px; height:14px; border:2px solid rgba(255,255,255,.4); border-top-color:#fff; border-radius:50%; animation:spin .8s linear infinite; vertical-align:middle; }
.visually-hidden { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); }
.content-header-actions { display:flex; gap:8px; flex-wrap:wrap; }
/* ── Supply Chain Page Styles ─────────────────────────── */ /* ── Supply Chain Page Styles ─────────────────────────── */
.filters-bar { .filters-bar {
display: flex; display: flex;
@@ -380,21 +437,28 @@
<!-- PWA:end --> <!-- PWA:end -->
</head> </head>
<body> <body>
<div class="app-layout"> <!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2" data-i18n="supply_chain.title">Sicurezza Supply Chain</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span data-i18n="supply_chain.title">Sicurezza Supply Chain</span>
<button class="btn btn-secondary" onclick="openCategoriesModal()"><svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-bookmark"></use></svg> <span data-i18n="sp.categories_btn">Categorie</span></button> </nav>
<button class="btn btn-secondary" onclick="openTemplatesModal()"><svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-files"></use></svg> <span data-i18n="sp.templates_btn">Template</span></button> <div class="page-head">
<button class="btn btn-secondary" onclick="openImportModal()"><svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-upload"></use></svg> <span data-i18n="sp.import_btn">Importa</span></button> <div>
<button class="btn btn-primary" onclick="openCreateModal()"><span data-i18n="sp.new_supplier">+ Nuovo Fornitore</span></button> <h1 data-i18n="supply_chain.title">Sicurezza Supply Chain</h1>
<p class="sub">Valutazione e monitoraggio del rischio della catena di fornitura (art. 24 D.Lgs. 138/2024)</p>
</div>
<div class="content-header-actions">
<button class="btn-ghost" onclick="openCategoriesModal()"><i data-lucide="bookmark" class="ic"></i><span data-i18n="sp.categories_btn">Categorie</span></button>
<button class="btn-ghost" onclick="openTemplatesModal()"><i data-lucide="files" class="ic"></i><span data-i18n="sp.templates_btn">Template</span></button>
<button class="btn-ghost" onclick="openImportModal()"><i data-lucide="upload" class="ic"></i><span data-i18n="sp.import_btn">Importa</span></button>
<button class="btn-primary" onclick="openCreateModal()"><i data-lucide="plus" class="ic"></i><span data-i18n="sp.new_supplier">Nuovo Fornitore</span></button>
</div>
</div> </div>
</header>
<div class="content-body" id="page-content"> <div class="stack" id="page-content">
<!-- Risk Overview --> <!-- Risk Overview -->
<div class="risk-overview" id="risk-overview"> <div class="risk-overview" id="risk-overview">
<div class="risk-card"> <div class="risk-card">
@@ -475,28 +539,22 @@
<div id="supplier-detail-view" style="display:none;"></div> <div id="supplier-detail-view" style="display:none;"></div>
</div> </div>
</main> </main>
</div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<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="/js/i18n.js?v=20260631"></script>
<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=20260621sp"></script>
<script src="/js/help.js?v=20260625d"></script> <script src="/js/help.js?v=20260625d"></script>
<script> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar(); // = loadTopnavV3()
I18n.init(); if (typeof I18n !== 'undefined') I18n.init();
HelpSystem.init(); if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
// ── Labels ────────────────────────────────────────────── // ── Labels ──────────────────────────────────────────────
const CRITICALITY_LABELS = { const CRITICALITY_LABELS = {
@@ -1224,7 +1282,7 @@
ov.innerHTML = ` ov.innerHTML = `
<div style="background:#fff;border-radius:10px;max-width:640px;width:100%;max-height:90vh;overflow:auto;"> <div style="background:#fff;border-radius:10px;max-width:640px;width:100%;max-height:90vh;overflow:auto;">
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #e2e8f0;"> <div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #e2e8f0;">
<h3 style="margin:0;"><svg class="icon icon-sm" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-upload"></use></svg> ${I18n.t('sp.import_title')}</h3> <h3 style="margin:0;">${I18n.t('sp.import_title')}</h3>
<button onclick="document.getElementById('importModalDyn').remove()" style="background:none;border:none;font-size:22px;cursor:pointer;line-height:1;">&times;</button> <button onclick="document.getElementById('importModalDyn').remove()" style="background:none;border:none;font-size:22px;cursor:pointer;line-height:1;">&times;</button>
</div> </div>
<div style="padding:20px;"> <div style="padding:20px;">
@@ -1241,7 +1299,7 @@
</div> </div>
<div style="display:flex;justify-content:flex-end;gap:10px;padding:16px 20px;border-top:1px solid #e2e8f0;"> <div style="display:flex;justify-content:flex-end;gap:10px;padding:16px 20px;border-top:1px solid #e2e8f0;">
<button class="btn btn-secondary" onclick="document.getElementById('importModalDyn').remove()">${I18n.t('sp.cancel')}</button> <button class="btn btn-secondary" onclick="document.getElementById('importModalDyn').remove()">${I18n.t('sp.cancel')}</button>
<button class="btn btn-primary" id="importRunBtn" onclick="runSupplierImport()"><svg class="icon icon-sm icon-white" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-upload"></use></svg> ${I18n.t('sp.import_run')}</button> <button class="btn btn-primary" id="importRunBtn" onclick="runSupplierImport()">${I18n.t('sp.import_run')}</button>
</div> </div>
</div>`; </div>`;
document.body.appendChild(ov); document.body.appendChild(ov);
@@ -1319,7 +1377,7 @@
} catch (e) { } catch (e) {
resEl.innerHTML = `<span style="color:#dc2626;">Errore: ${(e && e.message) || e}</span>`; resEl.innerHTML = `<span style="color:#dc2626;">Errore: ${(e && e.message) || e}</span>`;
} finally { } finally {
btn.disabled = false; btn.innerHTML = '<svg class="icon icon-sm icon-white" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-upload"></use></svg> Importa'; btn.disabled = false; btn.innerHTML = 'Importa';
} }
} }
+136 -244
View File
@@ -3,222 +3,112 @@
<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>Formazione - NIS2 Agile</title> <title>Formazione · 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 external CDN, no Bootstrap Italia. -->
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
via JS (tabelle/modali/form) -> 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">
<style> <style>
/* V3+ page-specific (componenti non coperti da v3.css): tab interni, card corso,
tabella, badge stato, barra compliance. + componenti prodotti dalla JS inline via
common.js (showModal/showNotification): modal-* / form-* / .btn-secondary, mappati a token V3
perche' style.css e' rimosso. */
/* ── Tab Navigation ─────────────────────────────────────── */ /* ── Tab Navigation ─────────────────────────────────────── */
.tab-nav { .tab-nav { display:flex; gap:0; border-bottom:2px solid var(--line); margin-bottom:24px; }
display: flex; .tab-nav button { padding:12px 22px; background:none; border:none; border-bottom:2px solid transparent; margin-bottom:-2px; color:var(--muted); font:inherit; font-size:13.5px; font-weight:600; cursor:pointer; }
gap: 0; .tab-nav button:hover { color:var(--ink); background:#F5F8FC; }
border-bottom: 2px solid var(--gray-200); .tab-nav button.active { color:var(--c-blue); border-bottom-color:var(--c-blue); }
margin-bottom: 24px; .tab-panel { display:none; }
} .tab-panel.active { display:block; }
.tab-nav button {
padding: 12px 24px;
background: none;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
color: var(--gray-500);
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
}
.tab-nav button:hover {
color: var(--gray-700);
background: var(--gray-50);
}
.tab-nav button.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* ── Course Cards Grid ──────────────────────────────────── */ /* ── Course Cards Grid ──────────────────────────────────── */
.courses-grid { .courses-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(320px, 1fr)); gap:18px; }
display: grid; .course-card { background:#fff; border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow); padding:18px 20px; position:relative; display:flex; flex-direction:column; }
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); .course-card:hover { box-shadow:0 8px 20px rgba(16,40,80,.13); }
gap: 20px; .course-card-header { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; margin-bottom:12px; }
} .course-card-title { font-size:15px; font-weight:700; color:var(--ink); margin:0; }
.course-card { .course-card-desc { font-size:13px; color:var(--muted); margin-bottom:16px; flex-grow:1; line-height:1.5; }
background: var(--card-bg); .course-card-meta { display:flex; flex-wrap:wrap; gap:8px; align-items:center; font-size:12px; }
border-radius: var(--border-radius); .course-meta-item { display:inline-flex; align-items:center; gap:4px; color:var(--muted); background:#EEF2F7; padding:4px 10px; border-radius:20px; }
box-shadow: var(--card-shadow); .course-meta-item svg { width:14px; height:14px; }
padding: 20px; .badge-mandatory { background:#FBE9E9; color:#B23636; font-weight:700; padding:3px 10px; border-radius:20px; font-size:10.5px; text-transform:uppercase; letter-spacing:.04em; }
transition: box-shadow var(--transition);
position: relative;
display: flex;
flex-direction: column;
}
.course-card:hover {
box-shadow: var(--card-shadow-hover);
}
.course-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.course-card-title {
font-size: 1rem;
font-weight: 600;
color: var(--gray-800);
margin: 0;
}
.course-card-desc {
font-size: 0.85rem;
color: var(--gray-500);
margin-bottom: 16px;
flex-grow: 1;
line-height: 1.5;
}
.course-card-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
font-size: 0.8rem;
}
.course-meta-item {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--gray-500);
background: var(--gray-100);
padding: 4px 10px;
border-radius: 20px;
}
.course-meta-item svg { width: 14px; height: 14px; }
.badge-mandatory {
background: var(--danger-bg);
color: var(--danger);
font-weight: 600;
padding: 3px 10px;
border-radius: 20px;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ── Assignments Table ──────────────────────────────────── */ /* ── Tables ─────────────────────────────────────────────── */
.data-table { .data-table { width:100%; border-collapse:collapse; background:#fff; border:1px solid var(--line); border-radius:var(--radius); overflow:hidden; box-shadow:var(--shadow); }
width: 100%; .data-table thead { background:#FAFCFE; }
border-collapse: collapse; .data-table th { text-align:left; padding:12px 16px; font-size:11px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.04em; border-bottom:1px solid var(--line); }
background: var(--card-bg); .data-table td { padding:12px 16px; font-size:13.5px; color:var(--ink); border-bottom:1px solid var(--line); }
border-radius: var(--border-radius); .data-table tr:last-child td { border-bottom:none; }
overflow: hidden; .data-table tr:hover td { background:#FAFCFE; }
box-shadow: var(--card-shadow);
}
.data-table thead {
background: var(--gray-50);
}
.data-table th {
text-align: left;
padding: 12px 16px;
font-size: 0.8rem;
font-weight: 600;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--gray-200);
}
.data-table td {
padding: 12px 16px;
font-size: 0.875rem;
color: var(--gray-700);
border-bottom: 1px solid var(--gray-100);
}
.data-table tr:last-child td { border-bottom: none; }
.data-table tr:hover td { background: var(--gray-50); }
/* ── Status Badges ──────────────────────────────────────── */ /* ── Status Badges ──────────────────────────────────────── */
.status-badge { .status-badge { display:inline-flex; align-items:center; padding:4px 12px; border-radius:20px; font-size:11px; font-weight:700; gap:4px; }
display: inline-flex; .status-badge.assigned { background:#EEF2F7; color:#5b6b80; }
align-items: center; .status-badge.in_progress { background:#E0F2FE; color:#075985; }
padding: 4px 12px; .status-badge.completed { background:#E6F4EC; color:#1E7A4E; }
border-radius: 20px; .status-badge.overdue { background:#FBE9E9; color:#B23636; }
font-size: 0.78rem;
font-weight: 500;
gap: 4px;
}
.status-badge.assigned { background: var(--gray-100); color: var(--gray-600); }
.status-badge.in_progress { background: var(--info-bg); color: var(--info); }
.status-badge.completed { background: var(--secondary-bg); color: var(--secondary); }
.status-badge.overdue { background: var(--danger-bg); color: var(--danger); }
/* ── Compliance Percentage Bar ──────────────────────────── */ /* ── Compliance Bar ─────────────────────────────────────── */
.compliance-bar-container { .compliance-bar-container { display:flex; align-items:center; gap:10px; }
display: flex; .compliance-bar { flex-grow:1; height:8px; background:#EAF0F6; border-radius:4px; overflow:hidden; }
align-items: center; .compliance-bar-fill { height:100%; border-radius:4px; transition:width .3s ease; }
gap: 10px; .compliance-pct { font-size:13px; font-weight:700; min-width:45px; text-align:right; }
} .compliance-pct.high { color:var(--c-green); }
.compliance-bar { .compliance-pct.medium { color:var(--c-amber); }
flex-grow: 1; .compliance-pct.low { color:var(--c-red); }
height: 8px;
background: var(--gray-200);
border-radius: 4px;
overflow: hidden;
}
.compliance-bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
.compliance-pct {
font-size: 0.85rem;
font-weight: 600;
min-width: 45px;
text-align: right;
}
.compliance-pct.high { color: var(--secondary); }
.compliance-pct.medium { color: var(--warning); }
.compliance-pct.low { color: var(--danger); }
/* ── Spinner/Loading ────────────────────────────────────── */ /* ── Loading / Empty ────────────────────────────────────── */
.loading-state { .loading-state { text-align:center; padding:48px 20px; color:var(--muted); }
text-align: center; .loading-state .spinner { width:32px; height:32px; border:3px solid var(--line); border-top-color:var(--c-blue); border-radius:50%; animation:spin .8s linear infinite; margin:0 auto 12px; }
padding: 48px 20px; @keyframes spin { to { transform:rotate(360deg); } }
color: var(--gray-400); @media (prefers-reduced-motion:reduce){ .loading-state .spinner{ animation:none; } }
} .empty-state-box { text-align:center; padding:48px 20px; color:var(--muted); }
.loading-state .spinner { .empty-state-box svg { width:48px; height:48px; margin-bottom:12px; opacity:.45; }
width: 32px; .empty-state-box h4 { color:var(--ink); margin:0 0 4px; font-size:15px; font-weight:700; }
height: 32px; .text-muted { color:var(--muted); }
border: 3px solid var(--gray-200);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 12px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Empty State ────────────────────────────────────────── */ /* ── Modale (prodotta da common.js showModal — style.css rimosso) ── */
.empty-state-box { .modal-overlay { position:fixed; inset:0; background:rgba(15,38,60,.6); backdrop-filter:blur(4px); z-index:1000; display:flex; align-items:center; justify-content:center; opacity:0; visibility:hidden; transition:opacity .2s,visibility .2s; }
text-align: center; .modal-overlay.active { opacity:1; visibility:visible; }
padding: 48px 20px; .modal { background:#fff; border-radius:14px; box-shadow:0 20px 60px rgba(11,40,80,.25); width:90%; max-width:560px; max-height:85vh; overflow-y:auto; transform:translateY(20px) scale(.97); transition:transform .2s; }
color: var(--gray-400); .modal-overlay.active .modal { transform:translateY(0) scale(1); }
} .modal-header { padding:18px 22px; border-bottom:1px solid var(--line); display:flex; align-items:center; justify-content:space-between; }
.empty-state-box svg { .modal-header h3 { font-size:17px; font-weight:800; color:var(--ink); margin:0; }
width: 48px; .modal-close { background:none; border:none; color:#9aa8ba; cursor:pointer; padding:4px; border-radius:8px; line-height:1; font-size:22px; }
height: 48px; .modal-close:hover { color:var(--ink); }
margin-bottom: 12px; .modal-body { padding:22px; }
opacity: 0.5; .modal-footer { padding:14px 22px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px; }
}
.empty-state-box h4 { /* ── Form (dentro le modali) ────────────────────────────── */
color: var(--gray-600); .form-group { margin-bottom:14px; }
margin-bottom: 4px; .form-label { display:block; font-weight:600; font-size:12.5px; margin-bottom:5px; color:var(--ink); }
} .form-input, .form-select { width:100%; padding:9px 11px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
.form-input:focus, .form-select:focus { outline:2px solid var(--gold); outline-offset:1px; }
/* ── Bottoni extra (btn-secondary/btn-sm) usati dalla JS inline ── */
.btn-secondary { display:inline-flex; align-items:center; gap:7px; border:1px solid var(--line); background:#fff; border-radius:8px; padding:8px 13px; font:inherit; font-weight:600; font-size:12.5px; color:var(--ink); cursor:pointer; }
.btn-secondary:hover { background:#F5F8FC; border-color:#c7d4e4; }
.btn-sm { padding:6px 11px; font-size:12px; }
/* ── Notifiche (prodotte da common.js showNotification — style.css rimosso) ── */
.notification-container { position:fixed; top:20px; right:20px; z-index:2000; display:flex; flex-direction:column; gap:10px; }
.notification { display:flex; align-items:center; gap:10px; background:#fff; border:1px solid var(--line); border-left:4px solid var(--c-blue); border-radius:10px; box-shadow:0 8px 24px rgba(16,40,80,.16); padding:12px 14px; min-width:260px; max-width:380px; font-size:13px; transition:opacity .3s,transform .3s; }
.notification.success { border-left-color:var(--c-green); }
.notification.error { border-left-color:var(--c-red); }
.notification.warning { border-left-color:var(--c-amber); }
.notification.fade-out { opacity:0; transform:translateX(12px); }
.notification-icon svg { width:18px; height:18px; }
.notification.success .notification-icon { color:var(--c-green); }
.notification.error .notification-icon { color:var(--c-red); }
.notification.warning .notification-icon { color:var(--c-amber); }
.notification.info .notification-icon { color:var(--c-blue); }
.notification-message { flex:1; min-width:0; color:var(--ink); }
.notification-close { background:none; border:none; font-size:18px; line-height:1; cursor:pointer; color:#9aa8ba; }
/* ── Responsive ─────────────────────────────────────────── */
@media (max-width: 768px) { @media (max-width: 768px) {
.courses-grid { grid-template-columns: 1fr; } .courses-grid { grid-template-columns:1fr; }
.tab-nav button { padding: 10px 14px; font-size: 0.82rem; } .tab-nav button { padding:10px 14px; font-size:12.5px; }
.data-table { font-size: 0.8rem; } .data-table { font-size:12.5px; }
} }
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
@@ -236,19 +126,24 @@
<!-- PWA:end --> <!-- PWA:end -->
</head> </head>
<body> <body>
<div class="app-layout"> <div id="topnav"></div>
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content"> <main class="wrap">
<header class="content-header"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="h2" data-i18n="training.title">Formazione e Awareness</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<div class="content-header-actions"> <i data-lucide="chevron-right" class="ic"></i><span>Struttura interna</span>
<span class="text-muted" style="font-size:0.8rem; margin-right:8px;">Art. 20 NIS2</span> <i data-lucide="chevron-right" class="ic"></i><span data-i18n="training.title">Formazione</span>
<button class="btn btn-primary" onclick="showCreateCourseModal()">+ Nuovo Corso</button> </nav>
<div class="page-head">
<div>
<h1 data-i18n="training.title">Formazione e Awareness</h1>
<p class="sub">Piani formativi, assegnazioni e compliance — Art. 20 NIS2</p>
</div>
<button class="btn-primary" onclick="showCreateCourseModal()"><i data-lucide="plus" class="ic"></i>Nuovo Corso</button>
</div> </div>
</header>
<div class="content-body"> <div class="stack">
<section class="card card-pad">
<!-- Tab Navigation --> <!-- Tab Navigation -->
<div class="tab-nav"> <div class="tab-nav">
<button class="active" onclick="switchTab('courses', this)">Corsi</button> <button class="active" onclick="switchTab('courses', this)">Corsi</button>
@@ -279,7 +174,7 @@
<!-- Tab: Compliance Formativa --> <!-- Tab: Compliance Formativa -->
<div class="tab-panel" id="tab-compliance"> <div class="tab-panel" id="tab-compliance">
<div style="display:flex; justify-content:flex-end; margin-bottom:16px;"> <div style="display:flex; justify-content:flex-end; margin-bottom:16px;">
<button class="btn btn-primary" onclick="showAssignCourseModal()">Assegna Corso</button> <button class="btn-primary" onclick="showAssignCourseModal()"><i data-lucide="user-plus" class="ic"></i>Assegna Corso</button>
</div> </div>
<div id="compliance-container"> <div id="compliance-container">
<div class="loading-state"> <div class="loading-state">
@@ -288,30 +183,27 @@
</div> </div>
</div> </div>
</div> </div>
</section>
</div> </div>
</main> </main>
</div>
<!-- Scripts V3: api.js → common.js (helper+FAB reali+showModal/showNotification) →
Lucide self-host → topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help.
La logica della pagina e' inline (nessun training.js): preservata invariata. -->
<script src="/js/api.js?v=20260625d"></script> <script src="/js/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> <script>
// ── Auth & Init ───────────────────────────────────────── // ── Auth & Init (ordine V3) ─────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar(); // = loadTopnavV3() (topnav-v3.js)
I18n.init(); if (typeof I18n !== 'undefined') I18n.init();
HelpSystem.init(); if (typeof HelpSystem !== 'undefined') HelpSystem.init();
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
v3Icons();
// ── Labels ────────────────────────────────────────────── // ── Labels ──────────────────────────────────────────────
const roleLabels = { const roleLabels = {
@@ -460,8 +352,8 @@
`, { `, {
size: 'lg', size: 'lg',
footer: ` footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button> <button class="btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="createCourse()">Crea Corso</button> <button class="btn-primary" onclick="createCourse()">Crea Corso</button>
` `
}); });
} }
@@ -549,18 +441,18 @@
let actionBtn = ''; let actionBtn = '';
if (displayStatus === 'assigned' || displayStatus === 'overdue') { if (displayStatus === 'assigned' || displayStatus === 'overdue') {
actionBtn = `<button class="btn btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'in_progress')">Inizia</button>`; actionBtn = `<button class="btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'in_progress')">Inizia</button>`;
} else if (displayStatus === 'in_progress') { } else if (displayStatus === 'in_progress') {
actionBtn = `<button class="btn btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'completed')">Completa</button>`; actionBtn = `<button class="btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'completed')">Completa</button>`;
} else { } else {
actionBtn = `<span style="color:var(--secondary); font-weight:500; font-size:0.85rem;">Completato</span>`; actionBtn = `<span style="color:var(--c-green); font-weight:600; font-size:13px;">Completato</span>`;
} }
html += ` html += `
<tr> <tr>
<td> <td>
<div style="font-weight:500;">${escapeHtml(a.title || '')}</div> <div style="font-weight:600;">${escapeHtml(a.title || '')}</div>
${a.is_mandatory == 1 ? '<span style="font-size:0.75rem; color:var(--danger);">Obbligatorio</span>' : ''} ${a.is_mandatory == 1 ? '<span style="font-size:11px; color:var(--c-red);">Obbligatorio</span>' : ''}
</td> </td>
<td><span class="status-badge ${displayStatus}">${statusLabels[displayStatus] || displayStatus}</span></td> <td><span class="status-badge ${displayStatus}">${statusLabels[displayStatus] || displayStatus}</span></td>
<td>${a.due_date ? formatDate(a.due_date) : '-'}</td> <td>${a.due_date ? formatDate(a.due_date) : '-'}</td>
@@ -638,11 +530,11 @@
const total = assignments.length; const total = assignments.length;
const pct = m.mandatory_compliance != null ? m.mandatory_compliance : 100; const pct = m.mandatory_compliance != null ? m.mandatory_compliance : 100;
const pctClass = pct >= 80 ? 'high' : pct >= 50 ? 'medium' : 'low'; const pctClass = pct >= 80 ? 'high' : pct >= 50 ? 'medium' : 'low';
const barColor = pct >= 80 ? 'var(--secondary)' : pct >= 50 ? 'var(--warning)' : 'var(--danger)'; const barColor = pct >= 80 ? 'var(--c-green)' : pct >= 50 ? 'var(--c-amber)' : 'var(--c-red)';
html += ` html += `
<tr> <tr>
<td style="font-weight:500;">${escapeHtml(m.full_name || '-')}</td> <td style="font-weight:600;">${escapeHtml(m.full_name || '-')}</td>
<td>${escapeHtml(m.email || '-')}</td> <td>${escapeHtml(m.email || '-')}</td>
<td>${escapeHtml(m.role || '-')}</td> <td>${escapeHtml(m.role || '-')}</td>
<td>${completed} / ${total}</td> <td>${completed} / ${total}</td>
@@ -689,7 +581,7 @@
<label style="display:flex; align-items:center; gap:8px; padding:6px 0; cursor:pointer;"> <label style="display:flex; align-items:center; gap:8px; padding:6px 0; cursor:pointer;">
<input type="checkbox" class="assign-user-cb" value="${m.id}"> <input type="checkbox" class="assign-user-cb" value="${m.id}">
<span>${escapeHtml(m.full_name || m.email)}</span> <span>${escapeHtml(m.full_name || m.email)}</span>
<span style="color:var(--gray-400); font-size:0.8rem;">(${escapeHtml(m.role || '')})</span> <span style="color:var(--muted); font-size:12px;">(${escapeHtml(m.role || '')})</span>
</label> </label>
`).join(''); `).join('');
} }
@@ -705,15 +597,15 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Seleziona Utenti</label> <label class="form-label">Seleziona Utenti</label>
<div style="max-height:200px; overflow-y:auto; border:1px solid var(--gray-200); border-radius:var(--border-radius-sm); padding:8px 12px;"> <div style="max-height:200px; overflow-y:auto; border:1px solid var(--line); border-radius:8px; padding:8px 12px;">
${membersHtml} ${membersHtml}
</div> </div>
</div> </div>
`, { `, {
size: 'lg', size: 'lg',
footer: ` footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button> <button class="btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="doAssignCourse()">Assegna</button> <button class="btn-primary" onclick="doAssignCourse()">Assegna</button>
` `
}); });
} }
+102 -57
View File
@@ -3,32 +3,78 @@
<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>Segnalazioni Sicurezza - NIS2 Agile</title> <title>Segnalazioni Sicurezza · NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di <!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia CDN). -->
style.css cosi' il design system esistente resta autorevole per i widget <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
renderizzati via JS (tabelle/modali/timeline) -> parita' funzionale assoluta. <link rel="stylesheet" href="/css/v3.css?v=20260627">
BI veste la chrome (sidebar + font istituzionale). -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="/css/style.css?v=20260618">
<style> <style>
.priority-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; } /* ── Compat V3: token + classi emesse dal JS (ex style.css, niente CDN) ── */
.priority-critical { background: var(--danger-bg); color: var(--danger); } :root{
.priority-high { background: #fff7ed; color: #c2410c; } --primary:#1F5FA8;--primary-bg:#E7F0FB;--secondary:#1E8A5A;--secondary-bg:#E6F4EC;
.priority-medium { background: var(--warning-bg); color: #a16207; } --success:#1E8A5A;--success-bg:#E6F4EC;--warning:#D98324;--warning-bg:#FBF1E2;
.priority-low { background: var(--gray-100); color: var(--gray-600); } --danger:#C62828;--danger-bg:#FBE9E9;--info:#1F5FA8;--info-bg:#E7F0FB;
.status-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; } --gray-50:#F8FAFC;--gray-100:#F1F5F9;--gray-200:#E3E9F0;--gray-300:#CBD5E1;
.status-received { background: rgba(6,182,212,0.1); color: #0891b2; } --gray-400:#94A3B8;--gray-500:#64748B;--gray-600:#475569;--gray-700:#334155;--gray-800:#15263C;
.status-under_review { background: var(--warning-bg); color: #a16207; } --card-bg:#fff;--card-shadow:var(--shadow);--border-radius:8px;--border-radius-lg:12px;--border-radius-sm:6px;
.status-investigating { background: #f3e8ff; color: #7c3aed; } --font-mono:'SF Mono','Fira Code','Consolas',monospace;--transition-fast:150ms ease;
.status-resolved, .status-closed { background: var(--success-bg); color: var(--success); } }
.status-rejected { background: var(--danger-bg); color: var(--danger); } .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.anonymous-shield { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; background: rgba(16,185,129,0.1); border-radius: 12px; color: var(--success); font-size: 0.75rem; font-weight: 600; } .mb-16{margin-bottom:16px}.mb-24{margin-bottom:24px}
.art32-banner { background: linear-gradient(135deg, rgba(6,182,212,0.1), rgba(16,185,129,0.1)); border: 1px solid rgba(6,182,212,0.2); border-radius: var(--border-radius-lg); padding: 20px 24px; margin-bottom: 28px; } /* Buttons (parità con .btn-primary/.btn-ghost di v3.css) */
.art32-banner h3 { font-size: 0.9375rem; font-weight: 700; color: var(--gray-800); margin-bottom: 6px; } .btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:8px;padding:8px 14px;font:inherit;font-weight:600;font-size:12.5px;cursor:pointer;line-height:1.2}
.art32-banner p { font-size: 0.8125rem; color: var(--gray-600); } .btn:disabled{opacity:.55;cursor:not-allowed}
.track-form { background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: var(--border-radius); padding: 20px; margin-bottom: 24px; } .btn-primary{background:var(--c-blue);color:#fff}.btn-primary:hover:not(:disabled){background:#1A5494}
/* Icone sprite BI dimensionate inline per i bottoni azione */ .btn-secondary,.btn-outline{background:#fff;color:var(--ink);border-color:var(--line)}
.icon-inline { width: 16px; height: 16px; fill: currentColor; vertical-align: text-bottom; } .btn-secondary:hover:not(:disabled),.btn-outline:hover:not(:disabled){background:#F5F8FC;border-color:#c7d4e4}
.btn-sm{padding:5px 10px;font-size:11.5px}
.icon-inline{width:16px;height:16px;fill:currentColor;vertical-align:text-bottom}
/* Forms */
.form-group{margin-bottom:16px}
.form-label{display:block;font-size:12px;font-weight:600;color:var(--ink);margin-bottom:6px}
.form-control,.form-input,.form-select{width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:8px;font:inherit;font-size:13px;background:#fff;color:var(--ink)}
.form-control:focus,.form-input:focus,.form-select:focus{outline:none;border-color:var(--c-blue);box-shadow:0 0 0 3px rgba(31,95,168,.12)}
textarea.form-control{resize:vertical;min-height:80px}
/* Cards (override v3 .card already exists; add header/body) */
.card-header{padding:16px 20px;border-bottom:1px solid var(--line)}
.card-header h3{margin:0;font-size:15px;font-weight:700}
.card-body{padding:20px}
/* Stats grid (KPI) */
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px}
.stat-card{background:#fff;border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow);padding:16px 18px;border-left:4px solid var(--c-blue)}
.stat-label{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:#7a8aa0}
.stat-value{font-size:25px;font-weight:800;line-height:1.1;margin-top:4px;color:var(--ink)}
/* Table */
.table-container{overflow-x:auto;border:1px solid var(--line);border-radius:12px}
.table-container table{width:100%;border-collapse:collapse;font-size:13px;background:#fff}
.table-container thead{background:#F5F8FC}
.table-container th{text-align:left;padding:11px 14px;font-size:10.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:#7a8aa0;border-bottom:1px solid var(--line)}
.table-container td{padding:11px 14px;border-bottom:1px solid var(--line);vertical-align:middle}
.table-container tbody tr:last-child td{border-bottom:0}
.table-container tbody tr:hover td{background:#FAFCFE}
/* Badge / empty / spinner */
.badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:6px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}
.badge-neutral{background:#EEF2F7;color:#5b6b80}
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:40px 20px;text-align:center;color:var(--muted)}
.empty-state h4{margin:0;font-size:14px;font-weight:700;color:var(--ink)}
.empty-state p{margin:0;font-size:12.5px}
.spinner{width:30px;height:30px;border:3px solid var(--line);border-top-color:var(--c-blue);border-radius:50%;animation:v3spin .7s linear infinite}
@media (prefers-reduced-motion:reduce){.spinner{animation:none}}
/* Page-specific */
.priority-badge{display:inline-flex;align-items:center;padding:2px 9px;border-radius:6px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}
.priority-critical{background:var(--danger-bg);color:var(--danger)}
.priority-high{background:#FBF1E2;color:#9A6418}
.priority-medium{background:var(--warning-bg);color:#9A6418}
.priority-low{background:var(--gray-100);color:var(--gray-600)}
.status-badge{display:inline-flex;align-items:center;padding:2px 9px;border-radius:6px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.03em}
.status-received{background:#E7F0FB;color:#1F5FA8}
.status-under_review{background:var(--warning-bg);color:#9A6418}
.status-investigating{background:#f3e8ff;color:#7c3aed}
.status-resolved,.status-closed{background:var(--success-bg);color:var(--success)}
.status-rejected{background:var(--danger-bg);color:var(--danger)}
.anonymous-shield{display:inline-flex;align-items:center;gap:6px;padding:3px 9px;background:var(--success-bg);border-radius:6px;color:var(--success);font-size:11px;font-weight:600}
.art32-banner{background:linear-gradient(135deg,rgba(31,95,168,.07),rgba(30,138,90,.07));border:1px solid var(--line);border-radius:12px;padding:18px 22px;margin-bottom:18px}
.art32-banner h3{font-size:14px;font-weight:700;color:var(--ink);margin:0 0 6px}
.art32-banner p{font-size:12.5px;color:var(--muted);margin:0}
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest"> <link rel="manifest" href="/manifest.webmanifest">
@@ -45,22 +91,26 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<div class="page-header"> <main class="wrap">
<div class="page-header-content"> <nav class="crumb" aria-label="breadcrumb">
<h1 class="page-title">Segnalazioni Sicurezza</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<p class="page-subtitle">Canale interno Art.32 NIS2 — Segnala violazioni e anomalie di sicurezza</p> <i data-lucide="chevron-right" class="ic"></i><span>Monitoraggio</span>
<i data-lucide="chevron-right" class="ic"></i><span>Segnalazioni</span>
</nav>
<div class="page-head">
<div>
<h1>Segnalazioni Sicurezza</h1>
<p class="sub">Canale interno Art.32 NIS2 — Segnala violazioni e anomalie di sicurezza</p>
</div> </div>
<div class="page-header-actions"> <div style="display:flex;gap:10px;flex-wrap:wrap">
<button class="btn btn-secondary" onclick="switchView('track')"> <button class="btn-ghost" onclick="switchView('track')">
<svg class="icon-inline" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-search"></use></svg> <i data-lucide="search" class="ic"></i>Traccia segnalazione anonima
Traccia Segnalazione Anonima
</button> </button>
<button class="btn btn-primary" onclick="switchView('submit')"> <button class="btn-primary" onclick="switchView('submit')">
<svg class="icon-inline" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-plus"></use></svg> <i data-lucide="plus" class="ic"></i>Nuova segnalazione
Nuova Segnalazione
</button> </button>
</div> </div>
</div> </div>
@@ -124,8 +174,8 @@
<p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Segnala anonimamente o con il tuo nome. L'anonimato è garantito dalla piattaforma.</p> <p style="font-size:0.8125rem; color:var(--gray-500); margin-top:4px;">Segnala anonimamente o con il tuo nome. L'anonimato è garantito dalla piattaforma.</p>
</div> </div>
<div class="card-body"> <div class="card-body">
<div style="margin-bottom:20px; padding:12px 16px; background:rgba(16,185,129,0.08); border:1px solid rgba(16,185,129,0.2); border-radius:var(--border-radius); display:flex; align-items:center; gap:10px;"> <div style="margin-bottom:20px; padding:12px 16px; background:var(--success-bg); border:1px solid var(--line); border-radius:var(--border-radius); display:flex; align-items:center; gap:10px;">
<svg class="icon-inline" style="width:18px; height:18px; color:var(--success); flex-shrink:0;" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-lock"></use></svg> <i data-lucide="lock" class="ic" style="width:18px; height:18px; color:var(--success); flex-shrink:0;"></i>
<label style="display:flex; align-items:center; gap:8px; font-size:0.875rem; cursor:pointer;"> <label style="display:flex; align-items:center; gap:8px; font-size:0.875rem; cursor:pointer;">
<input type="checkbox" id="is-anonymous" checked style="accent-color:var(--success);"> <input type="checkbox" id="is-anonymous" checked style="accent-color:var(--success);">
<span><strong>Segnalazione anonima</strong> — La tua identità non verrà registrata</span> <span><strong>Segnalazione anonima</strong> — La tua identità non verrà registrata</span>
@@ -203,29 +253,23 @@
</div> </div>
</div> </div>
</main> </main>
</div>
<!-- Report Detail Modal handled by common.js showModal --> <!-- Report Detail Modal handled by common.js showModal -->
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina -->
<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> <script>
if (!checkAuth()) throw new Error('Not authenticated'); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); loadSidebar();
I18n.init(); if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init(); if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
let currentView = 'list'; let currentView = 'list';
@@ -302,10 +346,11 @@
if (!reports.length) { if (!reports.length) {
container.innerHTML = ` container.innerHTML = `
<div class="empty-state"> <div class="empty-state">
<svg class="icon-inline" style="width:40px; height:40px; color:var(--gray-300);" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-warning-circle"></use></svg> <i data-lucide="megaphone" style="width:34px;height:34px;color:var(--gray-300);" aria-hidden="true"></i>
<h4>Nessuna segnalazione trovata</h4> <h4>Nessuna segnalazione trovata</h4>
<p>Il canale di segnalazione è attivo. Le segnalazioni appariranno qui.</p> <p>Il canale di segnalazione è attivo. Le segnalazioni appariranno qui.</p>
</div>`; </div>`;
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch(e){} }
return; return;
} }
@@ -319,7 +364,7 @@
<td><strong style="font-size:0.875rem;">${escapeHtml(r.title)}</strong></td> <td><strong style="font-size:0.875rem;">${escapeHtml(r.title)}</strong></td>
<td><span class="priority-badge priority-${r.priority}">${escapeHtml(priorityLabels[r.priority] || r.priority)}</span></td> <td><span class="priority-badge priority-${r.priority}">${escapeHtml(priorityLabels[r.priority] || r.priority)}</span></td>
<td><span class="status-badge status-${r.status}">${escapeHtml(statusLabels[r.status] || r.status)}</span></td> <td><span class="status-badge status-${r.status}">${escapeHtml(statusLabels[r.status] || r.status)}</span></td>
<td>${r.is_anonymous ? '<span class="anonymous-shield"><svg class="icon-inline" style="width:12px;height:12px;" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-lock"></use></svg>Anonima</span>' : '<span style="color:var(--gray-400); font-size:0.75rem;">Firmata</span>'}</td> <td>${r.is_anonymous ? '<span class="anonymous-shield"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>Anonima</span>' : '<span style="color:var(--gray-400); font-size:0.75rem;">Firmata</span>'}</td>
<td>${r.assigned_to_name ? escapeHtml(r.assigned_to_name) : '<span style="color:var(--gray-400);">—</span>'}</td> <td>${r.assigned_to_name ? escapeHtml(r.assigned_to_name) : '<span style="color:var(--gray-400);">—</span>'}</td>
<td style="font-size:0.8rem;">${formatDate(r.created_at)}</td> <td style="font-size:0.8rem;">${formatDate(r.created_at)}</td>
<td><button class="btn btn-sm btn-secondary" onclick="viewReport(${r.id})">Gestisci</button></td> <td><button class="btn btn-sm btn-secondary" onclick="viewReport(${r.id})">Gestisci</button></td>
@@ -348,7 +393,7 @@
<div style="display:flex; gap:10px; flex-wrap:wrap; margin-bottom:16px;"> <div style="display:flex; gap:10px; flex-wrap:wrap; margin-bottom:16px;">
<span class="priority-badge priority-${r.priority}">${escapeHtml(priorityLabels[r.priority] || r.priority)}</span> <span class="priority-badge priority-${r.priority}">${escapeHtml(priorityLabels[r.priority] || r.priority)}</span>
<span class="status-badge status-${r.status}">${escapeHtml(statusLabels[r.status] || r.status)}</span> <span class="status-badge status-${r.status}">${escapeHtml(statusLabels[r.status] || r.status)}</span>
${r.is_anonymous ? '<span class="anonymous-shield"><svg class="icon-inline" style="width:12px;height:12px;" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-lock"></use></svg>Anonima</span>' : ''} ${r.is_anonymous ? '<span class="anonymous-shield"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-1px" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>Anonima</span>' : ''}
</div> </div>
<div style="padding:12px 16px; background:var(--gray-50); border-radius:var(--border-radius); margin-bottom:16px; font-size:0.875rem; line-height:1.6;"> <div style="padding:12px 16px; background:var(--gray-50); border-radius:var(--border-radius); margin-bottom:16px; font-size:0.875rem; line-height:1.6;">
${escapeHtml(r.description)} ${escapeHtml(r.description)}
+201 -374
View File
@@ -3,288 +3,106 @@
<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>Compliance Journey - NIS2 Agile</title> <title>Compliance Journey · NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN), PRIMA di style.css cosi' il design system esistente resta autorevole --> <!-- UI V3: Inter self-host + design system v3.css (NO Bootstrap Italia / NO FontAwesome CDN / NO Google Fonts). -->
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css"> <link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="css/style.css?v=20260618"> <link rel="stylesheet" href="/css/v3.css?v=20260627">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style> <style>
:root { /* Stili specifici della pagina Compliance Journey non coperti da v3.css (nessun CDN). */
--nis2-cyan: #06B6D4; :root { --nis2-cyan:#1F5FA8; }
--nis2-cyan-light: #ecfeff; .text-muted { color: var(--muted); }
--nis2-cyan-ring: rgba(6,182,212,.15); .ic-lg { width:22px; height:22px; }
}
/* ── Journey header ────────────────────────────────── */ /* ── Journey header ────────────────────────────────── */
.journey-header { .journey-header {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); background: linear-gradient(135deg, var(--navy-nav) 0%, var(--navy-top) 100%);
border-radius: var(--border-radius-lg); border-radius: var(--radius);
padding: 24px 28px; padding: 24px 28px; color:#fff; margin-bottom:20px;
color: white; display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:16px;
margin-bottom: 20px; box-shadow: var(--shadow);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
} }
.journey-score { .journey-score { font-size:2.8rem; font-weight:800; color:#fff; line-height:1; }
font-size: 2.8rem; .journey-progress-bar { height:10px; background:rgba(255,255,255,.14); border-radius:5px; margin-top:10px; overflow:hidden; }
font-weight: 800; .journey-progress-fill { height:100%; background:linear-gradient(90deg, var(--gold), #ffd76a); border-radius:5px; transition:width 1s ease; }
color: var(--nis2-cyan); .journey-meta { text-align:right; font-size:.78rem; opacity:.65; }
line-height: 1; .journey-meta strong { display:block; font-size:.9rem; opacity:1; }
}
.journey-label {
font-size: 0.8rem;
opacity: 0.6;
margin-top: 2px;
}
.journey-progress-wrap {
flex: 1;
min-width: 200px;
}
.journey-progress-bar {
height: 10px;
background: rgba(255,255,255,0.12);
border-radius: 5px;
margin-top: 10px;
overflow: hidden;
}
.journey-progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--nis2-cyan), #38bdf8);
border-radius: 5px;
transition: width 1s ease;
}
.journey-meta {
text-align: right;
font-size: 0.78rem;
opacity: 0.55;
}
.journey-meta strong { display: block; font-size: 0.9rem; opacity: 1; }
/* ── Next action banner ────────────────────────────── */ /* ── Next action banner ────────────────────────────── */
.next-action-banner { .next-action-banner {
background: linear-gradient(135deg, #0c4a6e, #075985); background: linear-gradient(135deg, #0c4a6e, #075985); color:#fff; border-radius:var(--radius);
color: white; padding:14px 20px; display:flex; align-items:center; gap:14px; margin-bottom:20px;
border-radius: var(--border-radius); border-left:4px solid var(--gold);
padding: 14px 20px;
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 20px;
border-left: 4px solid var(--nis2-cyan);
} }
.next-action-banner .na-icon { .next-action-banner .na-icon { color:var(--gold); flex-shrink:0; width:24px; height:24px; }
font-size: 1.5rem; .next-action-banner .na-label { font-size:.72rem; opacity:.75; text-transform:uppercase; letter-spacing:.05em; }
color: var(--nis2-cyan); .next-action-banner .na-text { font-size:.95rem; font-weight:600; }
flex-shrink: 0; .next-action-banner a { color:#fff; text-decoration:none; }
} .next-action-banner a:hover { text-decoration:underline; }
.next-action-banner .na-label {
font-size: 0.72rem;
opacity: 0.65;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.next-action-banner .na-text {
font-size: 0.9rem;
font-weight: 600;
}
.next-action-banner a {
color: var(--nis2-cyan);
text-decoration: none;
}
.next-action-banner a:hover { text-decoration: underline; }
/* ── Phase roadmap ─────────────────────────────────── */ /* ── Phase roadmap ─────────────────────────────────── */
.phase-roadmap { .phase-roadmap { display:flex; align-items:stretch; margin-bottom:20px; overflow-x:auto; padding-bottom:2px; gap:0; }
display: flex; .phase-item { flex:1; min-width:120px; position:relative; cursor:pointer; }
align-items: stretch; .phase-box { padding:14px 16px 12px 18px; color:#fff; height:100%; display:flex; flex-direction:column; justify-content:center; transition:filter .2s, box-shadow .2s; position:relative; }
margin-bottom: 20px; .phase-item:first-child .phase-box { border-radius: var(--radius) 0 0 var(--radius); }
overflow-x: auto; .phase-item:last-child .phase-box { border-radius: 0 var(--radius) var(--radius) 0; }
padding-bottom: 2px;
gap: 0;
}
.phase-item {
flex: 1;
min-width: 110px;
position: relative;
cursor: pointer;
}
.phase-box {
padding: 14px 16px 12px 18px;
color: white;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
transition: filter 0.2s, box-shadow 0.2s;
position: relative;
}
.phase-item:first-child .phase-box { border-radius: var(--border-radius) 0 0 var(--border-radius); }
.phase-item:last-child .phase-box { border-radius: 0 var(--border-radius) var(--border-radius) 0; }
/* Arrow connector */
.phase-item:not(:last-child) .phase-box::after { .phase-item:not(:last-child) .phase-box::after {
content: ''; content:''; position:absolute; right:-13px; top:50%; transform:translateY(-50%); width:0; height:0;
position: absolute; border-top:28px solid transparent; border-bottom:28px solid transparent; border-left:13px solid var(--ph-color); z-index:3;
right: -13px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-top: 28px solid transparent;
border-bottom: 28px solid transparent;
border-left: 13px solid var(--ph-color);
z-index: 3;
} }
.phase-item:not(:last-child)::after { .phase-item:not(:last-child)::after {
content: ''; content:''; position:absolute; right:-13px; top:50%; transform:translateY(-50%); width:0; height:0;
position: absolute; border-top:28px solid transparent; border-bottom:28px solid transparent; border-left:13px solid var(--bg); z-index:2;
right: -13px;
top: 50%;
transform: translateY(-50%);
width: 0;
height: 0;
border-top: 28px solid transparent;
border-bottom: 28px solid transparent;
border-left: 13px solid white;
z-index: 2;
}
.phase-item.active .phase-box {
filter: brightness(1.18);
box-shadow: 0 4px 18px rgba(0,0,0,0.25);
z-index: 1;
}
.phase-item:not(.active):hover .phase-box { filter: brightness(1.08); }
.phase-item.todo .phase-box { opacity: 0.6; }
.phase-num { font-size: 0.62rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.7; margin-bottom: 2px; }
.phase-name { font-size: 0.78rem; font-weight: 700; line-height: 1.2; }
.phase-status-row { display: flex; align-items: center; gap: 4px; margin-top: 5px; }
.phase-status-icon { font-size: 0.8rem; }
.phase-pct { font-size: 0.7rem; opacity: 0.85; }
.phase-mini-bar {
height: 3px;
background: rgba(255,255,255,0.2);
border-radius: 2px;
margin-top: 5px;
overflow: hidden;
}
.phase-mini-fill {
height: 100%;
background: rgba(255,255,255,0.75);
border-radius: 2px;
} }
.phase-item.active .phase-box { filter:brightness(1.18); box-shadow:0 4px 18px rgba(0,0,0,.25); z-index:1; }
.phase-item:not(.active):hover .phase-box { filter:brightness(1.08); }
.phase-item.todo .phase-box { opacity:.6; }
.phase-num { font-size:.62rem; font-weight:600; text-transform:uppercase; letter-spacing:.06em; opacity:.7; margin-bottom:2px; }
.phase-name { font-size:.78rem; font-weight:700; line-height:1.2; }
.phase-status-row { display:flex; align-items:center; gap:5px; margin-top:5px; }
.phase-status-icon { width:14px; height:14px; }
.phase-pct { font-size:.7rem; opacity:.85; }
.phase-mini-bar { height:3px; background:rgba(255,255,255,.2); border-radius:2px; margin-top:5px; overflow:hidden; }
.phase-mini-fill { height:100%; background:rgba(255,255,255,.8); border-radius:2px; }
/* ── Phase detail ──────────────────────────────────── */ /* ── Phase detail ──────────────────────────────────── */
.phase-detail-header { .phase-detail-header { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:16px; }
display: flex; .phase-detail-badge { display:inline-flex; align-items:center; gap:6px; padding:5px 14px; border-radius:20px; font-size:.78rem; font-weight:700; color:#fff; }
align-items: center; .phase-detail-badge i { width:15px; height:15px; }
gap: 10px; .phase-detail-desc { font-size:.85rem; color:var(--muted); margin-left:auto; }
flex-wrap: wrap;
}
.phase-detail-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 14px;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 700;
color: white;
}
.phase-detail-desc {
font-size: 0.82rem;
color: var(--gray-500);
margin-left: auto;
}
/* ── Step cards ────────────────────────────────────── */ /* ── Step cards ────────────────────────────────────── */
.step-cards { .step-cards { display:grid; grid-template-columns:repeat(auto-fill, minmax(220px,1fr)); gap:14px; }
display: grid; .step-card { background:#fff; border-radius:var(--radius); padding:16px; border:1.5px solid var(--line); cursor:pointer; transition:box-shadow .15s, border-color .15s, transform .15s; text-decoration:none; display:block; color:inherit; }
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); .step-card:hover { box-shadow:0 6px 20px rgba(16,40,80,.12); border-color:#9FBEDD; transform:translateY(-2px); }
gap: 14px; .step-card-top { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
} .step-icon { width:38px; height:38px; border-radius:10px; display:flex; align-items:center; justify-content:center; }
.step-card { .step-icon i { width:18px; height:18px; }
background: var(--card-bg); .step-badge { font-size:.67rem; padding:2px 9px; border-radius:10px; font-weight:700; text-transform:uppercase; letter-spacing:.03em; }
border-radius: var(--border-radius); .badge-done { background:#E6F4EC; color:#1E7A4E; }
padding: 16px; .badge-partial { background:#FBF1E2; color:#9A6418; }
border: 1.5px solid var(--gray-200); .badge-todo { background:#EEF2F7; color:#5b6b80; }
cursor: pointer; .badge-active { background:#E7F0FB; color:#1F5FA8; }
transition: box-shadow var(--transition), border-color var(--transition), transform var(--transition); .step-card h4 { font-size:.9rem; font-weight:700; margin:0 0 3px; color:var(--ink); }
text-decoration: none; .step-card p { font-size:.78rem; color:var(--muted); line-height:1.45; margin:0 0 12px; }
display: block; .step-metric { font-size:1.5rem; font-weight:800; color:var(--ink); line-height:1; }
color: inherit; .step-metric-lbl { font-size:.72rem; color:var(--muted); margin-top:1px; }
} .step-cta { display:inline-flex; align-items:center; gap:5px; margin-top:10px; font-size:.74rem; font-weight:600; color:var(--c-blue); }
.step-card:hover { .step-cta i { width:13px; height:13px; }
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
border-color: var(--nis2-cyan);
transform: translateY(-2px);
}
.step-card-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.step-icon {
width: 38px;
height: 38px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
}
.step-badge {
font-size: 0.67rem;
padding: 2px 8px;
border-radius: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.badge-done { background: var(--secondary-bg); color: var(--secondary); }
.badge-partial { background: var(--warning-bg); color: #92400e; }
.badge-todo { background: var(--gray-100); color: var(--gray-500); }
.badge-active { background: var(--primary-bg); color: var(--primary-dark); }
.step-card h4 { font-size: 0.85rem; font-weight: 600; margin-bottom: 3px; color: var(--gray-800); }
.step-card p { font-size: 0.76rem; color: var(--gray-500); line-height: 1.45; margin-bottom: 12px; }
.step-metric { font-size: 1.5rem; font-weight: 800; color: var(--gray-800); line-height: 1; }
.step-metric-lbl { font-size: 0.7rem; color: var(--gray-400); margin-top: 1px; }
.step-cta {
display: inline-flex;
align-items: center;
gap: 5px;
margin-top: 10px;
font-size: 0.74rem;
font-weight: 600;
color: var(--nis2-cyan);
}
/* ── Legend ────────────────────────────────────────── */ /* ── Legend ────────────────────────────────────────── */
.legend-row { .legend-row { display:flex; gap:16px; flex-wrap:wrap; margin-bottom:20px; font-size:.8rem; color:var(--muted); }
display: flex; .legend-item { display:flex; align-items:center; gap:6px; }
gap: 16px; .legend-item i { width:15px; height:15px; }
flex-wrap: wrap;
margin-bottom: 20px;
font-size: 0.78rem;
color: var(--gray-500);
}
.legend-item { display: flex; align-items: center; gap: 5px; }
@keyframes fadeSlideIn { @keyframes fadeSlideIn { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:translateY(0); } }
from { opacity: 0; transform: translateY(-6px); } .phase-detail-anim { animation: fadeSlideIn .2s ease; }
to { opacity: 1; transform: translateY(0); } @media (prefers-reduced-motion:reduce){ .phase-detail-anim{animation:none} .journey-progress-fill{transition:none} }
}
.phase-detail-anim { animation: fadeSlideIn 0.2s ease; }
@media (max-width: 640px) { @media (max-width:640px) {
.phase-name { font-size: 0.7rem; } .phase-name { font-size:.7rem; }
.phase-num { display: none; } .phase-num { display:none; }
.journey-header { flex-direction: column; } .journey-header { flex-direction:column; align-items:flex-start; }
.phase-detail-desc { margin-left:0; }
} }
</style> </style>
<!-- PWA:start --> <!-- PWA:start -->
@@ -302,108 +120,117 @@
<!-- 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 -->
<aside class="sidebar" id="sidebar"></aside> <div id="topnav"></div>
<main class="main-content">
<header class="content-header">
<h2>
<i class="fa-solid fa-route" style="color:var(--nis2-cyan);margin-right:8px;"></i>
Compliance Journey
</h2>
<div class="content-header-actions">
<span class="text-muted" id="org-name-header" style="font-size:0.85rem;"></span>
</div>
</header>
<div class="content-body" id="page-body"> <main class="wrap">
<div class="spinner-lg" style="margin:100px auto;display:block;"></div> <nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>Compliance Journey</span>
</nav>
<div class="page-head">
<div>
<h1>Compliance Journey</h1>
<p class="sub">Il percorso di conformità NIS2 in 6 fasi, con stato in tempo reale</p>
</div>
<div class="date"><i data-lucide="building" class="ic"></i><span id="org-name-header"></span></div>
</div>
<div id="page-body">
<div class="v3-loading"><span class="v3-spin"></span>Caricamento del percorso di conformità…</div>
</div> </div>
</main> </main>
</div>
<script src="js/api.js?v=20260631"></script> <!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
<script src="js/common.js?v=20260624g"></script> topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina. -->
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <script src="/js/api.js?v=20260625d"></script>
<script src="/js/common-bi.js?v=20260625d"></script> <script src="/js/common.js?v=20260627v3"></script>
<script src="js/i18n.js?v=20260631"></script> <script src="/vendor/lucide/lucide.min.js"></script>
<script src="js/help.js?v=20260625d"></script> <script src="/js/topnav-v3.js?v=20260627"></script>
<script> <script src="/js/i18n.js?v=20260631"></script>
if (!checkAuth()) throw new Error('Not authenticated'); <script src="/js/help.js?v=20260625d"></script>
loadSidebar(); <script>
I18n.init(); // ── Auth + chrome + i18n + help ──────────────────────────────
HelpSystem.init(); if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); // = loadTopnavV3() (topnav-v3.js)
if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
// ── Dati globali ──────────────────────────────────────────── // Re-render delle icone Lucide (chrome + markup dinamico)
let G = {}; function drawIcons(){ if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch(e){} } }
let activePhase = 0; drawIcons();
// ── Definizione fasi ──────────────────────────────────────── // ── Dati globali ────────────────────────────────────────────
const PHASES = [ let G = {};
let activePhase = 0;
// ── Definizione fasi (icone = nomi Lucide self-host) ─────────
const PHASES = [
{ {
id: 'preparazione', num: 1, name: 'Preparazione', id: 'preparazione', num: 1, name: 'Preparazione',
color: '#6366f1', icon: 'fa-rocket', color: '#6366f1', icon: 'rocket',
desc: 'Organizzazione, team e classificazione NIS2', desc: 'Organizzazione, team e classificazione NIS2',
steps: [ steps: [
{ id: 'onboarding', name: 'Onboarding NIS2', icon: 'fa-rocket', desc: 'Classificazione settore, dimensione e tipo soggetto NIS2', href: 'onboarding.html', mk: 'onboarding_done' }, { id: 'onboarding', name: 'Onboarding NIS2', icon: 'rocket', desc: 'Classificazione settore, dimensione e tipo soggetto NIS2', href: 'onboarding.html', mk: 'onboarding_done' },
{ id: 'org', name: 'Organizzazione', icon: 'fa-building', desc: 'Dati aziendali, P.IVA, classificazione completata', href: 'settings.html', mk: 'org_setup' }, { id: 'org', name: 'Organizzazione', icon: 'building', desc: 'Dati aziendali, P.IVA, classificazione completata', href: 'settings.html', mk: 'org_setup' },
{ id: 'team', name: 'Team & Ruoli', icon: 'fa-users', desc: 'Membri del team con ruoli CISO, Admin, Auditor assegnati', href: 'settings.html', mk: 'members_count' }, { id: 'team', name: 'Team & Ruoli', icon: 'users', desc: 'Membri del team con ruoli CISO, Admin, Auditor assegnati', href: 'settings.html', mk: 'members_count' },
] ]
}, },
{ {
id: 'valutazione', num: 2, name: 'Valutazione', id: 'valutazione', num: 2, name: 'Valutazione',
color: '#0891b2', icon: 'fa-magnifying-glass-chart', color: '#0891b2', icon: 'search-check',
desc: 'Gap analysis, asset e supply chain', desc: 'Gap analysis, asset e supply chain',
steps: [ steps: [
{ id: 'assessment', name: 'Gap Assessment', icon: 'fa-clipboard-check', desc: '80 domande Art.21 NIS2 su 10 categorie di misure di sicurezza', href: 'assessment.html', mk: 'assessment_pct' }, { id: 'assessment', name: 'Gap Assessment', icon: 'clipboard-check', desc: '80 domande Art.21 NIS2 su 10 categorie di misure di sicurezza', href: 'assessment.html', mk: 'assessment_pct' },
{ id: 'assets', name: 'Inventario Asset', icon: 'fa-server', desc: 'Censimento asset critici, classificazione e mappa dipendenze', href: 'assets.html', mk: 'assets_count' }, { id: 'assets', name: 'Inventario Asset', icon: 'server', desc: 'Censimento asset critici, classificazione e mappa dipendenze', href: 'assets.html', mk: 'assets_count' },
{ id: 'supply', name: 'Supply Chain', icon: 'fa-link', desc: 'Fornitori critici, valutazione rischio e certificazioni', href: 'supply-chain.html', mk: 'suppliers_count' }, { id: 'supply', name: 'Supply Chain', icon: 'link', desc: 'Fornitori critici, valutazione rischio e certificazioni', href: 'supply-chain.html', mk: 'suppliers_count' },
] ]
}, },
{ {
id: 'rischi', num: 3, name: 'Rischi', id: 'rischi', num: 3, name: 'Rischi',
color: '#dc2626', icon: 'fa-shield-halved', color: '#dc2626', icon: 'shield-half',
desc: 'Identificazione, valutazione e trattamento', desc: 'Identificazione, valutazione e trattamento',
steps: [ steps: [
{ id: 'risks', name: 'Risk Register', icon: 'fa-triangle-exclamation', desc: 'Identificazione e scoring rischi su scala ISO 27005 (5×5)', href: 'risks.html', mk: 'risks_count' }, { id: 'risks', name: 'Risk Register', icon: 'triangle-alert', desc: 'Identificazione e scoring rischi su scala ISO 27005 (5×5)', href: 'risks.html', mk: 'risks_count' },
{ id: 'treatments', name: 'Piano Trattamento', icon: 'fa-shield', desc: 'Azioni di mitigazione, accettazione o trasferimento rischi', href: 'risks.html', mk: 'treatments_pct' }, { id: 'treatments', name: 'Piano Trattamento', icon: 'shield', desc: 'Azioni di mitigazione, accettazione o trasferimento rischi', href: 'risks.html', mk: 'treatments_pct' },
{ id: 'ai_risk', name: 'AI Risk Suggest', icon: 'fa-wand-magic-sparkles', desc: 'Suggerimenti rischi generati da AI per settore e asset', href: 'risks.html', mk: 'ai_risks_done' }, { id: 'ai_risk', name: 'AI Risk Suggest', icon: 'wand-sparkles', desc: 'Suggerimenti rischi generati da AI per settore e asset', href: 'risks.html', mk: 'ai_risks_done' },
] ]
}, },
{ {
id: 'implementazione', num: 4, name: 'Implementazione', id: 'implementazione', num: 4, name: 'Implementazione',
color: '#16a34a', icon: 'fa-gears', color: '#16a34a', icon: 'settings',
desc: 'Policy, formazione e controlli di sicurezza', desc: 'Policy, formazione e controlli di sicurezza',
steps: [ steps: [
{ id: 'policies', name: 'Policy', icon: 'fa-file-shield', desc: 'Redazione, revisione AI e approvazione policy NIS2', href: 'policies.html', mk: 'policies_approved' }, { id: 'policies', name: 'Policy', icon: 'file-lock', desc: 'Redazione, revisione AI e approvazione policy NIS2', href: 'policies.html', mk: 'policies_approved' },
{ id: 'training', name: 'Formazione', icon: 'fa-graduation-cap', desc: 'Corsi obbligatori assegnati e completati dal personale', href: 'training.html', mk: 'training_pct' }, { id: 'training', name: 'Formazione', icon: 'graduation-cap', desc: 'Corsi obbligatori assegnati e completati dal personale', href: 'training.html', mk: 'training_pct' },
{ id: 'controls', name: 'Controlli Audit', icon: 'fa-list-check', desc: 'Implementazione controlli ISO 27001 / misure Art.21 NIS2', href: 'reports.html', mk: 'controls_pct' }, { id: 'controls', name: 'Controlli Audit', icon: 'list-checks', desc: 'Implementazione controlli ISO 27001 / misure Art.21 NIS2', href: 'reports.html', mk: 'controls_pct' },
] ]
}, },
{ {
id: 'monitoraggio', num: 5, name: 'Monitoraggio', id: 'monitoraggio', num: 5, name: 'Monitoraggio',
color: '#d97706', icon: 'fa-eye', color: '#d97706', icon: 'eye',
desc: 'Incidenti, non-conformità e normative', desc: 'Incidenti, non-conformità e normative',
steps: [ steps: [
{ id: 'incidents', name: 'Incidenti Art.23', icon: 'fa-bell', desc: 'Gestione e notifica CSIRT: 24h early warning, 72h, 1 mese', href: 'incidents.html', mk: 'incidents_count' }, { id: 'incidents', name: 'Incidenti Art.23', icon: 'bell', desc: 'Gestione e notifica CSIRT: 24h early warning, 72h, 1 mese', href: 'incidents.html', mk: 'incidents_count' },
{ id: 'ncr', name: 'NCR / CAPA', icon: 'fa-circle-xmark', desc: 'Non-conformità rilevate e azioni correttive in corso', href: 'reports.html', mk: 'ncr_open' }, { id: 'ncr', name: 'NCR / CAPA', icon: 'circle-x', desc: 'Non-conformità rilevate e azioni correttive in corso', href: 'reports.html', mk: 'ncr_open' },
{ id: 'normative', name: 'Aggiornamenti NIS2', icon: 'fa-newspaper', desc: 'Feed ACN, DORA, D.Lgs. 138/2024 con conferma di lettura', href: 'normative.html', mk: 'normative_pending' }, { id: 'normative', name: 'Aggiornamenti NIS2', icon: 'newspaper', desc: 'Feed ACN, DORA, D.Lgs. 138/2024 con conferma di lettura', href: 'normative.html', mk: 'normative_pending' },
{ id: 'wb', name: 'Segnalazioni', icon: 'fa-user-secret', desc: 'Canale whistleblowing anonimo Art.32 NIS2', href: 'whistleblowing.html', mk: 'wb_open' }, { id: 'wb', name: 'Segnalazioni', icon: 'eye-off', desc: 'Canale whistleblowing anonimo Art.32 NIS2', href: 'whistleblowing.html', mk: 'wb_open' },
] ]
}, },
{ {
id: 'reportistica', num: 6, name: 'Reportistica', id: 'reportistica', num: 6, name: 'Reportistica',
color: '#7c3aed', icon: 'fa-chart-line', color: '#7c3aed', icon: 'line-chart',
desc: 'Report esecutivi, audit chain e export', desc: 'Report esecutivi, audit chain e export',
steps: [ steps: [
{ id: 'exec_report', name: 'Report Compliance', icon: 'fa-file-chart-line', desc: 'Report esecutivo HTML stampabile come PDF per board e auditor', href: 'reports.html', mk: 'report_gen' }, { id: 'exec_report', name: 'Report Compliance', icon: 'file-chart-column', desc: 'Report esecutivo HTML stampabile come PDF per board e auditor', href: 'reports.html', mk: 'report_gen' },
{ id: 'audit_chain', name: 'Audit Chain', icon: 'fa-link', desc: 'Log immutabile con hash chain SHA-256, verifica integrità', href: 'reports.html', mk: 'audit_records' }, { id: 'audit_chain', name: 'Audit Chain', icon: 'link', desc: 'Log immutabile con hash chain SHA-256, verifica integrità', href: 'reports.html', mk: 'audit_records' },
{ id: 'export', name: 'Export Certificato', icon: 'fa-file-export', desc: 'Export CSV/JSON con firma SHA-256 per conservazione prove', href: 'reports.html', mk: 'export_done' }, { id: 'export', name: 'Export Certificato', icon: 'file-output', desc: 'Export CSV/JSON con firma SHA-256 per conservazione prove', href: 'reports.html', mk: 'export_done' },
] ]
} }
]; ];
// ── Caricamento dati parallelo ─────────────────────────────── // ── Caricamento dati parallelo ───────────────────────────────
async function loadAll() { async function loadAll() {
const [ov, sc, al, rl, pl, asl, sl, ts, ac] = await Promise.allSettled([ const [ov, sc, al, rl, pl, asl, sl, ts, ac] = await Promise.allSettled([
api.getDashboardOverview(), api.getDashboardOverview(),
api.getComplianceScore(), api.getComplianceScore(),
@@ -469,10 +296,10 @@ async function loadAll() {
// Seleziona automaticamente la fase "attiva" (prima non completata) // Seleziona automaticamente la fase "attiva" (prima non completata)
activePhase = computeActivePhase(); activePhase = computeActivePhase();
renderPage(); renderPage();
} }
// ── Logica status ──────────────────────────────────────────── // ── Logica status ────────────────────────────────────────────
function phaseStatus(id) { function phaseStatus(id) {
switch (id) { switch (id) {
case 'preparazione': return 'done'; case 'preparazione': return 'done';
case 'valutazione': case 'valutazione':
@@ -488,9 +315,9 @@ function phaseStatus(id) {
case 'reportistica': return G.report_gen > 0 ? 'partial' : 'todo'; case 'reportistica': return G.report_gen > 0 ? 'partial' : 'todo';
default: return 'todo'; default: return 'todo';
} }
} }
function phasePct(id) { function phasePct(id) {
switch (id) { switch (id) {
case 'preparazione': return 100; case 'preparazione': return 100;
case 'valutazione': return Math.round((G.assessment_pct + (G.assets_count > 0 ? 100 : 0)) / 2); case 'valutazione': return Math.round((G.assessment_pct + (G.assets_count > 0 ? 100 : 0)) / 2);
@@ -500,9 +327,9 @@ function phasePct(id) {
case 'reportistica': return G.report_gen > 0 ? 40 : 0; case 'reportistica': return G.report_gen > 0 ? 40 : 0;
default: return 0; default: return 0;
} }
} }
function stepStatus(mk) { function stepStatus(mk) {
switch (mk) { switch (mk) {
case 'onboarding_done': case 'onboarding_done':
case 'org_setup': return 'done'; case 'org_setup': return 'done';
@@ -522,9 +349,9 @@ function stepStatus(mk) {
case 'wb_open': return 'active'; case 'wb_open': return 'active';
default: return 'partial'; default: return 'partial';
} }
} }
function stepMetric(mk) { function stepMetric(mk) {
const m = { const m = {
onboarding_done: { val: '✓', lbl: 'configurato' }, onboarding_done: { val: '✓', lbl: 'configurato' },
org_setup: { val: '✓', lbl: 'configurato' }, org_setup: { val: '✓', lbl: 'configurato' },
@@ -551,33 +378,33 @@ function stepMetric(mk) {
export_done: { val: '—', lbl: 'export' }, export_done: { val: '—', lbl: 'export' },
}; };
return m[mk] || { val: '—', lbl: '' }; return m[mk] || { val: '—', lbl: '' };
} }
const ST_ICON = { done: 'fa-circle-check', partial: 'fa-circle-half-stroke', active: 'fa-circle-dot', todo: 'fa-circle' }; const ST_ICON = { done: 'circle-check', partial: 'circle-dashed', active: 'circle-dot', todo: 'circle' };
const ST_LBL = { done: 'Completato', partial: 'In corso', active: 'Attivo', todo: 'Da iniziare' }; const ST_LBL = { done: 'Completato', partial: 'In corso', active: 'Attivo', todo: 'Da iniziare' };
const ST_BADGE = { done: 'badge-done', partial: 'badge-partial', active: 'badge-active', todo: 'badge-todo' }; const ST_BADGE = { done: 'badge-done', partial: 'badge-partial', active: 'badge-active', todo: 'badge-todo' };
function computeActivePhase() { function computeActivePhase() {
for (let i = 0; i < PHASES.length; i++) { for (let i = 0; i < PHASES.length; i++) {
const st = phaseStatus(PHASES[i].id); const st = phaseStatus(PHASES[i].id);
if (st === 'partial' || st === 'todo' || st === 'active') return i; if (st === 'partial' || st === 'todo' || st === 'active') return i;
} }
return PHASES.length - 1; return PHASES.length - 1;
} }
function computeNextAction() { function computeNextAction() {
if (!G.assessments_total) return { text: 'Avvia il primo Gap Assessment NIS2', link: 'assessment.html', icon: 'fa-clipboard-check' }; if (!G.assessments_total) return { text: 'Avvia il primo Gap Assessment NIS2', link: 'assessment.html', icon: 'clipboard-check' };
if (G.assessment_pct < 100) return { text: 'Completa il Gap Assessment in corso (' + G.assessment_pct + '%)', link: 'assessment.html', icon: 'fa-clipboard-check' }; if (G.assessment_pct < 100) return { text: 'Completa il Gap Assessment in corso (' + G.assessment_pct + '%)', link: 'assessment.html', icon: 'clipboard-check' };
if (!G.assets_count) return { text: 'Censisci gli asset critici dell\'organizzazione', link: 'assets.html', icon: 'fa-server' }; if (!G.assets_count) return { text: 'Censisci gli asset critici dell\'organizzazione', link: 'assets.html', icon: 'server' };
if (!G.risks_count) return { text: 'Registra i rischi NIS2 nel Risk Register', link: 'risks.html', icon: 'fa-triangle-exclamation' }; if (!G.risks_count) return { text: 'Registra i rischi NIS2 nel Risk Register', link: 'risks.html', icon: 'triangle-alert' };
if (!G.policies_total) return { text: 'Genera la prima policy con AI', link: 'policies.html', icon: 'fa-file-shield' }; if (!G.policies_total) return { text: 'Genera la prima policy con AI', link: 'policies.html', icon: 'file-lock' };
if (G.training_pct < 50) return { text: 'Assegna i corsi di formazione al team', link: 'training.html', icon: 'fa-graduation-cap' }; if (G.training_pct < 50) return { text: 'Assegna i corsi di formazione al team', link: 'training.html', icon: 'graduation-cap' };
if (G.controls_pct < 60) return { text: 'Implementa i controlli di sicurezza Art.21', link: 'reports.html', icon: 'fa-list-check' }; if (G.controls_pct < 60) return { text: 'Implementa i controlli di sicurezza Art.21', link: 'reports.html', icon: 'list-checks' };
return { text: 'Genera il report di compliance per il board', link: 'reports.html', icon: 'fa-chart-line' }; return { text: 'Genera il report di compliance per il board', link: 'reports.html', icon: 'line-chart' };
} }
// ── Render ─────────────────────────────────────────────────── // ── Render ───────────────────────────────────────────────────
function renderPage() { function renderPage() {
const next = computeNextAction(); const next = computeNextAction();
const score = G.complianceScore; const score = G.complianceScore;
@@ -587,10 +414,10 @@ function renderPage() {
html += ` html += `
<div class="journey-header"> <div class="journey-header">
<div> <div>
<div style="font-size:0.75rem;opacity:0.55;text-transform:uppercase;letter-spacing:0.06em;margin-bottom:4px;">Progresso Art.21 NIS2</div> <div style="font-size:0.75rem;opacity:0.65;text-transform:uppercase;letter-spacing:0.06em;margin-bottom:4px;">Progresso Art.21 NIS2</div>
<div style="display:flex;align-items:baseline;gap:10px;"> <div style="display:flex;align-items:baseline;gap:10px;">
<span class="journey-score">${score}%</span> <span class="journey-score">${score}%</span>
<span style="color:rgba(255,255,255,0.5);font-size:0.82rem;">implementazione misure</span> <span style="color:rgba(255,255,255,0.6);font-size:0.82rem;">implementazione misure</span>
</div> </div>
<div class="journey-progress-bar" style="max-width:320px;"> <div class="journey-progress-bar" style="max-width:320px;">
<div class="journey-progress-fill" style="width:${score}%;"></div> <div class="journey-progress-fill" style="width:${score}%;"></div>
@@ -601,8 +428,8 @@ function renderPage() {
const pct = phasePct(ph.id); const pct = phasePct(ph.id);
const st = phaseStatus(ph.id); const st = phaseStatus(ph.id);
return `<div style="text-align:center;"> return `<div style="text-align:center;">
<div style="font-size:0.65rem;opacity:0.5;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:2px;">${ph.name}</div> <div style="font-size:0.65rem;opacity:0.6;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:2px;">${ph.name}</div>
<div style="font-size:1rem;font-weight:700;color:${st==='done'?'#4ade80':st==='todo'?'rgba(255,255,255,0.3)':'var(--nis2-cyan)'};">${pct}%</div> <div style="font-size:1rem;font-weight:700;color:${st==='done'?'#7ee0a8':st==='todo'?'rgba(255,255,255,0.4)':'var(--gold)'};">${pct}%</div>
</div>`; </div>`;
}).join('')} }).join('')}
</div> </div>
@@ -615,7 +442,7 @@ function renderPage() {
// ── Next action // ── Next action
html += ` html += `
<div class="next-action-banner"> <div class="next-action-banner">
<i class="fa-solid ${next.icon} na-icon"></i> <i data-lucide="${next.icon}" class="na-icon"></i>
<div> <div>
<div class="na-label">Prossima azione consigliata</div> <div class="na-label">Prossima azione consigliata</div>
<div class="na-text"><a href="${next.link}">${next.text} &rarr;</a></div> <div class="na-text"><a href="${next.link}">${next.text} &rarr;</a></div>
@@ -625,10 +452,10 @@ function renderPage() {
// ── Legend // ── Legend
html += ` html += `
<div class="legend-row"> <div class="legend-row">
<div class="legend-item"><i class="fa-solid fa-circle-check" style="color:var(--secondary);"></i> Completato</div> <div class="legend-item"><i data-lucide="circle-check" style="color:var(--c-green);"></i> Completato</div>
<div class="legend-item"><i class="fa-solid fa-circle-half-stroke" style="color:var(--warning);"></i> In corso</div> <div class="legend-item"><i data-lucide="circle-dashed" style="color:var(--c-amber);"></i> In corso</div>
<div class="legend-item"><i class="fa-solid fa-circle-dot" style="color:var(--primary);"></i> Attivo / Continuo</div> <div class="legend-item"><i data-lucide="circle-dot" style="color:var(--c-blue);"></i> Attivo / Continuo</div>
<div class="legend-item"><i class="fa-solid fa-circle" style="color:var(--gray-300);"></i> Da iniziare</div> <div class="legend-item"><i data-lucide="circle" style="color:#94a3b8;"></i> Da iniziare</div>
</div>`; </div>`;
// ── Phase roadmap // ── Phase roadmap
@@ -644,7 +471,7 @@ function renderPage() {
<div class="phase-num">Fase ${ph.num}</div> <div class="phase-num">Fase ${ph.num}</div>
<div class="phase-name">${ph.name}</div> <div class="phase-name">${ph.name}</div>
<div class="phase-status-row"> <div class="phase-status-row">
<i class="fa-solid ${ST_ICON[st]} phase-status-icon"></i> <i data-lucide="${ST_ICON[st]}" class="phase-status-icon"></i>
<span class="phase-pct">${pct}%</span> <span class="phase-pct">${pct}%</span>
</div> </div>
<div class="phase-mini-bar"> <div class="phase-mini-bar">
@@ -659,24 +486,22 @@ function renderPage() {
html += `<div id="phase-detail-wrap">${renderPhaseDetail(activePhase)}</div>`; html += `<div id="phase-detail-wrap">${renderPhaseDetail(activePhase)}</div>`;
document.getElementById('page-body').innerHTML = html; document.getElementById('page-body').innerHTML = html;
} drawIcons();
}
function renderPhaseDetail(idx) { function renderPhaseDetail(idx) {
const ph = PHASES[idx]; const ph = PHASES[idx];
const st = phaseStatus(ph.id); const st = phaseStatus(ph.id);
let html = ` let html = `
<div class="card phase-detail-anim"> <div class="card card-pad phase-detail-anim">
<div class="card-header">
<div class="phase-detail-header"> <div class="phase-detail-header">
<span class="phase-detail-badge" style="background:${ph.color};"> <span class="phase-detail-badge" style="background:${ph.color};">
<i class="fa-solid ${ph.icon}"></i> Fase ${ph.num} &mdash; ${ph.name} <i data-lucide="${ph.icon}"></i> Fase ${ph.num} &mdash; ${ph.name}
</span> </span>
<span class="step-badge ${ST_BADGE[st]}">${ST_LBL[st]}</span> <span class="step-badge ${ST_BADGE[st]}">${ST_LBL[st]}</span>
<span class="phase-detail-desc">${ph.desc}</span> <span class="phase-detail-desc">${ph.desc}</span>
</div> </div>
</div>
<div class="card-body">
<div class="step-cards">`; <div class="step-cards">`;
for (const step of ph.steps) { for (const step of ph.steps) {
@@ -686,7 +511,7 @@ function renderPhaseDetail(idx) {
<a href="${step.href}" class="step-card"> <a href="${step.href}" class="step-card">
<div class="step-card-top"> <div class="step-card-top">
<div class="step-icon" style="background:${ph.color}18;color:${ph.color};"> <div class="step-icon" style="background:${ph.color}18;color:${ph.color};">
<i class="fa-solid ${step.icon}"></i> <i data-lucide="${step.icon}"></i>
</div> </div>
<span class="step-badge ${ST_BADGE[sst]}">${ST_LBL[sst]}</span> <span class="step-badge ${ST_BADGE[sst]}">${ST_LBL[sst]}</span>
</div> </div>
@@ -694,30 +519,32 @@ function renderPhaseDetail(idx) {
<p>${step.desc}</p> <p>${step.desc}</p>
<div class="step-metric">${met.val}</div> <div class="step-metric">${met.val}</div>
<div class="step-metric-lbl">${met.lbl}</div> <div class="step-metric-lbl">${met.lbl}</div>
<div class="step-cta">Vai al modulo <i class="fa-solid fa-arrow-right" style="font-size:0.65rem;margin-left:2px;"></i></div> <div class="step-cta">Vai al modulo <i data-lucide="arrow-right"></i></div>
</a>`; </a>`;
} }
html += `</div></div></div>`; html += `</div></div>`;
return html; return html;
} }
function selectPhase(idx) { function selectPhase(idx) {
activePhase = idx; activePhase = idx;
document.querySelectorAll('.phase-item').forEach((el, i) => { document.querySelectorAll('.phase-item').forEach((el, i) => {
el.classList.toggle('active', i === idx); el.classList.toggle('active', i === idx);
}); });
document.getElementById('phase-detail-wrap').innerHTML = renderPhaseDetail(idx); document.getElementById('phase-detail-wrap').innerHTML = renderPhaseDetail(idx);
} drawIcons();
}
window.selectPhase = selectPhase;
// ── Org name in header ──────────────────────────────────────── // ── Org name in header ────────────────────────────────────────
try { try {
const u = JSON.parse(localStorage.getItem('nis2_user') || '{}'); const u = JSON.parse(localStorage.getItem('nis2_user') || '{}');
if (u.org_name) document.getElementById('org-name-header').textContent = u.org_name; if (u.org_name) document.getElementById('org-name-header').textContent = u.org_name;
} catch(e) {} } catch(e) {}
// ── Start ───────────────────────────────────────────────────── // ── Start ─────────────────────────────────────────────────────
loadAll(); loadAll();
</script> </script>
</body> </body>
</html> </html>