Files
nis2-agile/public/workflow.html
T
DevEnv nis2-agileandClaude Opus 4.8 7c711b727a [FEAT] Rischi: matrice sempre visibile, rimosse viste FAIR e KRI (ticket #500 fase 1)
Ticket #500 (Simon Fattori) - semplificazione pagina Gestione Rischi:
- matrice 5x5 ora sempre visibile sotto la tabella (rimosso bottone 'Matrice' e il view-toggle)
- rimosse le viste 'Quantitativo (FAIR)' e 'KRI' con relativi bottoni, blocchi HTML e JS
- help.js allineato (rimosse sezioni FAIR/KRI), cache-buster help.js -> 20260727a
- version.json -> 1.25.2
Rimandato a fase 2 (richiede modello dati): rischi derivati dai requisiti LEX&ISO
+ codifica RSK-<misura>Com.N + rimozione bottoni 'Nuovo Rischio'/'AI Suggerisci'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:15:22 +02:00

551 lines
33 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Compliance Journey · NIS2 Agile</title>
<!-- UI V3: Inter self-host + design system v3.css (NO Bootstrap Italia / NO FontAwesome CDN / NO Google Fonts). -->
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<style>
/* Stili specifici della pagina Compliance Journey non coperti da v3.css (nessun CDN). */
:root { --nis2-cyan:#1F5FA8; }
.text-muted { color: var(--muted); }
.ic-lg { width:22px; height:22px; }
/* ── Journey header ────────────────────────────────── */
.journey-header {
background: linear-gradient(135deg, var(--navy-nav) 0%, var(--navy-top) 100%);
border-radius: var(--radius);
padding: 24px 28px; color:#fff; margin-bottom:20px;
display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:16px;
box-shadow: var(--shadow);
}
.journey-score { font-size:2.8rem; font-weight:800; color:#fff; line-height:1; }
.journey-progress-bar { height:10px; background:rgba(255,255,255,.14); border-radius:5px; margin-top:10px; overflow:hidden; }
.journey-progress-fill { height:100%; background:linear-gradient(90deg, var(--gold), #ffd76a); border-radius:5px; transition:width 1s ease; }
.journey-meta { text-align:right; font-size:.78rem; opacity:.65; }
.journey-meta strong { display:block; font-size:.9rem; opacity:1; }
/* ── Next action banner ────────────────────────────── */
.next-action-banner {
background: linear-gradient(135deg, #0c4a6e, #075985); color:#fff; border-radius:var(--radius);
padding:14px 20px; display:flex; align-items:center; gap:14px; margin-bottom:20px;
border-left:4px solid var(--gold);
}
.next-action-banner .na-icon { color:var(--gold); flex-shrink:0; width:24px; height:24px; }
.next-action-banner .na-label { font-size:.72rem; opacity:.75; text-transform:uppercase; letter-spacing:.05em; }
.next-action-banner .na-text { font-size:.95rem; font-weight:600; }
.next-action-banner a { color:#fff; 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:120px; position:relative; cursor:pointer; }
.phase-box { padding:14px 16px 12px 18px; color:#fff; height:100%; display:flex; flex-direction:column; justify-content:center; transition:filter .2s, box-shadow .2s; position:relative; }
.phase-item:first-child .phase-box { border-radius: var(--radius) 0 0 var(--radius); }
.phase-item:last-child .phase-box { border-radius: 0 var(--radius) var(--radius) 0; }
.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 var(--bg); z-index:2;
}
.phase-item.active .phase-box { filter:brightness(1.18); box-shadow:0 4px 18px rgba(0,0,0,.25); z-index:1; }
.phase-item:not(.active):hover .phase-box { filter:brightness(1.08); }
.phase-item.todo .phase-box { opacity:.6; }
.phase-num { font-size:.62rem; font-weight:600; text-transform:uppercase; letter-spacing:.06em; opacity:.7; margin-bottom:2px; }
.phase-name { font-size:.78rem; font-weight:700; line-height:1.2; }
.phase-status-row { display:flex; align-items:center; gap:5px; margin-top:5px; }
.phase-status-icon { width:14px; height:14px; }
.phase-pct { font-size:.7rem; opacity:.85; }
.phase-mini-bar { height:3px; background:rgba(255,255,255,.2); border-radius:2px; margin-top:5px; overflow:hidden; }
.phase-mini-fill { height:100%; background:rgba(255,255,255,.8); border-radius:2px; }
/* ── Phase detail ──────────────────────────────────── */
.phase-detail-header { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:16px; }
.phase-detail-badge { display:inline-flex; align-items:center; gap:6px; padding:5px 14px; border-radius:20px; font-size:.78rem; font-weight:700; color:#fff; }
.phase-detail-badge i { width:15px; height:15px; }
.phase-detail-desc { font-size:.85rem; color:var(--muted); margin-left:auto; }
/* ── Step cards ────────────────────────────────────── */
.step-cards { display:grid; grid-template-columns:repeat(auto-fill, minmax(220px,1fr)); gap:14px; }
.step-card { background:#fff; border-radius:var(--radius); padding:16px; border:1.5px solid var(--line); cursor:pointer; transition:box-shadow .15s, border-color .15s, transform .15s; text-decoration:none; display:block; color:inherit; }
.step-card:hover { box-shadow:0 6px 20px rgba(16,40,80,.12); border-color:#9FBEDD; 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:10px; display:flex; align-items:center; justify-content:center; }
.step-icon i { width:18px; height:18px; }
.step-badge { font-size:.67rem; padding:2px 9px; border-radius:10px; font-weight:700; text-transform:uppercase; letter-spacing:.03em; }
.badge-done { background:#E6F4EC; color:#1E7A4E; }
.badge-partial { background:#FBF1E2; color:#9A6418; }
.badge-todo { background:#EEF2F7; color:#5b6b80; }
.badge-active { background:#E7F0FB; color:#1F5FA8; }
.step-card h4 { font-size:.9rem; font-weight:700; margin:0 0 3px; color:var(--ink); }
.step-card p { font-size:.78rem; color:var(--muted); line-height:1.45; margin:0 0 12px; }
.step-metric { font-size:1.5rem; font-weight:800; color:var(--ink); line-height:1; }
.step-metric-lbl { font-size:.72rem; color:var(--muted); margin-top:1px; }
.step-cta { display:inline-flex; align-items:center; gap:5px; margin-top:10px; font-size:.74rem; font-weight:600; color:var(--c-blue); }
.step-cta i { width:13px; height:13px; }
/* ── Legend ────────────────────────────────────────── */
.legend-row { display:flex; gap:16px; flex-wrap:wrap; margin-bottom:20px; font-size:.8rem; color:var(--muted); }
.legend-item { display:flex; align-items:center; gap:6px; }
.legend-item i { width:15px; height:15px; }
@keyframes fadeSlideIn { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:translateY(0); } }
.phase-detail-anim { animation: fadeSlideIn .2s ease; }
@media (prefers-reduced-motion:reduce){ .phase-detail-anim{animation:none} .journey-progress-fill{transition:none} }
@media (max-width:640px) {
.phase-name { font-size:.7rem; }
.phase-num { display:none; }
.journey-header { flex-direction:column; align-items:flex-start; }
.phase-detail-desc { margin-left:0; }
}
</style>
<!-- PWA:start -->
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#0066CC">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="NIS2 Agile">
<meta name="application-name" content="NIS2 Agile">
<script src="/js/pwa.js?v=20260614" defer></script>
<!-- PWA:end -->
</head>
<body>
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
<div id="topnav"></div>
<main class="wrap">
<nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><span>Compliance Journey</span>
</nav>
<div class="page-head">
<div>
<h1>Compliance Journey</h1>
<p class="sub">Il percorso di conformità NIS2 in 6 fasi, con stato in tempo reale</p>
</div>
<div class="date"><i data-lucide="building" class="ic"></i><span id="org-name-header"></span></div>
</div>
<div id="page-body">
<div class="v3-loading"><span class="v3-spin"></span>Caricamento del percorso di conformità…</div>
</div>
</main>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help → init pagina. -->
<script src="/js/api.js?v=20260627u"></script>
<script src="/js/common.js?v=20260627t"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627t"></script>
<script src="/js/i18n.js?v=20260627p"></script>
<script src="/js/help.js?v=20260727a"></script>
<script>
// ── Auth + chrome + i18n + help ──────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar(); // = loadTopnavV3() (topnav-v3.js)
if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
// Re-render delle icone Lucide (chrome + markup dinamico)
function drawIcons(){ if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch(e){} } }
drawIcons();
// ── Dati globali ────────────────────────────────────────────
let G = {};
let activePhase = 0;
// ── Definizione fasi (icone = nomi Lucide self-host) ─────────
const PHASES = [
{
id: 'preparazione', num: 1, name: 'Preparazione',
color: '#6366f1', icon: 'rocket',
desc: 'Organizzazione, team e classificazione NIS2',
steps: [
{ id: 'onboarding', name: 'Onboarding NIS2', icon: 'rocket', desc: 'Classificazione settore, dimensione e tipo soggetto NIS2', href: 'onboarding.html', mk: 'onboarding_done' },
{ id: 'org', name: 'Organizzazione', icon: 'building', desc: 'Dati aziendali, P.IVA, classificazione completata', href: 'settings.html', mk: 'org_setup' },
{ id: 'team', name: 'Team & Ruoli', icon: '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: 'search-check',
desc: 'Gap analysis, asset e supply chain',
steps: [
{ id: 'assessment', name: 'Gap Assessment', icon: '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: 'server', desc: 'Censimento asset critici, classificazione e mappa dipendenze', href: 'assets.html', mk: 'assets_count' },
{ id: 'supply', name: 'Supply Chain', icon: 'link', desc: 'Fornitori critici, valutazione rischio e certificazioni', href: 'supply-chain.html', mk: 'suppliers_count' },
]
},
{
id: 'rischi', num: 3, name: 'Rischi',
color: '#dc2626', icon: 'shield-half',
desc: 'Identificazione, valutazione e trattamento',
steps: [
{ id: 'risks', name: 'Risk Register', icon: 'triangle-alert', desc: 'Identificazione e scoring rischi su scala ISO 27005 (5×5)', href: 'risks.html', mk: 'risks_count' },
{ id: 'treatments', name: 'Piano Trattamento', icon: 'shield', desc: 'Azioni di mitigazione, accettazione o trasferimento rischi', href: 'risks.html', mk: 'treatments_pct' },
{ id: 'ai_risk', name: 'AI Risk Suggest', icon: 'wand-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: 'settings',
desc: 'Policy, formazione e controlli di sicurezza',
steps: [
{ id: 'policies', name: 'Policy', icon: 'file-lock', desc: 'Redazione, revisione AI e approvazione policy NIS2', href: 'policies.html', mk: 'policies_approved' },
{ id: 'training', name: 'Formazione', icon: 'graduation-cap', desc: 'Corsi obbligatori assegnati e completati dal personale', href: 'training.html', mk: 'training_pct' },
{ id: 'controls', name: 'Controlli Audit', icon: 'list-checks', desc: 'Implementazione controlli ISO 27001 / misure Art.21 NIS2', href: 'reports.html', mk: 'controls_pct' },
]
},
{
id: 'monitoraggio', num: 5, name: 'Monitoraggio',
color: '#d97706', icon: 'eye',
desc: 'Incidenti, non-conformità e normative',
steps: [
{ id: 'incidents', name: 'Incidenti Art.23', icon: 'bell', desc: 'Gestione e notifica CSIRT: 24h early warning, 72h, 1 mese', href: 'incidents.html', mk: 'incidents_count' },
{ id: 'ncr', name: 'NCR / CAPA', icon: 'circle-x', desc: 'Non-conformità rilevate e azioni correttive in corso', href: 'reports.html', mk: 'ncr_open' },
{ id: 'normative', name: 'Aggiornamenti NIS2', icon: 'newspaper', desc: 'Feed ACN, DORA, D.Lgs. 138/2024 con conferma di lettura', href: 'normative.html', mk: 'normative_pending' },
{ id: 'wb', name: 'Segnalazioni', icon: 'eye-off', desc: 'Canale whistleblowing anonimo Art.32 NIS2', href: 'whistleblowing.html', mk: 'wb_open' },
]
},
{
id: 'reportistica', num: 6, name: 'Reportistica',
color: '#7c3aed', icon: 'line-chart',
desc: 'Report esecutivi, audit chain e export',
steps: [
{ id: 'exec_report', name: 'Report Compliance', icon: 'file-chart-column', desc: 'Report esecutivo HTML stampabile come PDF per board e auditor', href: 'reports.html', mk: 'report_gen' },
{ id: 'audit_chain', name: 'Audit Chain', icon: 'link', desc: 'Log immutabile con hash chain SHA-256, verifica integrità', href: 'reports.html', mk: 'audit_records' },
{ id: 'export', name: 'Export Certificato', icon: 'file-output', 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: 'circle-check', partial: 'circle-dashed', active: 'circle-dot', todo: '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: 'clipboard-check' };
if (G.assessment_pct < 100) return { text: 'Completa il Gap Assessment in corso (' + G.assessment_pct + '%)', link: 'assessment.html', icon: 'clipboard-check' };
if (!G.assets_count) return { text: 'Censisci gli asset critici dell\'organizzazione', link: 'assets.html', icon: 'server' };
if (!G.risks_count) return { text: 'Registra i rischi NIS2 nel Risk Register', link: 'risks.html', icon: 'triangle-alert' };
if (!G.policies_total) return { text: 'Genera la prima policy con AI', link: 'policies.html', icon: 'file-lock' };
if (G.training_pct < 50) return { text: 'Assegna i corsi di formazione al team', link: 'training.html', icon: 'graduation-cap' };
if (G.controls_pct < 60) return { text: 'Implementa i controlli di sicurezza Art.21', link: 'reports.html', icon: 'list-checks' };
return { text: 'Genera il report di compliance per il board', link: 'reports.html', icon: 'line-chart' };
}
// ── 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.65;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.6);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.6;text-transform:uppercase;letter-spacing:0.04em;margin-bottom:2px;">${ph.name}</div>
<div style="font-size:1rem;font-weight:700;color:${st==='done'?'#7ee0a8':st==='todo'?'rgba(255,255,255,0.4)':'var(--gold)'};">${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 data-lucide="${next.icon}" class="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 data-lucide="circle-check" style="color:var(--c-green);"></i> Completato</div>
<div class="legend-item"><i data-lucide="circle-dashed" style="color:var(--c-amber);"></i> In corso</div>
<div class="legend-item"><i data-lucide="circle-dot" style="color:var(--c-blue);"></i> Attivo / Continuo</div>
<div class="legend-item"><i data-lucide="circle" style="color:#94a3b8;"></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 data-lucide="${ST_ICON[st]}" class="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;
drawIcons();
}
function renderPhaseDetail(idx) {
const ph = PHASES[idx];
const st = phaseStatus(ph.id);
let html = `
<div class="card card-pad phase-detail-anim">
<div class="phase-detail-header">
<span class="phase-detail-badge" style="background:${ph.color};">
<i data-lucide="${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 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 data-lucide="${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 data-lucide="arrow-right"></i></div>
</a>`;
}
html += `</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);
drawIcons();
}
window.selectPhase = selectPhase;
// ── 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>