[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
+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>
|
||||
|
||||
Reference in New Issue
Block a user