- workflow.html: roadmap orizzontale 6 fasi (Preparazione→Valutazione→Rischi→Implementazione→Monitoraggio→Reportistica) - Dati reali da 9 API in parallelo (assessment, rischi, policy, asset, fornitori, formazione, controlli) - Auto-selezione fase attiva + dettaglio step con metriche live - Banner "prossima azione consigliata" contestuale - Aggiunto link "Compliance Journey" nella sidebar (sezione Principale) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
705 lines
32 KiB
HTML
705 lines
32 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="it">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Compliance Journey - NIS2 Agile</title>
|
||
<link rel="stylesheet" href="css/style.css">
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||
<style>
|
||
:root {
|
||
--nis2-cyan: #06B6D4;
|
||
--nis2-cyan-light: #ecfeff;
|
||
--nis2-cyan-ring: rgba(6,182,212,.15);
|
||
}
|
||
|
||
/* ── Journey header ────────────────────────────────── */
|
||
.journey-header {
|
||
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||
border-radius: var(--border-radius-lg);
|
||
padding: 24px 28px;
|
||
color: white;
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-wrap: wrap;
|
||
gap: 16px;
|
||
}
|
||
.journey-score {
|
||
font-size: 2.8rem;
|
||
font-weight: 800;
|
||
color: var(--nis2-cyan);
|
||
line-height: 1;
|
||
}
|
||
.journey-label {
|
||
font-size: 0.8rem;
|
||
opacity: 0.6;
|
||
margin-top: 2px;
|
||
}
|
||
.journey-progress-wrap {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
}
|
||
.journey-progress-bar {
|
||
height: 10px;
|
||
background: rgba(255,255,255,0.12);
|
||
border-radius: 5px;
|
||
margin-top: 10px;
|
||
overflow: hidden;
|
||
}
|
||
.journey-progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, var(--nis2-cyan), #38bdf8);
|
||
border-radius: 5px;
|
||
transition: width 1s ease;
|
||
}
|
||
.journey-meta {
|
||
text-align: right;
|
||
font-size: 0.78rem;
|
||
opacity: 0.55;
|
||
}
|
||
.journey-meta strong { display: block; font-size: 0.9rem; opacity: 1; }
|
||
|
||
/* ── Next action banner ────────────────────────────── */
|
||
.next-action-banner {
|
||
background: linear-gradient(135deg, #0c4a6e, #075985);
|
||
color: white;
|
||
border-radius: var(--border-radius);
|
||
padding: 14px 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
margin-bottom: 20px;
|
||
border-left: 4px solid var(--nis2-cyan);
|
||
}
|
||
.next-action-banner .na-icon {
|
||
font-size: 1.5rem;
|
||
color: var(--nis2-cyan);
|
||
flex-shrink: 0;
|
||
}
|
||
.next-action-banner .na-label {
|
||
font-size: 0.72rem;
|
||
opacity: 0.65;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
.next-action-banner .na-text {
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
}
|
||
.next-action-banner a {
|
||
color: var(--nis2-cyan);
|
||
text-decoration: none;
|
||
}
|
||
.next-action-banner a:hover { text-decoration: underline; }
|
||
|
||
/* ── Phase roadmap ─────────────────────────────────── */
|
||
.phase-roadmap {
|
||
display: flex;
|
||
align-items: stretch;
|
||
margin-bottom: 20px;
|
||
overflow-x: auto;
|
||
padding-bottom: 2px;
|
||
gap: 0;
|
||
}
|
||
.phase-item {
|
||
flex: 1;
|
||
min-width: 110px;
|
||
position: relative;
|
||
cursor: pointer;
|
||
}
|
||
.phase-box {
|
||
padding: 14px 16px 12px 18px;
|
||
color: white;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
transition: filter 0.2s, box-shadow 0.2s;
|
||
position: relative;
|
||
}
|
||
.phase-item:first-child .phase-box { border-radius: var(--border-radius) 0 0 var(--border-radius); }
|
||
.phase-item:last-child .phase-box { border-radius: 0 var(--border-radius) var(--border-radius) 0; }
|
||
|
||
/* Arrow connector */
|
||
.phase-item:not(:last-child) .phase-box::after {
|
||
content: '';
|
||
position: absolute;
|
||
right: -13px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 0;
|
||
height: 0;
|
||
border-top: 28px solid transparent;
|
||
border-bottom: 28px solid transparent;
|
||
border-left: 13px solid var(--ph-color);
|
||
z-index: 3;
|
||
}
|
||
.phase-item:not(:last-child)::after {
|
||
content: '';
|
||
position: absolute;
|
||
right: -13px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 0;
|
||
height: 0;
|
||
border-top: 28px solid transparent;
|
||
border-bottom: 28px solid transparent;
|
||
border-left: 13px solid white;
|
||
z-index: 2;
|
||
}
|
||
.phase-item.active .phase-box {
|
||
filter: brightness(1.18);
|
||
box-shadow: 0 4px 18px rgba(0,0,0,0.25);
|
||
z-index: 1;
|
||
}
|
||
.phase-item:not(.active):hover .phase-box { filter: brightness(1.08); }
|
||
.phase-item.todo .phase-box { opacity: 0.6; }
|
||
|
||
.phase-num { font-size: 0.62rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.7; margin-bottom: 2px; }
|
||
.phase-name { font-size: 0.78rem; font-weight: 700; line-height: 1.2; }
|
||
.phase-status-row { display: flex; align-items: center; gap: 4px; margin-top: 5px; }
|
||
.phase-status-icon { font-size: 0.8rem; }
|
||
.phase-pct { font-size: 0.7rem; opacity: 0.85; }
|
||
.phase-mini-bar {
|
||
height: 3px;
|
||
background: rgba(255,255,255,0.2);
|
||
border-radius: 2px;
|
||
margin-top: 5px;
|
||
overflow: hidden;
|
||
}
|
||
.phase-mini-fill {
|
||
height: 100%;
|
||
background: rgba(255,255,255,0.75);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
/* ── Phase detail ──────────────────────────────────── */
|
||
.phase-detail-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.phase-detail-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 5px 14px;
|
||
border-radius: 20px;
|
||
font-size: 0.78rem;
|
||
font-weight: 700;
|
||
color: white;
|
||
}
|
||
.phase-detail-desc {
|
||
font-size: 0.82rem;
|
||
color: var(--gray-500);
|
||
margin-left: auto;
|
||
}
|
||
|
||
/* ── Step cards ────────────────────────────────────── */
|
||
.step-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||
gap: 14px;
|
||
}
|
||
.step-card {
|
||
background: var(--card-bg);
|
||
border-radius: var(--border-radius);
|
||
padding: 16px;
|
||
border: 1.5px solid var(--gray-200);
|
||
cursor: pointer;
|
||
transition: box-shadow var(--transition), border-color var(--transition), transform var(--transition);
|
||
text-decoration: none;
|
||
display: block;
|
||
color: inherit;
|
||
}
|
||
.step-card:hover {
|
||
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
|
||
border-color: var(--nis2-cyan);
|
||
transform: translateY(-2px);
|
||
}
|
||
.step-card-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 10px;
|
||
}
|
||
.step-icon {
|
||
width: 38px;
|
||
height: 38px;
|
||
border-radius: 9px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1rem;
|
||
}
|
||
.step-badge {
|
||
font-size: 0.67rem;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.03em;
|
||
}
|
||
.badge-done { background: var(--secondary-bg); color: var(--secondary); }
|
||
.badge-partial { background: var(--warning-bg); color: #92400e; }
|
||
.badge-todo { background: var(--gray-100); color: var(--gray-500); }
|
||
.badge-active { background: var(--primary-bg); color: var(--primary-dark); }
|
||
|
||
.step-card h4 { font-size: 0.85rem; font-weight: 600; margin-bottom: 3px; color: var(--gray-800); }
|
||
.step-card p { font-size: 0.76rem; color: var(--gray-500); line-height: 1.45; margin-bottom: 12px; }
|
||
.step-metric { font-size: 1.5rem; font-weight: 800; color: var(--gray-800); line-height: 1; }
|
||
.step-metric-lbl { font-size: 0.7rem; color: var(--gray-400); margin-top: 1px; }
|
||
.step-cta {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
margin-top: 10px;
|
||
font-size: 0.74rem;
|
||
font-weight: 600;
|
||
color: var(--nis2-cyan);
|
||
}
|
||
|
||
/* ── Legend ────────────────────────────────────────── */
|
||
.legend-row {
|
||
display: flex;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 20px;
|
||
font-size: 0.78rem;
|
||
color: var(--gray-500);
|
||
}
|
||
.legend-item { display: flex; align-items: center; gap: 5px; }
|
||
|
||
@keyframes fadeSlideIn {
|
||
from { opacity: 0; transform: translateY(-6px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
.phase-detail-anim { animation: fadeSlideIn 0.2s ease; }
|
||
|
||
@media (max-width: 640px) {
|
||
.phase-name { font-size: 0.7rem; }
|
||
.phase-num { display: none; }
|
||
.journey-header { flex-direction: column; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-layout">
|
||
<aside class="sidebar" id="sidebar"></aside>
|
||
<main class="main-content">
|
||
<header class="content-header">
|
||
<h2>
|
||
<i class="fa-solid fa-route" style="color:var(--nis2-cyan);margin-right:8px;"></i>
|
||
Compliance Journey
|
||
</h2>
|
||
<div class="content-header-actions">
|
||
<span class="text-muted" id="org-name-header" style="font-size:0.85rem;"></span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="content-body" id="page-body">
|
||
<div class="spinner-lg" style="margin:100px auto;display:block;"></div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<script src="js/api.js"></script>
|
||
<script src="js/common.js"></script>
|
||
<script src="js/i18n.js"></script>
|
||
<script>
|
||
if (!checkAuth()) throw new Error('Not authenticated');
|
||
loadSidebar();
|
||
I18n.init();
|
||
|
||
// ── Dati globali ────────────────────────────────────────────
|
||
let G = {};
|
||
let activePhase = 0;
|
||
|
||
// ── Definizione fasi ────────────────────────────────────────
|
||
const PHASES = [
|
||
{
|
||
id: 'preparazione', num: 1, name: 'Preparazione',
|
||
color: '#6366f1', icon: 'fa-rocket',
|
||
desc: 'Organizzazione, team e classificazione NIS2',
|
||
steps: [
|
||
{ id: 'onboarding', name: 'Onboarding NIS2', icon: 'fa-rocket', desc: 'Classificazione settore, dimensione e tipo soggetto NIS2', href: 'onboarding.html', mk: 'onboarding_done' },
|
||
{ id: 'org', name: 'Organizzazione', icon: 'fa-building', desc: 'Dati aziendali, P.IVA, classificazione completata', href: 'settings.html', mk: 'org_setup' },
|
||
{ id: 'team', name: 'Team & Ruoli', icon: 'fa-users', desc: 'Membri del team con ruoli CISO, Admin, Auditor assegnati', href: 'settings.html', mk: 'members_count' },
|
||
]
|
||
},
|
||
{
|
||
id: 'valutazione', num: 2, name: 'Valutazione',
|
||
color: '#0891b2', icon: 'fa-magnifying-glass-chart',
|
||
desc: 'Gap analysis, asset e supply chain',
|
||
steps: [
|
||
{ id: 'assessment', name: 'Gap Assessment', icon: 'fa-clipboard-check', desc: '80 domande Art.21 NIS2 su 10 categorie di misure di sicurezza', href: 'assessment.html', mk: 'assessment_pct' },
|
||
{ id: 'assets', name: 'Inventario Asset', icon: 'fa-server', desc: 'Censimento asset critici, classificazione e mappa dipendenze', href: 'assets.html', mk: 'assets_count' },
|
||
{ id: 'supply', name: 'Supply Chain', icon: 'fa-link', desc: 'Fornitori critici, valutazione rischio e certificazioni', href: 'supply-chain.html', mk: 'suppliers_count' },
|
||
]
|
||
},
|
||
{
|
||
id: 'rischi', num: 3, name: 'Rischi',
|
||
color: '#dc2626', icon: 'fa-shield-halved',
|
||
desc: 'Identificazione, valutazione e trattamento',
|
||
steps: [
|
||
{ id: 'risks', name: 'Risk Register', icon: 'fa-triangle-exclamation', desc: 'Identificazione e scoring rischi su scala ISO 27005 (5×5)', href: 'risks.html', mk: 'risks_count' },
|
||
{ id: 'treatments', name: 'Piano Trattamento', icon: 'fa-shield', desc: 'Azioni di mitigazione, accettazione o trasferimento rischi', href: 'risks.html', mk: 'treatments_pct' },
|
||
{ id: 'ai_risk', name: 'AI Risk Suggest', icon: 'fa-wand-magic-sparkles', desc: 'Suggerimenti rischi generati da AI per settore e asset', href: 'risks.html', mk: 'ai_risks_done' },
|
||
]
|
||
},
|
||
{
|
||
id: 'implementazione', num: 4, name: 'Implementazione',
|
||
color: '#16a34a', icon: 'fa-gears',
|
||
desc: 'Policy, formazione e controlli di sicurezza',
|
||
steps: [
|
||
{ id: 'policies', name: 'Policy', icon: 'fa-file-shield', desc: 'Redazione, revisione AI e approvazione policy NIS2', href: 'policies.html', mk: 'policies_approved' },
|
||
{ id: 'training', name: 'Formazione', icon: 'fa-graduation-cap', desc: 'Corsi obbligatori assegnati e completati dal personale', href: 'training.html', mk: 'training_pct' },
|
||
{ id: 'controls', name: 'Controlli Audit', icon: 'fa-list-check', desc: 'Implementazione controlli ISO 27001 / misure Art.21 NIS2', href: 'reports.html', mk: 'controls_pct' },
|
||
]
|
||
},
|
||
{
|
||
id: 'monitoraggio', num: 5, name: 'Monitoraggio',
|
||
color: '#d97706', icon: 'fa-eye',
|
||
desc: 'Incidenti, non-conformità e normative',
|
||
steps: [
|
||
{ id: 'incidents', name: 'Incidenti Art.23', icon: 'fa-bell', desc: 'Gestione e notifica CSIRT: 24h early warning, 72h, 30 giorni', href: 'incidents.html', mk: 'incidents_count' },
|
||
{ id: 'ncr', name: 'NCR / CAPA', icon: 'fa-circle-xmark', desc: 'Non-conformità rilevate e azioni correttive in corso', href: 'reports.html', mk: 'ncr_open' },
|
||
{ id: 'normative', name: 'Aggiornamenti NIS2', icon: 'fa-newspaper', desc: 'Feed ACN, DORA, D.Lgs. 138/2024 con conferma di lettura', href: 'normative.html', mk: 'normative_pending' },
|
||
{ id: 'wb', name: 'Segnalazioni', icon: 'fa-user-secret', desc: 'Canale whistleblowing anonimo Art.32 NIS2', href: 'whistleblowing.html', mk: 'wb_open' },
|
||
]
|
||
},
|
||
{
|
||
id: 'reportistica', num: 6, name: 'Reportistica',
|
||
color: '#7c3aed', icon: 'fa-chart-line',
|
||
desc: 'Report esecutivi, audit chain e export',
|
||
steps: [
|
||
{ id: 'exec_report', name: 'Report Compliance', icon: 'fa-file-chart-line', desc: 'Report esecutivo HTML stampabile come PDF per board e auditor', href: 'reports.html', mk: 'report_gen' },
|
||
{ id: 'audit_chain', name: 'Audit Chain', icon: 'fa-link', desc: 'Log immutabile con hash chain SHA-256, verifica integrità', href: 'reports.html', mk: 'audit_records' },
|
||
{ id: 'export', name: 'Export Certificato', icon: 'fa-file-export', desc: 'Export CSV/JSON con firma SHA-256 per conservazione prove', href: 'reports.html', mk: 'export_done' },
|
||
]
|
||
}
|
||
];
|
||
|
||
// ── Caricamento dati parallelo ───────────────────────────────
|
||
async function loadAll() {
|
||
const [ov, sc, al, rl, pl, asl, sl, ts, ac] = await Promise.allSettled([
|
||
api.getDashboardOverview(),
|
||
api.getComplianceScore(),
|
||
api.get('/assessments/list'),
|
||
api.get('/risks/list'),
|
||
api.get('/policies/list'),
|
||
api.get('/assets/list'),
|
||
api.get('/supply-chain/list'),
|
||
api.get('/training/compliance-status'),
|
||
api.get('/audit/controls'),
|
||
]);
|
||
|
||
const val = r => r.status === 'fulfilled' ? (r.value?.data || {}) : {};
|
||
const o = val(ov), s = val(sc), a = val(al);
|
||
const rr = val(rl), p = val(pl), as = val(asl);
|
||
const sp = val(sl), tr = val(ts), ct = val(ac);
|
||
|
||
const assessments = Array.isArray(a.assessments) ? a.assessments : [];
|
||
const risks = Array.isArray(rr.risks) ? rr.risks : [];
|
||
const policies = Array.isArray(p.policies) ? p.policies : [];
|
||
const assets = Array.isArray(as.assets) ? as.assets : [];
|
||
const suppliers = Array.isArray(sp.suppliers) ? sp.suppliers : [];
|
||
const controls = Array.isArray(ct.controls) ? ct.controls : [];
|
||
|
||
const completedA = assessments.filter(a => a.status === 'completed');
|
||
const assessPct = assessments.length ? Math.round(completedA.length / assessments.length * 100) : 0;
|
||
const approvedPol = policies.filter(p => p.status === 'approved' || p.status === 'published').length;
|
||
const implCtrls = controls.filter(c => c.implementation_status === 'implemented').length;
|
||
const controlsPct = controls.length ? Math.round(implCtrls / controls.length * 100) : 0;
|
||
const trainingPct = tr.completion_rate != null ? Math.round(tr.completion_rate)
|
||
: (tr.completed && tr.total ? Math.round(tr.completed / tr.total * 100) : 0);
|
||
const openRisks = risks.filter(r => !r.deleted_at).length;
|
||
const withTreat = risks.filter(r => r.treatments_count > 0 || r.treatment_status).length;
|
||
const treatPct = openRisks ? Math.round(withTreat / openRisks * 100) : 0;
|
||
const score = Math.round(s.avg_implementation || s.score || o.compliance_score || 0);
|
||
|
||
G = {
|
||
complianceScore: score,
|
||
onboarding_done: 1,
|
||
org_setup: 1,
|
||
members_count: o.members_count || '—',
|
||
assessments_total: assessments.length,
|
||
assessment_pct: assessPct,
|
||
assets_count: assets.length,
|
||
suppliers_count: suppliers.length,
|
||
risks_count: openRisks,
|
||
treatments_pct: treatPct,
|
||
ai_risks_done: risks.filter(r => r.ai_generated).length,
|
||
policies_approved: approvedPol,
|
||
policies_total: policies.length,
|
||
training_pct: trainingPct,
|
||
controls_pct: controlsPct,
|
||
controls_total: controls.length,
|
||
incidents_count: o.active_incidents || 0,
|
||
ncr_open: 0,
|
||
normative_pending: 0,
|
||
wb_open: 0,
|
||
report_gen: completedA.length,
|
||
audit_records: null,
|
||
export_done: null,
|
||
};
|
||
|
||
// Seleziona automaticamente la fase "attiva" (prima non completata)
|
||
activePhase = computeActivePhase();
|
||
renderPage();
|
||
}
|
||
|
||
// ── Logica status ────────────────────────────────────────────
|
||
function phaseStatus(id) {
|
||
switch (id) {
|
||
case 'preparazione': return 'done';
|
||
case 'valutazione':
|
||
if (!G.assessments_total && !G.assets_count) return 'todo';
|
||
return G.assessment_pct >= 100 && G.assets_count > 0 ? 'done' : 'partial';
|
||
case 'rischi':
|
||
if (!G.risks_count) return 'todo';
|
||
return G.treatments_pct >= 80 ? 'done' : 'partial';
|
||
case 'implementazione':
|
||
if (!G.policies_total && !G.controls_total) return 'todo';
|
||
return G.controls_pct >= 80 && G.training_pct >= 80 ? 'done' : 'partial';
|
||
case 'monitoraggio': return 'active';
|
||
case 'reportistica': return G.report_gen > 0 ? 'partial' : 'todo';
|
||
default: return 'todo';
|
||
}
|
||
}
|
||
|
||
function phasePct(id) {
|
||
switch (id) {
|
||
case 'preparazione': return 100;
|
||
case 'valutazione': return Math.round((G.assessment_pct + (G.assets_count > 0 ? 100 : 0)) / 2);
|
||
case 'rischi': return G.risks_count > 0 ? Math.max(10, G.treatments_pct) : 0;
|
||
case 'implementazione': return Math.round(((G.controls_pct || 0) + (G.training_pct || 0) + (G.policies_approved > 0 ? 60 : 0)) / 3);
|
||
case 'monitoraggio': return 100;
|
||
case 'reportistica': return G.report_gen > 0 ? 40 : 0;
|
||
default: return 0;
|
||
}
|
||
}
|
||
|
||
function stepStatus(mk) {
|
||
switch (mk) {
|
||
case 'onboarding_done':
|
||
case 'org_setup': return 'done';
|
||
case 'members_count': return 'done';
|
||
case 'assessment_pct': return G.assessments_total ? (G.assessment_pct >= 100 ? 'done' : 'partial') : 'todo';
|
||
case 'assets_count': return G.assets_count > 0 ? 'done' : 'todo';
|
||
case 'suppliers_count': return G.suppliers_count > 0 ? 'partial' : 'todo';
|
||
case 'risks_count': return G.risks_count > 0 ? 'partial' : 'todo';
|
||
case 'treatments_pct': return G.treatments_pct >= 80 ? 'done' : (G.treatments_pct > 0 ? 'partial' : 'todo');
|
||
case 'ai_risks_done': return G.ai_risks_done > 0 ? 'done' : 'todo';
|
||
case 'policies_approved':return G.policies_approved >= 3 ? 'done' : (G.policies_approved > 0 ? 'partial' : 'todo');
|
||
case 'training_pct': return G.training_pct >= 80 ? 'done' : (G.training_pct > 0 ? 'partial' : 'todo');
|
||
case 'controls_pct': return G.controls_pct >= 80 ? 'done' : (G.controls_pct > 0 ? 'partial' : 'todo');
|
||
case 'incidents_count': return 'active';
|
||
case 'ncr_open': return 'active';
|
||
case 'normative_pending':return 'active';
|
||
case 'wb_open': return 'active';
|
||
default: return 'partial';
|
||
}
|
||
}
|
||
|
||
function stepMetric(mk) {
|
||
const m = {
|
||
onboarding_done: { val: '✓', lbl: 'configurato' },
|
||
org_setup: { val: '✓', lbl: 'configurato' },
|
||
members_count: { val: G.members_count, lbl: 'membri' },
|
||
assessment_pct: G.assessments_total
|
||
? { val: G.assessment_pct + '%', lbl: 'completato' }
|
||
: { val: '0', lbl: 'assessment' },
|
||
assets_count: { val: G.assets_count || 0, lbl: 'asset censiti' },
|
||
suppliers_count: { val: G.suppliers_count || 0, lbl: 'fornitori' },
|
||
risks_count: { val: G.risks_count || 0, lbl: 'rischi aperti' },
|
||
treatments_pct: G.risks_count
|
||
? { val: G.treatments_pct + '%', lbl: 'con trattamento' }
|
||
: { val: '—', lbl: '' },
|
||
ai_risks_done: { val: G.ai_risks_done || 0, lbl: 'suggeriti da AI' },
|
||
policies_approved: { val: G.policies_approved || 0, lbl: 'approvate' },
|
||
training_pct: { val: (G.training_pct || 0) + '%', lbl: 'completato' },
|
||
controls_pct: { val: (G.controls_pct || 0) + '%', lbl: 'implementati' },
|
||
incidents_count: { val: G.incidents_count || 0, lbl: 'attivi' },
|
||
ncr_open: { val: G.ncr_open || 0, lbl: 'aperte' },
|
||
normative_pending: { val: G.normative_pending || 0, lbl: 'da leggere' },
|
||
wb_open: { val: G.wb_open || 0, lbl: 'in gestione' },
|
||
report_gen: { val: G.report_gen || 0, lbl: 'assessment completati' },
|
||
audit_records: { val: '—', lbl: 'record' },
|
||
export_done: { val: '—', lbl: 'export' },
|
||
};
|
||
return m[mk] || { val: '—', lbl: '' };
|
||
}
|
||
|
||
const ST_ICON = { done: 'fa-circle-check', partial: 'fa-circle-half-stroke', active: 'fa-circle-dot', todo: 'fa-circle' };
|
||
const ST_LBL = { done: 'Completato', partial: 'In corso', active: 'Attivo', todo: 'Da iniziare' };
|
||
const ST_BADGE = { done: 'badge-done', partial: 'badge-partial', active: 'badge-active', todo: 'badge-todo' };
|
||
|
||
function computeActivePhase() {
|
||
for (let i = 0; i < PHASES.length; i++) {
|
||
const st = phaseStatus(PHASES[i].id);
|
||
if (st === 'partial' || st === 'todo' || st === 'active') return i;
|
||
}
|
||
return PHASES.length - 1;
|
||
}
|
||
|
||
function computeNextAction() {
|
||
if (!G.assessments_total) return { text: 'Avvia il primo Gap Assessment NIS2', link: 'assessment.html', icon: 'fa-clipboard-check' };
|
||
if (G.assessment_pct < 100) return { text: 'Completa il Gap Assessment in corso (' + G.assessment_pct + '%)', link: 'assessment.html', icon: 'fa-clipboard-check' };
|
||
if (!G.assets_count) return { text: 'Censisci gli asset critici dell\'organizzazione', link: 'assets.html', icon: 'fa-server' };
|
||
if (!G.risks_count) return { text: 'Registra i rischi NIS2 nel Risk Register', link: 'risks.html', icon: 'fa-triangle-exclamation' };
|
||
if (!G.policies_total) return { text: 'Genera la prima policy con AI', link: 'policies.html', icon: 'fa-file-shield' };
|
||
if (G.training_pct < 50) return { text: 'Assegna i corsi di formazione al team', link: 'training.html', icon: 'fa-graduation-cap' };
|
||
if (G.controls_pct < 60) return { text: 'Implementa i controlli di sicurezza Art.21', link: 'reports.html', icon: 'fa-list-check' };
|
||
return { text: 'Genera il report di compliance per il board', link: 'reports.html', icon: 'fa-chart-line' };
|
||
}
|
||
|
||
// ── Render ───────────────────────────────────────────────────
|
||
function renderPage() {
|
||
const next = computeNextAction();
|
||
const score = G.complianceScore;
|
||
|
||
let html = '';
|
||
|
||
// ── Journey header
|
||
html += `
|
||
<div class="journey-header">
|
||
<div>
|
||
<div style="font-size:0.75rem;opacity:0.55;text-transform:uppercase;letter-spacing:0.06em;margin-bottom:4px;">Progresso Art.21 NIS2</div>
|
||
<div style="display:flex;align-items:baseline;gap:10px;">
|
||
<span class="journey-score">${score}%</span>
|
||
<span style="color:rgba(255,255,255,0.5);font-size:0.82rem;">implementazione misure</span>
|
||
</div>
|
||
<div class="journey-progress-bar" style="max-width:320px;">
|
||
<div class="journey-progress-fill" style="width:${score}%;"></div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:24px;flex-wrap:wrap;">
|
||
${PHASES.map(ph => {
|
||
const pct = phasePct(ph.id);
|
||
const st = phaseStatus(ph.id);
|
||
return `<div style="text-align:center;">
|
||
<div style="font-size:0.65rem;opacity:0.5;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:2px;">${ph.name}</div>
|
||
<div style="font-size:1rem;font-weight:700;color:${st==='done'?'#4ade80':st==='todo'?'rgba(255,255,255,0.3)':'var(--nis2-cyan)'};">${pct}%</div>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
<div class="journey-meta">
|
||
<strong>${new Date().toLocaleDateString('it-IT', {day:'numeric',month:'long',year:'numeric'})}</strong>
|
||
Compliance Journey NIS2
|
||
</div>
|
||
</div>`;
|
||
|
||
// ── Next action
|
||
html += `
|
||
<div class="next-action-banner">
|
||
<i class="fa-solid ${next.icon} na-icon"></i>
|
||
<div>
|
||
<div class="na-label">Prossima azione consigliata</div>
|
||
<div class="na-text"><a href="${next.link}">${next.text} →</a></div>
|
||
</div>
|
||
</div>`;
|
||
|
||
// ── Legend
|
||
html += `
|
||
<div class="legend-row">
|
||
<div class="legend-item"><i class="fa-solid fa-circle-check" style="color:var(--secondary);"></i> Completato</div>
|
||
<div class="legend-item"><i class="fa-solid fa-circle-half-stroke" style="color:var(--warning);"></i> In corso</div>
|
||
<div class="legend-item"><i class="fa-solid fa-circle-dot" style="color:var(--primary);"></i> Attivo / Continuo</div>
|
||
<div class="legend-item"><i class="fa-solid fa-circle" style="color:var(--gray-300);"></i> Da iniziare</div>
|
||
</div>`;
|
||
|
||
// ── Phase roadmap
|
||
html += `<div class="phase-roadmap">`;
|
||
PHASES.forEach((ph, idx) => {
|
||
const st = phaseStatus(ph.id);
|
||
const pct = phasePct(ph.id);
|
||
const isActive = activePhase === idx;
|
||
html += `
|
||
<div class="phase-item ${isActive ? 'active' : ''} ${st}" onclick="selectPhase(${idx})"
|
||
style="--ph-color:${ph.color}${st==='todo'?'99':''};">
|
||
<div class="phase-box" style="background:${ph.color}${st==='todo'?'66':''};">
|
||
<div class="phase-num">Fase ${ph.num}</div>
|
||
<div class="phase-name">${ph.name}</div>
|
||
<div class="phase-status-row">
|
||
<i class="fa-solid ${ST_ICON[st]} phase-status-icon"></i>
|
||
<span class="phase-pct">${pct}%</span>
|
||
</div>
|
||
<div class="phase-mini-bar">
|
||
<div class="phase-mini-fill" style="width:${pct}%;"></div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
});
|
||
html += `</div>`;
|
||
|
||
// ── Phase detail
|
||
html += `<div id="phase-detail-wrap">${renderPhaseDetail(activePhase)}</div>`;
|
||
|
||
document.getElementById('page-body').innerHTML = html;
|
||
}
|
||
|
||
function renderPhaseDetail(idx) {
|
||
const ph = PHASES[idx];
|
||
const st = phaseStatus(ph.id);
|
||
|
||
let html = `
|
||
<div class="card phase-detail-anim">
|
||
<div class="card-header">
|
||
<div class="phase-detail-header">
|
||
<span class="phase-detail-badge" style="background:${ph.color};">
|
||
<i class="fa-solid ${ph.icon}"></i> Fase ${ph.num} — ${ph.name}
|
||
</span>
|
||
<span class="step-badge ${ST_BADGE[st]}">${ST_LBL[st]}</span>
|
||
<span class="phase-detail-desc">${ph.desc}</span>
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="step-cards">`;
|
||
|
||
for (const step of ph.steps) {
|
||
const sst = stepStatus(step.mk);
|
||
const met = stepMetric(step.mk);
|
||
html += `
|
||
<a href="${step.href}" class="step-card">
|
||
<div class="step-card-top">
|
||
<div class="step-icon" style="background:${ph.color}18;color:${ph.color};">
|
||
<i class="fa-solid ${step.icon}"></i>
|
||
</div>
|
||
<span class="step-badge ${ST_BADGE[sst]}">${ST_LBL[sst]}</span>
|
||
</div>
|
||
<h4>${step.name}</h4>
|
||
<p>${step.desc}</p>
|
||
<div class="step-metric">${met.val}</div>
|
||
<div class="step-metric-lbl">${met.lbl}</div>
|
||
<div class="step-cta">Vai al modulo <i class="fa-solid fa-arrow-right" style="font-size:0.65rem;margin-left:2px;"></i></div>
|
||
</a>`;
|
||
}
|
||
|
||
html += `</div></div></div>`;
|
||
return html;
|
||
}
|
||
|
||
function selectPhase(idx) {
|
||
activePhase = idx;
|
||
document.querySelectorAll('.phase-item').forEach((el, i) => {
|
||
el.classList.toggle('active', i === idx);
|
||
});
|
||
document.getElementById('phase-detail-wrap').innerHTML = renderPhaseDetail(idx);
|
||
}
|
||
|
||
// ── Org name in header ────────────────────────────────────────
|
||
try {
|
||
const u = JSON.parse(localStorage.getItem('nis2_user') || '{}');
|
||
if (u.org_name) document.getElementById('org-name-header').textContent = u.org_name;
|
||
} catch(e) {}
|
||
|
||
// ── Start ─────────────────────────────────────────────────────
|
||
loadAll();
|
||
</script>
|
||
</body>
|
||
</html>
|