[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:
co-authored by
Claude Opus 4.8
parent
b0fc190704
commit
a4150b5389
@@ -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
@@ -3,184 +3,34 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Pannello Amministrazione - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css resta autorevole per i widget renderizzati via JS -> parita' funzionale.
|
||||
Pagina in /admin/: gli asset usano path ASSOLUTI (/vendor, /js, /css) serviti dal web root. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Pannello Amministrazione · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css. Pagina in /admin/: asset con path ASSOLUTI. -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<style>
|
||||
.admin-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.admin-stat-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--card-shadow);
|
||||
padding: 20px;
|
||||
transition: box-shadow var(--transition), transform var(--transition);
|
||||
}
|
||||
.admin-stat-card:hover {
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.admin-stat-label {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--gray-500);
|
||||
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;
|
||||
}
|
||||
}
|
||||
/* ── Grafico distribuzione piani (non coperto da v3.css) ────────── */
|
||||
.chart-container { padding:8px 4px 4px; }
|
||||
.bar-chart { display:flex; align-items:flex-end; gap:32px; height:200px; padding:0 20px; border-bottom:2px solid var(--line); }
|
||||
.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 .8s ease; min-height:4px; position:relative; }
|
||||
.bar-chart-bar.free { background:#94A3B8; }
|
||||
.bar-chart-bar.professional { background:var(--c-blue); }
|
||||
.bar-chart-bar.enterprise { background:#7C4DD6; }
|
||||
.bar-chart-value { font-size:.875rem; font-weight:700; color:var(--ink); margin-bottom:8px; }
|
||||
.bar-chart-label { font-size:.75rem; color:var(--muted); margin-top:12px; font-weight:600; text-transform:uppercase; letter-spacing:.03em; }
|
||||
@media (prefers-reduced-motion:reduce) { .bar-chart-bar{transition:none;} }
|
||||
|
||||
/* Quick links */
|
||||
.quick-links-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:16px; }
|
||||
.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; }
|
||||
.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; }
|
||||
.quick-link-icon .ic { width:22px; height:22px; }
|
||||
.quick-link-info h4 { font-size:.9375rem; font-weight:700; color:var(--ink); margin:0 0 2px; }
|
||||
.quick-link-info p { font-size:.8125rem; color:var(--muted); margin:0; }
|
||||
|
||||
@media (max-width:1024px) { .grid3 { grid-template-columns:repeat(2,1fr); } }
|
||||
@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; } }
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -197,144 +47,134 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2">Pannello Amministrazione</h1>
|
||||
<div class="content-header-actions">
|
||||
<span class="badge badge-danger">Super Admin</span>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="/dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Amministrazione</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Pannello Amministrazione</h1>
|
||||
<p class="sub">Panoramica della piattaforma e gestione globale</p>
|
||||
</div>
|
||||
<span class="badge-t red">Super Admin</span>
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
<!-- Stats Row 1 -->
|
||||
<div class="admin-stats-grid" id="stats-row-1">
|
||||
<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-pa"></use></svg>
|
||||
</div>
|
||||
<div class="admin-stat-label" id="lbl-total-orgs">Totale Organizzazioni</div>
|
||||
<div class="admin-stat-value" id="stat-total-orgs" aria-labelledby="lbl-total-orgs">--</div>
|
||||
</div>
|
||||
<div 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 class="stack">
|
||||
<!-- Stats Row 1 -->
|
||||
<div class="grid3" id="stats-row-1" style="grid-template-columns:repeat(4,1fr)">
|
||||
<div class="card card-pad stat blue">
|
||||
<div class="ibox"><i data-lucide="building-2" class="ic"></i></div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="lbl" id="lbl-total-orgs">Totale Organizzazioni</div>
|
||||
<div class="val" id="stat-total-orgs" aria-labelledby="lbl-total-orgs">--</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Row 2 -->
|
||||
<div class="admin-stats-grid" id="stats-row-2">
|
||||
<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-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 class="card card-pad stat green">
|
||||
<div class="ibox"><i data-lucide="check-circle" class="ic"></i></div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="lbl" id="lbl-active-orgs">Organizzazioni Attive</div>
|
||||
<div class="val" id="stat-active-orgs" aria-labelledby="lbl-active-orgs">--</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plans Distribution -->
|
||||
<div class="card mb-24">
|
||||
<div class="card-header">
|
||||
<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="spinner" style="margin:40px auto;"></div>
|
||||
</div>
|
||||
<div class="card card-pad stat blue">
|
||||
<div class="ibox"><i data-lucide="users" class="ic"></i></div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="lbl" id="lbl-total-users">Totale Utenti</div>
|
||||
<div class="val" id="stat-total-users" aria-labelledby="lbl-total-users">--</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Gestione Piattaforma</h3>
|
||||
</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 class="card card-pad stat green">
|
||||
<div class="ibox"><i data-lucide="user-check" class="ic"></i></div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="lbl" id="lbl-active-users">Utenti Attivi</div>
|
||||
<div class="val" id="stat-active-users" aria-labelledby="lbl-active-users">--</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
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 ──────────────────────────────────────────
|
||||
(async function checkAdmin() {
|
||||
|
||||
+76
-142
@@ -3,103 +3,47 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Gestione Organizzazioni - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (tabella org/paginazione/badge) -> parita' funzionale assoluta. BI veste la chrome.
|
||||
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">
|
||||
<title>Gestione Organizzazioni · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css. Pagina in /admin/: asset con path ASSOLUTI. -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<style>
|
||||
.entity-type-badge {
|
||||
display: inline-flex;
|
||||
padding: 3px 10px;
|
||||
border-radius: 100px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.entity-type-badge.essential { background: var(--danger-bg); color: var(--danger); }
|
||||
.entity-type-badge.important { background: var(--warning-bg); color: #a16207; }
|
||||
.entity-type-badge.not_applicable { background: var(--gray-100); color: var(--gray-600); }
|
||||
.plan-badge {
|
||||
display: inline-flex;
|
||||
padding: 3px 10px;
|
||||
border-radius: 100px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.plan-badge.free { background: var(--gray-100); color: var(--gray-600); }
|
||||
.plan-badge.professional { background: var(--primary-bg); color: var(--primary); }
|
||||
.plan-badge.enterprise { background: #f3e8ff; color: #7c3aed; }
|
||||
.status-dot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.status-dot::before {
|
||||
content: '';
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 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;
|
||||
}
|
||||
/* ── Tabella admin (v3.css non include stili tabella; markup sobrio, no CDN) ── */
|
||||
.table-container { width:100%; overflow-x:auto; }
|
||||
table { width:100%; border-collapse:collapse; font-size:13px; }
|
||||
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; }
|
||||
tbody td { padding:12px 14px; border-bottom:1px solid var(--line); vertical-align:middle; }
|
||||
tbody tr:last-child td { border-bottom:0; }
|
||||
tbody tr:hover { background:#F7FAFD; }
|
||||
td strong { font-weight:700; }
|
||||
.text-muted { color:var(--muted); }
|
||||
|
||||
.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.essential { background:#FBE9E9; color:#B23636; }
|
||||
.entity-type-badge.important { background:#FBF1E2; color:#9A6418; }
|
||||
.entity-type-badge.not_applicable { background:#EEF2F7; color:#5b6b80; }
|
||||
.plan-badge.free { background:#EEF2F7; color:#5b6b80; }
|
||||
.plan-badge.professional { background:#E7F0FB; color:#1F5FA8; }
|
||||
.plan-badge.enterprise { background:#F0E9FB; color:#7C4DD6; }
|
||||
|
||||
.status-dot { display:inline-flex; align-items:center; gap:6px; font-size:.8125rem; }
|
||||
.status-dot::before { content:''; width:8px; height:8px; border-radius:50%; flex-shrink:0; }
|
||||
.status-dot.active::before { background:var(--c-green); }
|
||||
.status-dot.inactive::before { background:#94A3B8; }
|
||||
|
||||
.score-inline { display:inline-flex; align-items:center; gap:6px; font-size:.8125rem; font-weight:600; }
|
||||
.score-inline .score-dot { width:8px; height:8px; border-radius:50%; }
|
||||
|
||||
.pagination { display:flex; align-items:center; justify-content:space-between; padding:14px 14px 2px; border-top:1px solid var(--line); margin-top:6px; }
|
||||
.pagination-info { font-size:.8125rem; color:var(--muted); }
|
||||
.pagination-controls { display:flex; gap:8px; }
|
||||
.btn-ghost[disabled] { opacity:.5; cursor:not-allowed; }
|
||||
.btn-sm { padding:6px 11px; font-size:11.5px; }
|
||||
|
||||
.empty-state { text-align:center; padding:60px 24px; color:var(--muted); }
|
||||
.empty-state .ic { width:48px; height:48px; color:var(--line); margin:0 auto 16px; display:block; }
|
||||
.empty-state h4 { font-size:1rem; font-weight:700; color:var(--ink); margin:0 0 8px; }
|
||||
.empty-state p { margin:0; }
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -116,55 +60,44 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2">Gestione Organizzazioni</h1>
|
||||
<div class="content-header-actions">
|
||||
<span class="badge badge-danger">Super Admin</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-body">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="admin-breadcrumb" aria-label="Percorso di navigazione">
|
||||
<a href="../admin/index.html">Amministrazione</a>
|
||||
<svg class="icon icon-sm" aria-hidden="true"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-chevron-right"></use></svg>
|
||||
<span aria-current="page">Organizzazioni</span>
|
||||
</nav>
|
||||
|
||||
<!-- 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>
|
||||
<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><a href="/admin/index.html">Amministrazione</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Organizzazioni</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Gestione Organizzazioni</h1>
|
||||
<p class="sub">Tutte le organizzazioni registrate sulla piattaforma</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<span class="badge-t red">Super Admin</span>
|
||||
</div>
|
||||
|
||||
<script src="../js/api.js?v=20260625d"></script>
|
||||
<script src="../js/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="../vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('../vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="../js/common-bi.js?v=20260625d"></script>
|
||||
<script src="../js/i18n.js?v=20260620"></script>
|
||||
<section class="card card-pad">
|
||||
<div id="orgs-container">
|
||||
<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>
|
||||
</div>
|
||||
</section>
|
||||
</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/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
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;
|
||||
|
||||
@@ -205,7 +138,7 @@
|
||||
// ── Caricamento ──────────────────────────────────────────
|
||||
async function loadOrganizations(page) {
|
||||
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;
|
||||
|
||||
try {
|
||||
@@ -231,10 +164,11 @@
|
||||
if (!orgs || orgs.length === 0) {
|
||||
container.innerHTML = `
|
||||
<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>
|
||||
<p>Le organizzazioni registrate appariranno qui.</p>
|
||||
</div>`;
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -304,10 +238,10 @@
|
||||
Visualizzati ${start}-${end} di ${total} organizzazioni
|
||||
</div>
|
||||
<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' : ''}>
|
||||
« Precedente
|
||||
</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 »
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+89
-169
@@ -3,125 +3,54 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Gestione Utenti - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (tabella utenti/azioni) -> parita' funzionale assoluta. BI veste la chrome.
|
||||
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">
|
||||
<title>Gestione Utenti · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css. Pagina in /admin/: asset con path ASSOLUTI. -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<style>
|
||||
.role-badge {
|
||||
display: inline-flex;
|
||||
padding: 3px 10px;
|
||||
border-radius: 100px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.role-badge.super_admin { background: var(--danger-bg); color: var(--danger); }
|
||||
.role-badge.org_admin { background: var(--primary-bg); color: var(--primary); }
|
||||
.role-badge.compliance_manager { background: var(--secondary-bg); color: #15803d; }
|
||||
.role-badge.board_member { background: #f3e8ff; color: #7c3aed; }
|
||||
.role-badge.auditor { background: var(--info-bg); color: var(--primary); }
|
||||
.role-badge.employee { background: var(--gray-100); color: var(--gray-600); }
|
||||
.active-toggle {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.active-toggle input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.toggle-slider {
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
background: var(--gray-300);
|
||||
border-radius: 10px;
|
||||
transition: background var(--transition-fast);
|
||||
position: relative;
|
||||
}
|
||||
.toggle-slider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
top: 2px;
|
||||
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);
|
||||
}
|
||||
/* ── Tabella admin (v3.css non include stili tabella; markup sobrio, no CDN) ── */
|
||||
.table-container { width:100%; overflow-x:auto; }
|
||||
table { width:100%; border-collapse:collapse; font-size:13px; }
|
||||
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; }
|
||||
tbody td { padding:12px 14px; border-bottom:1px solid var(--line); vertical-align:middle; }
|
||||
tbody tr:last-child td { border-bottom:0; }
|
||||
tbody tr:hover { background:#F7FAFD; }
|
||||
td strong { font-weight:700; }
|
||||
.text-muted { color:var(--muted); }
|
||||
|
||||
.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 { 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.super_admin { background:#FBE9E9; color:#B23636; }
|
||||
.role-badge.org_admin { background:#E7F0FB; color:#1F5FA8; }
|
||||
.role-badge.compliance_manager { background:#E6F4EC; color:#1E7A4E; }
|
||||
.role-badge.board_member { background:#F0E9FB; color:#7C4DD6; }
|
||||
.role-badge.auditor { background:#E7F0FB; color:#1F5FA8; }
|
||||
.role-badge.employee { background:#EEF2F7; color:#5b6b80; }
|
||||
|
||||
.active-toggle { position:relative; display:inline-flex; align-items:center; cursor:pointer; }
|
||||
.active-toggle input { position:absolute; opacity:0; width:0; height:0; }
|
||||
.toggle-slider { width:36px; height:20px; background:#CBD5E1; border-radius:10px; transition:background .15s; position:relative; }
|
||||
.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); }
|
||||
.active-toggle input:checked + .toggle-slider { background:var(--c-green); }
|
||||
.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; }
|
||||
@media (prefers-reduced-motion:reduce) { .toggle-slider, .toggle-slider::after { transition:none; } }
|
||||
|
||||
.org-tags { display:flex; flex-wrap:wrap; gap:4px; }
|
||||
.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; }
|
||||
|
||||
.pagination { display:flex; align-items:center; justify-content:space-between; padding:14px 14px 2px; border-top:1px solid var(--line); margin-top:6px; }
|
||||
.pagination-info { font-size:.8125rem; color:var(--muted); }
|
||||
.pagination-controls { display:flex; gap:8px; }
|
||||
.btn-ghost[disabled] { opacity:.5; cursor:not-allowed; }
|
||||
.btn-sm { padding:6px 11px; font-size:11.5px; }
|
||||
.btn-ghost .ic { color:var(--c-blue); }
|
||||
|
||||
.empty-state { text-align:center; padding:60px 24px; color:var(--muted); }
|
||||
.empty-state .ic { width:48px; height:48px; color:var(--line); margin:0 auto 16px; display:block; }
|
||||
.empty-state h4 { font-size:1rem; font-weight:700; color:var(--ink); margin:0 0 8px; }
|
||||
.empty-state p { margin:0; }
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -138,55 +67,44 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2">Gestione Utenti</h1>
|
||||
<div class="content-header-actions">
|
||||
<span class="badge badge-danger">Super Admin</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-body">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="admin-breadcrumb" aria-label="Percorso di navigazione">
|
||||
<a href="../admin/index.html">Amministrazione</a>
|
||||
<svg class="icon icon-sm" aria-hidden="true"><use href="../vendor/bootstrap-italia/dist/svg/sprites.svg#it-chevron-right"></use></svg>
|
||||
<span aria-current="page">Utenti</span>
|
||||
</nav>
|
||||
|
||||
<!-- 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>
|
||||
<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><a href="/admin/index.html">Amministrazione</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Utenti</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Gestione Utenti</h1>
|
||||
<p class="sub">Tutti gli utenti registrati sulla piattaforma</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<span class="badge-t red">Super Admin</span>
|
||||
</div>
|
||||
|
||||
<script src="../js/api.js?v=20260625d"></script>
|
||||
<script src="../js/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="../vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('../vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="../js/common-bi.js?v=20260625d"></script>
|
||||
<script src="../js/i18n.js?v=20260620"></script>
|
||||
<section class="card card-pad">
|
||||
<div id="users-container" aria-live="polite">
|
||||
<div class="v3-loading"><span class="v3-spin"></span>Caricamento utenti…</div>
|
||||
</div>
|
||||
</section>
|
||||
</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/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
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;
|
||||
|
||||
@@ -196,12 +114,12 @@
|
||||
const me = await api.getMe();
|
||||
if (!me.success || !me.data || me.data.role !== 'super_admin') {
|
||||
showNotification('Accesso non autorizzato.', 'error');
|
||||
setTimeout(() => { window.location.href = '../dashboard.html'; }, 1500);
|
||||
setTimeout(() => { window.location.href = '/dashboard.html'; }, 1500);
|
||||
return;
|
||||
}
|
||||
loadUsers(1);
|
||||
} catch (e) {
|
||||
window.location.href = '../dashboard.html';
|
||||
window.location.href = '/dashboard.html';
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -218,7 +136,7 @@
|
||||
// ── Caricamento ──────────────────────────────────────────
|
||||
async function loadUsers(page) {
|
||||
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;
|
||||
|
||||
try {
|
||||
@@ -244,10 +162,11 @@
|
||||
if (!users || users.length === 0) {
|
||||
container.innerHTML = `
|
||||
<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>
|
||||
<p>Gli utenti registrati appariranno qui.</p>
|
||||
</div>`;
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -306,8 +225,8 @@
|
||||
<td>${formatDate(user.created_at)}</td>
|
||||
<td>
|
||||
${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">
|
||||
<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>
|
||||
? `<button class="btn-ghost btn-sm" onclick="impersonateUser(${user.id}, '${escapeHtml(user.email)}')" title="Entra come questo utente per 1 ora">
|
||||
<i data-lucide="repeat" class="ic"></i>
|
||||
Impersonate
|
||||
</button>`
|
||||
: '<span class="text-muted">-</span>'}
|
||||
@@ -332,10 +251,10 @@
|
||||
Visualizzati ${start}-${end} di ${total} utenti
|
||||
</div>
|
||||
<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' : ''}>
|
||||
« Precedente
|
||||
</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 »
|
||||
</button>
|
||||
</div>
|
||||
@@ -351,6 +270,7 @@
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
}
|
||||
|
||||
// ── Toggle Attivo/Inattivo ───────────────────────────────
|
||||
@@ -388,7 +308,7 @@
|
||||
sessionStorage.setItem('nis2_impersonate_target_email', email);
|
||||
// Sostituisce token con quello impersonate (no refresh per default)
|
||||
api.setTokens(res.data.access_token, '');
|
||||
window.location.href = '../dashboard.html';
|
||||
window.location.href = '/dashboard.html';
|
||||
} else {
|
||||
showNotification(res.message || 'Impersonate fallito.', 'error');
|
||||
}
|
||||
|
||||
+59
-25
@@ -3,11 +3,39 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<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 -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="css/style.css?v=20260618">
|
||||
<title>Architettura · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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 ────────────────────────────────── */
|
||||
|
||||
.arch-section {
|
||||
@@ -634,18 +662,21 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2>Architettura del Sistema</h2>
|
||||
<div class="content-header-actions">
|
||||
<span class="badge badge-neutral">v1.0</span>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Architettura</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Architettura del Sistema</h1>
|
||||
<p class="sub">Stack tecnologico, modello dati e moduli della piattaforma</p>
|
||||
</div>
|
||||
<span class="badge-t blue">v1.0</span>
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
|
||||
@@ -1087,19 +1118,22 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="js/api.js?v=20260631"></script>
|
||||
<script src="js/common.js?v=20260624g"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="js/i18n.js?v=20260631"></script>
|
||||
<script src="js/help.js?v=20260625d"></script>
|
||||
<!-- 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/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>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
I18n.init();
|
||||
loadSidebar(); // = loadTopnavV3()
|
||||
if (typeof I18n !== 'undefined') I18n.init();
|
||||
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+323
-181
@@ -3,12 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Gap Analysis - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (wizard/form/gauge) -> parita' funzionale assoluta. BI veste la chrome. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>GAP Analysis · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css. Self-host only: no external font/script hosts. -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<meta name="theme-color" content="#0066CC">
|
||||
@@ -22,158 +20,273 @@
|
||||
<meta name="application-name" content="NIS2 Agile">
|
||||
<script src="/js/pwa.js?v=20260614" defer></script>
|
||||
<!-- 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>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2" data-i18n="assessment.title">Gap Analysis NIS2</h1>
|
||||
<div class="content-header-actions">
|
||||
<button class="btn btn-outline btn-sm" id="btn-ai-analyze" style="display:none;" onclick="aiAnalyze()">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
|
||||
Analisi AI
|
||||
</button>
|
||||
<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="assessment.title">Gap Analysis NIS2</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="assessment.title">Gap Analysis NIS2</h1>
|
||||
<p class="sub">Scostamento tra lo stato attuale e i requisiti della Direttiva (UE) 2022/2555</p>
|
||||
</div>
|
||||
<div class="content-header-actions">
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<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 class="ga-prog-track">
|
||||
<div id="progress-bar-fill" class="ga-prog-fill"></div>
|
||||
</div>
|
||||
|
||||
<!-- Wizard Container -->
|
||||
<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 class="ga-prog-sub">
|
||||
<span id="progress-answered">0</span> di <span id="progress-total">0</span> domande con risposta
|
||||
</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/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
I18n.init();
|
||||
HelpSystem.init();
|
||||
|
||||
// ── Init chrome + i18n + help ────────────────────────────
|
||||
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 ────────────────────────────────────────────────
|
||||
let currentAssessmentId = null;
|
||||
@@ -183,7 +296,7 @@
|
||||
let currentQuestionIdx = 0;
|
||||
let responses = {}; // questionId -> { answer, maturity, notes, evidence }
|
||||
|
||||
// ── Init ─────────────────────────────────────────────────
|
||||
// ── Init (dati reali) ────────────────────────────────────
|
||||
loadAssessments();
|
||||
|
||||
async function loadAssessments() {
|
||||
@@ -193,8 +306,8 @@
|
||||
const container = document.getElementById('existing-assessments');
|
||||
container.style.display = 'block';
|
||||
|
||||
let html = '<h4 style="margin-bottom:12px; color:var(--gray-700);">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>';
|
||||
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="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 => {
|
||||
const statusBadge = a.status === 'completed'
|
||||
@@ -204,11 +317,11 @@
|
||||
<tr>
|
||||
<td>${formatDate(a.created_at)}</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>
|
||||
${a.status === 'completed'
|
||||
? `<button class="btn btn-sm btn-ghost" onclick="viewResults('${a.id}')">Risultati</button>`
|
||||
: `<button class="btn btn-sm btn-primary" onclick="resumeAssessment('${a.id}')">Riprendi</button>`
|
||||
? `<button class="btn-ghost btn-sm" onclick="viewResults('${a.id}')">Risultati</button>`
|
||||
: `<button class="btn-primary btn-sm" onclick="resumeAssessment('${a.id}')">Riprendi</button>`
|
||||
}
|
||||
</td>
|
||||
</tr>`;
|
||||
@@ -216,12 +329,38 @@
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
container.innerHTML = html;
|
||||
refreshIcons();
|
||||
|
||||
// KPI sintetici (dati reali)
|
||||
renderKpis(result.data);
|
||||
}
|
||||
} catch (e) {
|
||||
// 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() {
|
||||
const btn = document.getElementById('btn-new-assessment');
|
||||
btn.disabled = true;
|
||||
@@ -239,7 +378,8 @@
|
||||
showNotification('Errore di connessione.', 'error');
|
||||
} finally {
|
||||
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 = `
|
||||
<div style="margin-bottom:20px;">
|
||||
<p style="font-size:1rem; font-weight:600; color:var(--gray-900); margin-bottom:4px;">
|
||||
${escapeHtml(q.question_text || q.text || q.title || '')}
|
||||
</p>
|
||||
${q.guidance_it ? `<p class="text-muted" style="font-size:0.8125rem;">${escapeHtml(q.guidance_it)}</p>` : ''}
|
||||
${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>` : ''}
|
||||
<p class="q-text">${escapeHtml(q.question_text || q.text || q.title || '')}</p>
|
||||
${q.guidance_it ? `<p class="q-guidance">${escapeHtml(q.guidance_it)}</p>` : ''}
|
||||
${q.nis2_article ? `<span class="q-tag">Art. ${escapeHtml(q.nis2_article)}</span>` : ''}
|
||||
${q.iso27001_control ? `<span class="q-tag">ISO ${escapeHtml(q.iso27001_control)}</span>` : ''}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
${answers.map(a => `
|
||||
<label class="form-radio ${r.answer === a.value ? 'selected' : ''}">
|
||||
@@ -396,7 +534,7 @@
|
||||
</div>
|
||||
|
||||
<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"
|
||||
value="${r.maturity != null ? r.maturity : 0}" id="maturity-slider"
|
||||
oninput="document.getElementById('maturity-value').textContent = this.value">
|
||||
@@ -413,7 +551,7 @@
|
||||
placeholder="Aggiungi eventuali note o osservazioni...">${escapeHtml(r.notes || '')}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group" style="margin-bottom:0;">
|
||||
<label class="form-label">Descrizione Evidenza</label>
|
||||
<input type="text" class="form-input" id="question-evidence"
|
||||
placeholder="Documenti, procedure, strumenti a supporto..."
|
||||
@@ -430,12 +568,13 @@
|
||||
const isLast = currentCategoryIdx === categories.length - 1 && currentQuestionIdx === cat.questions.length - 1;
|
||||
const nextBtn = document.getElementById('btn-next');
|
||||
if (isLast) {
|
||||
nextBtn.innerHTML = 'Completa Assessment';
|
||||
nextBtn.className = 'btn btn-success';
|
||||
nextBtn.innerHTML = '<i data-lucide="check" class="ic"></i> Completa Assessment';
|
||||
nextBtn.className = 'btn-success';
|
||||
} 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.className = 'btn btn-primary';
|
||||
nextBtn.innerHTML = 'Successiva <i data-lucide="chevron-right" class="ic"></i>';
|
||||
nextBtn.className = 'btn-primary';
|
||||
}
|
||||
refreshIcons();
|
||||
}
|
||||
|
||||
function onAnswerChange(radio) {
|
||||
@@ -477,7 +616,7 @@
|
||||
const r = responses[q.id];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -570,6 +709,7 @@
|
||||
document.getElementById('assessment-wizard').style.display = 'none';
|
||||
document.getElementById('assessment-results').style.display = 'block';
|
||||
document.getElementById('btn-ai-analyze').style.display = '';
|
||||
refreshIcons();
|
||||
|
||||
try {
|
||||
const result = await api.getAssessmentReport(currentAssessmentId);
|
||||
@@ -637,29 +777,29 @@
|
||||
let html = '';
|
||||
|
||||
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) {
|
||||
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>`; });
|
||||
html += '</ul></div>';
|
||||
}
|
||||
|
||||
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>`; });
|
||||
html += '</ul></div>';
|
||||
}
|
||||
|
||||
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>`; });
|
||||
html += '</ol></div>';
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -681,7 +821,7 @@
|
||||
|
||||
const btn = document.getElementById('btn-generate-ncr');
|
||||
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 {
|
||||
const result = await api.createNCRsFromAssessment(currentAssessmentId);
|
||||
@@ -694,29 +834,31 @@
|
||||
const majorCount = (result.data.created || []).filter(n => n.severity === 'major').length;
|
||||
const minorCount = total - majorCount;
|
||||
resultDiv.innerHTML = `
|
||||
<div style="background:var(--secondary-bg); border:1px solid #bbf7d0; border-radius:var(--border-radius); padding:16px; text-align:left;">
|
||||
<strong style="color:#15803d;">${total} non conformita' generate con successo</strong>
|
||||
<div style="margin-top:8px; font-size:0.8125rem; color:var(--gray-600);">
|
||||
${majorCount > 0 ? `<span style="color:var(--danger); font-weight:600;">${majorCount} major</span> ` : ''}
|
||||
${minorCount > 0 ? `<span style="color:var(--warning); font-weight:600;">${minorCount} minor</span>` : ''}
|
||||
<div style="background:#E6F4EC; border:1px solid #bbf7d0; border-radius:10px; padding:16px;">
|
||||
<strong style="color:#15803d;">${total} non conformità generate con successo</strong>
|
||||
<div style="margin-top:8px; font-size:12.5px; color:var(--muted);">
|
||||
${majorCount > 0 ? `<span style="color:var(--c-red); font-weight:600;">${majorCount} major</span> ` : ''}
|
||||
${minorCount > 0 ? `<span style="color:var(--c-amber); font-weight:600;">${minorCount} minor</span>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
showNotification(`${total} non conformita' generate.`, 'success');
|
||||
showNotification(`${total} non conformità generate.`, 'success');
|
||||
} 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');
|
||||
}
|
||||
btn.style.display = 'none';
|
||||
} 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');
|
||||
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) {
|
||||
showNotification('Errore di connessione.', 'error');
|
||||
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>
|
||||
|
||||
+33
-33
@@ -3,12 +3,12 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Inventario - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (tabelle/modali/form) -> parita' funzionale assoluta. BI veste la chrome. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Inventario · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (self-host only, nessun CDN esterno). -->
|
||||
<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/modali/form/badge). È self-hostato (nessun CDN). v3.css governa chrome + shell. -->
|
||||
<style>
|
||||
/* ── Stats Cards ────────────────────────────────────────── */
|
||||
.asset-stats {
|
||||
@@ -280,20 +280,26 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2" data-i18n="assets.title">Inventario</h1>
|
||||
<div class="content-header-actions">
|
||||
<span class="text-muted" style="font-size:0.8rem; margin-right:8px;">Art. 21.2.i NIS2</span>
|
||||
<button class="btn btn-secondary" onclick="showDependencyMap()" title="Mappa delle dipendenze tra i beni">Mappa Dipendenze</button>
|
||||
<button class="btn btn-secondary" onclick="openImportModal()" title="Importa beni da CSV/CMDB">Importa</button>
|
||||
<button class="btn btn-secondary" onclick="exportAssets()" title="Esporta l'inventario in CSV (stesso formato dell'importazione)">Esporta</button>
|
||||
<button class="btn btn-primary" onclick="showCreateAssetModal()">+ Nuovo bene</button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="assets.title">Inventario</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="assets.title">Inventario</h1>
|
||||
<p class="sub">Beni, perimetro tecnico e rilevanza NIS2 · Art. 21.2.i NIS2</p>
|
||||
</div>
|
||||
<div class="content-header-actions" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<button class="btn-ghost" onclick="showDependencyMap()" title="Mappa delle dipendenze tra i beni"><i data-lucide="git-fork" class="ic"></i>Mappa Dipendenze</button>
|
||||
<button class="btn-ghost" onclick="openImportModal()" title="Importa beni da CSV/CMDB"><i data-lucide="upload" class="ic"></i>Importa</button>
|
||||
<button class="btn-ghost" onclick="exportAssets()" title="Esporta l'inventario in CSV (stesso formato dell'importazione)"><i data-lucide="download" class="ic"></i>Esporta</button>
|
||||
<button class="btn-primary" onclick="showCreateAssetModal()"><i data-lucide="plus" class="ic"></i>Nuovo bene</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
<!-- Stats Cards -->
|
||||
@@ -367,29 +373,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 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/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
|
||||
I18n.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")
|
||||
|
||||
// ── Labels ──────────────────────────────────────────────
|
||||
|
||||
+202
-163
@@ -3,65 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Calendario scadenze - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<style>
|
||||
.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>
|
||||
<title>Calendario · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css. Self-host only: no external font/script hosts. -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<meta name="theme-color" content="#0066CC">
|
||||
@@ -75,94 +20,189 @@
|
||||
<meta name="application-name" content="NIS2 Agile">
|
||||
<script src="/js/pwa.js?v=20260614" defer></script>
|
||||
<!-- 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>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<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>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<!-- KPI sintetici -->
|
||||
<div class="cal-summary" id="cal-summary" aria-live="polite"></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 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-bar">
|
||||
<div class="cal-nav">
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" onclick="calPrevMonth()" aria-label="Mese precedente">‹</button>
|
||||
<h3 id="cal-month-label">—</h3>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" onclick="calNextMonth()" aria-label="Mese successivo">›</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>
|
||||
<div class="cal-app">
|
||||
<!-- Sidebar: KPI + legenda/filtri categoria + stato -->
|
||||
<aside class="cal-side">
|
||||
<div class="cal-kpis" id="cal-summary" aria-live="polite"></div>
|
||||
|
||||
<!-- Filtri -->
|
||||
<div class="cal-filters">
|
||||
<h4>Filtra per tipo</h4>
|
||||
<div class="legend">
|
||||
<p>Filtra per tipo</p>
|
||||
<div class="cal-chips" id="cal-type-chips"></div>
|
||||
<div class="cal-filter-row">
|
||||
<h4>Filtra per stato</h4>
|
||||
<div class="cal-chips" id="cal-status-chips"></div>
|
||||
</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>
|
||||
|
||||
<!-- Vista griglia mensile -->
|
||||
<section id="pane-grid" role="tabpanel" aria-labelledby="view-grid" aria-live="polite">
|
||||
<table class="cal-grid">
|
||||
<thead>
|
||||
<tr id="cal-weekhead"></tr>
|
||||
</thead>
|
||||
<tbody id="cal-grid-body"></tbody>
|
||||
</table>
|
||||
<section id="pane-grid" class="mv" role="tabpanel" aria-labelledby="view-grid" aria-live="polite">
|
||||
<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>
|
||||
<div class="mv-grid" id="mv-grid"></div>
|
||||
</section>
|
||||
|
||||
<!-- Vista lista -->
|
||||
<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>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Popover eventi di un giorno -->
|
||||
<div class="cal-pop" id="cal-pop" role="dialog" aria-modal="true" aria-labelledby="cal-pop-title">
|
||||
<div class="cal-pop-card">
|
||||
<div class="cal-pop-head">
|
||||
<!-- Popover eventi di un giorno (modale) -->
|
||||
<div class="cal-modal" id="cal-pop" role="dialog" aria-modal="true" aria-labelledby="cal-pop-title">
|
||||
<div class="cal-modal-card">
|
||||
<div class="cal-modal-head">
|
||||
<h3 id="cal-pop-title">Eventi</h3>
|
||||
<button class="cal-pop-close" type="button" aria-label="Chiudi" onclick="calPopClose()">×</button>
|
||||
<button class="cal-modal-x" type="button" aria-label="Chiudi" onclick="calPopClose()">×</button>
|
||||
</div>
|
||||
<div class="cal-pop-body" id="cal-pop-body"></div>
|
||||
<div class="cal-modal-body" id="cal-pop-body"></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/common.js?v=20260624g"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
'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.
|
||||
* 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 = {
|
||||
view: 'grid',
|
||||
@@ -206,12 +246,14 @@
|
||||
var day = String(d.getDate()).padStart(2, '0');
|
||||
return d.getFullYear() + '-' + m + '-' + day;
|
||||
}
|
||||
function refreshIcons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
||||
if (window.I18n && I18n.init) I18n.init('it');
|
||||
if (typeof loadSidebar === 'function') loadSidebar();
|
||||
if (typeof loadSidebar === 'function') loadSidebar(); // = loadTopnavV3()
|
||||
if (window.I18n && I18n.init) I18n.init();
|
||||
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
|
||||
refreshIcons();
|
||||
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(); });
|
||||
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 to = new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() + 3, 0);
|
||||
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 {
|
||||
var data = await api.calendarEvents({ from: CAL.rangeFrom, to: CAL.rangeTo });
|
||||
CAL.events = (data && data.events) ? data.events : [];
|
||||
} catch (e) {
|
||||
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();
|
||||
render();
|
||||
@@ -242,7 +284,7 @@
|
||||
var bs = (s && s.by_status) || {};
|
||||
var defs = [['overdue', 'In ritardo'], ['due_soon', 'In scadenza'], ['upcoming', 'Future'], ['done', 'Assolte']];
|
||||
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('');
|
||||
} catch (e) { el('cal-summary').innerHTML = ''; }
|
||||
}
|
||||
@@ -252,12 +294,12 @@
|
||||
el('cal-type-chips').innerHTML = Object.keys(TYPE_META).map(function (t) {
|
||||
var m = TYPE_META[t];
|
||||
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('');
|
||||
el('cal-status-chips').innerHTML = Object.keys(STATUS_META).map(function (s) {
|
||||
var m = STATUS_META[s];
|
||||
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('');
|
||||
}
|
||||
function onTypeFilter() {
|
||||
@@ -294,8 +336,8 @@
|
||||
}
|
||||
function calSetView(v) {
|
||||
CAL.view = v;
|
||||
el('view-grid').classList.toggle('active', v === 'grid');
|
||||
el('view-list').classList.toggle('active', v === 'list');
|
||||
el('view-grid').classList.toggle('on', v === 'grid');
|
||||
el('view-list').classList.toggle('on', v === 'list');
|
||||
el('view-grid').setAttribute('aria-selected', v === 'grid' ? 'true' : 'false');
|
||||
el('view-list').setAttribute('aria-selected', v === 'list' ? 'true' : 'false');
|
||||
el('pane-grid').classList.toggle('hidden', v !== 'grid');
|
||||
@@ -308,10 +350,8 @@
|
||||
if (CAL.view === 'grid') renderGrid(); else renderList();
|
||||
}
|
||||
|
||||
// ── Vista griglia mensile ──
|
||||
// ── Vista griglia mensile (.mv-grid del mockup) ──
|
||||
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 firstOfMonth = new Date(year, month, 1);
|
||||
// settimana inizia lunedi (getDay: 0=dom)
|
||||
@@ -324,32 +364,31 @@
|
||||
visibleEvents().forEach(function (e) { (byDay[e.date] = byDay[e.date] || []).push(e); });
|
||||
|
||||
var html = '';
|
||||
for (var w = 0; w < 6; w++) {
|
||||
html += '<tr>';
|
||||
for (var d = 0; d < 7; d++) {
|
||||
var cur = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + (w * 7 + d));
|
||||
var key = ymd(cur);
|
||||
var inMonth = (cur.getMonth() === month);
|
||||
var cls = inMonth ? '' : ' cal-out';
|
||||
if (key === todayStr) cls += ' cal-today';
|
||||
var evs = byDay[key] || [];
|
||||
var dots = '';
|
||||
var shown = evs.slice(0, 6);
|
||||
shown.forEach(function (e) {
|
||||
var c = (TYPE_META[e.type] || {}).color || '#64748b';
|
||||
var tip = escAttr((TYPE_META[e.type] || { label: e.type }).label + ' — ' + e.title);
|
||||
dots += '<span class="cal-dot st-' + esc(e.status) + '" style="background:' + c + '" title="' + tip + '" onclick="calPopDay(\'' + key + '\')"></span>';
|
||||
});
|
||||
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>';
|
||||
for (var i = 0; i < 42; i++) {
|
||||
var cur = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i);
|
||||
var key = ymd(cur);
|
||||
var inMonth = (cur.getMonth() === month);
|
||||
var evs = byDay[key] || [];
|
||||
var cls = 'mc' + (inMonth ? '' : ' other') + (key === todayStr ? ' today' : '') + (evs.length ? ' has' : '');
|
||||
var chips = '';
|
||||
evs.slice(0, 3).forEach(function (e) {
|
||||
var c = (TYPE_META[e.type] || {}).color || '#64748b';
|
||||
var tip = escAttr((TYPE_META[e.type] || { label: e.type }).label + ' — ' + e.title);
|
||||
chips += '<div class="chip" title="' + tip + '"><span class="cdot" style="background:' + c + '"></span>' + esc(e.title) + '</div>';
|
||||
});
|
||||
var more = evs.length > 3 ? '<div class="more">+' + (evs.length - 3) + ' altri</div>' : '';
|
||||
var click = evs.length ? ' role="button" tabindex="0" aria-label="' + escAttr(cur.getDate() + ': ' + evs.length + ' scadenze') + '" data-day="' + key + '"' : '';
|
||||
html += '<div class="' + cls + '"' + click + '>' +
|
||||
'<div class="dnum">' + cur.getDate() + '</div>' + chips + more + '</div>';
|
||||
}
|
||||
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) ──
|
||||
@@ -364,8 +403,8 @@
|
||||
var m = TYPE_META[e.type] || { label: e.type, color: '#64748b' };
|
||||
var sm = STATUS_META[e.status] || { label: e.status };
|
||||
return '<tr class="row-' + esc(e.status) + '">' +
|
||||
'<td>' + esc(formatDate(e.date)) + '</td>' +
|
||||
'<td><span class="type-tag" style="background:' + m.color + '22;color:' + m.color + '">' + esc(m.label) + '</span></td>' +
|
||||
'<td>' + esc(calFmtDate(e.date)) + '</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><span class="st-badge st-' + esc(e.status) + '">' + esc(sm.label) + '</span></td>' +
|
||||
'</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>';
|
||||
}
|
||||
|
||||
function formatDate(s) {
|
||||
function calFmtDate(s) {
|
||||
var p = String(s).split('-');
|
||||
if (p.length !== 3) return s;
|
||||
return p[2] + '/' + p[1] + '/' + p[0];
|
||||
@@ -384,15 +423,15 @@
|
||||
function calPopDay(dateKey) {
|
||||
var evs = visibleEvents().filter(function (e) { return e.date === dateKey; });
|
||||
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) {
|
||||
var m = TYPE_META[e.type] || { label: e.type, color: '#64748b' };
|
||||
var sm = STATUS_META[e.status] || { label: e.status };
|
||||
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>' +
|
||||
'<div style="flex:1 1 auto">' +
|
||||
'<span class="cdot" style="background:' + m.color + '"></span>' +
|
||||
'<div style="flex:1 1 auto;min-width:0">' +
|
||||
'<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>' +
|
||||
'</div></div>';
|
||||
}).join('');
|
||||
|
||||
+130
-339
@@ -3,270 +3,62 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<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. -->
|
||||
<link rel="stylesheet" href="vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="css/style.css?v=20260618">
|
||||
<title>Le Mie Aziende · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Bootstrap Italia / NO CDN). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<style>
|
||||
.companies-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 28px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* ── Card aziende (non coperte da v3.css; nessun CDN) ───────────── */
|
||||
.companies-toolbar { display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:18px; }
|
||||
.search-box { position:relative; min-width:240px; flex:1; max-width:340px; }
|
||||
.search-box svg { position:absolute; left:12px; top:50%; transform:translateY(-50%); color:var(--muted); pointer-events:none; }
|
||||
.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; }
|
||||
.search-box input:focus { border-color:#9FBEDD; }
|
||||
|
||||
.companies-header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--gray-900);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.companies-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(300px, 1fr)); gap:18px; }
|
||||
.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; }
|
||||
.company-card:hover { box-shadow:0 8px 20px rgba(16,40,80,.13); border-color:#9FBEDD; transform:translateY(-1px); }
|
||||
.company-card-header { display:flex; align-items:flex-start; gap:14px; }
|
||||
.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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.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; }
|
||||
.entity-badge.essential { background:#FBE9E9; color:#B23636; }
|
||||
.entity-badge.important { background:#FBF1E2; color:#9A6418; }
|
||||
.entity-badge.not_applicable, .entity-badge.voluntary { background:#EEF2F7; color:#5b6b80; }
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
min-width: 240px;
|
||||
}
|
||||
.company-score-row { display:flex; align-items:center; gap:12px; }
|
||||
.company-score-label { font-size:.8125rem; color:var(--muted); white-space:nowrap; }
|
||||
.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 {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--gray-400);
|
||||
pointer-events: none;
|
||||
}
|
||||
.company-stats { display:flex; gap:16px; }
|
||||
.company-stat { display:flex; flex-direction:column; gap:2px; }
|
||||
.company-stat-value { font-size:1.125rem; font-weight:800; color:var(--ink); }
|
||||
.company-stat-label { font-size:.75rem; color:var(--muted); }
|
||||
|
||||
.search-box input {
|
||||
padding-left: 38px;
|
||||
}
|
||||
.company-card-footer { border-top:1px solid var(--line); padding-top:16px; display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
||||
.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-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.companies-empty { text-align:center; padding:60px 24px; color:var(--muted); grid-column:1 / -1; }
|
||||
.companies-empty svg { width:56px; height:56px; color:var(--line); margin:0 auto 18px; display:block; }
|
||||
.companies-empty h3 { font-size:1.05rem; font-weight:700; color:var(--ink); margin-bottom:8px; }
|
||||
.companies-empty p { font-size:.9375rem; margin-bottom:22px; line-height:1.6; }
|
||||
|
||||
.company-card {
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: 24px;
|
||||
transition: all var(--transition);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
/* Skeleton loader */
|
||||
.skeleton { background:linear-gradient(90deg,#EEF2F7 25%,#F6F9FC 50%,#EEF2F7 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;} }
|
||||
@media (prefers-reduced-motion:reduce) { .skeleton{animation:none;} }
|
||||
.skeleton-card { background:var(--card); border:1px solid var(--line); border-radius:var(--radius); padding:20px; display:flex; flex-direction:column; gap:16px; }
|
||||
|
||||
.company-card:hover {
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
border-color: var(--primary-light);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
/* Modale connettori (reskin sobrio, nessun CDN) */
|
||||
.conn-row { border:1px solid var(--line); border-radius:8px; padding:12px; margin-bottom:10px; }
|
||||
.conn-row input { padding:7px 9px; border:1px solid var(--line); border-radius:6px; font:inherit; font-size:13px; outline:none; }
|
||||
.conn-row input:focus { border-color:#9FBEDD; }
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -283,80 +75,76 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<!-- Sidebar -->
|
||||
<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>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></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>
|
||||
</main>
|
||||
</div>
|
||||
<a href="onboarding.html" class="btn-primary" id="nis2-companies-add-btn">
|
||||
<i data-lucide="plus" class="ic"></i>Aggiungi Cliente
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260631"></script>
|
||||
<script src="js/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle + override sidebar BI (common-bi.js dopo common.js: ridefinisce solo loadSidebar) -->
|
||||
<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/help.js?v=20260625d"></script>
|
||||
<section class="card card-pad">
|
||||
<div class="companies-toolbar">
|
||||
<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" id="search-input" placeholder="Cerca azienda..." oninput="filterCompanies(this.value)" aria-label="Cerca azienda">
|
||||
</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: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>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -367,7 +155,9 @@
|
||||
}
|
||||
|
||||
loadSidebar();
|
||||
if (typeof I18n !== 'undefined') I18n.init();
|
||||
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
|
||||
// ── Data ────────────────────────────────────────────────────────
|
||||
let allCompanies = [];
|
||||
@@ -405,6 +195,7 @@
|
||||
function renderCompanies(orgs) {
|
||||
const grid = document.getElementById('companies-grid');
|
||||
grid.innerHTML = orgs.map(org => renderCompanyCard(org)).join('');
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
}
|
||||
|
||||
function renderCompanyCard(org) {
|
||||
@@ -454,12 +245,12 @@
|
||||
|
||||
<div class="company-card-footer">
|
||||
<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
|
||||
</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="manageCompany(${orgId})">
|
||||
<button class="btn-primary btn-sm" onclick="manageCompany(${orgId})">
|
||||
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>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -480,45 +271,45 @@
|
||||
headers: { 'Authorization': `Bearer ${api.token}`, 'X-Organization-Id': orgId }
|
||||
});
|
||||
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 = {};
|
||||
(data.data.connectors || []).forEach(c => existing[c.connector_type] = c);
|
||||
const rows = (data.data.available_types || []).map(t => {
|
||||
const c = existing[t] || {};
|
||||
const cfg = (c.config && typeof c.config === 'object') ? c.config : {};
|
||||
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];
|
||||
return `<div class="conn-row" data-type="${t}" style="border:1px solid var(--gray-200);border-radius:8px;padding:12px;margin-bottom:10px;">
|
||||
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}">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<strong>${escapeHtml(CONNECTOR_LABELS[t] || t)}</strong>
|
||||
<label style="font-size:.8rem;"><input type="checkbox" class="c-en" ${c.enabled ? 'checked' : ''}> Attivo</label>
|
||||
</div>
|
||||
<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-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-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 || '')}">
|
||||
</div>
|
||||
<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>
|
||||
<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>`;
|
||||
}).join('');
|
||||
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
|
||||
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}`);
|
||||
} 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) {
|
||||
const old = document.getElementById('conn-modal'); if (old) old.remove();
|
||||
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 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;">
|
||||
<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></div>`);
|
||||
}
|
||||
|
||||
+86
-65
@@ -3,61 +3,62 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Competenze - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Competenze · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css. No external CDN, no Bootstrap Italia. -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<style>
|
||||
.cmp-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:16px; font-size:.92rem; line-height:1.6; }
|
||||
.cmp-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 18px; }
|
||||
/* V3+ page-specific (componenti non coperti da v3.css): tab interni, tabella, gap card, modali. Token V3. */
|
||||
.cmp-intro { background:#EFF4FB; border-left:4px solid var(--c-blue); padding:14px 18px; border-radius:10px; margin-bottom:14px; font-size:13.5px; line-height:1.6; color:var(--ink); }
|
||||
.cmp-note { font-size:12.5px; color:#9A6418; background:#FBF1E2; border:1px solid #f3d9a8; border-radius:10px; padding:10px 12px; margin:0 0 18px; }
|
||||
.cmp-tabs { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:18px; }
|
||||
.cmp-tab { flex:1 1 160px; min-width:140px; padding:10px 12px; border:1.5px solid var(--gray-200,#e5e7eb); border-radius:10px; background:#fff; cursor:pointer; font-size:.85rem; font-weight:600; text-align:center; }
|
||||
.cmp-tab.active { border-color:var(--primary,#2563eb); background:#eff6ff; color:var(--primary,#2563eb); }
|
||||
.cmp-tab { flex:1 1 160px; min-width:140px; padding:11px 12px; border:1.5px solid var(--line); border-radius:10px; background:#fff; cursor:pointer; font:inherit; font-size:13px; font-weight:600; text-align:center; color:var(--ink); }
|
||||
.cmp-tab.active { border-color:var(--c-blue); background:#EFF4FB; color:var(--c-blue); }
|
||||
.cmp-panel { display:none; }
|
||||
.cmp-panel.active { display:block; }
|
||||
.cmp-table { width:100%; border-collapse:collapse; font-size:.88rem; }
|
||||
.cmp-table th, .cmp-table td { text-align:left; padding:9px 10px; border-bottom:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
|
||||
.cmp-table th { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
|
||||
.cmp-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; }
|
||||
.cmp-badge.global { background:#ede9fe; color:#5b21b6; }
|
||||
.cmp-badge.area { background:#f1f5f9; color:#475569; }
|
||||
.cmp-badge.lvl { background:#e0f2fe; color:#075985; }
|
||||
.cmp-table { width:100%; border-collapse:collapse; font-size:13.5px; }
|
||||
.cmp-table th, .cmp-table td { text-align:left; padding:10px 11px; border-bottom:1px solid var(--line); vertical-align:top; }
|
||||
.cmp-table th { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); font-weight:700; }
|
||||
.cmp-badge { display:inline-block; font-size:10.5px; font-weight:700; padding:2px 8px; border-radius:6px; }
|
||||
.cmp-badge.global { background:#EFE7FB; color:#5b21b6; }
|
||||
.cmp-badge.area { background:#EEF2F7; color:#5b6b80; }
|
||||
.cmp-badge.lvl { background:#E7F0FB; color:#1F5FA8; }
|
||||
.cmp-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
|
||||
.cmp-toolbar select, .cmp-inline select, .cmp-inline input { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; }
|
||||
.cmp-inline { display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end; background:#f9fafb; border:1px solid var(--gray-100,#f3f4f6); border-radius:10px; padding:12px; margin-bottom:14px; }
|
||||
.cmp-toolbar select, .cmp-inline select, .cmp-inline input { padding:8px 10px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
|
||||
.cmp-inline { display:flex; gap:8px; flex-wrap:wrap; align-items:flex-end; background:#FAFCFE; border:1px solid var(--line); border-radius:10px; padding:12px; margin-bottom:14px; }
|
||||
.cmp-inline .fld { display:flex; flex-direction:column; gap:3px; }
|
||||
.cmp-inline .fld label { font-size:.72rem; font-weight:600; color:var(--gray-500,#6b7280); }
|
||||
.cmp-inline .fld label { font-size:11px; font-weight:700; color:var(--muted); }
|
||||
.cmp-actions-cell { display:flex; gap:6px; flex-wrap:wrap; }
|
||||
.cmp-actions-cell .btn, .cmp-toolbar .btn { padding:4px 9px; font-size:.76rem; }
|
||||
/* Gap cards */
|
||||
.gap-role { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; }
|
||||
.gap-role-head { background:var(--gray-50,#f9fafb); padding:11px 14px; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:6px; }
|
||||
.gap-role { border:1px solid var(--line); border-radius:11px; margin-bottom:14px; overflow:hidden; }
|
||||
.gap-role-head { background:#FAFCFE; padding:12px 14px; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:6px; }
|
||||
.gap-role-title { font-weight:700; }
|
||||
.gap-role-sub { font-size:.82rem; color:var(--gray-500,#6b7280); }
|
||||
.gap-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; padding:10px 14px; border-top:1px solid var(--gray-100,#f3f4f6); }
|
||||
.gap-row .skill { flex:1 1 220px; font-weight:600; font-size:.9rem; }
|
||||
.gap-meter { font-size:.82rem; }
|
||||
.gap-pill { display:inline-block; font-size:.72rem; font-weight:800; padding:2px 9px; border-radius:20px; }
|
||||
.gap-pill.ok { background:#dcfce7; color:#166534; }
|
||||
.gap-pill.bad { background:#fee2e2; color:#991b1b; }
|
||||
.gap-role-sub { font-size:12px; color:var(--muted); }
|
||||
.gap-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; padding:11px 14px; border-top:1px solid var(--line); }
|
||||
.gap-row .skill { flex:1 1 220px; font-weight:600; font-size:13.5px; }
|
||||
.gap-meter { font-size:12.5px; color:var(--muted); }
|
||||
.gap-pill { display:inline-block; font-size:10.5px; font-weight:800; padding:2px 9px; border-radius:20px; }
|
||||
.gap-pill.ok { background:#E6F4EC; color:#1E7A4E; }
|
||||
.gap-pill.bad { background:#FBE9E9; color:#B23636; }
|
||||
.gap-row .ctas { display:flex; gap:6px; flex-wrap:wrap; }
|
||||
.gap-row .ctas .btn { padding:4px 9px; font-size:.76rem; }
|
||||
.cmp-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); }
|
||||
.cmp-empty { text-align:center; padding:34px 16px; color:var(--muted); }
|
||||
/* Modale */
|
||||
.cm-overlay { display:none; position:fixed; inset:0; background:rgba(15,23,42,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
|
||||
.cm-overlay { display:none; position:fixed; inset:0; background:rgba(15,38,60,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
|
||||
.cm-overlay.open { display:flex; }
|
||||
.cm-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(0,0,0,.25); }
|
||||
.cm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||||
.cm-head h3 { margin:0; font-size:1.05rem; }
|
||||
.cm-dialog { background:#fff; border-radius:14px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(11,40,80,.25); }
|
||||
.cm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--line); }
|
||||
.cm-head h3 { margin:0; font-size:17px; font-weight:800; }
|
||||
.cm-body { padding:18px 20px; }
|
||||
.cm-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; }
|
||||
.cm-foot { padding:14px 20px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px; }
|
||||
.cm-field { margin-bottom:14px; }
|
||||
.cm-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; }
|
||||
.cm-field input[type=text], .cm-field select, .cm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; }
|
||||
.cm-field label { display:block; font-weight:600; font-size:12.5px; margin-bottom:5px; }
|
||||
.cm-field input[type=text], .cm-field select, .cm-field textarea { width:100%; padding:9px 11px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
|
||||
.cm-field textarea { min-height:64px; resize:vertical; }
|
||||
.cm-err { color:#b91c1c; font-size:.82rem; min-height:18px; }
|
||||
.cm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); }
|
||||
.cm-err { color:var(--c-red); font-size:12.5px; min-height:18px; }
|
||||
.cm-close { background:none; border:none; font-size:22px; line-height:1; cursor:pointer; color:#9aa8ba; }
|
||||
.cm-maplist { margin-top:10px; }
|
||||
.cm-maprow { display:flex; justify-content:space-between; align-items:center; padding:7px 0; border-top:1px solid var(--gray-100,#f3f4f6); font-size:.86rem; }
|
||||
.cm-maprow { display:flex; justify-content:space-between; align-items:center; padding:8px 0; border-top:1px solid var(--line); font-size:13px; }
|
||||
.savehint { font-size:12px; color:var(--muted); }
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -74,17 +75,24 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2 data-i18n="comp.title">Competenze</h2>
|
||||
<div class="content-header-actions">
|
||||
<span class="savehint" id="cmp-hint" role="status" aria-live="polite"></span>
|
||||
</div>
|
||||
</header>
|
||||
<div id="topnav"></div>
|
||||
|
||||
<div class="content-body">
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Struttura interna</span>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="comp.title">Competenze</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="comp.title">Competenze</h1>
|
||||
<p class="sub">Skill matrix, requisiti per ruolo e copertura dei gap</p>
|
||||
</div>
|
||||
<span class="savehint" id="cmp-hint" role="status" aria-live="polite"></span>
|
||||
</div>
|
||||
|
||||
<div class="stack">
|
||||
<section class="card card-pad">
|
||||
<div class="cmp-intro">
|
||||
<strong>Competenze, requisiti e gap.</strong>
|
||||
Definisci le competenze necessarie, assegnale ai ruoli dell'organigramma, registra quelle possedute dalle persone e individua i divari (gap) da colmare con formazione o azioni correttive.
|
||||
@@ -103,7 +111,7 @@
|
||||
<!-- TAB 1 — Catalogo competenze -->
|
||||
<div class="cmp-panel active" id="panel-catalog">
|
||||
<div class="cmp-toolbar">
|
||||
<button class="btn btn-primary btn-sm" onclick="cmpOpenSkill()">+ Nuova competenza</button>
|
||||
<button class="btn-primary" onclick="cmpOpenSkill()"><i data-lucide="plus" class="ic"></i>Nuova competenza</button>
|
||||
<span class="gap-role-sub" id="cmp-catalog-count"></span>
|
||||
</div>
|
||||
<div id="cmp-catalog"></div>
|
||||
@@ -112,13 +120,13 @@
|
||||
<!-- TAB 2 — Requisiti per ruolo -->
|
||||
<div class="cmp-panel" id="panel-roles">
|
||||
<div class="cmp-toolbar">
|
||||
<label for="cmp-role-sel" style="font-weight:600;font-size:.86rem;">Ruolo:</label>
|
||||
<label for="cmp-role-sel" style="font-weight:600;font-size:13px;">Ruolo:</label>
|
||||
<select id="cmp-role-sel" onchange="cmpLoadRoleSkills()"></select>
|
||||
</div>
|
||||
<div class="cmp-inline" id="cmp-role-add" style="display:none;">
|
||||
<div class="fld"><label>Competenza</label><select id="cmp-rs-skill"></select></div>
|
||||
<div class="fld"><label>Livello richiesto</label><select id="cmp-rs-level"></select></div>
|
||||
<button class="btn btn-primary btn-sm" onclick="cmpAddRoleSkill()">Aggiungi requisito</button>
|
||||
<button class="btn-primary" onclick="cmpAddRoleSkill()">Aggiungi requisito</button>
|
||||
</div>
|
||||
<div id="cmp-role-skills"></div>
|
||||
</div>
|
||||
@@ -126,7 +134,7 @@
|
||||
<!-- TAB 3 — Competenze persone -->
|
||||
<div class="cmp-panel" id="panel-users">
|
||||
<div class="cmp-toolbar">
|
||||
<label for="cmp-user-sel" style="font-weight:600;font-size:.86rem;">Persona:</label>
|
||||
<label for="cmp-user-sel" style="font-weight:600;font-size:13px;">Persona:</label>
|
||||
<select id="cmp-user-sel" onchange="cmpLoadUserSkills()"></select>
|
||||
</div>
|
||||
<div class="cmp-inline" id="cmp-user-add" style="display:none;">
|
||||
@@ -134,7 +142,7 @@
|
||||
<div class="fld"><label>Livello posseduto</label><select id="cmp-us-level"></select></div>
|
||||
<div class="fld"><label>Corso (opz.)</label><select id="cmp-us-course"></select></div>
|
||||
<div class="fld"><label>Evidenza (opz.)</label><input type="text" id="cmp-us-evidence" maxlength="500" placeholder="es. attestato, data"></div>
|
||||
<button class="btn btn-primary btn-sm" onclick="cmpAddUserSkill()">Registra</button>
|
||||
<button class="btn-primary" onclick="cmpAddUserSkill()">Registra</button>
|
||||
</div>
|
||||
<div id="cmp-user-skills"></div>
|
||||
</div>
|
||||
@@ -142,14 +150,14 @@
|
||||
<!-- TAB 4 — Gap & azioni -->
|
||||
<div class="cmp-panel" id="panel-gap">
|
||||
<div class="cmp-toolbar">
|
||||
<button class="btn btn-outline btn-sm" onclick="cmpLoadGap()">Ricalcola gap</button>
|
||||
<button class="btn-ghost" onclick="cmpLoadGap()"><i data-lucide="refresh-cw" class="ic"></i>Ricalcola gap</button>
|
||||
<span class="gap-role-sub" id="cmp-gap-summary"></span>
|
||||
</div>
|
||||
<div id="cmp-gap"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Modale competenza (crea/modifica) -->
|
||||
<div class="cm-overlay" id="cmp-skill-modal" role="dialog" aria-modal="true" aria-labelledby="cmp-skill-title">
|
||||
@@ -162,7 +170,7 @@
|
||||
<div class="cm-field"><label for="cmp-skill-desc">Descrizione</label><textarea id="cmp-skill-desc" maxlength="2000"></textarea></div>
|
||||
<div class="cm-err" id="cmp-skill-err" role="alert"></div>
|
||||
</div>
|
||||
<div class="cm-foot"><button class="btn btn-outline" type="button" onclick="cmpClose('cmp-skill-modal')">Annulla</button><button class="btn btn-primary" type="button" id="cmp-skill-save" onclick="cmpSaveSkill()">Salva</button></div>
|
||||
<div class="cm-foot"><button class="btn-ghost" type="button" onclick="cmpClose('cmp-skill-modal')">Annulla</button><button class="btn-primary" type="button" id="cmp-skill-save" onclick="cmpSaveSkill()">Salva</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -174,21 +182,34 @@
|
||||
<input type="hidden" id="cmp-courses-skill-id">
|
||||
<div class="cm-field" style="display:flex;gap:8px;align-items:flex-end;">
|
||||
<div style="flex:1;"><label for="cmp-courses-sel">Aggiungi corso</label><select id="cmp-courses-sel"></select></div>
|
||||
<button class="btn btn-primary" type="button" onclick="cmpMapCourse()">Aggiungi</button>
|
||||
<button class="btn-primary" type="button" onclick="cmpMapCourse()">Aggiungi</button>
|
||||
</div>
|
||||
<div class="cm-maplist" id="cmp-courses-list"></div>
|
||||
<div class="cm-err" id="cmp-courses-err" role="alert"></div>
|
||||
</div>
|
||||
<div class="cm-foot"><button class="btn btn-outline" type="button" onclick="cmpClose('cmp-courses-modal')">Chiudi</button></div>
|
||||
<div class="cm-foot"><button class="btn-ghost" type="button" onclick="cmpClose('cmp-courses-modal')">Chiudi</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts V3. competenze.js si auto-inizializza su DOMContentLoaded (checkAuth/loadSidebar/I18n/Help). -->
|
||||
<script src="/js/api.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260624g"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script src="/js/competenze.js?v=20260617"></script>
|
||||
<script src="/js/competenze.js?v=20260627"></script>
|
||||
<script>
|
||||
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
|
||||
v3Icons();
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
v3Icons();
|
||||
var root = document.querySelector('main.wrap');
|
||||
if (root && window.MutationObserver) {
|
||||
var mo = new MutationObserver(function () { v3Icons(); });
|
||||
mo.observe(root, { childList: true, subtree: true });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Connettori Discovery - NIS2 Agile</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Connettori Discovery · NIS2 Agile</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<!-- UI V3: Inter self-host + design system v3.css (self-host only, nessun CDN esterno). -->
|
||||
<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">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="content-title">Connettori Discovery</h1>
|
||||
<div class="content-header-actions">
|
||||
<button class="btn btn-primary" id="btn-dc-new">+ Nuovo connettore</button>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></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>Connettori Discovery</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Connettori Discovery</h1>
|
||||
<p class="sub">Scansione rete/cloud → auto-popola l'Inventario (asset, dipendenze, flussi ID.AM-03)</p>
|
||||
</div>
|
||||
<div 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="card" style="margin-bottom:16px">
|
||||
<div class="card-body">
|
||||
@@ -40,12 +52,12 @@
|
||||
<div class="card-body">
|
||||
<div class="form-row" style="display:flex;gap:12px;flex-wrap:wrap">
|
||||
<div class="form-group" style="flex:1;min-width:200px">
|
||||
<label>Nome</label>
|
||||
<input id="dc-name" class="form-control" placeholder="Es. Rete sede Modena" maxlength="120">
|
||||
<label class="form-label" for="dc-name">Nome</label>
|
||||
<input id="dc-name" class="form-input" placeholder="Es. Rete sede Modena" maxlength="120">
|
||||
</div>
|
||||
<div class="form-group" style="min-width:160px">
|
||||
<label>Tipo</label>
|
||||
<select id="dc-type" class="form-control">
|
||||
<label class="form-label" for="dc-type">Tipo</label>
|
||||
<select id="dc-type" class="form-select">
|
||||
<option value="network">Rete (scan IP)</option>
|
||||
<option value="aws">AWS</option>
|
||||
<option value="azure">Azure</option>
|
||||
@@ -56,8 +68,8 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="min-width:160px">
|
||||
<label>Schedulazione</label>
|
||||
<select id="dc-schedule" class="form-control">
|
||||
<label class="form-label" for="dc-schedule">Schedulazione</label>
|
||||
<select id="dc-schedule" class="form-select">
|
||||
<option value="manual">Manuale</option>
|
||||
<option value="daily">Giornaliera</option>
|
||||
<option value="hourly">Oraria</option>
|
||||
@@ -66,12 +78,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>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">
|
||||
<label class="form-label" for="dc-targets">Target (CIDR/IP, separati da virgola) — descrittivo / usato dall'agente</label>
|
||||
<input id="dc-targets" class="form-input" placeholder="192.168.1.0/24, 10.0.0.5">
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;margin-top:8px">
|
||||
<button class="btn btn-primary" id="btn-dc-create">Crea connettore</button>
|
||||
<button class="btn btn-outline-primary" id="btn-dc-cancel">Annulla</button>
|
||||
<button class="btn-primary" id="btn-dc-create"><i data-lucide="check" class="ic"></i>Crea connettore</button>
|
||||
<button class="btn-ghost" id="btn-dc-cancel">Annulla</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,26 +94,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<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="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts'); }
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script src="/js/connettori-discovery.js?v=20260625"></script>
|
||||
<script src="/js/connettori-discovery.js?v=20260627"></script>
|
||||
<script>
|
||||
if (typeof checkAuth === 'function' && !checkAuth()) {
|
||||
// checkAuth reindirizza a login
|
||||
} else {
|
||||
if (typeof loadSidebar === 'function') loadSidebar();
|
||||
if (typeof loadSidebar === 'function') loadSidebar(); // = loadTopnavV3()
|
||||
if (typeof I18n !== 'undefined' && I18n.init) I18n.init();
|
||||
if (typeof HelpSystem !== 'undefined' && HelpSystem.init) HelpSystem.init();
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
if (window.DiscoveryUI) DiscoveryUI.init();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,10 +3,21 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Controlli periodici - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Controlli periodici · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia CDN). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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-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; }
|
||||
@@ -50,16 +61,23 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2 data-i18n="pctl.title">Controlli periodici</h2>
|
||||
<div class="content-header-actions">
|
||||
<button class="btn btn-primary btn-sm" onclick="pcOpen()">+ Nuovo controllo</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content-body">
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></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>Monitoraggio</span>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="pctl.title">Controlli periodici</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="pctl.title">Controlli periodici</h1>
|
||||
<p class="sub">Controlli ricorrenti, esecuzioni con evidenza ed esiti (ISO/IEC 27001 §9.1 / A.8.16)</p>
|
||||
</div>
|
||||
<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">
|
||||
<strong>Controlli periodici (monitoraggio).</strong>
|
||||
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.
|
||||
</div>
|
||||
<div id="pc-list" aria-live="polite"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Modale controllo (crea/modifica) -->
|
||||
<div class="sm-overlay" id="pc-modal" role="dialog" aria-modal="true" aria-labelledby="pc-modal-title">
|
||||
@@ -136,10 +153,12 @@
|
||||
</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/common.js?v=20260624g"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
@@ -155,9 +174,10 @@
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
||||
if (window.I18n && I18n.init) I18n.init('it');
|
||||
if (typeof loadSidebar === 'function') loadSidebar();
|
||||
if (window.I18n && I18n.init) I18n.init('it');
|
||||
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(); } });
|
||||
['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 = []; }
|
||||
|
||||
+480
-629
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||
.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
@@ -3,12 +3,42 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Guida all'uso - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css resta autorevole per la chrome (sidebar/widget). BI veste la guida. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Guida all'uso · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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) ──────── */
|
||||
.guide-wrap { max-width: 940px; margin: 0 auto; }
|
||||
|
||||
@@ -103,16 +133,20 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2">Guida all'uso di NIS2 Agile</h1>
|
||||
<div class="content-header-actions">
|
||||
<span class="text-muted">Per chi inizia da zero</span>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Guida all'uso</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Guida all'uso di NIS2 Agile</h1>
|
||||
<p class="sub">Per chi inizia da zero — parole semplici, esempi pratici, norma</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
<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>.
|
||||
</p>
|
||||
<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
|
||||
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
|
||||
@@ -143,7 +177,7 @@
|
||||
<nav class="card card-bg shadow-sm mb-4 guide-toc-card" aria-label="Indice">
|
||||
<div class="card-body">
|
||||
<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
|
||||
</h2>
|
||||
<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 class="accordion-body">
|
||||
<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
|
||||
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,
|
||||
@@ -194,7 +228,7 @@
|
||||
<li><strong>Controllare i fornitori</strong> che hanno accesso ai propri sistemi (Art. 21, lettera d).</li>
|
||||
</ol>
|
||||
<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é
|
||||
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
|
||||
@@ -220,7 +254,7 @@
|
||||
<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="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
|
||||
<em>cosa fai</em> (settore) e <em>quanto sei grande</em> (dipendenti e fatturato).
|
||||
</div>
|
||||
@@ -265,7 +299,7 @@
|
||||
<li>la <strong>Pubblica Amministrazione</strong> nei casi previsti.</li>
|
||||
</ul>
|
||||
<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
|
||||
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
|
||||
@@ -273,7 +307,7 @@
|
||||
l'esenzione "piccola impresa" qui non si applica.
|
||||
</div>
|
||||
<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,
|
||||
settore sanitario → <strong>essenziale + grande</strong> → pienamente in scope. Deve registrarsi sul portale
|
||||
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
|
||||
fonti da cui la piattaforma raccoglie automaticamente le evidenze di conformità.</p>
|
||||
<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.
|
||||
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
|
||||
@@ -345,7 +379,7 @@
|
||||
<li><strong>consultant</strong> — gestisce <em>più aziende clienti</em> (sezioni "Aziende" e "Connettori").</li>
|
||||
</ul>
|
||||
<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
|
||||
consulenti/studi che gestiscono più clienti: se sei un utente singolo potresti non vederle.
|
||||
</div>
|
||||
@@ -366,7 +400,7 @@
|
||||
<li><strong>Genera il report esecutivo</strong> dalla sezione Report — utile per il board.</li>
|
||||
</ol>
|
||||
<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
|
||||
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.
|
||||
@@ -377,7 +411,7 @@
|
||||
(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>
|
||||
<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
|
||||
tuo settore ci sono almeno 3 organizzazioni, e non mostra mai i dati di una singola azienda.
|
||||
</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 class="accordion-body">
|
||||
<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".
|
||||
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>.
|
||||
@@ -433,7 +467,7 @@
|
||||
critici. Anche SMS è meglio di nulla, app authenticator è meglio di SMS, hardware token (FIDO2)
|
||||
è il top.</p></div>
|
||||
<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
|
||||
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.
|
||||
@@ -462,7 +496,7 @@
|
||||
<strong>+6 misure e +29 requisiti</strong> rispetto agli importanti.</li>
|
||||
</ul>
|
||||
<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
|
||||
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
|
||||
@@ -472,7 +506,7 @@
|
||||
comunicata e applicata.
|
||||
</div>
|
||||
<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
|
||||
(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
|
||||
@@ -499,7 +533,7 @@
|
||||
<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="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
|
||||
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
|
||||
@@ -532,7 +566,7 @@
|
||||
(cita solo requisiti effettivamente esistenti).</li>
|
||||
</ol>
|
||||
<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
|
||||
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).
|
||||
@@ -551,7 +585,7 @@
|
||||
<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="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
|
||||
<em>potrebbero</em> accadere, con quanto è probabile e quanto farebbe male. Per ognuna decidi
|
||||
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>
|
||||
</ul>
|
||||
<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,
|
||||
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).
|
||||
@@ -601,7 +635,7 @@
|
||||
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>
|
||||
<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,
|
||||
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.
|
||||
@@ -609,7 +643,7 @@
|
||||
<p>Il <strong>Registro Quantitativo</strong> somma l'ALE di tutti i rischi quantificati, dando
|
||||
l'esposizione economica complessiva del portafoglio.</p>
|
||||
<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>
|
||||
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
|
||||
@@ -624,7 +658,7 @@
|
||||
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>
|
||||
<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
|
||||
attuale 18% → semaforo <strong>ambra</strong>. Sale a 30% → <strong>rosso</strong>: intervento prioritario.
|
||||
</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 class="accordion-body">
|
||||
<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
|
||||
(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.
|
||||
@@ -660,7 +694,7 @@
|
||||
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>
|
||||
<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
|
||||
aiutano a capire se sei vicino alla significatività): ≥ 500 utenti colpiti · blocco servizi > 4 ore ·
|
||||
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.
|
||||
<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">
|
||||
<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ì.
|
||||
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).
|
||||
@@ -705,7 +739,7 @@
|
||||
IS-1…IS-4, gravità) e fa partire i tempi Art. 23. Gli alert duplicati vengono riconosciuti e non
|
||||
creano doppioni.</p>
|
||||
<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>
|
||||
con una API key dedicata (scope <code>ingest:incidents</code>). Trasforma il modulo incidenti da
|
||||
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 class="accordion-body">
|
||||
<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>
|
||||
si fa una cosa in azienda. Serve sia per fare bene, sia per dimostrare alle autorità che ci hai pensato.
|
||||
</div>
|
||||
@@ -740,7 +774,7 @@
|
||||
<li>Acceptable Use Policy (per i dipendenti).</li>
|
||||
</ol>
|
||||
<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
|
||||
"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
|
||||
@@ -753,7 +787,7 @@
|
||||
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>
|
||||
<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
|
||||
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.
|
||||
@@ -776,7 +810,7 @@
|
||||
<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="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
|
||||
in casa tua. NIS2 ti obbliga a valutare i fornitori "critici" (quelli con accesso ai tuoi sistemi
|
||||
o dati sensibili).
|
||||
@@ -791,7 +825,7 @@
|
||||
<li>Inserire clausole NIS2 nel contratto (right to audit, notifica incidenti, etc.).</li>
|
||||
</ol>
|
||||
<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
|
||||
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
|
||||
@@ -805,7 +839,7 @@
|
||||
<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>
|
||||
<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:
|
||||
7 "sì" e 1 "parziale" → punteggio 94/100, requisiti di sicurezza soddisfatti.
|
||||
</div>
|
||||
@@ -826,7 +860,7 @@
|
||||
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>
|
||||
<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
|
||||
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
|
||||
@@ -850,7 +884,7 @@
|
||||
<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="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)
|
||||
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.
|
||||
@@ -863,7 +897,7 @@
|
||||
almeno annuale.</li>
|
||||
</ol>
|
||||
<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
|
||||
IT, una sessione obbligatoria di 1h per tutti i dipendenti più simulazioni di phishing trimestrali.
|
||||
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 class="accordion-body">
|
||||
<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.
|
||||
L'inventario asset elenca tutti i sistemi, applicazioni, database e infrastrutture critiche.
|
||||
</div>
|
||||
@@ -911,7 +945,7 @@
|
||||
<li><strong>Facility</strong>: data center, sale server.</li>
|
||||
</ul>
|
||||
<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,
|
||||
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.
|
||||
@@ -923,7 +957,7 @@
|
||||
carichi un CSV (o un export dal CMDB/cloud) e la piattaforma crea gli asset calcolando
|
||||
<strong>automaticamente</strong> il punteggio per ciascuno.</p>
|
||||
<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>),
|
||||
re-importare lo stesso asset lo <strong>aggiorna</strong> invece di duplicarlo: ideale per
|
||||
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 class="accordion-body">
|
||||
<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
|
||||
compliance, devi poter mostrare documenti, evidenze, registri. Qui li trovi tutti.
|
||||
</div>
|
||||
@@ -964,17 +998,17 @@
|
||||
<strong>arancio</strong> (evidenza scaduta), <strong>rosso</strong> (non conforme), grigio (non
|
||||
monitorato). La copertura indica quanti controlli sono alimentati da evidenze automatiche.</p>
|
||||
<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é
|
||||
fotografata una volta l'anno: è il modello dei migliori strumenti internazionali.
|
||||
</div>
|
||||
<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>
|
||||
(vedi cap. 3): senza connettori i controlli restano "non monitorati" (grigio) e la copertura è 0%.
|
||||
</div>
|
||||
<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
|
||||
rischi e controlli, scarica l'audit log certificato. L'auditor ha tutto in 5 minuti.
|
||||
</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 class="accordion-body">
|
||||
<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>segnalazione anonima di vulnerabilità</strong> (prevista da NIS2, art. 12, divulgazione
|
||||
coordinata) e il <strong>whistleblowing</strong> dei dipendenti (segnalazione di illeciti),
|
||||
@@ -1006,7 +1040,7 @@
|
||||
classificate dall'AI e (opzionalmente) risolte automaticamente.</li>
|
||||
</ul>
|
||||
<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
|
||||
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à.
|
||||
@@ -1025,7 +1059,7 @@
|
||||
<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="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
|
||||
è bravissima a partire dal foglio bianco, ma <strong>non sostituisce</strong> il tuo giudizio:
|
||||
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>
|
||||
</ul>
|
||||
<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.
|
||||
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à.
|
||||
@@ -1139,47 +1173,56 @@
|
||||
|
||||
</div><!-- /guide-wrap -->
|
||||
</div><!-- /content-body -->
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 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/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (accordion) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// La guida è accessibile anche da non autenticati (utile per onboarding)
|
||||
// ma se sei loggato carichi sidebar + i18n normalmente. Stessa logica
|
||||
// della guida.html originale: ZERO modifiche backend.
|
||||
if (typeof loadSidebar === 'function') loadSidebar();
|
||||
// ma se sei loggato carichi chrome + i18n normalmente. ZERO modifiche backend.
|
||||
if (typeof loadSidebar === 'function') loadSidebar(); // = loadTopnavV3()
|
||||
if (typeof I18n !== 'undefined' && I18n.init) I18n.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).
|
||||
(function openFromHash() {
|
||||
var h = window.location.hash;
|
||||
if (!h) return;
|
||||
var num = h.replace('#cap-', '');
|
||||
var body = document.getElementById('body-' + num);
|
||||
if (body && window.bootstrap && bootstrap.Collapse) {
|
||||
bootstrap.Collapse.getOrCreateInstance(body).show();
|
||||
document.getElementById('cap-' + num).scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
openChapter(h.replace('#cap-', ''), true);
|
||||
})();
|
||||
window.addEventListener('hashchange', function () {
|
||||
var num = window.location.hash.replace('#cap-', '');
|
||||
var body = document.getElementById('body-' + num);
|
||||
if (body && window.bootstrap && bootstrap.Collapse) {
|
||||
bootstrap.Collapse.getOrCreateInstance(body).show();
|
||||
}
|
||||
openChapter(window.location.hash.replace('#cap-', ''), false);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
+30
-29
@@ -3,12 +3,12 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Gestione Incidenti - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (tabelle/decision-tree/modali) -> parita' funzionale assoluta. BI veste la chrome. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Incidenti · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (self-host only, nessun CDN esterno). -->
|
||||
<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/decision-tree/timeline/modali/badge). Self-hostato (nessun CDN). v3.css governa chrome + shell. -->
|
||||
<style>
|
||||
/* ── Filters Bar ─────────────────────────────────────── */
|
||||
.filters-bar {
|
||||
@@ -274,16 +274,23 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2" data-i18n="incidents.title">Gestione Incidenti</h1>
|
||||
<div class="content-header-actions">
|
||||
<button class="btn btn-primary" onclick="showCreateIncidentModal()">+ Nuovo Incidente</button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="incidents.title">Gestione Incidenti</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="incidents.title">Gestione Incidenti</h1>
|
||||
<p class="sub">Registro incidenti e notifiche CSIRT · Art. 23 NIS2 (24h / 72h / 30g)</p>
|
||||
</div>
|
||||
<div class="content-header-actions">
|
||||
<button class="btn-primary" onclick="showCreateIncidentModal()"><i data-lucide="plus" class="ic"></i>Nuovo Incidente</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
<!-- List View -->
|
||||
@@ -348,29 +355,23 @@
|
||||
<!-- Detail View -->
|
||||
<div id="view-detail" class="hidden"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 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/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// ── Auth & Init ──────────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
|
||||
I18n.init();
|
||||
HelpSystem.init();
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
|
||||
// ── Labels ───────────────────────────────────────────────────
|
||||
const CLASSIFICATION_LABELS = {
|
||||
|
||||
+56
-27
@@ -3,11 +3,17 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>NIS2 Agile — Integrazioni Esterne</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN), PRIMA di style.css cosi' il design system esistente resta autorevole -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="css/style.css?v=20260618">
|
||||
<title>Integrazioni Esterne · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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 ── */
|
||||
.integ-hero {
|
||||
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 th {
|
||||
background: rgba(255,255,255,.04);
|
||||
background: #F5F8FC;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600; text-align: left;
|
||||
padding: .6rem .75rem;
|
||||
@@ -63,10 +69,14 @@
|
||||
}
|
||||
.api-table td {
|
||||
padding: .55rem .75rem;
|
||||
border-bottom: 1px solid rgba(255,255,255,.04);
|
||||
border-bottom: 1px solid var(--line);
|
||||
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 {
|
||||
background: rgba(6,182,212,.1);
|
||||
color: var(--primary);
|
||||
@@ -123,7 +133,7 @@
|
||||
counter-increment: steps;
|
||||
display: flex; align-items: flex-start; gap: .75rem;
|
||||
padding: .65rem 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,.04);
|
||||
border-bottom: 1px solid var(--line);
|
||||
font-size: .85rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
@@ -169,7 +179,7 @@
|
||||
.event-list { display: flex; flex-direction: column; gap: .5rem; margin-bottom: 1.5rem; }
|
||||
.event-row {
|
||||
display: flex; align-items: center; gap: .75rem;
|
||||
background: rgba(255,255,255,.03);
|
||||
background: #F8FAFD;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: .6rem .9rem;
|
||||
@@ -221,12 +231,19 @@
|
||||
</head>
|
||||
<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">
|
||||
<div class="page-header">
|
||||
<h1>Integrazioni Esterne</h1>
|
||||
<p>API, webhook e connettori per sistemi GRC di terze parti</p>
|
||||
<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>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>
|
||||
|
||||
<!-- Hero -->
|
||||
@@ -912,20 +929,32 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
|
||||
|
||||
</main>
|
||||
|
||||
<script src="js/common.js?v=20260624g"></script>
|
||||
<script src="js/i18n.js?v=20260631"></script>
|
||||
<!-- 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/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>
|
||||
function showTab(id) {
|
||||
document.querySelectorAll('.tab-btn').forEach((b, i) => {
|
||||
const ids = ['api', 'invites', 'lg231', 'webhook', 'quickstart'];
|
||||
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' });
|
||||
}
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar(); // = loadTopnavV3()
|
||||
if (typeof I18n !== 'undefined') I18n.init();
|
||||
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
|
||||
function showTab(id) {
|
||||
document.querySelectorAll('.tab-btn').forEach((b, i) => {
|
||||
const ids = ['api', 'invites', 'lg231', 'webhook', 'quickstart'];
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+40
-18
@@ -3,10 +3,21 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Audit interni - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Audit interni · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia CDN). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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-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; }
|
||||
@@ -68,13 +79,23 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2 data-i18n="iaud.title">Audit interni</h2>
|
||||
</header>
|
||||
<div class="content-body">
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></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>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">
|
||||
<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),
|
||||
@@ -85,11 +106,9 @@
|
||||
(<strong>GV.PO-02</strong>, monitoraggio del programma di sicurezza). Strumento di supporto organizzativo, non un parere legale.
|
||||
</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>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Modale crea/modifica audit -->
|
||||
<div class="sm-overlay" id="aud-modal" role="dialog" aria-modal="true" aria-labelledby="aud-modal-title">
|
||||
@@ -139,10 +158,12 @@
|
||||
</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/common.js?v=20260624g"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
@@ -172,9 +193,10 @@
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
||||
if (window.I18n && I18n.init) I18n.init('it');
|
||||
if (typeof loadSidebar === 'function') loadSidebar();
|
||||
if (window.I18n && I18n.init) I18n.init('it');
|
||||
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(); } });
|
||||
['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();
|
||||
|
||||
+173
-159
@@ -3,49 +3,54 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Modello Organizzativo SGSI (ISO 27001) - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (wizard/SoA/tabelle) -> parita' funzionale assoluta. BI veste la chrome. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Modello Organizzativo SGSI (ISO 27001) · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css. Self-host only, nessun CDN esterno. -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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; }
|
||||
.isms-steps { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:20px; }
|
||||
.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; }
|
||||
.isms-step.active { border-color:var(--primary,#2563eb); background:#eff6ff; font-weight:700; }
|
||||
.isms-step.done { border-color:#16a34a; }
|
||||
.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; }
|
||||
.isms-step.done .num { background:#16a34a; color:#fff; }
|
||||
/* Stili specifici della pagina SGSI non coperti da v3.css (nessun CDN). */
|
||||
.text-muted { color: var(--muted); }
|
||||
.sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); }
|
||||
/* Base .btn usata dai bottoni generati via JS (es. "btn btn-sm btn-outline") */
|
||||
.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; }
|
||||
.btn .icon, .btn .ic { width:15px; height:15px; }
|
||||
.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.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; }
|
||||
.raci-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||||
.raci-row input, .raci-row select { padding:6px 8px; border:1px solid var(--gray-200,#e5e7eb); border-radius:6px; font-size:.85rem; }
|
||||
.soa-std { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; }
|
||||
.soa-std-head { background:var(--gray-50,#f9fafb); padding:12px 16px; cursor:pointer; display:flex; justify-content:space-between; align-items:center; }
|
||||
.isms-panel h3 { margin:0 0 12px; font-size:17px; font-weight:800; }
|
||||
.isms-panel h4 { margin:16px 0 8px; font-size:14px; font-weight:700; }
|
||||
.isms-disclaimer { font-size:.8rem; color:#92400e; background:#FBF1E2; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:12px 0; }
|
||||
.raci-row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; padding:8px 0; border-bottom:1px solid var(--line); }
|
||||
.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.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-ctrl { padding:12px 0; border-top:1px solid var(--gray-100,#f3f4f6); }
|
||||
.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(--line); }
|
||||
.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-derived { background:#ecfdf5; color:#065f46; font-size:.68rem; padding:1px 6px; border-radius:5px; }
|
||||
.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:#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-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.sel-implemented,.soa-opt.sel-verified { background:#16a34a; color:#fff; border-color:#16a34a; }
|
||||
.soa-opt.sel-in_progress { background:#f59e0b; color:#fff; border-color:#f59e0b; }
|
||||
.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:var(--c-green); color:#fff; border-color:var(--c-green); }
|
||||
.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-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 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; }
|
||||
.ck-item { display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--gray-100,#f3f4f6); font-size:.9rem; }
|
||||
.ck-yes { color:#16a34a; font-weight:700; } .ck-no { color:#9ca3af; }
|
||||
.savehint { font-size:.78rem; color:var(--gray-400,#9ca3af); 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; }
|
||||
.ck-item { display:flex; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--line); font-size:.9rem; }
|
||||
.ck-yes { color:var(--c-green); font-weight:700; } .ck-no { color:#9ca3af; }
|
||||
.savehint { font-size:.78rem; color:var(--muted); min-height:16px; }
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -62,142 +67,151 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2 data-i18n="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>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<div class="content-body">
|
||||
<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>
|
||||
<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>SGSI (ISO 27001)</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="isms.title">Modello Organizzativo SGSI (ISO 27001)</h1>
|
||||
<p class="sub">Procedura guidata ISO/IEC 27001:2022 — clausole 4–10 + Statement of Applicability</p>
|
||||
</div>
|
||||
<div class="content-header-actions" style="display:flex; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||
<span class="savehint" id="isms-savehint" role="status" aria-live="polite"></span>
|
||||
<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-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 Autorità (ACN/CSIRT) 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>
|
||||
<div class="isms-steps" id="isms-steps"></div>
|
||||
|
||||
<!-- STEP 2 - Leadership (cl.5) -->
|
||||
<div class="isms-panel" id="panel-1">
|
||||
<div class="card"><div class="card-body">
|
||||
<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(--gray-200,#e5e7eb); 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 btn-sm btn-primary" id="nis2-isms-btn-add-role" onclick="addRole()" data-i18n="isms.s2.add">Aggiungi ruolo</button>
|
||||
</div>
|
||||
<button class="btn btn-outline" id="nis2-isms-btn-next-step2" style="margin-top:14px;" onclick="goStep(2)" data-i18n="isms.next">Continua</button>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 3 - Risk (cl.6) -->
|
||||
<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>
|
||||
<!-- STEP 1 - Contesto & Ambito (cl.4) -->
|
||||
<div class="isms-panel" id="panel-0">
|
||||
<div class="card card-pad">
|
||||
<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 Autorità (ACN/CSIRT) 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-primary" id="nis2-isms-btn-save-step1" onclick="saveStep1()" data-i18n="isms.save_next">Salva e continua</button>
|
||||
</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/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script 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>
|
||||
</html>
|
||||
|
||||
+62
-44
@@ -3,15 +3,36 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Knowledge Base - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (lista doc, search) -> parita' funzionale assoluta. BI veste la chrome. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Knowledge Base · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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 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-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; }
|
||||
@@ -19,8 +40,6 @@
|
||||
.kb-scope-badge.SYSTEM { background: #DBEAFE; color: #1E40AF; }
|
||||
.kb-scope-badge.FIRM { background: #FEF3C7; color: #92400E; }
|
||||
.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>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -37,19 +56,23 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2">Knowledge Base</h1>
|
||||
<div class="content-header-actions">
|
||||
<button class="btn btn-primary" id="btn-kb-upload">
|
||||
<svg class="icon icon-sm" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-upload"></use></svg>
|
||||
Carica documento
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Knowledge Base</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Knowledge Base</h1>
|
||||
<p class="sub">Documenti che l'AI usa per rispondere con citazioni precise</p>
|
||||
</div>
|
||||
<button class="btn-primary" id="btn-kb-upload">
|
||||
<i data-lucide="upload" class="ic"></i>Carica documento
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
<p class="text-muted" style="margin-top:0;">
|
||||
@@ -60,7 +83,7 @@
|
||||
</p>
|
||||
|
||||
<!-- 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="form-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="form-group">
|
||||
@@ -120,7 +143,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Lista documenti -->
|
||||
<div class="card kb-doc-list">
|
||||
<div class="card card-pad kb-doc-list">
|
||||
<div class="card-header">
|
||||
<h3>Documenti caricati visibili</h3>
|
||||
</div>
|
||||
@@ -132,47 +155,42 @@
|
||||
</div>
|
||||
|
||||
<!-- 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-body">
|
||||
<div style="display:flex;gap:8px;">
|
||||
<label for="kb-search-query" class="sr-only visually-hidden">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 style="display:flex;gap:8px;align-items:flex-end;">
|
||||
<div style="flex:1;">
|
||||
<label for="kb-search-query">Query di ricerca semantica</label>
|
||||
<input type="text" id="kb-search-query" class="form-control" placeholder="Es: come gestire un incidente NIS2?" aria-label="Query di ricerca semantica">
|
||||
</div>
|
||||
<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>
|
||||
Cerca
|
||||
<i data-lucide="search" class="icon"></i>Cerca
|
||||
</button>
|
||||
</div>
|
||||
<div id="kb-search-results" style="margin-top:12px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). -->
|
||||
<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 → kb.js -->
|
||||
<script src="/js/api.js?v=20260625d"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script src="/js/kb.js"></script>
|
||||
<script>
|
||||
// Gate auth + chrome come le altre pagine -bi.
|
||||
// Gate auth + chrome come le altre pagine V3.
|
||||
if (typeof checkAuth === 'function' && !checkAuth()) {
|
||||
// checkAuth reindirizza a login se non autenticato
|
||||
} else {
|
||||
if (typeof loadSidebar === 'function') loadSidebar();
|
||||
if (typeof loadSidebar === 'function') loadSidebar(); // = loadTopnavV3()
|
||||
if (typeof I18n !== 'undefined' && I18n.init) I18n.init();
|
||||
if (typeof HelpSystem !== 'undefined' && HelpSystem.init) HelpSystem.init();
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<h1>Manutenzione in corso</h1>
|
||||
<p>Stiamo aggiornando l'<strong>interfaccia (UI)</strong> di NIS2 Agile su richiesta di modifica.
|
||||
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>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -3,10 +3,21 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Riesame di Direzione - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Riesame di Direzione · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia CDN). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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-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; }
|
||||
@@ -62,13 +73,23 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2 data-i18n="mreview.title">Riesame di Direzione</h2>
|
||||
</header>
|
||||
<div class="content-body">
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></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>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">
|
||||
<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
|
||||
@@ -79,11 +100,9 @@
|
||||
art. 23 D.Lgs. 138/2024). Strumento di supporto e pre-audit: non sostituisce l'auditor.
|
||||
</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>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Modale dettaglio / verbale -->
|
||||
<div class="sm-overlay" id="mr-modal" role="dialog" aria-modal="true" aria-labelledby="mr-modal-title">
|
||||
@@ -126,10 +145,12 @@
|
||||
</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/common.js?v=20260624g"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
@@ -160,9 +181,10 @@
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
||||
if (window.I18n && I18n.init) I18n.init('it');
|
||||
if (typeof loadSidebar === 'function') loadSidebar();
|
||||
if (window.I18n && I18n.init) I18n.init('it');
|
||||
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(); });
|
||||
const ov = el('mr-modal'); if (ov) ov.addEventListener('click', e => { if (e.target === ov) mrClose(); });
|
||||
await mrLoadAll();
|
||||
|
||||
+112
-114
@@ -3,35 +3,35 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Misure e Requisiti - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Misure e Requisiti · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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; }
|
||||
.mr-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
|
||||
.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-stats { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:16px; }
|
||||
.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-stat .n { font-size:1.5rem; font-weight:800; line-height:1; }
|
||||
.mr-stat .l { font-size:.76rem; color:var(--gray-500,#6b7280); margin-top:4px; }
|
||||
.mr-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
|
||||
.mr-toolbar label { font-weight:600; font-size:.82rem; }
|
||||
.mr-toolbar select, .mr-toolbar input[type=search] { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; }
|
||||
/* V3 — specifico pagina Misure/Requisiti (non coperto da v3.css) */
|
||||
.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-note { font-size:12.5px; color:#92400e; background:#FBF1E2; border:1px solid #fde68a; border-radius:10px; padding:10px 12px; margin:0 0 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-stats { display:flex; flex-wrap:wrap; gap:12px; margin-bottom:16px; }
|
||||
.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 .n { font-size:27px; font-weight:800; line-height:1; color:var(--c-blue); }
|
||||
.mr-stat .l { font-size:11px; font-weight:700; letter-spacing:.06em; text-transform:uppercase; color:#7a8aa0; margin-top:6px; }
|
||||
.mr-toolbar { display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
|
||||
.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-group { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; }
|
||||
.mr-group-head { background:#f8fafc; padding:11px 14px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||||
.mr-group-head .code { font-weight:800; color:#1e3a8a; font-size:.95rem; }
|
||||
.mr-group-head .area { font-size:.78rem; color:var(--gray-500,#6b7280); }
|
||||
.mr-group-head .descr { font-size:.86rem; 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.ess { background:#fef3c7; color:#92400e; }
|
||||
.mr-table { width:100%; border-collapse:collapse; font-size:.85rem; }
|
||||
.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 { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); background:#fff; position:sticky; top:0; }
|
||||
.mr-group { border:1px solid var(--line); border-radius:11px; margin-bottom:14px; overflow:hidden; }
|
||||
.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:14px; }
|
||||
.mr-group-head .area { font-size:12px; color:var(--muted); }
|
||||
.mr-group-head .descr { font-size:13px; color:#334155; margin-top:3px; line-height:1.5; }
|
||||
.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:#FBF1E2; color:#92400e; }
|
||||
.mr-table { width:100%; border-collapse:collapse; font-size:13px; }
|
||||
.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: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-code-pill { display:inline-block; font-size:.7rem; 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 */
|
||||
.mr-code-pill { display:inline-block; font-size:11px; font-weight:700; color:#3730a3; background:#eef2ff; border-radius:6px; padding:1px 6px; }
|
||||
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:focus-visible { outline:2px solid #3730a3; outline-offset:1px; }
|
||||
@@ -41,31 +41,32 @@
|
||||
.mr-yn.n { color:#cbd5e1; }
|
||||
.mr-row.spento { opacity:.4; }
|
||||
.mr-row.spento td { font-style:italic; }
|
||||
.mr-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); }
|
||||
.sv-badge { display:inline-block; font-size:.72rem; font-weight:800; padding:3px 10px; border-radius:20px; border:none; cursor:pointer; }
|
||||
.sv-badge.sv-todo { background:#f1f5f9; color:#475569; }
|
||||
.mr-empty { text-align:center; padding:34px 16px; color:var(--muted); }
|
||||
.savehint { font-size:12.5px; color:var(--muted); min-height:16px; }
|
||||
.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-ko { background:#fee2e2; color:#991b1b; }
|
||||
.sv-badge.sv-mid { background:#fef3c7; color:#92400e; }
|
||||
.sv-badge.sv-ok { background:#dcfce7; color:#166534; }
|
||||
.sv-val { font-size:.76rem; color:#64748b; margin-top:4px; max-width:300px; }
|
||||
/* Modale valutazione */
|
||||
.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; }
|
||||
.sv-badge.sv-ko { background:#FBE9E9; color:#991b1b; }
|
||||
.sv-badge.sv-mid { background:#FBF1E2; color:#92400e; }
|
||||
.sv-badge.sv-ok { background:#E6F4EC; color:#166534; }
|
||||
.sv-val { font-size:11.5px; color:#64748b; margin-top:4px; max-width:300px; }
|
||||
/* Modale valutazione (page-owned) */
|
||||
.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-dialog { background:#fff; border-radius:12px; width:100%; max-width:600px; box-shadow:0 20px 50px rgba(0,0,0,.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 h3 { margin:0; font-size:1.02rem; }
|
||||
.rm-head .sub { font-size:.8rem; color:var(--gray-500,#6b7280); margin-top:4px; }
|
||||
.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(--line); }
|
||||
.rm-head h3 { margin:0; font-size:16px; }
|
||||
.rm-head .sub { font-size:12.5px; color:var(--muted); margin-top:4px; }
|
||||
.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 label { display:block; font-weight:600; font-size:.86rem; 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 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(--line); border-radius:8px; font:inherit; font-size:14px; }
|
||||
.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 .pill { display:inline-block; font-size:.7rem; 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-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); }
|
||||
.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:11px; font-weight:700; color:#3730a3; background:#eef2ff; border-radius:6px; padding:1px 6px; margin-right:4px; }
|
||||
.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(--muted); }
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -82,56 +83,62 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2 data-i18n="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>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<div class="content-body">
|
||||
<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>
|
||||
<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>Lex e ISO</span>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Misure e requisiti</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="mr.title">Misure e Requisiti</h1>
|
||||
<p class="sub">Elenco canonico delle misure di base ACN e dei relativi requisiti</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<span class="savehint" id="mr-hint" role="status" aria-live="polite"></span>
|
||||
</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) -->
|
||||
<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>
|
||||
<div class="rm-foot">
|
||||
<button class="btn btn-outline" type="button" onclick="mrCloseEval()">Annulla</button>
|
||||
<button class="btn btn-primary" type="button" id="mr-eval-save" onclick="mrSaveEval()">Salva</button>
|
||||
<button class="btn-ghost" type="button" onclick="mrCloseEval()">Annulla</button>
|
||||
<button class="btn-primary" type="button" id="mr-eval-save" onclick="mrSaveEval()">Salva</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts V3 (ordine ESATTO come dashboard.html) -->
|
||||
<script src="/js/api.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260624g"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
@@ -185,12 +193,9 @@
|
||||
function el(id) { return document.getElementById(id); }
|
||||
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).
|
||||
// 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; }
|
||||
|
||||
// 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) {
|
||||
const pols = req.policies || [];
|
||||
if (pols.length) {
|
||||
@@ -220,6 +225,7 @@
|
||||
if (window.I18n && I18n.init) I18n.init('it');
|
||||
if (typeof loadSidebar === 'function') loadSidebar();
|
||||
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
await mrLoad();
|
||||
});
|
||||
|
||||
@@ -230,17 +236,14 @@
|
||||
MR.org_class = (data && data.org_class) || null;
|
||||
MR.has_org = !!(data && data.has_org);
|
||||
MR.stati = (data && data.stati) || Object.keys(STATO_META);
|
||||
// indicizza i requisiti per id (per la modale)
|
||||
MR.byId = {};
|
||||
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';
|
||||
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-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);
|
||||
if (focusReq) el('mr-class').value = 'all'; // evita che la riga sia filtrata via
|
||||
if (focusReq) el('mr-class').value = 'all';
|
||||
mrRender();
|
||||
if (focusReq) mrFocusRequisito(focusReq);
|
||||
} 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) {
|
||||
const row = document.getElementById('req-' + reqId);
|
||||
if (!row) return;
|
||||
@@ -257,14 +259,12 @@
|
||||
setTimeout(function () { row.classList.remove('mr-row-focus'); }, 4000);
|
||||
}
|
||||
|
||||
// classe effettiva selezionata (risolve "auto" sulla classe dell'org)
|
||||
function mrEffectiveClass() {
|
||||
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;
|
||||
return sel; // important | essential
|
||||
return sel;
|
||||
}
|
||||
// un requisito è "attivo" per la classe effettiva?
|
||||
function reqActive(req, cls) {
|
||||
if (!cls) return true;
|
||||
return cls === 'important' ? !!req.applies_important : !!req.applies_essential;
|
||||
@@ -283,9 +283,8 @@
|
||||
const q = (el('mr-q').value || '').trim();
|
||||
const view = el('mr-view').value;
|
||||
|
||||
// costruisco le righe filtrate (misura -> requisiti visibili)
|
||||
let activeCount = 0;
|
||||
const groupsByKey = {}; // key -> {label, descr, area, code, essOnly, rows:[]}
|
||||
const groupsByKey = {};
|
||||
|
||||
MR.measures.forEach(function (m) {
|
||||
const essOnly = !m.applies_important;
|
||||
@@ -398,7 +397,6 @@
|
||||
(function () {
|
||||
const ov = el('mr-modal'); if (!ov) return;
|
||||
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) {
|
||||
if (e.key !== 'Tab' || !ov.classList.contains('open')) return;
|
||||
const nodes = ov.querySelectorAll('select, textarea, button');
|
||||
|
||||
+166
-147
@@ -3,29 +3,63 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Aggiornamenti Normativi - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (card aggiornamenti/badge/modali) -> parita' funzionale assoluta. BI veste la chrome. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Aggiornamenti Normativi · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<style>
|
||||
.impact-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; }
|
||||
.impact-critical { background: var(--danger-bg); color: var(--danger); }
|
||||
.impact-high { background: #fff7ed; color: #c2410c; }
|
||||
.impact-medium { background: var(--warning-bg); color: #a16207; }
|
||||
.impact-low { background: var(--gray-100); color: var(--gray-600); }
|
||||
.impact-informational { background: rgba(6,182,212,0.1); color: #0891b2; }
|
||||
.ack-badge { display: inline-flex; align-items: center; gap: 5px; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 600; }
|
||||
.ack-done { background: var(--success-bg); color: var(--success); }
|
||||
.ack-pending { background: var(--warning-bg); color: #a16207; }
|
||||
.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 { border: 1px solid var(--gray-200); border-radius: var(--border-radius-lg); padding: 20px; margin-bottom: 16px; transition: all var(--transition-fast); }
|
||||
.update-card:hover { border-color: var(--primary); box-shadow: 0 2px 8px rgba(6,182,212,0.1); }
|
||||
.update-card.action-required { border-left: 3px solid var(--warning); }
|
||||
.update-card.acked { opacity: 0.7; }
|
||||
.ack-progress { height: 6px; background: var(--gray-200); border-radius: 3px; overflow: hidden; }
|
||||
.ack-progress-bar { height: 100%; background: var(--primary); border-radius: 3px; transition: width 0.5s ease; }
|
||||
/* V3 — specifico pagina Normative (non coperto da v3.css) */
|
||||
.nrm-bars{display:grid;gap:18px}
|
||||
.nrm-progress-track{height:8px;background:#EAF0F6;border-radius:99px;overflow:hidden}
|
||||
.nrm-progress-fill{height:100%;background:var(--c-blue);border-radius:99px;transition:width .5s ease}
|
||||
.nrm-filters{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
|
||||
.nrm-select{padding:8px 12px;border:1px solid var(--line);border-radius:8px;background:#fff;font:inherit;font-size:13px;color:var(--ink)}
|
||||
.nrm-check{display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;color:var(--ink)}
|
||||
.nrm-check input{accent-color:var(--c-blue)}
|
||||
.update-card{border:1px solid var(--line);border-radius:11px;padding:18px;margin-bottom:14px;transition:border-color .12s,box-shadow .12s;background:#fff}
|
||||
.update-card:hover{border-color:#9FBEDD;box-shadow:0 4px 12px rgba(16,40,80,.08)}
|
||||
.update-card.action-required{border-left:3px solid var(--c-amber)}
|
||||
.update-card.acked{opacity:.72}
|
||||
.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}
|
||||
.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}
|
||||
.impact-critical{background:#FBE9E9;color:#B23636}
|
||||
.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>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -42,95 +76,82 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<div class="page-header-content">
|
||||
<h1 class="page-title">Aggiornamenti Normativi</h1>
|
||||
<p class="page-subtitle">Feed NIS2 / ACN / DORA — Presa visione obbligatoria per audit compliance</p>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn btn-secondary" onclick="loadPending()">
|
||||
<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>
|
||||
Solo Pendenti
|
||||
</button>
|
||||
</div>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
|
||||
<div id="topnav"></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>Lex e ISO</span>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Normative</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="normative.title">Aggiornamenti Normativi</h1>
|
||||
<p class="sub">Feed NIS2 / ACN / DORA — Presa visione obbligatoria per audit compliance</p>
|
||||
</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>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-grid mb-24" id="stats-grid">
|
||||
<div class="stat-card"><div class="spinner"></div></div>
|
||||
</div>
|
||||
<!-- Avanzamento presa visione -->
|
||||
<section class="card card-pad">
|
||||
<div class="sec-head"><h3>Avanzamento presa visione</h3><span class="hint" id="ack-pct">—</span></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 -->
|
||||
<div class="card mb-24">
|
||||
<div class="card-body" style="padding:16px 20px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">
|
||||
<span style="font-size:0.875rem; font-weight:600; color:var(--gray-700);">Avanzamento presa visione</span>
|
||||
<span id="ack-pct" style="font-size:0.875rem; font-weight:700; color:var(--primary);">—</span>
|
||||
</div>
|
||||
<div class="ack-progress">
|
||||
<div class="ack-progress-bar" id="ack-bar" style="width:0%"></div>
|
||||
</div>
|
||||
<p style="font-size:0.75rem; color:var(--gray-500); margin-top:6px;">Documentare la presa visione degli aggiornamenti normativi è richiesto per la compliance NIS2 Art.21.</p>
|
||||
<!-- Filtri -->
|
||||
<section class="card card-pad">
|
||||
<div class="nrm-filters">
|
||||
<label for="filter-source" class="visually-hidden" style="position:absolute;width:1px;height:1px;overflow:hidden">Filtra per fonte normativa</label>
|
||||
<select id="filter-source" class="nrm-select" 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" 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>
|
||||
</section>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card mb-16">
|
||||
<div class="card-body" style="padding:14px 20px;">
|
||||
<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>
|
||||
<div id="updates-container"><div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Scripts V3 (ordine ESATTO come dashboard.html) -->
|
||||
<script src="/js/api.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// ── Auth + chrome + i18n + help ──────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
I18n.init();
|
||||
loadSidebar(); // = loadTopnavV3()
|
||||
if (typeof I18n !== 'undefined') I18n.init();
|
||||
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
|
||||
function refreshIcons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
|
||||
|
||||
const sourceLabels = {
|
||||
nis2_directive: 'NIS2 Directive', dlgs_138_2024: 'D.Lgs. 138/2024',
|
||||
@@ -149,30 +170,27 @@
|
||||
|
||||
function renderStats(d) {
|
||||
document.getElementById('stats-grid').innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Aggiornamenti Totali</div>
|
||||
<div class="stat-value">${d.total_updates || 0}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Prese Visione</div>
|
||||
<div class="stat-value" style="color:var(--success);">${d.acknowledged || 0}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Pendenti</div>
|
||||
<div class="stat-value" style="color:var(--warning);">${d.pending || 0}</div>
|
||||
</div>
|
||||
<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>`;
|
||||
<a class="card card-pad stat blue" style="cursor:default">
|
||||
<div class="ibox"><i data-lucide="layers" class="ic"></i></div>
|
||||
<div style="flex:1;min-width:0"><div class="lbl">Aggiornamenti totali</div><div class="val blue">${d.total_updates || 0}</div></div>
|
||||
</a>
|
||||
<a class="card card-pad stat green" style="cursor:default">
|
||||
<div class="ibox"><i data-lucide="check-check" class="ic"></i></div>
|
||||
<div style="flex:1;min-width:0"><div class="lbl">Prese visione</div><div class="val green">${d.acknowledged || 0}</div></div>
|
||||
</a>
|
||||
<a class="card card-pad stat amber" style="cursor:default">
|
||||
<div class="ibox"><i data-lucide="clock" class="ic"></i></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>
|
||||
</a>`;
|
||||
|
||||
document.getElementById('ack-pct').textContent = (d.ack_rate || 0) + '%';
|
||||
document.getElementById('ack-bar').style.width = (d.ack_rate || 0) + '%';
|
||||
refreshIcons();
|
||||
}
|
||||
|
||||
async function loadUpdates() {
|
||||
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 {
|
||||
const params = new URLSearchParams();
|
||||
const source = document.getElementById('filter-source').value;
|
||||
@@ -185,30 +203,36 @@
|
||||
const result = await api.request('GET', '/normative/list?' + params);
|
||||
if (result.success) renderUpdates(result.data.updates || []);
|
||||
} 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() {
|
||||
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 {
|
||||
const result = await api.request('GET', '/normative/pending');
|
||||
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) {
|
||||
const container = document.getElementById('updates-container');
|
||||
if (!updates.length) {
|
||||
container.innerHTML = `<div class="empty-state">
|
||||
<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>
|
||||
<h4>${pendingOnly ? 'Nessun aggiornamento pendente — tutto preso in visione!' : 'Nessun aggiornamento trovato'}</h4>
|
||||
</div>`;
|
||||
container.innerHTML = `<section class="card card-pad"><div class="v3-empty">
|
||||
<i data-lucide="file-text" class="ic"></i>
|
||||
<b>${pendingOnly ? 'Nessun aggiornamento pendente — tutto preso in visione!' : 'Nessun aggiornamento trovato'}</b>
|
||||
<span>I prossimi aggiornamenti normativi appariranno qui.</span>
|
||||
</div></section>`;
|
||||
refreshIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
let html = '<div class="stack">';
|
||||
updates.forEach(u => {
|
||||
const acked = u.is_acknowledged || !!(u.acknowledged_at);
|
||||
const actionClass = u.action_required ? 'action-required' : '';
|
||||
@@ -218,51 +242,46 @@
|
||||
).join(' ');
|
||||
|
||||
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 style="flex:1;">
|
||||
<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:8px;">
|
||||
<div class="nrm-card-grid">
|
||||
<div style="flex:1;min-width:0;">
|
||||
<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="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
|
||||
? `<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>'
|
||||
}
|
||||
</div>
|
||||
<h3 style="font-size:1rem; font-weight:700; color:var(--gray-900); margin-bottom:6px;">${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>` : ''}
|
||||
<p style="font-size:0.8125rem; color:var(--gray-600); line-height:1.6; margin-bottom:10px;">${escapeHtml(u.summary)}</p>
|
||||
<h3 class="nrm-uupd-h3">${escapeHtml(u.title)}</h3>
|
||||
${u.reference ? `<p class="nrm-ref"><code>${escapeHtml(u.reference)}</code></p>` : ''}
|
||||
<p class="nrm-summary">${escapeHtml(u.summary)}</p>
|
||||
${domains ? `<div style="display:flex; gap:6px; flex-wrap:wrap;">${domains}</div>` : ''}
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:8px; align-items:flex-end; flex-shrink:0;">
|
||||
${u.effective_date ? `<p style="font-size:0.75rem; color:var(--gray-500);">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;">
|
||||
<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>
|
||||
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 class="nrm-card-side">
|
||||
${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-ghost"><i data-lucide="external-link" class="ic"></i>Fonte</a>` : ''}
|
||||
${!acked ? `<button class="btn-primary" onclick="acknowledgeUpdate(${u.id})"><i data-lucide="check" class="ic"></i>Prendi in visione</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
refreshIcons();
|
||||
}
|
||||
|
||||
async function acknowledgeUpdate(id) {
|
||||
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.
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
</div>
|
||||
`, `<button class="btn btn-primary" onclick="confirmAck(${id})">Conferma presa visione</button>
|
||||
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>`);
|
||||
`, { footer: `<button class="btn-primary" onclick="confirmAck(${id})">Conferma presa visione</button>
|
||||
<button class="btn-ghost" onclick="closeModal()">Annulla</button>` });
|
||||
}
|
||||
|
||||
async function confirmAck(id) {
|
||||
@@ -278,7 +297,7 @@
|
||||
} catch (e) { showNotification('Errore di connessione.', 'error'); }
|
||||
}
|
||||
|
||||
// Init
|
||||
// ── Init dati reali ──────────────────────────────────────
|
||||
loadStats();
|
||||
loadUpdates();
|
||||
</script>
|
||||
|
||||
+80
-53
@@ -3,50 +3,50 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Organigramma - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Organigramma · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css. No external CDN, no Bootstrap Italia. -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<style>
|
||||
.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; }
|
||||
.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-stats { display:flex; flex-wrap:wrap; gap:10px; margin-bottom: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-stat .n { font-size:1.5rem; font-weight:800; color:var(--primary,#2563eb); line-height:1; }
|
||||
.org-stat .l { font-size:.78rem; color:var(--gray-500,#6b7280); margin-top:4px; }
|
||||
/* V3+ page-specific (componenti non coperti da v3.css): albero + modale ruolo. Token V3. */
|
||||
.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-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-stats { display:flex; flex-wrap:wrap; gap:12px; margin-bottom:18px; }
|
||||
.org-stat { flex:1 1 150px; min-width:130px; background:#fff; border:1px solid var(--line); border-radius:11px; padding:13px 15px; }
|
||||
.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 */
|
||||
.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-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.gov { border-color:#b45309; background:#fffdf7; }
|
||||
.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:#e0b25a; background:#FFFDF7; }
|
||||
.org-card-main { flex:1 1 auto; min-width:0; }
|
||||
.org-role-name { font-weight:700; font-size:.98rem; }
|
||||
.org-holder { font-size:.85rem; color:var(--gray-600,#4b5563); margin-top:2px; }
|
||||
.org-holder.vacant { color:#9ca3af; font-style:italic; }
|
||||
.org-desc { font-size:.82rem; color:var(--gray-500,#6b7280); 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.gov { background:#fef3c7; color:#92400e; }
|
||||
.org-role-name { font-weight:700; font-size:14px; }
|
||||
.org-holder { font-size:12.5px; color:var(--muted); margin-top:3px; }
|
||||
.org-holder.vacant { color:#9aa8ba; font-style:italic; }
|
||||
.org-desc { font-size:12.5px; color:var(--muted); margin-top:6px; line-height:1.5; }
|
||||
.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:#FBF1E2; color:#9A6418; }
|
||||
.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(--gray-500,#6b7280); }
|
||||
.org-empty .big { font-size:1.05rem; font-weight:700; color:var(--gray-700,#374151); margin-bottom:6px; }
|
||||
.org-empty { text-align:center; padding:48px 16px; color:var(--muted); }
|
||||
.org-empty .big { font-size:16px; font-weight:700; color:var(--ink); margin-bottom:6px; }
|
||||
/* 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-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(0,0,0,.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 h3 { margin:0; font-size:1.05rem; }
|
||||
.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(--line); }
|
||||
.om-head h3 { margin:0; font-size:17px; font-weight:800; }
|
||||
.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 label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; }
|
||||
.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 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 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-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-err { color:#b91c1c; font-size:.82rem; 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-err { color:var(--c-red); font-size:12.5px; min-height:18px; margin-top:2px; }
|
||||
.om-close { background:none; border:none; font-size:22px; line-height:1; cursor:pointer; color:#9aa8ba; }
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -63,17 +63,27 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2 data-i18n="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>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<div class="content-body">
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Struttura interna</span>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="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">
|
||||
<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.
|
||||
@@ -93,12 +103,12 @@
|
||||
<div id="org-empty" class="org-empty" style="display:none;">
|
||||
<div class="big">Nessun ruolo definito</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 btn-outline" onclick="orgScaffold()" style="margin-left:8px;">Crea struttura di base</button>
|
||||
<button class="btn-primary" onclick="orgOpenModal()"><i data-lucide="plus" class="ic"></i>Crea il primo ruolo</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>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Modale crea/modifica ruolo -->
|
||||
<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>
|
||||
<div class="om-foot">
|
||||
<button class="btn btn-outline" type="button" onclick="orgCloseModal()">Annulla</button>
|
||||
<button class="btn btn-primary" type="button" id="org-f-save" onclick="orgSaveRole()">Salva</button>
|
||||
<button class="btn-ghost" type="button" onclick="orgCloseModal()">Annulla</button>
|
||||
<button class="btn-primary" type="button" id="org-f-save" onclick="orgSaveRole()">Salva</button>
|
||||
</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/common.js?v=20260624g"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script src="/js/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>
|
||||
</html>
|
||||
|
||||
+33
-33
@@ -3,12 +3,12 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Policy - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (tabelle/editor/modali) -> parita' funzionale assoluta. BI veste la chrome. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Policy · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (self-host only, nessun CDN esterno). -->
|
||||
<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/editor/modali/badge/tab). Self-hostato (nessun CDN). v3.css governa chrome + shell. -->
|
||||
<style>
|
||||
/* ── Policy Page Styles ──────────────────────────────────── */
|
||||
.filters-bar {
|
||||
@@ -254,20 +254,26 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2" data-i18n="policies.title">Gestione Policy</h1>
|
||||
<div class="content-header-actions">
|
||||
<button id="nis2-policies-btn-ai-generate" class="btn btn-outline" onclick="openAIGenerateModal()">
|
||||
<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>
|
||||
Genera con AI
|
||||
</button>
|
||||
<button id="nis2-policies-btn-new" class="btn btn-primary" onclick="openCreateModal()">+ Nuova Policy</button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="policies.title">Gestione Policy</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="policies.title">Gestione Policy</h1>
|
||||
<p class="sub">Politiche di sicurezza: bozze, revisione, approvazione e template NIS2</p>
|
||||
</div>
|
||||
<div class="content-header-actions" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<button id="nis2-policies-btn-ai-generate" class="btn-ghost" onclick="openAIGenerateModal()">
|
||||
<i data-lucide="sparkles" class="ic"></i>Genera con AI
|
||||
</button>
|
||||
<button id="nis2-policies-btn-new" class="btn-primary" onclick="openCreateModal()"><i data-lucide="plus" class="ic"></i>Nuova Policy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-body" id="page-content">
|
||||
<!-- Tabs -->
|
||||
@@ -330,29 +336,23 @@
|
||||
<!-- Detail View (hidden by default) -->
|
||||
<div id="policy-detail-view" style="display:none;"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 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/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
|
||||
I18n.init();
|
||||
HelpSystem.init();
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
|
||||
// ── Labels ──────────────────────────────────────────────
|
||||
const CATEGORY_LABELS = {
|
||||
|
||||
+75
-53
@@ -3,52 +3,54 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Matrice RACI - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Matrice RACI · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css. No external CDN, no Bootstrap Italia. -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<style>
|
||||
.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; }
|
||||
.raci-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 18px; }
|
||||
/* V3+ page-specific (componenti non coperti da v3.css): tab interni, matrice RACI, link m2m. Token V3. */
|
||||
.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-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.active { border-color:var(--primary,#2563eb); background:#eff6ff; color:var(--primary,#2563eb); }
|
||||
.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(--c-blue); background:#EFF4FB; color:var(--c-blue); }
|
||||
.cmp-panel { display:none; }
|
||||
.cmp-panel.active { display:block; }
|
||||
.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-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); }
|
||||
.gap-role-sub { font-size:.82rem; color:var(--gray-500,#6b7280); }
|
||||
.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(--muted); }
|
||||
.gap-role-sub { font-size:12px; color:var(--muted); }
|
||||
/* Matrice RACI */
|
||||
.raci-matrix-wrap { overflow-x:auto; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; }
|
||||
.raci-matrix { border-collapse:collapse; font-size:.84rem; 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 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-wrap { overflow-x:auto; border:1px solid var(--line); border-radius:11px; }
|
||||
.raci-matrix { border-collapse:collapse; font-size:13px; min-width:100%; }
|
||||
.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:#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 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 thead th.raci-corner { position:sticky; left:0; z-index:3; background:var(--gray-50,#f9fafb); text-align:left; }
|
||||
.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:#FAFCFE; text-align:left; }
|
||||
.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-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.has-r { background:#dcfce7; 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-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-legend { display:flex; gap:14px; flex-wrap:wrap; font-size:.78rem; margin:12px 2px 0; color:var(--gray-600,#4b5563); }
|
||||
.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: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:#E6F4EC; border-color:#86efac; color:#166534; 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-i { background:#E0F2FE; border-color:#7dd3fc; color:#075985; font-weight:700; }
|
||||
.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 .l-r b { background:#dcfce7; color:#166534; }
|
||||
.raci-legend .l-a b { background:#fee2e2; color:#991b1b; }
|
||||
.raci-legend .l-c b { background:#fef9c3; color:#854d0e; }
|
||||
.raci-legend .l-i b { background:#e0f2fe; color:#075985; }
|
||||
.raci-legend .l-r b { background:#E6F4EC; color:#166534; }
|
||||
.raci-legend .l-a b { background:#FBE9E9; color:#991b1b; }
|
||||
.raci-legend .l-c b { background:#FEF9C3; color:#854d0e; }
|
||||
.raci-legend .l-i b { background:#E0F2FE; color:#075985; }
|
||||
/* 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 label { font-size:.72rem; font-weight:600; color:var(--gray-500,#6b7280); }
|
||||
.raci-inline select { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; min-width:200px; }
|
||||
.raci-linklist { width:100%; border-collapse:collapse; font-size:.88rem; }
|
||||
.raci-linklist th, .raci-linklist td { text-align:left; padding:9px 10px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||||
.raci-linklist th { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
|
||||
.raci-linklist .btn { padding:4px 9px; font-size:.76rem; }
|
||||
.raci-arrow { color:var(--gray-400,#9ca3af); padding:0 6px; }
|
||||
.raci-inline .fld label { font-size:11px; font-weight:700; color:var(--muted); }
|
||||
.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:13.5px; }
|
||||
.raci-linklist th, .raci-linklist td { text-align:left; padding:10px 11px; border-bottom:1px solid var(--line); }
|
||||
.raci-linklist th { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); font-weight:700; }
|
||||
.raci-arrow { color:#9aa8ba; padding:0 6px; }
|
||||
.savehint { font-size:12px; color:var(--muted); }
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -65,17 +67,24 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2 data-i18n="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 id="topnav"></div>
|
||||
|
||||
<div class="content-body">
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Struttura interna</span>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="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">
|
||||
<strong>Matrice RACI & 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.
|
||||
@@ -123,20 +132,33 @@
|
||||
<label id="raci-blabel" for="raci-bside">Lato B</label>
|
||||
<select id="raci-bside"></select>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" onclick="raciAddLink()">Aggiungi collegamento</button>
|
||||
<button class="btn-primary" onclick="raciAddLink()">Aggiungi collegamento</button>
|
||||
</div>
|
||||
<div id="raci-links"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</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/common.js?v=20260624g"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script src="/js/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>
|
||||
</html>
|
||||
|
||||
+194
-415
@@ -3,344 +3,127 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Audit e Report - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (tabelle/modali/form) -> parita' funzionale assoluta. BI veste la chrome. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Audit e Report · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (self-host only, nessun CDN esterno). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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-nav {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 2px solid var(--gray-200);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.tab-nav { display:flex; gap:0; border-bottom:2px solid var(--gray-200); margin-bottom:24px; flex-wrap:wrap; }
|
||||
.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);
|
||||
padding:12px 22px; background:none; border:none; border-bottom:2px solid transparent; margin-bottom:-2px;
|
||||
color:var(--gray-500); font:inherit; font-size:.9rem; font-weight:600; cursor:pointer; transition:all var(--transition-fast);
|
||||
}
|
||||
.tab-nav button:hover {
|
||||
color: var(--gray-700);
|
||||
background: var(--gray-50);
|
||||
}
|
||||
.tab-nav button.active {
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
.tab-nav button:hover { color:var(--gray-700); background:var(--gray-50); }
|
||||
.tab-nav button.active { color:var(--primary); border-bottom-color:var(--primary); }
|
||||
.tab-nav button:focus-visible { outline:2px solid var(--primary); outline-offset:-2px; border-radius:4px 4px 0 0; }
|
||||
.tab-panel { display:none; }
|
||||
.tab-panel.active { display:block; }
|
||||
|
||||
/* ── 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);
|
||||
}
|
||||
.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); }
|
||||
.data-table { width:100%; border-collapse:collapse; background:var(--card-bg); border-radius:var(--border-radius); overflow:hidden; box-shadow:var(--card-shadow); }
|
||||
.data-table thead { background:var(--gray-50); }
|
||||
.data-table th { text-align:left; padding: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); }
|
||||
.data-table td { padding:11px 16px; font-size:.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); }
|
||||
.data-table tbody tr.clickable-row { cursor:pointer; }
|
||||
|
||||
/* ── Status Badges ──────────────────────────────────────── */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
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); }
|
||||
.status-badge { display:inline-flex; align-items:center; padding:3px 10px; border-radius:20px; font-size:.78rem; font-weight:600; }
|
||||
.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-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.progress-bar {
|
||||
flex-grow: 1;
|
||||
height: 6px;
|
||||
background: var(--gray-200);
|
||||
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);
|
||||
}
|
||||
.progress-bar-container { display:flex; align-items:center; gap:8px; }
|
||||
.progress-bar { flex-grow:1; height:6px; background:var(--gray-200); border-radius:3px; overflow:hidden; min-width:80px; }
|
||||
.progress-bar-fill { height:100%; border-radius:3px; transition:width .3s ease; }
|
||||
.progress-pct { font-size:.8rem; font-weight:700; min-width:36px; text-align:right; color:var(--gray-600); }
|
||||
|
||||
/* ── stat-box (monitoraggio) ────────────────────────────── */
|
||||
.stat-box { padding:10px 14px; background:var(--gray-50); border-radius:8px; }
|
||||
.stat-box .stat-value { font-size:1.4rem; font-weight:800; }
|
||||
.stat-box .stat-label { font-size:.72rem; color:var(--gray-500); }
|
||||
|
||||
/* ── Report Section ─────────────────────────────────────── */
|
||||
.report-container {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
.report-header {
|
||||
background: var(--gray-800);
|
||||
color: white;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
.report-header h3 {
|
||||
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;
|
||||
}
|
||||
.report-container { background:var(--card-bg); border:1px solid var(--gray-200); border-radius:var(--border-radius); overflow:hidden; }
|
||||
.report-header { background:var(--navy-nav,#142B4D); color:#fff; padding:30px; text-align:center; }
|
||||
.report-header h3 { font-size:1.3rem; margin:0 0 4px; }
|
||||
.report-header p { color:#cbd5e1; font-size:.85rem; margin:0; }
|
||||
.report-body { padding:24px 30px; }
|
||||
.report-section { margin-bottom:24px; }
|
||||
.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); }
|
||||
.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:800; color:var(--gray-800); line-height:1.2; }
|
||||
.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-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; }
|
||||
|
||||
/* ── ISO Mapping ────────────────────────────────────────── */
|
||||
.iso-mapping-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
}
|
||||
.iso-mapping-row:last-child { border-bottom: none; }
|
||||
.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;
|
||||
}
|
||||
.iso-mapping-row { display:flex; align-items:center; gap:16px; padding:12px 0; border-bottom:1px solid var(--gray-100); }
|
||||
.iso-mapping-row:last-child { border-bottom:none; }
|
||||
.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; }
|
||||
.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:.78rem; font-weight:600; }
|
||||
.iso-title { font-size:.85rem; color:var(--gray-500); min-width:200px; }
|
||||
|
||||
/* ── Pagination ─────────────────────────────────────────── */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
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);
|
||||
}
|
||||
.pagination { display:flex; justify-content:center; align-items:center; gap:12px; 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:inherit; font-size:.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:.4; cursor:not-allowed; }
|
||||
.pagination .page-info { font-size:.85rem; color:var(--gray-500); }
|
||||
|
||||
/* ── Audit Log Action ───────────────────────────────────── */
|
||||
.log-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
/* ── Audit Log ──────────────────────────────────────────── */
|
||||
.log-action { display:inline-flex; align-items:center; gap:4px; font-size:.82rem; }
|
||||
.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); }
|
||||
.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; }
|
||||
|
||||
/* ── Controls Row Clickable ─────────────────────────────── */
|
||||
.data-table tbody tr.clickable-row { cursor: pointer; }
|
||||
|
||||
/* ── Loading/Empty ──────────────────────────────────────── */
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 48px 20px;
|
||||
color: var(--gray-400);
|
||||
}
|
||||
.loading-state .spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid var(--gray-200);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin: 0 auto 12px;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.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;
|
||||
}
|
||||
/* ── Loading / Empty ────────────────────────────────────── */
|
||||
.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; }
|
||||
@keyframes rpt-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:.5; }
|
||||
.empty-state-box h4 { color:var(--gray-600); margin:0 0 4px; }
|
||||
.empty-state-box p { margin:0; }
|
||||
|
||||
/* ── Print ──────────────────────────────────────────────── */
|
||||
@media print {
|
||||
.sidebar, .content-header, .tab-nav, .btn { display: none !important; }
|
||||
.main-content { margin: 0 !important; padding: 0 !important; }
|
||||
.report-container { box-shadow: none; border: 1px solid #ddd; }
|
||||
body { background: white; }
|
||||
#topnav, .crumb, .page-head .btn-primary, .tab-nav, .btn { display:none !important; }
|
||||
.wrap { max-width:100% !important; padding:0 !important; }
|
||||
.report-container { box-shadow:none; border:1px solid #ddd; }
|
||||
body { background:#fff; }
|
||||
}
|
||||
|
||||
/* ── Responsive ─────────────────────────────────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.tab-nav button { padding: 10px 12px; font-size: 0.8rem; }
|
||||
.iso-mapping-row { flex-direction: column; align-items: flex-start; }
|
||||
.iso-title { min-width: auto; }
|
||||
.report-summary-grid { grid-template-columns: 1fr 1fr; }
|
||||
@media (max-width:768px) {
|
||||
.tab-nav button { padding:10px 12px; font-size:.8rem; }
|
||||
.iso-mapping-row { flex-direction:column; align-items:flex-start; }
|
||||
.iso-title { min-width:auto; }
|
||||
.report-summary-grid { grid-template-columns:1fr 1fr; }
|
||||
}
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
@@ -358,111 +141,110 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2" data-i18n="audit.title">Audit & Report</h1>
|
||||
<div class="content-header-actions">
|
||||
<button class="btn btn-primary" onclick="generateReport()">Genera Report</button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Audit e Report</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="audit.title">Audit & 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">
|
||||
<!-- Tab Navigation -->
|
||||
<div class="tab-nav">
|
||||
<button class="active" onclick="switchTab('report', this)">Report Compliance</button>
|
||||
<button onclick="switchTab('controls', this)">Controlli</button>
|
||||
<button onclick="switchTab('audit', this)">Audit Log</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('acn', this)">Requisiti ACN</button>
|
||||
</div>
|
||||
<section class="card card-pad">
|
||||
<!-- Tab Navigation -->
|
||||
<div class="tab-nav">
|
||||
<button class="active" onclick="switchTab('report', this)">Report Compliance</button>
|
||||
<button onclick="switchTab('controls', this)">Controlli</button>
|
||||
<button onclick="switchTab('audit', this)">Audit Log</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('acn', this)">Requisiti ACN</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab: Report Compliance -->
|
||||
<div class="tab-panel active" id="tab-report">
|
||||
<div id="report-container">
|
||||
<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>
|
||||
<h4>Nessun report generato</h4>
|
||||
<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>
|
||||
<!-- Tab: Report Compliance -->
|
||||
<div class="tab-panel active" id="tab-report">
|
||||
<div id="report-container">
|
||||
<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>
|
||||
<h4>Nessun report generato</h4>
|
||||
<p>Clicca "Genera Report" per creare un report di compliance aggiornato.</p>
|
||||
</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/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
I18n.init();
|
||||
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) {} }
|
||||
|
||||
// ── Labels ──────────────────────────────────────────────
|
||||
const controlStatusLabels = {
|
||||
@@ -1017,14 +799,11 @@
|
||||
}
|
||||
|
||||
let html = `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Mapping NIS2 - ISO 27001:2022</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p style="font-size:0.85rem; color:var(--gray-500); margin-bottom:20px;">
|
||||
Corrispondenza tra gli articoli della Direttiva NIS2 e i controlli dello standard ISO/IEC 27001:2022.
|
||||
</p>`;
|
||||
<div>
|
||||
<h3 style="margin:0 0 4px;font-size:1.05rem;">Mapping NIS2 - ISO 27001:2022</h3>
|
||||
<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 => {
|
||||
const isoControls = (m.iso27001 || '').split(',').map(c => c.trim()).filter(c => c);
|
||||
@@ -1041,7 +820,7 @@
|
||||
</div>`;
|
||||
});
|
||||
|
||||
html += '</div></div>';
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
|
||||
+101
-85
@@ -3,51 +3,60 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Scadenziario - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Scadenziario · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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; }
|
||||
.rev-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 18px; }
|
||||
.rev-stats { display:flex; flex-wrap:wrap; gap:10px; margin-bottom: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-stat .n { font-size:1.5rem; font-weight:800; line-height:1; }
|
||||
.rev-stat .l { font-size:.78rem; color:var(--gray-500,#6b7280); margin-top:4px; }
|
||||
/* V3 — specifico pagina Scadenziario (non coperto da v3.css) */
|
||||
.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-note { font-size:12.5px; color:#92400e; background:#FBF1E2; border:1px solid #fde68a; border-radius:10px; padding:10px 12px; margin:0 0 18px; }
|
||||
.rev-stats { display:flex; flex-wrap:wrap; gap:12px; margin-bottom:18px; }
|
||||
.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 .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-due .n { color:#b45309; }
|
||||
.rev-stat.s-ok .n { color:#166534; }
|
||||
.rev-toolbar { display:flex; gap:8px; 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 { display:flex; gap:10px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
|
||||
.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-table { width:100%; border-collapse:collapse; font-size:.88rem; }
|
||||
.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 { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
|
||||
.rev-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; }
|
||||
.rev-badge.type { background:#f1f5f9; color:#475569; }
|
||||
.gap-pill { display:inline-block; font-size:.72rem; font-weight:800; padding:2px 9px; border-radius:20px; }
|
||||
.gap-pill.ok { background:#dcfce7; color:#166534; }
|
||||
.gap-pill.due { background:#fef3c7; color:#92400e; }
|
||||
.gap-pill.bad { background:#fee2e2; color:#991b1b; }
|
||||
.rev-table { width:100%; border-collapse:collapse; font-size:13px; }
|
||||
.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:10.5px; text-transform:uppercase; letter-spacing:.04em; color:#7a8aa0; }
|
||||
.rev-badge { display:inline-block; font-size:10.5px; font-weight:700; padding:2px 8px; border-radius:6px; }
|
||||
.rev-badge.type { background:#EEF2F7; color:#475569; }
|
||||
.gap-pill { display:inline-block; font-size:11px; font-weight:800; padding:2px 9px; border-radius:20px; }
|
||||
.gap-pill.ok { background:#E6F4EC; color:#166534; }
|
||||
.gap-pill.due { background:#FBF1E2; color:#92400e; }
|
||||
.gap-pill.bad { background:#FBE9E9; color:#991b1b; }
|
||||
.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(--gray-500,#6b7280); }
|
||||
/* Modale */
|
||||
.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; }
|
||||
.rev-empty { text-align:center; padding:34px 16px; color:var(--muted); }
|
||||
/* Modale (page-owned) */
|
||||
.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-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(0,0,0,.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 h3 { margin:0; font-size:1.05rem; }
|
||||
.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(--line); }
|
||||
.rm-head h3 { margin:0; font-size:16px; }
|
||||
.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 label { display:block; font-weight:600; font-size:.86rem; 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 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(--line); border-radius:8px; font:inherit; font-size:14px; }
|
||||
.rm-field textarea { min-height:64px; resize:vertical; }
|
||||
.rm-row { display:flex; gap:12px; }
|
||||
.rm-row > .rm-field { flex:1 1 0; }
|
||||
.rm-err { color:#b91c1c; font-size:.82rem; min-height:18px; }
|
||||
.rm-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); }
|
||||
.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(--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>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -64,49 +73,55 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2 data-i18n="rev.title">Scadenziario</h2>
|
||||
<div class="content-header-actions">
|
||||
<span class="savehint" id="rev-hint" role="status" aria-live="polite"></span>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<div class="content-body">
|
||||
<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 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>
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Monitoraggio</span>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Scadenziario</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="rev.title">Scadenziario</h1>
|
||||
<p class="sub">Revisioni periodiche di ruoli, competenze, inventario, procedure, rischi, fornitori e misure</p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<span class="savehint" id="rev-hint" role="status" aria-live="polite" style="font-size:12.5px;color:var(--muted)"></span>
|
||||
</div>
|
||||
|
||||
<section class="card card-pad">
|
||||
<div class="rev-intro">
|
||||
<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) -->
|
||||
<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-err" id="rev-err" role="alert"></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>
|
||||
|
||||
<!-- Scripts V3 (ordine ESATTO come dashboard.html) -->
|
||||
<script src="/js/api.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260624g"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
@@ -180,6 +196,7 @@
|
||||
if (window.I18n && I18n.init) I18n.init('it');
|
||||
if (typeof loadSidebar === 'function') loadSidebar();
|
||||
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(); });
|
||||
const ov = el('rev-modal');
|
||||
@@ -195,7 +212,6 @@
|
||||
REV.roles = (rolesData && rolesData.roles) || [];
|
||||
revFillOwnerSelect();
|
||||
} catch (e) {
|
||||
// l'organigramma può non essere ancora popolato: non bloccare la pagina
|
||||
REV.roles = [];
|
||||
}
|
||||
}
|
||||
@@ -210,7 +226,7 @@
|
||||
}
|
||||
|
||||
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 {
|
||||
const data = await api.revList();
|
||||
REV.items = (data && data.items) || [];
|
||||
@@ -250,16 +266,16 @@
|
||||
const freq = (i.frequency_months != null && i.frequency_months !== '') ? (esc(i.frequency_months) + ' mesi') : '';
|
||||
return '<tr>'
|
||||
+ '<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>' + owner + '</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><div class="rev-actions-cell">'
|
||||
+ '<button class="btn btn-outline" onclick="revComplete(' + i.id + ')">Segna revisionato</button>'
|
||||
+ '<button class="btn btn-outline" onclick="revOpen(' + i.id + ')">Modifica</button>'
|
||||
+ '<button class="btn btn-outline" onclick="revDelete(' + i.id + ')">Elimina</button>'
|
||||
+ '<button class="btn-ghost" onclick="revComplete(' + i.id + ')">Segna revisionato</button>'
|
||||
+ '<button class="btn-ghost" onclick="revOpen(' + i.id + ')">Modifica</button>'
|
||||
+ '<button class="btn-ghost" onclick="revDelete(' + i.id + ')">Elimina</button>'
|
||||
+ '</div></td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
|
||||
+39
-38
@@ -3,12 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Gestione Rischi - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (tabelle/matrice/modali) -> parita' funzionale assoluta. BI veste la chrome. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Rischi · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (self-host only, nessun CDN esterno). -->
|
||||
<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/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>
|
||||
/* ── View Toggle ─────────────────────────────────────── */
|
||||
.view-toggle {
|
||||
@@ -313,26 +314,32 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2" data-i18n="risks.title">Gestione Rischi</h1>
|
||||
<div class="content-header-actions">
|
||||
<div class="view-toggle" id="view-toggle">
|
||||
<button class="active" onclick="switchView('table')">Tabella</button>
|
||||
<button onclick="switchView('matrix')">Matrice</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>
|
||||
<button class="btn btn-secondary" id="btn-ai-suggest" onclick="aiSuggest()">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor" width="18" height="18"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
|
||||
AI Suggerisci
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="showRiskModal()">+ Nuovo Rischio</button>
|
||||
<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="risks.title">Gestione Rischi</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="risks.title">Gestione Rischi</h1>
|
||||
<p class="sub">Registro rischi, matrice 5×5, analisi quantitativa FAIR e KRI</p>
|
||||
</div>
|
||||
<div class="content-header-actions" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<div class="view-toggle" id="view-toggle">
|
||||
<button class="active" onclick="switchView('table')">Tabella</button>
|
||||
<button onclick="switchView('matrix')">Matrice</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>
|
||||
</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">
|
||||
<!-- Table View -->
|
||||
@@ -491,29 +498,23 @@
|
||||
<!-- Detail View -->
|
||||
<div id="view-detail" class="hidden"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 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/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// ── Auth & Init ──────────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
loadSidebar(); // = loadTopnavV3() (definito in topnav-v3.js)
|
||||
I18n.init();
|
||||
HelpSystem.init();
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
|
||||
// ── Labels ───────────────────────────────────────────────────
|
||||
const CATEGORY_LABELS = {
|
||||
|
||||
+526
-486
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,22 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Attività stakeholder - NIS2 Agile</title>
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Attività stakeholder · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg CDN / NO Bootstrap Italia). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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-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; }
|
||||
@@ -72,12 +83,21 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2 data-i18n="sact.title">Attività stakeholder</h2>
|
||||
</header>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
|
||||
<div id="topnav"></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><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="sa-intro">
|
||||
<strong>Attività verso gli stakeholder.</strong>
|
||||
@@ -106,8 +126,7 @@
|
||||
<div id="tpl-list" aria-live="polite"></div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Modale questionario tipo -->
|
||||
<div class="sm-overlay" id="tpl-modal" role="dialog" aria-modal="true" aria-labelledby="tpl-modal-title">
|
||||
@@ -194,10 +213,12 @@
|
||||
</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/common.js?v=20260624g"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
@@ -217,8 +238,9 @@
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
||||
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.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
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'); }); });
|
||||
await saLoadAll();
|
||||
|
||||
+42
-22
@@ -3,12 +3,22 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Stakeholder - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Stakeholder · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg CDN / NO Bootstrap Italia). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
|
||||
.stk-toolbar .grow { flex:1 1 auto; }
|
||||
@@ -84,18 +94,26 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h2 data-i18n="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>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<div class="content-body">
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span 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">
|
||||
<strong>Mappatura e gestione degli stakeholder.</strong>
|
||||
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>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Modale stakeholder (crea/modifica) -->
|
||||
<div class="sm-overlay" id="stk-modal" role="dialog" aria-modal="true" aria-labelledby="stk-modal-title">
|
||||
@@ -191,10 +208,12 @@
|
||||
</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/common.js?v=20260624g"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
@@ -223,8 +242,9 @@
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
||||
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.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
|
||||
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') { stkClose(); stkTypeClose(); } });
|
||||
['stk-modal','stk-type-modal'].forEach(function (mid) {
|
||||
|
||||
+97
-39
@@ -3,13 +3,70 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Supply Chain - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (tabelle/modali/form) -> parita' funzionale assoluta. BI veste la chrome. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Supply Chain · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg CDN / NO Bootstrap Italia). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<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 ─────────────────────────── */
|
||||
.filters-bar {
|
||||
display: flex;
|
||||
@@ -380,21 +437,28 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js -->
|
||||
<div id="topnav"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2" data-i18n="supply_chain.title">Sicurezza Supply Chain</h1>
|
||||
<div class="content-header-actions">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<button class="btn btn-primary" onclick="openCreateModal()"><span data-i18n="sp.new_supplier">+ Nuovo Fornitore</span></button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="supply_chain.title">Sicurezza Supply Chain</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="supply_chain.title">Sicurezza Supply Chain</h1>
|
||||
<p class="sub">Valutazione e monitoraggio del rischio della catena di fornitura (art. 24 D.Lgs. 138/2024)</p>
|
||||
</div>
|
||||
<div class="content-header-actions">
|
||||
<button class="btn-ghost" onclick="openCategoriesModal()"><i data-lucide="bookmark" class="ic"></i><span data-i18n="sp.categories_btn">Categorie</span></button>
|
||||
<button class="btn-ghost" onclick="openTemplatesModal()"><i data-lucide="files" class="ic"></i><span data-i18n="sp.templates_btn">Template</span></button>
|
||||
<button class="btn-ghost" onclick="openImportModal()"><i data-lucide="upload" class="ic"></i><span data-i18n="sp.import_btn">Importa</span></button>
|
||||
<button class="btn-primary" onclick="openCreateModal()"><i data-lucide="plus" class="ic"></i><span data-i18n="sp.new_supplier">Nuovo Fornitore</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-body" id="page-content">
|
||||
<div class="stack" id="page-content">
|
||||
<!-- Risk Overview -->
|
||||
<div class="risk-overview" id="risk-overview">
|
||||
<div class="risk-card">
|
||||
@@ -474,29 +538,23 @@
|
||||
<!-- Detail View (hidden by default) -->
|
||||
<div id="supplier-detail-view" style="display:none;"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</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/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/i18n.js?v=20260621sp"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
I18n.init();
|
||||
HelpSystem.init();
|
||||
loadSidebar(); // = loadTopnavV3()
|
||||
if (typeof I18n !== 'undefined') I18n.init();
|
||||
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
|
||||
|
||||
// ── Labels ──────────────────────────────────────────────
|
||||
const CRITICALITY_LABELS = {
|
||||
@@ -1224,7 +1282,7 @@
|
||||
ov.innerHTML = `
|
||||
<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;">
|
||||
<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;">×</button>
|
||||
</div>
|
||||
<div style="padding:20px;">
|
||||
@@ -1241,7 +1299,7 @@
|
||||
</div>
|
||||
<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-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>`;
|
||||
document.body.appendChild(ov);
|
||||
@@ -1319,7 +1377,7 @@
|
||||
} catch (e) {
|
||||
resEl.innerHTML = `<span style="color:#dc2626;">Errore: ${(e && e.message) || e}</span>`;
|
||||
} 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
@@ -3,222 +3,112 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Formazione - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css
|
||||
cosi' style.css (design system esistente) resta autorevole per i widget renderizzati
|
||||
via JS (tabelle/modali/form) -> parita' funzionale assoluta. BI veste la chrome. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260618">
|
||||
<title>Formazione · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css. No external CDN, no Bootstrap Italia. -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<style>
|
||||
/* V3+ page-specific (componenti non coperti da v3.css): tab interni, card corso,
|
||||
tabella, badge stato, barra compliance. + componenti prodotti dalla JS inline via
|
||||
common.js (showModal/showNotification): modal-* / form-* / .btn-secondary, mappati a token V3
|
||||
perche' style.css e' rimosso. */
|
||||
|
||||
/* ── Tab Navigation ─────────────────────────────────────── */
|
||||
.tab-nav {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 2px solid var(--gray-200);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.tab-nav button {
|
||||
padding: 12px 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
color: var(--gray-500);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
.tab-nav button:hover {
|
||||
color: var(--gray-700);
|
||||
background: var(--gray-50);
|
||||
}
|
||||
.tab-nav button.active {
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
.tab-nav { display:flex; gap:0; border-bottom:2px solid var(--line); margin-bottom:24px; }
|
||||
.tab-nav button { padding:12px 22px; background:none; border:none; border-bottom:2px solid transparent; margin-bottom:-2px; color:var(--muted); font:inherit; font-size:13.5px; font-weight:600; cursor:pointer; }
|
||||
.tab-nav button:hover { color:var(--ink); background:#F5F8FC; }
|
||||
.tab-nav button.active { color:var(--c-blue); border-bottom-color:var(--c-blue); }
|
||||
.tab-panel { display:none; }
|
||||
.tab-panel.active { display:block; }
|
||||
|
||||
/* ── Course Cards Grid ──────────────────────────────────── */
|
||||
.courses-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.course-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
padding: 20px;
|
||||
transition: box-shadow var(--transition);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.course-card:hover {
|
||||
box-shadow: var(--card-shadow-hover);
|
||||
}
|
||||
.course-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.course-card-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-800);
|
||||
margin: 0;
|
||||
}
|
||||
.course-card-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--gray-500);
|
||||
margin-bottom: 16px;
|
||||
flex-grow: 1;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.course-card-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.course-meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--gray-500);
|
||||
background: var(--gray-100);
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.course-meta-item svg { width: 14px; height: 14px; }
|
||||
.badge-mandatory {
|
||||
background: var(--danger-bg);
|
||||
color: var(--danger);
|
||||
font-weight: 600;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.courses-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(320px, 1fr)); gap:18px; }
|
||||
.course-card { background:#fff; border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow); padding:18px 20px; position:relative; display:flex; flex-direction:column; }
|
||||
.course-card:hover { box-shadow:0 8px 20px rgba(16,40,80,.13); }
|
||||
.course-card-header { display:flex; justify-content:space-between; align-items:flex-start; gap:10px; margin-bottom:12px; }
|
||||
.course-card-title { font-size:15px; font-weight:700; color:var(--ink); margin:0; }
|
||||
.course-card-desc { font-size:13px; color:var(--muted); margin-bottom:16px; flex-grow:1; line-height:1.5; }
|
||||
.course-card-meta { display:flex; flex-wrap:wrap; gap:8px; align-items:center; font-size:12px; }
|
||||
.course-meta-item { display:inline-flex; align-items:center; gap:4px; color:var(--muted); background:#EEF2F7; padding:4px 10px; border-radius:20px; }
|
||||
.course-meta-item svg { width:14px; height:14px; }
|
||||
.badge-mandatory { background:#FBE9E9; color:#B23636; font-weight:700; padding:3px 10px; border-radius:20px; font-size:10.5px; text-transform:uppercase; letter-spacing:.04em; }
|
||||
|
||||
/* ── Assignments Table ──────────────────────────────────── */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
.data-table thead {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
.data-table th {
|
||||
text-align: left;
|
||||
padding: 12px 16px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--gray-500);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
.data-table td {
|
||||
padding: 12px 16px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-700);
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
}
|
||||
.data-table tr:last-child td { border-bottom: none; }
|
||||
.data-table tr:hover td { background: var(--gray-50); }
|
||||
/* ── Tables ─────────────────────────────────────────────── */
|
||||
.data-table { width:100%; border-collapse:collapse; background:#fff; border:1px solid var(--line); border-radius:var(--radius); overflow:hidden; box-shadow:var(--shadow); }
|
||||
.data-table thead { background:#FAFCFE; }
|
||||
.data-table th { text-align:left; padding:12px 16px; font-size:11px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.04em; border-bottom:1px solid var(--line); }
|
||||
.data-table td { padding:12px 16px; font-size:13.5px; color:var(--ink); border-bottom:1px solid var(--line); }
|
||||
.data-table tr:last-child td { border-bottom:none; }
|
||||
.data-table tr:hover td { background:#FAFCFE; }
|
||||
|
||||
/* ── Status Badges ──────────────────────────────────────── */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
gap: 4px;
|
||||
}
|
||||
.status-badge.assigned { background: var(--gray-100); color: var(--gray-600); }
|
||||
.status-badge.in_progress { background: var(--info-bg); color: var(--info); }
|
||||
.status-badge.completed { background: var(--secondary-bg); color: var(--secondary); }
|
||||
.status-badge.overdue { background: var(--danger-bg); color: var(--danger); }
|
||||
.status-badge { display:inline-flex; align-items:center; padding:4px 12px; border-radius:20px; font-size:11px; font-weight:700; gap:4px; }
|
||||
.status-badge.assigned { background:#EEF2F7; color:#5b6b80; }
|
||||
.status-badge.in_progress { background:#E0F2FE; color:#075985; }
|
||||
.status-badge.completed { background:#E6F4EC; color:#1E7A4E; }
|
||||
.status-badge.overdue { background:#FBE9E9; color:#B23636; }
|
||||
|
||||
/* ── Compliance Percentage Bar ──────────────────────────── */
|
||||
.compliance-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.compliance-bar {
|
||||
flex-grow: 1;
|
||||
height: 8px;
|
||||
background: var(--gray-200);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.compliance-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.compliance-pct {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
min-width: 45px;
|
||||
text-align: right;
|
||||
}
|
||||
.compliance-pct.high { color: var(--secondary); }
|
||||
.compliance-pct.medium { color: var(--warning); }
|
||||
.compliance-pct.low { color: var(--danger); }
|
||||
/* ── Compliance Bar ─────────────────────────────────────── */
|
||||
.compliance-bar-container { display:flex; align-items:center; gap:10px; }
|
||||
.compliance-bar { flex-grow:1; height:8px; background:#EAF0F6; border-radius:4px; overflow:hidden; }
|
||||
.compliance-bar-fill { height:100%; border-radius:4px; transition:width .3s ease; }
|
||||
.compliance-pct { font-size:13px; font-weight:700; min-width:45px; text-align:right; }
|
||||
.compliance-pct.high { color:var(--c-green); }
|
||||
.compliance-pct.medium { color:var(--c-amber); }
|
||||
.compliance-pct.low { color:var(--c-red); }
|
||||
|
||||
/* ── Spinner/Loading ────────────────────────────────────── */
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 48px 20px;
|
||||
color: var(--gray-400);
|
||||
}
|
||||
.loading-state .spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid var(--gray-200);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin: 0 auto 12px;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
/* ── Loading / Empty ────────────────────────────────────── */
|
||||
.loading-state { text-align:center; padding:48px 20px; color:var(--muted); }
|
||||
.loading-state .spinner { width:32px; height:32px; border:3px solid var(--line); border-top-color:var(--c-blue); border-radius:50%; animation:spin .8s linear infinite; margin:0 auto 12px; }
|
||||
@keyframes spin { to { transform:rotate(360deg); } }
|
||||
@media (prefers-reduced-motion:reduce){ .loading-state .spinner{ animation:none; } }
|
||||
.empty-state-box { text-align:center; padding:48px 20px; color:var(--muted); }
|
||||
.empty-state-box svg { width:48px; height:48px; margin-bottom:12px; opacity:.45; }
|
||||
.empty-state-box h4 { color:var(--ink); margin:0 0 4px; font-size:15px; font-weight:700; }
|
||||
.text-muted { color:var(--muted); }
|
||||
|
||||
/* ── Empty State ────────────────────────────────────────── */
|
||||
.empty-state-box {
|
||||
text-align: center;
|
||||
padding: 48px 20px;
|
||||
color: var(--gray-400);
|
||||
}
|
||||
.empty-state-box svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.empty-state-box h4 {
|
||||
color: var(--gray-600);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
/* ── Modale (prodotta da common.js showModal — style.css rimosso) ── */
|
||||
.modal-overlay { position:fixed; inset:0; background:rgba(15,38,60,.6); backdrop-filter:blur(4px); z-index:1000; display:flex; align-items:center; justify-content:center; opacity:0; visibility:hidden; transition:opacity .2s,visibility .2s; }
|
||||
.modal-overlay.active { opacity:1; visibility:visible; }
|
||||
.modal { background:#fff; border-radius:14px; box-shadow:0 20px 60px rgba(11,40,80,.25); width:90%; max-width:560px; max-height:85vh; overflow-y:auto; transform:translateY(20px) scale(.97); transition:transform .2s; }
|
||||
.modal-overlay.active .modal { transform:translateY(0) scale(1); }
|
||||
.modal-header { padding:18px 22px; border-bottom:1px solid var(--line); display:flex; align-items:center; justify-content:space-between; }
|
||||
.modal-header h3 { font-size:17px; font-weight:800; color:var(--ink); margin:0; }
|
||||
.modal-close { background:none; border:none; color:#9aa8ba; cursor:pointer; padding:4px; border-radius:8px; line-height:1; font-size:22px; }
|
||||
.modal-close:hover { color:var(--ink); }
|
||||
.modal-body { padding:22px; }
|
||||
.modal-footer { padding:14px 22px; border-top:1px solid var(--line); display:flex; justify-content:flex-end; gap:10px; }
|
||||
|
||||
/* ── Form (dentro le modali) ────────────────────────────── */
|
||||
.form-group { margin-bottom:14px; }
|
||||
.form-label { display:block; font-weight:600; font-size:12.5px; margin-bottom:5px; color:var(--ink); }
|
||||
.form-input, .form-select { width:100%; padding:9px 11px; border:1px solid var(--line); border-radius:8px; font:inherit; font-size:13.5px; background:#fff; color:var(--ink); }
|
||||
.form-input:focus, .form-select:focus { outline:2px solid var(--gold); outline-offset:1px; }
|
||||
|
||||
/* ── Bottoni extra (btn-secondary/btn-sm) usati dalla JS inline ── */
|
||||
.btn-secondary { display:inline-flex; align-items:center; gap:7px; border:1px solid var(--line); background:#fff; border-radius:8px; padding:8px 13px; font:inherit; font-weight:600; font-size:12.5px; color:var(--ink); cursor:pointer; }
|
||||
.btn-secondary:hover { background:#F5F8FC; border-color:#c7d4e4; }
|
||||
.btn-sm { padding:6px 11px; font-size:12px; }
|
||||
|
||||
/* ── Notifiche (prodotte da common.js showNotification — style.css rimosso) ── */
|
||||
.notification-container { position:fixed; top:20px; right:20px; z-index:2000; display:flex; flex-direction:column; gap:10px; }
|
||||
.notification { display:flex; align-items:center; gap:10px; background:#fff; border:1px solid var(--line); border-left:4px solid var(--c-blue); border-radius:10px; box-shadow:0 8px 24px rgba(16,40,80,.16); padding:12px 14px; min-width:260px; max-width:380px; font-size:13px; transition:opacity .3s,transform .3s; }
|
||||
.notification.success { border-left-color:var(--c-green); }
|
||||
.notification.error { border-left-color:var(--c-red); }
|
||||
.notification.warning { border-left-color:var(--c-amber); }
|
||||
.notification.fade-out { opacity:0; transform:translateX(12px); }
|
||||
.notification-icon svg { width:18px; height:18px; }
|
||||
.notification.success .notification-icon { color:var(--c-green); }
|
||||
.notification.error .notification-icon { color:var(--c-red); }
|
||||
.notification.warning .notification-icon { color:var(--c-amber); }
|
||||
.notification.info .notification-icon { color:var(--c-blue); }
|
||||
.notification-message { flex:1; min-width:0; color:var(--ink); }
|
||||
.notification-close { background:none; border:none; font-size:18px; line-height:1; cursor:pointer; color:#9aa8ba; }
|
||||
|
||||
/* ── Responsive ─────────────────────────────────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.courses-grid { grid-template-columns: 1fr; }
|
||||
.tab-nav button { padding: 10px 14px; font-size: 0.82rem; }
|
||||
.data-table { font-size: 0.8rem; }
|
||||
.courses-grid { grid-template-columns:1fr; }
|
||||
.tab-nav button { padding:10px 14px; font-size:12.5px; }
|
||||
.data-table { font-size:12.5px; }
|
||||
}
|
||||
</style>
|
||||
<!-- PWA:start -->
|
||||
@@ -236,19 +126,24 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<div id="topnav"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="content-header">
|
||||
<h1 class="h2" data-i18n="training.title">Formazione e Awareness</h1>
|
||||
<div class="content-header-actions">
|
||||
<span class="text-muted" style="font-size:0.8rem; margin-right:8px;">Art. 20 NIS2</span>
|
||||
<button class="btn btn-primary" onclick="showCreateCourseModal()">+ Nuovo Corso</button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="wrap">
|
||||
<nav class="crumb" aria-label="breadcrumb">
|
||||
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Struttura interna</span>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="training.title">Formazione</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1 data-i18n="training.title">Formazione e Awareness</h1>
|
||||
<p class="sub">Piani formativi, assegnazioni e compliance — Art. 20 NIS2</p>
|
||||
</div>
|
||||
<button class="btn-primary" onclick="showCreateCourseModal()"><i data-lucide="plus" class="ic"></i>Nuovo Corso</button>
|
||||
</div>
|
||||
|
||||
<div class="content-body">
|
||||
<div class="stack">
|
||||
<section class="card card-pad">
|
||||
<!-- Tab Navigation -->
|
||||
<div class="tab-nav">
|
||||
<button class="active" onclick="switchTab('courses', this)">Corsi</button>
|
||||
@@ -279,7 +174,7 @@
|
||||
<!-- Tab: Compliance Formativa -->
|
||||
<div class="tab-panel" id="tab-compliance">
|
||||
<div style="display:flex; justify-content:flex-end; margin-bottom:16px;">
|
||||
<button class="btn btn-primary" onclick="showAssignCourseModal()">Assegna Corso</button>
|
||||
<button class="btn-primary" onclick="showAssignCourseModal()"><i data-lucide="user-plus" class="ic"></i>Assegna Corso</button>
|
||||
</div>
|
||||
<div id="compliance-container">
|
||||
<div class="loading-state">
|
||||
@@ -288,30 +183,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Scripts V3: api.js → common.js (helper+FAB reali+showModal/showNotification) →
|
||||
Lucide self-host → topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help.
|
||||
La logica della pagina e' inline (nessun training.js): preservata invariata. -->
|
||||
<script src="/js/api.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
// ── Auth & Init (ordine V3) ─────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
I18n.init();
|
||||
HelpSystem.init();
|
||||
loadSidebar(); // = loadTopnavV3() (topnav-v3.js)
|
||||
if (typeof I18n !== 'undefined') I18n.init();
|
||||
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
|
||||
function v3Icons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
|
||||
v3Icons();
|
||||
|
||||
// ── Labels ──────────────────────────────────────────────
|
||||
const roleLabels = {
|
||||
@@ -460,8 +352,8 @@
|
||||
`, {
|
||||
size: 'lg',
|
||||
footer: `
|
||||
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
||||
<button class="btn btn-primary" onclick="createCourse()">Crea Corso</button>
|
||||
<button class="btn-secondary" onclick="closeModal()">Annulla</button>
|
||||
<button class="btn-primary" onclick="createCourse()">Crea Corso</button>
|
||||
`
|
||||
});
|
||||
}
|
||||
@@ -549,18 +441,18 @@
|
||||
|
||||
let actionBtn = '';
|
||||
if (displayStatus === 'assigned' || displayStatus === 'overdue') {
|
||||
actionBtn = `<button class="btn btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'in_progress')">Inizia</button>`;
|
||||
actionBtn = `<button class="btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'in_progress')">Inizia</button>`;
|
||||
} else if (displayStatus === 'in_progress') {
|
||||
actionBtn = `<button class="btn btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'completed')">Completa</button>`;
|
||||
actionBtn = `<button class="btn-primary btn-sm" onclick="updateAssignment(${a.id}, 'completed')">Completa</button>`;
|
||||
} else {
|
||||
actionBtn = `<span style="color:var(--secondary); font-weight:500; font-size:0.85rem;">Completato</span>`;
|
||||
actionBtn = `<span style="color:var(--c-green); font-weight:600; font-size:13px;">Completato</span>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-weight:500;">${escapeHtml(a.title || '')}</div>
|
||||
${a.is_mandatory == 1 ? '<span style="font-size:0.75rem; color:var(--danger);">Obbligatorio</span>' : ''}
|
||||
<div style="font-weight:600;">${escapeHtml(a.title || '')}</div>
|
||||
${a.is_mandatory == 1 ? '<span style="font-size:11px; color:var(--c-red);">Obbligatorio</span>' : ''}
|
||||
</td>
|
||||
<td><span class="status-badge ${displayStatus}">${statusLabels[displayStatus] || displayStatus}</span></td>
|
||||
<td>${a.due_date ? formatDate(a.due_date) : '-'}</td>
|
||||
@@ -638,11 +530,11 @@
|
||||
const total = assignments.length;
|
||||
const pct = m.mandatory_compliance != null ? m.mandatory_compliance : 100;
|
||||
const pctClass = pct >= 80 ? 'high' : pct >= 50 ? 'medium' : 'low';
|
||||
const barColor = pct >= 80 ? 'var(--secondary)' : pct >= 50 ? 'var(--warning)' : 'var(--danger)';
|
||||
const barColor = pct >= 80 ? 'var(--c-green)' : pct >= 50 ? 'var(--c-amber)' : 'var(--c-red)';
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td style="font-weight:500;">${escapeHtml(m.full_name || '-')}</td>
|
||||
<td style="font-weight:600;">${escapeHtml(m.full_name || '-')}</td>
|
||||
<td>${escapeHtml(m.email || '-')}</td>
|
||||
<td>${escapeHtml(m.role || '-')}</td>
|
||||
<td>${completed} / ${total}</td>
|
||||
@@ -689,7 +581,7 @@
|
||||
<label style="display:flex; align-items:center; gap:8px; padding:6px 0; cursor:pointer;">
|
||||
<input type="checkbox" class="assign-user-cb" value="${m.id}">
|
||||
<span>${escapeHtml(m.full_name || m.email)}</span>
|
||||
<span style="color:var(--gray-400); font-size:0.8rem;">(${escapeHtml(m.role || '')})</span>
|
||||
<span style="color:var(--muted); font-size:12px;">(${escapeHtml(m.role || '')})</span>
|
||||
</label>
|
||||
`).join('');
|
||||
}
|
||||
@@ -705,15 +597,15 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Seleziona Utenti</label>
|
||||
<div style="max-height:200px; overflow-y:auto; border:1px solid var(--gray-200); border-radius:var(--border-radius-sm); padding:8px 12px;">
|
||||
<div style="max-height:200px; overflow-y:auto; border:1px solid var(--line); border-radius:8px; padding:8px 12px;">
|
||||
${membersHtml}
|
||||
</div>
|
||||
</div>
|
||||
`, {
|
||||
size: 'lg',
|
||||
footer: `
|
||||
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
|
||||
<button class="btn btn-primary" onclick="doAssignCourse()">Assegna</button>
|
||||
<button class="btn-secondary" onclick="closeModal()">Annulla</button>
|
||||
<button class="btn-primary" onclick="doAssignCourse()">Assegna</button>
|
||||
`
|
||||
});
|
||||
}
|
||||
|
||||
+107
-62
@@ -3,32 +3,78 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Segnalazioni Sicurezza - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di
|
||||
style.css cosi' il design system esistente resta autorevole per i widget
|
||||
renderizzati via JS (tabelle/modali/timeline) -> parita' funzionale assoluta.
|
||||
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">
|
||||
<title>Segnalazioni Sicurezza · NIS2 Agile</title>
|
||||
<!-- UI V3: Inter self-host + design system v3.css (NO Google Fonts / NO unpkg / NO Bootstrap Italia CDN). -->
|
||||
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
||||
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
||||
<style>
|
||||
.priority-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; }
|
||||
.priority-critical { background: var(--danger-bg); color: var(--danger); }
|
||||
.priority-high { background: #fff7ed; color: #c2410c; }
|
||||
.priority-medium { background: var(--warning-bg); color: #a16207; }
|
||||
.priority-low { background: var(--gray-100); color: var(--gray-600); }
|
||||
.status-badge { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; }
|
||||
.status-received { background: rgba(6,182,212,0.1); color: #0891b2; }
|
||||
.status-under_review { background: var(--warning-bg); color: #a16207; }
|
||||
.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 10px; background: rgba(16,185,129,0.1); border-radius: 12px; color: var(--success); font-size: 0.75rem; font-weight: 600; }
|
||||
.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; }
|
||||
.art32-banner h3 { font-size: 0.9375rem; font-weight: 700; color: var(--gray-800); margin-bottom: 6px; }
|
||||
.art32-banner p { font-size: 0.8125rem; color: var(--gray-600); }
|
||||
.track-form { background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: var(--border-radius); padding: 20px; margin-bottom: 24px; }
|
||||
/* Icone sprite BI dimensionate inline per i bottoni azione */
|
||||
.icon-inline { width: 16px; height: 16px; fill: currentColor; vertical-align: text-bottom; }
|
||||
/* ── Compat V3: token + classi emesse dal JS (ex style.css, niente CDN) ── */
|
||||
:root{
|
||||
--primary:#1F5FA8;--primary-bg:#E7F0FB;--secondary:#1E8A5A;--secondary-bg:#E6F4EC;
|
||||
--success:#1E8A5A;--success-bg:#E6F4EC;--warning:#D98324;--warning-bg:#FBF1E2;
|
||||
--danger:#C62828;--danger-bg:#FBE9E9;--info:#1F5FA8;--info-bg:#E7F0FB;
|
||||
--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:#15263C;
|
||||
--card-bg:#fff;--card-shadow:var(--shadow);--border-radius:8px;--border-radius-lg:12px;--border-radius-sm:6px;
|
||||
--font-mono:'SF Mono','Fira Code','Consolas',monospace;--transition-fast:150ms ease;
|
||||
}
|
||||
.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}
|
||||
.mb-16{margin-bottom:16px}.mb-24{margin-bottom:24px}
|
||||
/* Buttons (parità con .btn-primary/.btn-ghost di v3.css) */
|
||||
.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}
|
||||
.btn:disabled{opacity:.55;cursor:not-allowed}
|
||||
.btn-primary{background:var(--c-blue);color:#fff}.btn-primary:hover:not(:disabled){background:#1A5494}
|
||||
.btn-secondary,.btn-outline{background:#fff;color:var(--ink);border-color:var(--line)}
|
||||
.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>
|
||||
<!-- PWA:start -->
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
@@ -45,25 +91,29 @@
|
||||
<!-- PWA:end -->
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-layout">
|
||||
<aside class="sidebar" id="sidebar"></aside>
|
||||
<main class="main-content">
|
||||
<div class="page-header">
|
||||
<div class="page-header-content">
|
||||
<h1 class="page-title">Segnalazioni Sicurezza</h1>
|
||||
<p class="page-subtitle">Canale interno Art.32 NIS2 — Segnala violazioni e anomalie di sicurezza</p>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn btn-secondary" onclick="switchView('track')">
|
||||
<svg class="icon-inline" aria-hidden="true"><use href="/vendor/bootstrap-italia/dist/svg/sprites.svg#it-search"></use></svg>
|
||||
Traccia Segnalazione Anonima
|
||||
</button>
|
||||
<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>
|
||||
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
||||
<div id="topnav"></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>Monitoraggio</span>
|
||||
<i data-lucide="chevron-right" class="ic"></i><span>Segnalazioni</span>
|
||||
</nav>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>Segnalazioni Sicurezza</h1>
|
||||
<p class="sub">Canale interno Art.32 NIS2 — Segnala violazioni e anomalie di sicurezza</p>
|
||||
</div>
|
||||
<div 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 -->
|
||||
<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>
|
||||
</div>
|
||||
<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;">
|
||||
<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>
|
||||
<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;">
|
||||
<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;">
|
||||
<input type="checkbox" id="is-anonymous" checked style="accent-color:var(--success);">
|
||||
<span><strong>Segnalazione anonima</strong> — La tua identità non verrà registrata</span>
|
||||
@@ -202,30 +252,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 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/common.js?v=20260624g"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>
|
||||
if (window.bootstrap && bootstrap.loadFonts) {
|
||||
bootstrap.loadFonts('/vendor/bootstrap-italia/dist/fonts');
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260625d"></script>
|
||||
<script src="/js/common.js?v=20260627v3"></script>
|
||||
<script src="/vendor/lucide/lucide.min.js"></script>
|
||||
<script src="/js/topnav-v3.js?v=20260627"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625d"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
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 currentView = 'list';
|
||||
|
||||
@@ -302,10 +346,11 @@
|
||||
if (!reports.length) {
|
||||
container.innerHTML = `
|
||||
<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>
|
||||
<p>Il canale di segnalazione è attivo. Le segnalazioni appariranno qui.</p>
|
||||
</div>`;
|
||||
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch(e){} }
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -319,7 +364,7 @@
|
||||
<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="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 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>
|
||||
@@ -348,7 +393,7 @@
|
||||
<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="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 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)}
|
||||
|
||||
+460
-633
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user