[FEAT] Compliance Journey — workflow visivo 6 fasi NIS2

- 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>
This commit is contained in:
DevEnv nis2-agile 2026-03-09 07:54:15 +01:00
parent ab0e3755f4
commit 3e8f24eb49
2 changed files with 707 additions and 2 deletions

View File

@ -185,8 +185,9 @@ function loadSidebar() {
{
label: 'Principale', i18nKey: 'nav.main',
items: [
{ name: 'Dashboard', href: 'dashboard.html', icon: iconGrid(), i18nKey: 'nav.dashboard' },
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
{ name: 'Dashboard', href: 'dashboard.html', icon: iconGrid(), i18nKey: 'nav.dashboard' },
{ name: 'Compliance Journey', href: 'workflow.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"/></svg>` },
{ name: 'Gap Analysis', href: 'assessment.html', icon: iconClipboardCheck(), i18nKey: 'nav.gap_analysis' },
]
},
{

704
public/workflow.html Normal file
View File

@ -0,0 +1,704 @@
<!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} &rarr;</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} &mdash; ${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>