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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-27 15:57:35 +02:00
co-authored by Claude Opus 4.8
parent b0fc190704
commit a4150b5389
37 changed files with 4655 additions and 4922 deletions
+323 -181
View File
@@ -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>