[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).
+138 -298
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> <div class="page-head">
</header> <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 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> <div class="lbl" id="lbl-total-orgs">Totale Organizzazioni</div>
<div class="admin-stat-label" id="lbl-total-orgs">Totale Organizzazioni</div> <div class="val" id="stat-total-orgs" aria-labelledby="lbl-total-orgs">--</div>
<div class="admin-stat-value" id="stat-total-orgs" aria-labelledby="lbl-total-orgs">--</div>
</div>
<div class="admin-stat-card">
<div class="admin-stat-icon green" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check-circle"></use></svg>
</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 class="admin-stat-card">
<div class="admin-stat-icon blue" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg>
</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 class="admin-stat-card">
<div class="admin-stat-icon green" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg>
</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>
<div class="card card-pad stat green">
<!-- Stats Row 2 --> <div class="ibox"><i data-lucide="check-circle" class="ic"></i></div>
<div class="admin-stats-grid" id="stats-row-2"> <div style="flex:1;min-width:0">
<div class="admin-stat-card"> <div class="lbl" id="lbl-active-orgs">Organizzazioni Attive</div>
<div class="admin-stat-icon purple" aria-hidden="true"> <div class="val" id="stat-active-orgs" aria-labelledby="lbl-active-orgs">--</div>
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-check"></use></svg>
</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 class="admin-stat-card">
<div class="admin-stat-icon orange" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-warning-circle"></use></svg>
</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 class="admin-stat-card">
<div class="admin-stat-icon red" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-error"></use></svg>
</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 class="admin-stat-card">
<div class="admin-stat-icon purple" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-software"></use></svg>
</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>
<div class="card card-pad stat blue">
<!-- Plans Distribution --> <div class="ibox"><i data-lucide="users" class="ic"></i></div>
<div class="card mb-24"> <div style="flex:1;min-width:0">
<div class="card-header"> <div class="lbl" id="lbl-total-users">Totale Utenti</div>
<h3>Distribuzione Piani</h3> <div class="val" id="stat-total-users" aria-labelledby="lbl-total-users">--</div>
</div>
<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="spinner" style="margin:40px auto;"></div>
</div>
</div> </div>
</div> </div>
<div class="card card-pad stat green">
<!-- Quick Links --> <div class="ibox"><i data-lucide="user-check" class="ic"></i></div>
<div class="card"> <div style="flex:1;min-width:0">
<div class="card-header"> <div class="lbl" id="lbl-active-users">Utenti Attivi</div>
<h3>Gestione Piattaforma</h3> <div class="val" id="stat-active-users" aria-labelledby="lbl-active-users">--</div>
</div>
<div class="card-body">
<nav class="quick-links-grid" aria-label="Collegamenti rapidi gestione piattaforma">
<a href="/admin/organizations.html" class="quick-link-card">
<div class="quick-link-icon" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-pa"></use></svg>
</div>
<div class="quick-link-info">
<h4>Gestisci Organizzazioni</h4>
<p>Visualizza e gestisci tutte le organizzazioni registrate sulla piattaforma.</p>
</div>
</a>
<a href="/admin/users.html" class="quick-link-card">
<div class="quick-link-icon" aria-hidden="true">
<svg class="icon" role="img"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-user"></use></svg>
</div>
<div class="quick-link-info">
<h4>Gestisci Utenti</h4>
<p>Visualizza e gestisci tutti gli utenti registrati sulla piattaforma.</p>
</div>
</a>
</nav>
</div> </div>
</div> </div>
</div> </div>
</main>
</div>
<!-- Stats Row 2 -->
<div class="grid3" id="stats-row-2" style="grid-template-columns:repeat(4,1fr)">
<div class="card card-pad stat blue">
<div class="ibox"><i data-lucide="clipboard-check" class="ic"></i></div>
<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="card card-pad stat amber">
<div class="ibox"><i data-lucide="alert-triangle" class="ic"></i></div>
<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="card card-pad stat amber">
<div class="ibox"><i data-lucide="shield-alert" class="ic"></i></div>
<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="card card-pad stat blue">
<div class="ibox"><i data-lucide="sparkles" class="ic"></i></div>
<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>
<!-- Plans Distribution -->
<section class="card card-pad">
<div class="sec-head"><h3>Distribuzione Piani</h3></div>
<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="v3-loading" style="flex:1"><span class="v3-spin"></span>Caricamento…</div>
</div>
</div>
</section>
<!-- Quick Links -->
<section class="card card-pad">
<div class="sec-head"><h3>Gestione Piattaforma</h3></div>
<nav class="quick-links-grid" aria-label="Collegamenti rapidi gestione piattaforma">
<a href="/admin/organizations.html" class="quick-link-card">
<div class="quick-link-icon" aria-hidden="true"><i data-lucide="building-2" class="ic"></i></div>
<div class="quick-link-info">
<h4>Gestisci Organizzazioni</h4>
<p>Visualizza e gestisci tutte le organizzazioni registrate sulla piattaforma.</p>
</div>
</a>
<a href="/admin/users.html" class="quick-link-card">
<div class="quick-link-icon" aria-hidden="true"><i data-lucide="users" class="ic"></i></div>
<div class="quick-link-info">
<h4>Gestisci Utenti</h4>
<p>Visualizza e gestisci tutti gli utenti registrati sulla piattaforma.</p>
</div>
</a>
</nav>
</section>
</div>
</main>
<!-- 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() {
+76 -142
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> </nav>
</header> <div class="page-head">
<div>
<div class="content-body"> <h1>Gestione Organizzazioni</h1>
<!-- Breadcrumb --> <p class="sub">Tutte le organizzazioni registrate sulla piattaforma</p>
<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>
<!-- Table -->
<div class="card">
<div class="card-body" style="padding:0;">
<div id="orgs-container">
<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento in corso"></div>
</div>
</div>
</div>
</div> </div>
</main> <span class="badge-t red">Super Admin</span>
</div> </div>
<script src="../js/api.js?v=20260625d"></script> <section class="card card-pad">
<script src="../js/common.js?v=20260624g"></script> <div id="orgs-container">
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, </div>
riusando tutti gli helper di common.js (zero backend). --> </section>
<script src="../vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> </main>
<script>
if (window.bootstrap && bootstrap.loadFonts) { <!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide → topnav-v3.js (chrome) → i18n → init pagina -->
bootstrap.loadFonts('../vendor/bootstrap-italia/dist/fonts'); <script src="/js/api.js?v=20260625d"></script>
} <script src="/js/common.js?v=20260627v3"></script>
</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=20260620"></script> <script src="/js/i18n.js?v=20260631"></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(); 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>
+89 -169
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> </nav>
</header> <div class="page-head">
<div>
<div class="content-body"> <h1>Gestione Utenti</h1>
<!-- Breadcrumb --> <p class="sub">Tutti gli utenti registrati sulla piattaforma</p>
<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>
<!-- Table -->
<div class="card">
<div class="card-body" style="padding:0;">
<div id="users-container" aria-live="polite">
<div class="spinner" style="margin:60px auto;" role="status" aria-label="Caricamento utenti"></div>
</div>
</div>
</div>
</div> </div>
</main> <span class="badge-t red">Super Admin</span>
</div> </div>
<script src="../js/api.js?v=20260625d"></script> <section class="card card-pad">
<script src="../js/common.js?v=20260624g"></script> <div id="users-container" aria-live="polite">
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js). <div class="v3-loading"><span class="v3-spin"></span>Caricamento utenti…</div>
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI, </div>
riusando tutti gli helper di common.js (zero backend). --> </section>
<script src="../vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> </main>
<script>
if (window.bootstrap && bootstrap.loadFonts) { <!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide → topnav-v3.js (chrome) → i18n → init pagina -->
bootstrap.loadFonts('../vendor/bootstrap-italia/dist/fonts'); <script src="/js/api.js?v=20260625d"></script>
} <script src="/js/common.js?v=20260627v3"></script>
</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=20260620"></script> <script src="/js/i18n.js?v=20260631"></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(); 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');
} }
+59 -25
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> <div>
</header> <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 class="content-body"> <div class="content-body">
@@ -1087,19 +1118,22 @@
</div> </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 -->
<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>
+323 -181
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,158 +20,273 @@
<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>
<div class="content-header-actions"> </nav>
<button class="btn btn-outline btn-sm" id="btn-ai-analyze" style="display:none;" onclick="aiAnalyze()"> <div class="page-head">
<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>
Analisi AI <h1 data-i18n="assessment.title">Gap Analysis NIS2</h1>
</button> <p class="sub">Scostamento tra lo stato attuale e i requisiti della Direttiva (UE) 2022/2555</p>
</div>
<div class="content-header-actions">
<button class="btn-outline" id="btn-ai-analyze" style="display:none;" onclick="aiAnalyze()">
<i data-lucide="sparkles" class="ic"></i> Analisi AI
</button>
</div>
</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>
<!-- Assessment Selection / Create -->
<section class="card mb-24" id="assessment-start">
<div class="center-card">
<h3>Assessment di Conformità NIS2</h3>
<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; text-align:left;"></div>
<button class="btn-primary btn-lg" id="btn-new-assessment" onclick="createNewAssessment()">
<i data-lucide="plus" class="ic"></i> Nuovo Assessment
</button>
</div>
</section>
<!-- Wizard Container -->
<div id="assessment-wizard" style="display:none;">
<!-- Steps Indicator -->
<div class="steps" id="steps-indicator"></div>
<!-- Progress Bar -->
<div class="ga-prog-wrap">
<div class="ga-prog-top">
<span class="ga-prog-lab">Avanzamento risposta domande</span>
<span class="ga-prog-pct" id="progress-pct">0%</span>
</div> </div>
</header> <div class="ga-prog-track">
<div id="progress-bar-fill" class="ga-prog-fill"></div>
<div class="content-body">
<!-- Assessment Selection / Create -->
<div id="assessment-start" class="card mb-24">
<div class="card-body text-center" style="padding:48px;">
<h3 style="font-size:1.25rem; color:var(--gray-900); margin-bottom:8px;">Assessment di Conformita' NIS2</h3>
<p class="text-muted mb-24">Valuta il livello di conformita' della tua organizzazione rispetto ai requisiti della direttiva NIS2.</p>
<div id="existing-assessments" class="mb-24" style="display:none;"></div>
<button class="btn 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>
Nuovo Assessment
</button>
</div>
</div> </div>
<div class="ga-prog-sub">
<!-- Wizard Container --> <span id="progress-answered">0</span> di <span id="progress-total">0</span> domande con risposta
<div id="assessment-wizard" style="display:none;">
<!-- Steps Indicator -->
<div class="steps" id="steps-indicator"></div>
<!-- Progress Bar -->
<div style="margin-bottom:16px;">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
<span style="font-size:0.78rem; font-weight:600; color:var(--gray-600);">Avanzamento risposta domande</span>
<span style="font-size:0.78rem; font-weight:700; color:var(--primary);" id="progress-pct">0%</span>
</div>
<div style="background:var(--gray-200); border-radius:99px; height:8px; overflow:hidden;">
<div id="progress-bar-fill" style="height:100%; background:var(--primary); border-radius:99px; transition:width 0.3s ease; width:0%;"></div>
</div>
<div style="font-size:0.7rem; color:var(--gray-400); margin-top:4px;">
<span id="progress-answered">0</span> di <span id="progress-total">0</span> domande con risposta
</div>
</div>
<!-- Question Card -->
<div class="card mb-24">
<div class="card-header">
<h3 id="category-title">Caricamento...</h3>
<span class="badge badge-neutral" id="question-counter">0/0</span>
</div>
<div class="card-body" id="question-area">
<div class="spinner-lg" style="margin:40px auto;"></div>
</div>
<div class="card-footer">
<div style="display:flex; justify-content:space-between; align-items:center;">
<button class="btn btn-secondary" id="btn-prev" onclick="prevQuestion()" disabled>
<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 class="btn btn-ghost btn-sm" onclick="saveCurrentResponse()">
Salva Progresso
</button>
<button class="btn btn-primary" id="btn-next" onclick="nextQuestion()">
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>
</button>
</div>
</div>
</div>
</div>
<!-- Results Container -->
<div id="assessment-results" style="display:none;">
<div class="card mb-24">
<div class="card-header">
<h3>Risultati Assessment</h3>
<button class="btn 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>
Analisi AI
</button>
</div>
<div class="card-body">
<!-- Overall Score -->
<div class="text-center mb-32" id="results-gauge"></div>
<!-- Category Scores -->
<h4 style="margin-bottom:16px; color:var(--gray-900);">Punteggio per Categoria</h4>
<div id="category-scores"></div>
</div>
</div>
<!-- AI Analysis result -->
<div class="card mb-24" id="ai-analysis-card" style="display:none;">
<div class="card-header">
<h3>Analisi AI</h3>
<span class="badge badge-info">Intelligenza Artificiale</span>
</div>
<div class="card-body" id="ai-analysis-content"></div>
</div>
<!-- Generate NCR from gaps -->
<div class="card mb-24" id="ncr-generation-card">
<div class="card-header">
<h3>Non Conformita'</h3>
</div>
<div class="card-body" style="text-align:center; padding:32px;">
<p style="color:var(--gray-600); margin-bottom:16px; font-size:0.875rem;">
Genera automaticamente le non conformita' (NCR) dai gap identificati nell'assessment.
I controlli con stato "Non Implementato" saranno classificati come <strong>major</strong>,
quelli "Parzialmente Implementato" come <strong>minor</strong>.
</p>
<button class="btn 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>
Genera Non Conformita' dai Gap
</button>
<div id="ncr-generation-result" style="display:none; margin-top:16px;"></div>
</div>
</div>
</div> </div>
</div> </div>
</main>
</div>
<!-- Question Card -->
<section class="card mb-24">
<div class="qcard-head">
<h3 id="category-title">Caricamento...</h3>
<span class="badge badge-neutral" id="question-counter">0/0</span>
</div>
<div class="card-pad" id="question-area">
<div class="spinner-lg" style="margin:40px auto;"></div>
</div>
<div class="qcard-foot">
<button class="btn-secondary" id="btn-prev" onclick="prevQuestion()" disabled>
<i data-lucide="chevron-left" class="ic"></i> Precedente
</button>
<button class="btn-ghost btn-sm" onclick="saveCurrentResponse()">
Salva Progresso
</button>
<button class="btn-primary" id="btn-next" onclick="nextQuestion()">
Successiva <i data-lucide="chevron-right" class="ic"></i>
</button>
</div>
</section>
</div>
<!-- Results Container -->
<div id="assessment-results" style="display:none;">
<section class="card mb-24">
<div class="qcard-head">
<h3>Risultati Assessment</h3>
<button class="btn-outline btn-sm" onclick="aiAnalyze()">
<i data-lucide="sparkles" class="ic"></i> Analisi AI
</button>
</div>
<div class="card-pad">
<!-- Overall Score -->
<div style="text-align:center;" class="mb-24" id="results-gauge"></div>
<!-- Category Scores -->
<h4 style="margin:0 0 16px; font-size:15px; font-weight:700;">Punteggio per Categoria</h4>
<div id="category-scores"></div>
</div>
</section>
<!-- AI Analysis result -->
<section class="card mb-24" id="ai-analysis-card" style="display:none;">
<div class="qcard-head">
<h3>Analisi AI</h3>
<span class="badge badge-info">Intelligenza Artificiale</span>
</div>
<div class="card-pad" id="ai-analysis-content"></div>
</section>
<!-- Generate NCR from gaps -->
<section class="card mb-24" id="ncr-generation-card">
<div class="qcard-head">
<h3>Non Conformità</h3>
</div>
<div class="center-card">
<p>
Genera automaticamente le non conformità (NCR) dai gap identificati nell'assessment.
I controlli con stato "Non Implementato" saranno classificati come <strong>major</strong>,
quelli "Parzialmente Implementato" come <strong>minor</strong>.
</p>
<button class="btn-primary" id="btn-generate-ncr" onclick="generateNCRs()">
<i data-lucide="file-plus" class="ic"></i> Genera Non Conformità dai Gap
</button>
<div id="ncr-generation-result" style="display:none; margin-top:16px; text-align:left;"></div>
</div>
</section>
</div>
</main>
<!-- 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>
+33 -33
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>
</header> <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 class="content-body"> <div class="content-body">
<!-- Stats Cards --> <!-- Stats Cards -->
@@ -367,29 +373,23 @@
</div> </div>
</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 ──────────────────────────────────────────────
+202 -163
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>
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="cal.title">Calendario</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="cal.title">Calendario scadenze</h1>
<p class="sub">Vista temporale aggregata di scadenze, revisioni e controlli del sistema</p>
</div>
</div>
<!-- Barra: navigazione mese + selettore vista --> <div class="cal-app">
<div class="cal-bar"> <!-- Sidebar: KPI + legenda/filtri categoria + stato -->
<div class="cal-nav"> <aside class="cal-side">
<button class="btn btn-outline-primary btn-sm" type="button" onclick="calPrevMonth()" aria-label="Mese precedente">&lsaquo;</button> <div class="cal-kpis" id="cal-summary" aria-live="polite"></div>
<h3 id="cal-month-label">—</h3>
<button class="btn btn-outline-primary btn-sm" type="button" onclick="calNextMonth()" aria-label="Mese successivo">&rsaquo;</button>
<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>
<!-- Filtri --> <div class="legend">
<div class="cal-filters"> <p>Filtra per tipo</p>
<h4>Filtra per tipo</h4>
<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="cal-chips" id="cal-status-chips"></div> <div class="legend">
<p>Filtra per stato</p>
<div class="cal-chips" id="cal-status-chips"></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> </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>
</div> </section>
</main> </div>
</div> </main>
<!-- 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 key = ymd(cur);
var cur = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + (w * 7 + d)); var inMonth = (cur.getMonth() === month);
var key = ymd(cur); var evs = byDay[key] || [];
var inMonth = (cur.getMonth() === month); var cls = 'mc' + (inMonth ? '' : ' other') + (key === todayStr ? ' today' : '') + (evs.length ? ' has' : '');
var cls = inMonth ? '' : ' cal-out'; var chips = '';
if (key === todayStr) cls += ' cal-today'; evs.slice(0, 3).forEach(function (e) {
var evs = byDay[key] || []; var c = (TYPE_META[e.type] || {}).color || '#64748b';
var dots = ''; var tip = escAttr((TYPE_META[e.type] || { label: e.type }).label + ' — ' + e.title);
var shown = evs.slice(0, 6); chips += '<div class="chip" title="' + tip + '"><span class="cdot" style="background:' + c + '"></span>' + esc(e.title) + '</div>';
shown.forEach(function (e) { });
var c = (TYPE_META[e.type] || {}).color || '#64748b'; var more = evs.length > 3 ? '<div class="more">+' + (evs.length - 3) + ' altri</div>' : '';
var tip = escAttr((TYPE_META[e.type] || { label: e.type }).label + ' — ' + e.title); var click = evs.length ? ' role="button" tabindex="0" aria-label="' + escAttr(cur.getDate() + ': ' + evs.length + ' scadenze') + '" data-day="' + key + '"' : '';
dots += '<span class="cal-dot st-' + esc(e.status) + '" style="background:' + c + '" title="' + tip + '" onclick="calPopDay(\'' + key + '\')"></span>'; html += '<div class="' + cls + '"' + click + '>' +
}); '<div class="dnum">' + cur.getDate() + '</div>' + chips + more + '</div>';
var more = evs.length > shown.length ? '<div class="cal-more" onclick="calPopDay(\'' + key + '\')">+' + (evs.length - shown.length) + ' 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 + '\');}"' : '';
html += '<td class="' + cls + '">' +
'<div class="cal-daynum"' + clickDay + '>' + cur.getDate() + '</div>' +
(evs.length ? '<div class="cal-day-dots">' + dots + '</div>' + more : '') +
'</td>';
}
html += '</tr>';
} }
el('cal-grid-body').innerHTML = html; var grid = el('mv-grid');
grid.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('');
+130 -339
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,80 +75,76 @@
<!-- 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="main-content">
<div class="page-container">
<!-- Page Header -->
<div class="companies-header" id="nis2-companies-header">
<div>
<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>
</div>
<div class="companies-header-actions">
<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>
<input type="text" class="form-input" id="search-input" placeholder="Cerca azienda..." oninput="filterCompanies(this.value)">
</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>
<!-- Companies Grid -->
<div class="companies-grid" id="companies-grid">
<!-- Skeleton loaders -->
<div class="skeleton-card">
<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 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 class="skeleton" style="height:6px"></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>
<div class="skeleton-card">
<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 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 class="skeleton" style="height:6px"></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>
</div>
<!-- Empty state (hidden initially) -->
<div class="companies-empty" id="companies-empty" style="display:none">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z"/>
</svg>
<h3>Nessuna azienda ancora</h3>
<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">
<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 Primo Cliente
</a>
</div>
<main class="wrap">
<nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>Le Mie Aziende</span>
</nav>
<div class="page-head" id="nis2-companies-header">
<div>
<h1>Le Mie Aziende</h1>
<p class="sub">Gestisci il portafoglio clienti e monitora la compliance NIS2</p>
</div> </div>
</main> <a href="onboarding.html" class="btn-primary" id="nis2-companies-add-btn">
</div> <i data-lucide="plus" class="ic"></i>Aggiungi Cliente
</a>
</div>
<script src="js/api.js?v=20260631"></script> <section class="card card-pad">
<script src="js/common.js?v=20260624g"></script> <div class="companies-toolbar">
<!-- Bootstrap Italia bundle + override sidebar BI (common-bi.js dopo common.js: ridefinisce solo loadSidebar) --> <div class="search-box">
<script src="vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script> <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>
<script>if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('vendor/bootstrap-italia/dist/fonts'); }</script> <input type="text" id="search-input" placeholder="Cerca azienda..." oninput="filterCompanies(this.value)" aria-label="Cerca azienda">
<script src="js/common-bi.js?v=20260625d"></script> </div>
<script src="js/i18n.js?v=20260631"></script> </div>
<script src="js/help.js?v=20260625d"></script>
<!-- Companies Grid -->
<div class="companies-grid" id="companies-grid">
<!-- Skeleton loaders -->
<div class="skeleton-card">
<div style="display:flex;gap:12px;align-items:center">
<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>
<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 class="skeleton" style="height:36px"></div>
</div>
<div class="skeleton-card">
<div style="display:flex;gap:12px;align-items:center">
<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>
<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 class="skeleton" style="height:36px"></div>
</div>
</div>
<!-- Empty state (hidden initially) -->
<div class="companies-empty" id="companies-empty" style="display:none">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z"/>
</svg>
<h3>Nessuna azienda ancora</h3>
<p>Aggiungi il tuo primo cliente per iniziare a monitorare la compliance NIS2.</p>
<a href="onboarding.html" class="btn-primary" id="nis2-companies-add-first-btn" style="display:inline-flex">
<i data-lucide="plus" class="ic"></i>Aggiungi Primo Cliente
</a>
</div>
</section>
</main>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide → topnav-v3.js (chrome) → i18n → help → init pagina -->
<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>
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>`);
} }
+86 -65
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">
<header class="content-header">
<h2 data-i18n="comp.title">Competenze</h2>
<div class="content-header-actions">
<span class="savehint" id="cmp-hint" role="status" aria-live="polite"></span>
</div>
</header>
<div class="content-body"> <main class="wrap">
<nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>Struttura interna</span>
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="comp.title">Competenze</span>
</nav>
<div class="page-head">
<div>
<h1 data-i18n="comp.title">Competenze</h1>
<p class="sub">Skill matrix, requisiti per ruolo e copertura dei gap</p>
</div>
<span class="savehint" id="cmp-hint" role="status" aria-live="polite"></span>
</div>
<div class="stack">
<section class="card card-pad">
<div class="cmp-intro"> <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>
</div> </section>
</main> </div>
</div> </main>
<!-- 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>
+44 -33
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>
</div> </nav>
</header> <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 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>
@@ -82,26 +94,25 @@
</div> </div>
</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>
+40 -20
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>
</div> <i data-lucide="chevron-right" class="ic"></i><span data-i18n="pctl.title">Controlli periodici</span>
</header> </nav>
<div class="content-body"> <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>
<button class="btn-primary" onclick="pcOpen()"><i data-lucide="plus" class="ic"></i>Nuovo controllo</button>
</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 = []; }
+480 -629
View File
File diff suppressed because it is too large Load Diff
+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); }
+129 -86
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> <div class="page-head">
</header> <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 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à.
@@ -1139,47 +1173,56 @@
</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>
+30 -29
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> <div class="page-head">
</header> <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 class="content-body"> <div class="content-body">
<!-- List View --> <!-- List View -->
@@ -348,29 +355,23 @@
<!-- Detail View --> <!-- Detail View -->
<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 = {
+56 -27
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">
<h1>Integrazioni Esterne</h1> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<p>API, webhook e connettori per sistemi GRC di terze parti</p> <i data-lucide="chevron-right" class="ic"></i><span>Integrazioni Esterne</span>
</nav>
<div class="page-head">
<div>
<h1>Integrazioni Esterne</h1>
<p class="sub">API, webhook e connettori per sistemi GRC di terze parti</p>
</div>
</div> </div>
<!-- Hero --> <!-- Hero -->
@@ -912,20 +929,32 @@ 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');
document.querySelectorAll('.tab-btn').forEach((b, i) => { loadSidebar(); // = loadTopnavV3()
const ids = ['api', 'invites', 'lg231', 'webhook', 'quickstart']; if (typeof I18n !== 'undefined') I18n.init();
b.classList.toggle('active', ids[i] === id); if (typeof HelpSystem !== 'undefined') HelpSystem.init();
}); if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
document.querySelectorAll('.tab-pane').forEach(p => {
p.classList.toggle('active', p.id === 'tab-' + id); function showTab(id) {
}); document.querySelectorAll('.tab-btn').forEach((b, i) => {
// scroll to tabs const ids = ['api', 'invites', 'lg231', 'webhook', 'quickstart'];
document.querySelector('.tab-nav').scrollIntoView({ behavior: 'smooth', block: 'start' }); b.classList.toggle('active', ids[i] === id);
} });
document.querySelectorAll('.tab-pane').forEach(p => {
p.classList.toggle('active', p.id === 'tab-' + id);
});
// scroll to tabs
document.querySelector('.tab-nav').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
</script> </script>
</body> </body>
</html> </html>
+40 -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>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();
+173 -159
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,142 +67,151 @@
<!-- 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="isms.title">Modello Organizzativo SGSI (ISO 27001)</h2>
<div class="content-header-actions">
<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">
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-file"></use></svg>
<span>Esporta SGSI</span>
</button>
</div>
</header>
<div class="content-body"> <main class="wrap">
<div class="isms-intro" id="nis2-isms-intro"> <nav class="crumb" aria-label="breadcrumb">
<strong data-i18n="isms.intro.title">Costruisci il tuo Sistema di Gestione della Sicurezza delle Informazioni.</strong> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<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> <i data-lucide="chevron-right" class="ic"></i><span>SGSI (ISO 27001)</span>
</div> </nav>
<div class="isms-disclaimer" id="nis2-isms-disclaimer" data-i18n="isms.disclaimer">Strumento di supporto e pre-audit: non costituisce una certificazione ISO 27001 né un parere professionale vincolante. Gli obblighi normativi in Italia derivano da NIS2 / D.Lgs. 138/2024 / Determinazioni ACN; ISO 27001/27017/27018 sono best practice.</div> <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>
<button class="btn-ghost" id="nis2-isms-btn-export" onclick="ismsExportView()" data-i18n="isms.export">
<i data-lucide="file-text" class="ic"></i>
<span>Esporta SGSI</span>
</button>
</div>
</div>
<div class="isms-steps" id="isms-steps"></div> <section class="card card-pad">
<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>
<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>
</div>
<div class="isms-disclaimer" id="nis2-isms-disclaimer" data-i18n="isms.disclaimer">Strumento di supporto e pre-audit: non costituisce una certificazione ISO 27001 né un parere professionale vincolante. Gli obblighi normativi in Italia derivano da NIS2 / D.Lgs. 138/2024 / Determinazioni ACN; ISO 27001/27017/27018 sono best practice.</div>
<!-- STEP 1 - Contesto & Ambito (cl.4) --> <div class="isms-steps" id="isms-steps"></div>
<div class="isms-panel" id="panel-0">
<div class="card"><div class="card-body">
<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-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-ext" data-i18n="isms.s1.ctx_ext">Contesto esterno</label><textarea id="f-ctx-ext" rows="2"></textarea></div>
<div class="isms-field"><label for="f-parties" data-i18n="isms.s1.parties">Parti interessate (una per riga)</label><textarea id="f-parties" rows="3" placeholder="Clienti&#10;Autorità (ACN/CSIRT)&#10;Fornitori ICT"></textarea></div>
<div class="isms-field"><label for="f-boundaries" data-i18n="isms.s1.boundaries">Confini del SGSI</label><textarea id="f-boundaries" rows="2"></textarea></div>
<div class="isms-field"><label for="f-exclusions" data-i18n="isms.s1.exclusions">Esclusioni motivate</label><textarea id="f-exclusions" rows="2"></textarea></div>
<div class="isms-flags">
<strong data-i18n="isms.s1.cloud">Estensioni cloud (attivano i controlli condizionali nel SoA)</strong>
<label><input type="checkbox" id="f-uses-cloud"> <span data-i18n="isms.s1.uses_cloud">Usiamo servizi cloud pubblici (Microsoft 365, AWS, Google...) → ISO 27017</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>
</div>
<button class="btn btn-primary" id="nis2-isms-btn-save-step1" onclick="saveStep1()" data-i18n="isms.save_next">Salva e continua</button>
</div></div>
</div>
<!-- STEP 2 - Leadership (cl.5) --> <!-- STEP 1 - Contesto & Ambito (cl.4) -->
<div class="isms-panel" id="panel-1"> <div class="isms-panel" id="panel-0">
<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.s1.title">1. Contesto e Ambito (cl. 4)</h3>
<p class="text-muted" data-i18n="isms.s2.hint">Definisci i ruoli del SGSI con la matrice RACI (Responsible, Accountable, Consulted, Informed).</p> <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 id="roles-list"></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="raci-row" style="border-top:2px solid var(--gray-200,#e5e7eb); margin-top:10px;"> <div class="isms-field"><label for="f-ctx-ext" data-i18n="isms.s1.ctx_ext">Contesto esterno</label><textarea id="f-ctx-ext" rows="2"></textarea></div>
<input id="r-name" placeholder="Ruolo (es. CISO)" aria-label="Ruolo" style="flex:1 1 160px;"> <div class="isms-field"><label for="f-parties" data-i18n="isms.s1.parties">Parti interessate (una per riga)</label><textarea id="f-parties" rows="3" placeholder="Clienti&#10;Autorità (ACN/CSIRT)&#10;Fornitori ICT"></textarea></div>
<input id="r-resp" placeholder="Responsabilità" aria-label="Responsabilità" style="flex:2 1 220px;"> <div class="isms-field"><label for="f-boundaries" data-i18n="isms.s1.boundaries">Confini del SGSI</label><textarea id="f-boundaries" rows="2"></textarea></div>
<select id="r-raci" aria-label="RACI"><option value="">RACI</option><option>R</option><option>A</option><option>C</option><option>I</option></select> <div class="isms-field"><label for="f-exclusions" data-i18n="isms.s1.exclusions">Esclusioni motivate</label><textarea id="f-exclusions" rows="2"></textarea></div>
<button class="btn btn-sm btn-primary" id="nis2-isms-btn-add-role" onclick="addRole()" data-i18n="isms.s2.add">Aggiungi ruolo</button> <div class="isms-flags">
</div> <strong data-i18n="isms.s1.cloud">Estensioni cloud (attivano i controlli condizionali nel SoA)</strong>
<button class="btn btn-outline" id="nis2-isms-btn-next-step2" style="margin-top:14px;" onclick="goStep(2)" data-i18n="isms.next">Continua</button> <label><input type="checkbox" id="f-uses-cloud"> <span data-i18n="isms.s1.uses_cloud">Usiamo servizi cloud pubblici (Microsoft 365, AWS, Google...) → ISO 27017</span></label>
</div></div> <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>
</div> <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>
<!-- STEP 3 - Risk (cl.6) --> <button class="btn-primary" id="nis2-isms-btn-save-step1" onclick="saveStep1()" data-i18n="isms.save_next">Salva e continua</button>
<div class="isms-panel" id="panel-2">
<div class="card"><div class="card-body">
<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-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>
<button class="btn btn-primary" id="nis2-isms-btn-save-step3" onclick="saveStep3()" data-i18n="isms.save_next">Salva e continua</button>
</div></div>
</div>
<!-- STEP 4 - SoA -->
<div class="isms-panel" id="panel-3">
<div class="card"><div class="card-body">
<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>
<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>
<span id="soa-stats" class="text-muted" style="margin-left:12px; font-size:.85rem;"></span>
</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>
</div></div>
</div>
<!-- STEP 5 - Documented information (cl.7-8) -->
<div class="isms-panel" id="panel-4">
<div class="card"><div class="card-body">
<h3 data-i18n="isms.s5.title">5. Documented Information (cl. 7-8)</h3>
<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>
<select id="d-type" style="flex:1 1 200px;" aria-label="Tipo di documento">
<option value="policy_sgsi">Policy SGSI</option>
<option value="dichiarazione_applicabilita">Dichiarazione di Applicabilità</option>
<option value="politica_controllo_accessi">Politica controllo accessi</option>
<option value="procedura_incident">Procedura gestione incidenti</option>
<option value="procedura_continuita">Procedura continuità operativa</option>
<option value="politica_fornitori">Politica sicurezza fornitori</option>
</select>
<button class="btn btn-sm btn-primary" onclick="aiGenDoc()" id="btn-aigen" data-i18n="isms.s5.aigen">Genera bozza con AI</button>
</div>
<div class="savehint" id="aigen-hint" role="status" aria-live="polite"></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>
</div></div>
</div>
<!-- STEP 6 - Monitoring & Readiness (cl.9-10) -->
<div class="isms-panel" id="panel-5">
<div class="card"><div class="card-body">
<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>
<h4 data-i18n="isms.s6.readiness">Completamento del SGSI</h4>
<div style="text-align:center; margin:14px 0;">
<div style="font-size:2.6rem; font-weight:800;" id="readiness-pct">0%</div>
</div>
<div id="readiness-checklist"></div>
</div></div>
</div> </div>
</div> </div>
</main>
</div>
<!-- STEP 2 - Leadership (cl.5) -->
<div class="isms-panel" id="panel-1">
<div class="card card-pad">
<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>
<div id="roles-list"></div>
<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-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>
<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>
<!-- STEP 3 - Risk (cl.6) -->
<div class="isms-panel" id="panel-2">
<div class="card card-pad">
<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-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>
<button class="btn-primary" id="nis2-isms-btn-save-step3" onclick="saveStep3()" data-i18n="isms.save_next">Salva e continua</button>
</div>
</div>
<!-- STEP 4 - SoA -->
<div class="isms-panel" id="panel-3">
<div class="card card-pad">
<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>
<div style="margin:10px 0;">
<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>
</div>
<div id="soa-groups"></div>
<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>
<!-- STEP 5 - Documented information (cl.7-8) -->
<div class="isms-panel" id="panel-4">
<div class="card card-pad">
<h3 data-i18n="isms.s5.title">5. Documented Information (cl. 7-8)</h3>
<div class="raci-row" style="border:none;">
<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">
<option value="policy_sgsi">Policy SGSI</option>
<option value="dichiarazione_applicabilita">Dichiarazione di Applicabilità</option>
<option value="politica_controllo_accessi">Politica controllo accessi</option>
<option value="procedura_incident">Procedura gestione incidenti</option>
<option value="procedura_continuita">Procedura continuità operativa</option>
<option value="politica_fornitori">Politica sicurezza fornitori</option>
</select>
<button class="btn-primary btn-sm" onclick="aiGenDoc()" id="btn-aigen" data-i18n="isms.s5.aigen">Genera bozza con AI</button>
</div>
<div class="savehint" id="aigen-hint" role="status" aria-live="polite"></div>
<div id="docs-list" style="margin-top:14px;"></div>
<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>
<!-- STEP 6 - Monitoring & Readiness (cl.9-10) -->
<div class="isms-panel" id="panel-5">
<div class="card card-pad">
<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>
<h4 data-i18n="isms.s6.readiness">Completamento del SGSI</h4>
<div style="text-align:center; margin:14px 0;">
<div style="font-size:2.6rem; font-weight:800; color:var(--c-blue);" id="readiness-pct">0%</div>
</div>
<div id="readiness-checklist"></div>
</div>
</div>
</section>
</main>
<!-- 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>
+62 -44
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>
</button> <h1>Knowledge Base</h1>
</div> <p class="sub">Documenti che l'AI usa per rispondere con citazioni precise</p>
</header> </div>
<button class="btn-primary" id="btn-kb-upload">
<i data-lucide="upload" class="ic"></i>Carica documento
</button>
</div>
<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,47 +155,42 @@
</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;">
<input type="text" id="kb-search-query" class="form-control" placeholder="Es: come gestire un incidente NIS2?" aria-label="Query di ricerca semantica"> <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">
</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>
</div> </div>
</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>
+40 -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>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();
+112 -114
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,56 +83,62 @@
<!-- 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="mr.title">Misure e Requisiti</h2>
<div class="content-header-actions">
<span class="savehint" id="mr-hint" role="status" aria-live="polite"></span>
</div>
</header>
<div class="content-body"> <main class="wrap">
<div class="mr-intro"> <nav class="crumb" aria-label="breadcrumb">
<strong>L'elenco canonico delle misure di sicurezza di base e dei relativi requisiti.</strong> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
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. <i data-lucide="chevron-right" class="ic"></i><span>Lex e ISO</span>
</div> <i data-lucide="chevron-right" class="ic"></i><span>Misure e requisiti</span>
<div class="mr-ro"> </nav>
🔒 <span>Elenco di riferimento, <strong>sola lettura</strong>: la codifica (misure, requisiti, ordine) non è modificabile. Policy e rischi qui elencati vengono proposti come <em>default</em> nei rispettivi moduli, dove potrai personalizzarli.</span> <div class="page-head">
</div> <div>
<div class="mr-note"> <h1 data-i18n="mr.title">Misure e Requisiti</h1>
Fonte: <strong>Determinazione ACN n. 164179 del 14 aprile 2025</strong> (misure di base, Allegati 1–2). Le misure applicabili dipendono dalla classe del soggetto: <strong>importanti</strong> 37 misure / 92 requisiti, <strong>essenziali</strong> 43 / 116. Strumento di supporto, non un parere legale. <p class="sub">Elenco canonico delle misure di base ACN e dei relativi requisiti</p>
</div>
<div class="mr-stats" id="mr-stats" style="display:none;">
<div class="mr-stat"><div class="n" id="mr-stat-mis">0</div><div class="l">Misure</div></div>
<div class="mr-stat"><div class="n" id="mr-stat-req">0</div><div class="l">Requisiti</div></div>
<div class="mr-stat"><div class="n" id="mr-stat-active">0</div><div class="l">Applicabili al soggetto</div></div>
</div>
<div class="mr-toolbar">
<label for="mr-view">Vista:</label>
<select id="mr-view" onchange="mrRender()">
<option value="misura">Per misura</option>
<option value="area">Per area politica</option>
</select>
<label for="mr-class">Soggetto:</label>
<select id="mr-class" onchange="mrRender()">
<option value="auto">Automatico (dalla mia org)</option>
<option value="important">Importante</option>
<option value="essential">Essenziale</option>
<option value="all">Tutti (mostra tutto)</option>
</select>
<label style="font-weight:500;"><input type="checkbox" id="mr-hide" onchange="mrRender()"> nascondi non applicabili</label>
<span class="grow"></span>
<input type="search" id="mr-q" placeholder="Cerca codice o testo…" oninput="mrRender()" style="min-width:200px;">
</div>
<div id="mr-list" aria-live="polite"><div class="mr-empty">Caricamento…</div></div>
</div> </div>
</main> <span class="savehint" id="mr-hint" role="status" aria-live="polite"></span>
</div> </div>
<section class="card card-pad">
<div class="mr-intro">
<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.
</div>
<div class="mr-ro">
🔒 <span>Elenco di riferimento, <strong>sola lettura</strong>: la codifica (misure, requisiti, ordine) non è modificabile. Policy e rischi qui elencati vengono proposti come <em>default</em> nei rispettivi moduli, dove potrai personalizzarli.</span>
</div>
<div class="mr-note">
Fonte: <strong>Determinazione ACN n. 164179 del 14 aprile 2025</strong> (misure di base, Allegati 1–2). Le misure applicabili dipendono dalla classe del soggetto: <strong>importanti</strong> 37 misure / 92 requisiti, <strong>essenziali</strong> 43 / 116. Strumento di supporto, non un parere legale.
</div>
<div class="mr-stats" id="mr-stats" style="display:none;">
<div class="mr-stat"><div class="n" id="mr-stat-mis">0</div><div class="l">Misure</div></div>
<div class="mr-stat"><div class="n" id="mr-stat-req">0</div><div class="l">Requisiti</div></div>
<div class="mr-stat"><div class="n" id="mr-stat-active">0</div><div class="l">Applicabili al soggetto</div></div>
</div>
<div class="mr-toolbar">
<label for="mr-view">Vista:</label>
<select id="mr-view" onchange="mrRender()">
<option value="misura">Per misura</option>
<option value="area">Per area politica</option>
</select>
<label for="mr-class">Soggetto:</label>
<select id="mr-class" onchange="mrRender()">
<option value="auto">Automatico (dalla mia org)</option>
<option value="important">Importante</option>
<option value="essential">Essenziale</option>
<option value="all">Tutti (mostra tutto)</option>
</select>
<label style="font-weight:500;"><input type="checkbox" id="mr-hide" onchange="mrRender()"> nascondi non applicabili</label>
<span class="grow"></span>
<input type="search" id="mr-q" placeholder="Cerca codice o testo…" oninput="mrRender()" style="min-width:200px;">
</div>
<div id="mr-list" aria-live="polite"><div class="mr-empty">Caricamento…</div></div>
</section>
</main>
<!-- 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');
+166 -147
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,95 +76,82 @@
<!-- 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 class="stack">
<!-- Stats (dati reali da /normative/stats) -->
<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>
<!-- Stats --> <!-- Avanzamento presa visione -->
<div class="stats-grid mb-24" id="stats-grid"> <section class="card card-pad">
<div class="stat-card"><div class="spinner"></div></div> <div class="sec-head"><h3>Avanzamento presa visione</h3><span class="hint" id="ack-pct">—</span></div>
</div> <div class="nrm-progress-track"><div class="nrm-progress-fill" id="ack-bar" style="width:0%"></div></div>
<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>
</section>
<!-- Ack Progress bar --> <!-- Filtri -->
<div class="card mb-24"> <section class="card card-pad">
<div class="card-body" style="padding:16px 20px;"> <div class="nrm-filters">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;"> <label for="filter-source" class="visually-hidden" style="position:absolute;width:1px;height:1px;overflow:hidden">Filtra per fonte normativa</label>
<span style="font-size:0.875rem; font-weight:600; color:var(--gray-700);">Avanzamento presa visione</span> <select id="filter-source" class="nrm-select" onchange="loadUpdates()" aria-label="Filtra per fonte normativa">
<span id="ack-pct" style="font-size:0.875rem; font-weight:700; color:var(--primary);">—</span> <option value="">Tutte le fonti</option>
</div> <option value="dlgs_138_2024">D.Lgs. 138/2024</option>
<div class="ack-progress"> <option value="nis2_directive">Direttiva NIS2</option>
<div class="ack-progress-bar" id="ack-bar" style="width:0%"></div> <option value="acn_guideline">ACN Guideline</option>
</div> <option value="dora">DORA</option>
<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> <option value="enisa">ENISA</option>
<option value="iso27001">ISO 27001</option>
</select>
<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="nrm-select" onchange="loadUpdates()" aria-label="Filtra per livello di impatto">
<option value="">Tutti i livelli</option>
<option value="critical">Critico</option>
<option value="high">Alto</option>
<option value="medium">Medio</option>
<option value="low">Basso</option>
<option value="informational">Informativo</option>
</select>
<label class="nrm-check"><input type="checkbox" id="filter-action" onchange="loadUpdates()"> Solo azione richiesta</label>
</div> </div>
</div> </section>
<!-- Filters --> <div id="updates-container"><div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div></div>
<div class="card mb-16"> </div>
<div class="card-body" style="padding:14px 20px;"> </main>
<div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
<label for="filter-source" class="visually-hidden">Filtra per fonte normativa</label>
<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="dlgs_138_2024">D.Lgs. 138/2024</option>
<option value="nis2_directive">Direttiva NIS2</option>
<option value="acn_guideline">ACN Guideline</option>
<option value="dora">DORA</option>
<option value="enisa">ENISA</option>
<option value="iso27001">ISO 27001</option>
</select>
<label for="filter-impact" class="visually-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">
<option value="">Tutti i livelli</option>
<option value="critical">Critico</option>
<option value="high">Alto</option>
<option value="medium">Medio</option>
<option value="low">Basso</option>
<option value="informational">Informativo</option>
</select>
<label style="display:flex; align-items:center; gap:8px; font-size:0.875rem; cursor:pointer;">
<input type="checkbox" id="filter-action" onchange="loadUpdates()" style="accent-color:var(--primary);">
Solo azione richiesta
</label>
</div>
</div>
</div>
<div id="updates-container"><div class="spinner" style="margin:60px auto;"></div></div>
</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>
+80 -53
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>
</div> </section>
</main> </div>
</div> </main>
<!-- 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>
+33 -33
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>
</header> <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 class="content-body" id="page-content"> <div class="content-body" id="page-content">
<!-- Tabs --> <!-- Tabs -->
@@ -330,29 +336,23 @@
<!-- Detail View (hidden by default) --> <!-- Detail View (hidden by default) -->
<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 = {
+75 -53
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">
<header class="content-header">
<h2 data-i18n="raci.title">Matrice RACI</h2>
<div class="content-header-actions">
<span class="savehint" id="raci-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>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>
</div>
<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>
</div> </section>
</main> </div>
</div> </main>
<!-- 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>
+194 -415
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,111 +141,110 @@
<!-- 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> <div class="page-head">
</header> <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 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>
<button onclick="switchTab('controls', this)">Controlli</button> <button onclick="switchTab('controls', this)">Controlli</button>
<button onclick="switchTab('audit', this)">Audit Log</button> <button onclick="switchTab('audit', this)">Audit Log</button>
<button onclick="switchTab('iso', this)">ISO 27001</button> <button onclick="switchTab('iso', this)">ISO 27001</button>
<button onclick="switchTab('monitoring', this)" data-i18n="audit.monitoring_tab">Monitoraggio Continuo</button> <button onclick="switchTab('monitoring', this)" data-i18n="audit.monitoring_tab">Monitoraggio Continuo</button>
<button onclick="switchTab('acn', this)">Requisiti ACN</button> <button onclick="switchTab('acn', this)">Requisiti ACN</button>
</div> </div>
<!-- Tab: Report Compliance --> <!-- Tab: Report Compliance -->
<div class="tab-panel active" id="tab-report"> <div class="tab-panel active" id="tab-report">
<div id="report-container"> <div id="report-container">
<div class="empty-state-box"> <div class="empty-state-box">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg> <svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>
<h4>Nessun report generato</h4> <h4>Nessun report generato</h4>
<p>Clicca "Genera Report" per creare un report di compliance aggiornato.</p> <p>Clicca "Genera Report" per creare un report di compliance aggiornato.</p>
</div>
</div>
</div>
<!-- Tab: Controlli -->
<div class="tab-panel" id="tab-controls">
<div id="controls-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento controlli...</p>
</div>
</div>
</div>
<!-- Tab: Audit Log -->
<div class="tab-panel" id="tab-audit">
<div id="audit-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento log...</p>
</div>
</div>
</div>
<!-- Tab: ISO 27001 -->
<div class="tab-panel" id="tab-iso">
<div id="iso-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento mapping...</p>
</div>
</div>
</div>
<!-- Tab: Continuous Control Monitoring -->
<div class="tab-panel" id="tab-monitoring">
<div id="monitoring-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento monitoraggio...</p>
</div>
</div>
</div>
<div class="tab-panel" id="tab-acn">
<div id="acn-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento requisiti ACN...</p>
</div>
</div> </div>
</div> </div>
</div> </div>
</main>
</div>
<!-- Tab: Controlli -->
<div class="tab-panel" id="tab-controls">
<div id="controls-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento controlli...</p>
</div>
</div>
</div>
<!-- Tab: Audit Log -->
<div class="tab-panel" id="tab-audit">
<div id="audit-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento log...</p>
</div>
</div>
</div>
<!-- Tab: ISO 27001 -->
<div class="tab-panel" id="tab-iso">
<div id="iso-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento mapping...</p>
</div>
</div>
</div>
<!-- Tab: Continuous Control Monitoring -->
<div class="tab-panel" id="tab-monitoring">
<div id="monitoring-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento monitoraggio...</p>
</div>
</div>
</div>
<div class="tab-panel" id="tab-acn">
<div id="acn-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento requisiti ACN...</p>
</div>
</div>
</div>
</section>
</main>
<!-- 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,14 +799,11 @@
} }
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> <p style="font-size:0.85rem; color:var(--gray-500); margin-bottom:20px;">
</div> Corrispondenza tra gli articoli della Direttiva NIS2 e i controlli dello standard ISO/IEC 27001:2022.
<div class="card-body"> </p>`;
<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.
</p>`;
mapping.forEach(m => { mapping.forEach(m => {
const isoControls = (m.iso27001 || '').split(',').map(c => c.trim()).filter(c => c); const isoControls = (m.iso27001 || '').split(',').map(c => c.trim()).filter(c => c);
@@ -1041,7 +820,7 @@
</div>`; </div>`;
}); });
html += '</div></div>'; html += '</div>';
container.innerHTML = html; container.innerHTML = html;
} }
+101 -85
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,49 +73,55 @@
<!-- 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">
<div class="rev-intro"> <nav class="crumb" aria-label="breadcrumb">
<strong>Revisioni e scadenze in un unico posto.</strong> <i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
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. <i data-lucide="chevron-right" class="ic"></i><span>Monitoraggio</span>
</div> <i data-lucide="chevron-right" class="ic"></i><span>Scadenziario</span>
<div class="rev-note"> </nav>
Ancoraggio: <strong>GV.PO-02</strong> (riesame periodico delle policy), <strong>GV.SC-07</strong> (revisione dei fornitori), <strong>PR.AT</strong>, <strong>DE.CM</strong> (monitoraggio continuo). Gli obblighi di gestione del rischio fanno capo all'<strong>art. 24 D.Lgs. 138/2024</strong>. La periodicità puntuale è una buona prassi dove non fissata da norma o contratto. Strumento di supporto, non un parere legale. <div class="page-head">
</div> <div>
<h1 data-i18n="rev.title">Scadenziario</h1>
<div class="rev-stats" id="rev-stats" style="display:none;"> <p class="sub">Revisioni periodiche di ruoli, competenze, inventario, procedure, rischi, fornitori e misure</p>
<div class="rev-stat"><div class="n" id="rev-stat-total">0</div><div class="l">Voci totali</div></div>
<div class="rev-stat s-overdue"><div class="n" id="rev-stat-overdue">0</div><div class="l">Scadute</div></div>
<div class="rev-stat s-due"><div class="n" id="rev-stat-due">0</div><div class="l">In scadenza (30 gg)</div></div>
<div class="rev-stat s-ok"><div class="n" id="rev-stat-ok">0</div><div class="l">In regola</div></div>
</div>
<div class="rev-toolbar">
<button class="btn btn-primary btn-sm" onclick="revOpen()">+ Nuova scadenza</button>
<button class="btn btn-outline btn-sm" id="rev-sync-btn" onclick="revDoSync()">Sincronizza scadenze esistenti</button>
<span class="grow"></span>
<label for="rev-filter" style="font-weight:600;font-size:.82rem;">Stato:</label>
<select id="rev-filter" onchange="revRender()">
<option value="all">Tutti</option>
<option value="overdue">Scadute</option>
<option value="due">In scadenza</option>
<option value="ok">In regola</option>
</select>
</div>
<div id="rev-list" aria-live="polite"></div>
</div> </div>
</main> <span class="savehint" id="rev-hint" role="status" aria-live="polite" style="font-size:12.5px;color:var(--muted)"></span>
</div> </div>
<section class="card card-pad">
<div class="rev-intro">
<strong>Revisioni e scadenze in un unico posto.</strong>
Tieni traccia delle revisioni periodiche di ruoli, competenze, inventario, procedure, rischi, fornitori e misure. Ogni voce mostra quando è prevista la prossima revisione e ti avvisa quando è in scadenza o scaduta.
</div>
<div class="rev-note">
Ancoraggio: <strong>GV.PO-02</strong> (riesame periodico delle policy), <strong>GV.SC-07</strong> (revisione dei fornitori), <strong>PR.AT</strong>, <strong>DE.CM</strong> (monitoraggio continuo). Gli obblighi di gestione del rischio fanno capo all'<strong>art. 24 D.Lgs. 138/2024</strong>. La periodicità puntuale è una buona prassi dove non fissata da norma o contratto. Strumento di supporto, non un parere legale.
</div>
<div class="rev-stats" id="rev-stats" style="display:none;">
<div class="rev-stat"><div class="n" id="rev-stat-total">0</div><div class="l">Voci totali</div></div>
<div class="rev-stat s-overdue"><div class="n" id="rev-stat-overdue">0</div><div class="l">Scadute</div></div>
<div class="rev-stat s-due"><div class="n" id="rev-stat-due">0</div><div class="l">In scadenza (30 gg)</div></div>
<div class="rev-stat s-ok"><div class="n" id="rev-stat-ok">0</div><div class="l">In regola</div></div>
</div>
<div class="rev-toolbar">
<button class="btn-primary" onclick="revOpen()"><i data-lucide="plus" class="ic"></i>Nuova scadenza</button>
<button class="btn-ghost" id="rev-sync-btn" onclick="revDoSync()"><i data-lucide="refresh-cw" class="ic"></i>Sincronizza scadenze esistenti</button>
<span class="grow"></span>
<label for="rev-filter" style="font-weight:600;font-size:13px;">Stato:</label>
<select id="rev-filter" onchange="revRender()">
<option value="all">Tutti</option>
<option value="overdue">Scadute</option>
<option value="due">In scadenza</option>
<option value="ok">In regola</option>
</select>
</div>
<div id="rev-list" aria-live="polite"></div>
</section>
</main>
<!-- Modale scadenza (crea/modifica) --> <!-- 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('');
+39 -38
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>
<div class="view-toggle" id="view-toggle"> </nav>
<button class="active" onclick="switchView('table')">Tabella</button> <div class="page-head">
<button onclick="switchView('matrix')">Matrice</button> <div>
<button onclick="switchView('fair')" data-i18n="risks.fair_tab">Quantitativo (FAIR)</button> <h1 data-i18n="risks.title">Gestione Rischi</h1>
<button onclick="switchView('kri')" data-i18n="risks.kri_tab">KRI</button> <p class="sub">Registro rischi, matrice 5×5, analisi quantitativa FAIR e KRI</p>
</div> </div>
<button class="btn btn-secondary" id="btn-ai-suggest" onclick="aiSuggest()"> <div class="content-header-actions" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<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> <div class="view-toggle" id="view-toggle">
AI Suggerisci <button class="active" onclick="switchView('table')">Tabella</button>
</button> <button onclick="switchView('matrix')">Matrice</button>
<button class="btn btn-primary" onclick="showRiskModal()">+ Nuovo Rischio</button> <button onclick="switchView('fair')" data-i18n="risks.fair_tab">Quantitativo (FAIR)</button>
<button onclick="switchView('kri')" data-i18n="risks.kri_tab">KRI</button>
</div> </div>
</header> <button class="btn-ghost" id="btn-ai-suggest" onclick="aiSuggest()">
<i data-lucide="sparkles" class="ic"></i>AI Suggerisci
</button>
<button class="btn-primary" onclick="showRiskModal()"><i data-lucide="plus" class="ic"></i>Nuovo Rischio</button>
</div>
</div>
<div class="content-body"> <div class="content-body">
<!-- Table View --> <!-- Table View -->
@@ -491,29 +498,23 @@
<!-- Detail View --> <!-- Detail View -->
<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 = {
+526 -486
View File
File diff suppressed because it is too large Load Diff
+38 -16
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>
@@ -106,8 +126,7 @@
<div id="tpl-list" aria-live="polite"></div> <div id="tpl-list" aria-live="polite"></div>
</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();
+42 -22
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),
@@ -129,8 +147,7 @@
<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) {
+97 -39
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>
</div> <p class="sub">Valutazione e monitoraggio del rischio della catena di fornitura (art. 24 D.Lgs. 138/2024)</p>
</header> </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 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">
@@ -474,29 +538,23 @@
<!-- Detail View (hidden by default) --> <!-- Detail View (hidden by default) -->
<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';
} }
} }
+139 -247
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> <div class="page-head">
</header> <div>
<h1 data-i18n="training.title">Formazione e Awareness</h1>
<p class="sub">Piani formativi, assegnazioni e compliance — Art. 20 NIS2</p>
</div>
<button class="btn-primary" onclick="showCreateCourseModal()"><i data-lucide="plus" class="ic"></i>Nuovo Corso</button>
</div>
<div class="content-body"> <div class="stack">
<section class="card card-pad">
<!-- Tab Navigation --> <!-- 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>
</div> </section>
</main> </div>
</div> </main>
<!-- Scripts V3: api.js → common.js (helper+FAB reali+showModal/showNotification) →
Lucide self-host → topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help.
La logica della pagina e' inline (nessun training.js): preservata invariata. -->
<script src="/js/api.js?v=20260625d"></script> <script src="/js/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>
` `
}); });
} }
+107 -62
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,25 +91,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">
<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>
</div> <i data-lucide="chevron-right" class="ic"></i><span>Segnalazioni</span>
<div class="page-header-actions"> </nav>
<button class="btn btn-secondary" onclick="switchView('track')"> <div class="page-head">
<svg class="icon-inline" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-search"></use></svg> <div>
Traccia Segnalazione Anonima <h1>Segnalazioni Sicurezza</h1>
</button> <p class="sub">Canale interno Art.32 NIS2 — Segnala violazioni e anomalie di sicurezza</p>
<button class="btn 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>
Nuova Segnalazione
</button>
</div>
</div> </div>
<div style="display:flex;gap:10px;flex-wrap:wrap">
<button class="btn-ghost" onclick="switchView('track')">
<i data-lucide="search" class="ic"></i>Traccia segnalazione anonima
</button>
<button class="btn-primary" onclick="switchView('submit')">
<i data-lucide="plus" class="ic"></i>Nuova segnalazione
</button>
</div>
</div>
<!-- Art.32 Banner --> <!-- Art.32 Banner -->
<div class="art32-banner"> <div class="art32-banner">
@@ -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>
@@ -202,30 +252,24 @@
</div> </div>
</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)}
+460 -633
View File
File diff suppressed because it is too large Load Diff