Files
nis2-agile/public/onboarding.html
T
DevEnv nis2-agileandClaude Opus 4.8 1c64211685 [UI] SWAP V2: Bootstrap Italia ora UFFICIALE su 27 pagine app (kill della vecchia UI)
Promosse le pagine -bi a nomi ufficiali (contenuto BI), link -bi->plain, titoli '(BI)' rimossi.
La vecchia UI delle pagine app e sostituita. Backup: git (commit 36967f9) + /tmp/nis2-v1-backup.

Promosse (27): auth (login/register/onboarding/forgot/reset) + app (dashboard/assessment/
risks/incidents/policies/supply-chain/training/assets/reports/settings/isms/acn-gap/normative/
whistleblowing/kb/cross-analysis/service-continuity/guida/companies) + admin (index/orgs/users).
common-bi.js: sidebar BI ora linka le pagine ufficiali.

ESCLUSA: index.html (landing marketing 1298 righe) -> conversione dedicata a parte (la -bi
era 557 righe, rischio perdita contenuti). Pagine marketing/demo (presentation/simulate/
workflow/architecture/index-en) restano vecchio stile (fuori scope app autenticata).

Verifica produzione: 27/27 servite 200 con BI, 0 link -bi residui, 0 '(BI)' nei titoli,
auth intatta (401 su pwd errata), common-bi.js node --check OK.

Rollback: git revert HEAD  (oppure ripristino da /tmp/nis2-v1-backup).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 14:49:15 +02:00

882 lines
57 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Onboarding - NIS2 Agile</title>
<!-- Bootstrap Italia v2.18.1 — self-hostato da /vendor (MAI CDN). Variante "-bi" della onboarding.html. -->
<link rel="stylesheet" href="vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<style>
body { background: #f5f6f7; }
.wizard-page { min-height: 100vh; display: flex; flex-direction: column; }
.wizard-header { background: #fff; border-bottom: 1px solid #d4d8db; padding: 16px 32px;
display: flex; align-items: center; gap: 12px; }
.wizard-header-logo { width: 36px; height: 36px; background: #06c; border-radius: 6px;
display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.wizard-header-logo svg { width: 20px; height: 20px; fill: #fff; }
.wizard-header-text h1 { font-size: 1.125rem; font-weight: 700; color: #17324d; margin: 0; }
.wizard-header-text span { font-size: .7rem; color: #5a6772; text-transform: uppercase; letter-spacing: .05em; }
.wizard-stepper { background: #fff; border-bottom: 1px solid #d4d8db; padding: 20px 32px; }
.stepper { display: flex; align-items: center; justify-content: center; max-width: 800px; margin: 0 auto; }
.stepper-step { display: flex; align-items: center; gap: 10px; white-space: nowrap; }
.stepper-number { width: 32px; height: 32px; border-radius: 50%; background: #e6e9ed; color: #5a6772;
display: flex; align-items: center; justify-content: center; font-size: .8rem; font-weight: 700;
flex-shrink: 0; transition: all .2s; }
.stepper-label { font-size: .8rem; font-weight: 500; color: #5a6772; transition: color .2s; }
.stepper-step.active .stepper-number { background: #06c; color: #fff; box-shadow: 0 0 0 4px rgba(0,102,204,.15); }
.stepper-step.active .stepper-label { color: #06c; font-weight: 600; }
.stepper-step.completed .stepper-number { background: #008758; color: #fff; }
.stepper-step.completed .stepper-label { color: #008758; }
.stepper-connector { width: 40px; height: 2px; background: #d4d8db; margin: 0 8px; flex-shrink: 0; transition: background .2s; }
.stepper-connector.completed { background: #008758; }
.wizard-body { flex: 1; display: flex; justify-content: center; padding: 40px 24px 60px; }
.wizard-content { width: 100%; max-width: 800px; }
.wizard-step { display: none; }
.wizard-step.active { display: block; }
.wizard-step-title { font-size: 1.5rem; font-weight: 700; color: #17324d; margin-bottom: 8px; }
.wizard-step-subtitle { font-size: .9375rem; color: #5a6772; margin-bottom: 32px; line-height: 1.6; }
.form-grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0 20px; }
.form-grid-2 .form-group-full { grid-column: 1 / -1; }
.profile-form-wrapper { max-width: 520px; margin: 0 auto; }
.wizard-loading { display: none; text-align: center; padding: 48px 24px; }
.wizard-loading.active { display: block; }
.wizard-loading-text { font-size: 1rem; font-weight: 600; color: #17324d; margin-bottom: 4px; }
.wizard-loading-subtext { font-size: .8125rem; color: #5a6772; }
.data-source-banner { display: none; }
.data-source-banner.visible { display: block; }
/* Classification result (BI palette) */
.classification-result { padding: 32px; border-radius: 8px; border: 2px solid #d4d8db; text-align: center; margin-bottom: 24px; }
.classification-result.essential { border-color: #06c; background: #e6f0ff; }
.classification-result.important { border-color: #e58a00; background: #fff5e6; }
.classification-result.not-applicable { border-color: #d4d8db; background: #f5f6f7; }
.classification-result.voluntary { border-color: #06c; background: #e6f0ff; }
.classification-result-icon { width: 64px; height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; }
.classification-result.essential .classification-result-icon { background: #06c; color: #fff; }
.classification-result.important .classification-result-icon { background: #e58a00; color: #fff; }
.classification-result.not-applicable .classification-result-icon { background: #b1b9c0; color: #fff; }
.classification-result.voluntary .classification-result-icon { background: #06c; color: #fff; }
.classification-result-icon svg { width: 32px; height: 32px; fill: currentColor; }
.classification-result-label { font-size: 1.5rem; font-weight: 800; margin-bottom: 8px; }
.classification-result.essential .classification-result-label { color: #06c; }
.classification-result.important .classification-result-label { color: #e58a00; }
.classification-result.not-applicable .classification-result-label { color: #5a6772; }
.classification-result.voluntary .classification-result-label { color: #06c; }
.classification-result-desc { font-size: .875rem; color: #5a6772; line-height: 1.6; max-width: 600px; margin: 0 auto; }
.voluntary-section { display: none; margin-bottom: 24px; }
.voluntary-section.visible { display: block; }
.voluntary-card { border: 2px solid #b3d1ff; border-radius: 8px; padding: 24px; display: flex; align-items: flex-start; gap: 16px; background: #fff; }
.voluntary-card.checked { border-color: #06c; background: #e6f0ff; }
.voluntary-card input[type="checkbox"] { width: 20px; height: 20px; flex-shrink: 0; margin-top: 2px; }
.voluntary-card h4 { font-size: .9375rem; font-weight: 700; color: #17324d; margin-bottom: 6px; }
.voluntary-card p { font-size: .8125rem; color: #5a6772; line-height: 1.6; margin: 0; }
.summary-item-label { font-size: .75rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: #5a6772; margin-bottom: 4px; }
.summary-item-value { font-size: .9375rem; font-weight: 500; color: #17324d; }
.summary-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.wizard-actions { display: flex; justify-content: space-between; align-items: center; padding-top: 32px; border-top: 1px solid #d4d8db; margin-top: 32px; }
.wizard-actions-right { display: flex; gap: 12px; }
.auto-filled .form-control, .auto-filled .form-select, .auto-filled select { border-color: #008758; background: #f0faf4; }
.auto-fill-badge { display: inline-flex; align-items: center; padding: 2px 8px; margin-left: 8px; font-size: .65rem; font-weight: 600; color: #008758; background: #d6f0e2; border-radius: 100px; text-transform: uppercase; vertical-align: middle; }
.ico { width: 1.1em; height: 1.1em; fill: currentColor; vertical-align: -.18em; }
@media (max-width: 768px) {
.wizard-header { padding: 12px 16px; }
.wizard-stepper { padding: 16px; overflow-x: auto; }
.stepper { min-width: 600px; }
.stepper-label { display: none; }
.wizard-body { padding: 24px 16px 40px; }
.form-grid-2, .summary-grid { grid-template-columns: 1fr; }
.wizard-step-title { font-size: 1.25rem; }
}
</style>
</head>
<body>
<div class="wizard-page">
<!-- Header -->
<div class="wizard-header">
<div class="wizard-header-logo">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/>
<path d="M10 12.5l-2-2-1.41 1.41L10 15.32l5.41-5.41L14 8.5l-4 4z"/>
</svg>
</div>
<div class="wizard-header-text">
<h1>NIS2 Agile</h1>
<span>Configurazione Iniziale</span>
</div>
</div>
<!-- Stepper -->
<div class="wizard-stepper">
<div class="stepper" id="stepper">
<div class="stepper-step active" data-step="1">
<div class="stepper-number">1</div>
<span class="stepper-label">Partita IVA</span>
</div>
<div class="stepper-connector" data-after="1"></div>
<div class="stepper-step" data-step="2">
<div class="stepper-number">2</div>
<span class="stepper-label">Dati Aziendali</span>
</div>
<div class="stepper-connector" data-after="2"></div>
<div class="stepper-step" data-step="3">
<div class="stepper-number">3</div>
<span class="stepper-label">Il Tuo Profilo</span>
</div>
<div class="stepper-connector" data-after="3"></div>
<div class="stepper-step" data-step="4">
<div class="stepper-number">4</div>
<span class="stepper-label">Classificazione</span>
</div>
</div>
</div>
<!-- Wizard Body -->
<div class="wizard-body">
<div class="wizard-content">
<!-- ═══ STEP 1: Partita IVA ═══ -->
<div class="wizard-step active" id="step-1">
<h2 class="wizard-step-title">Inserisci la Partita IVA dell'azienda</h2>
<p class="wizard-step-subtitle">Inserisci la Partita IVA dell'azienda da configurare. I dati verranno recuperati automaticamente dal registro delle imprese.</p>
<div class="card card-bg has-bkg-grey shadow" style="max-width:520px; margin:0 auto;">
<div class="card-body">
<div class="form-group">
<label class="active" for="s1-vat">Partita IVA <span aria-hidden="true">*</span></label>
<div style="display:flex; gap:.5rem; align-items:stretch;">
<span style="display:flex;align-items:center;padding:0 12px;background:#e6e9ed;border:1px solid #b1b9c0;border-radius:4px;font-weight:600;color:#5a6772;">IT</span>
<input type="text" id="s1-vat" class="form-control" placeholder="12345678901" maxlength="11"
style="flex:1;" inputmode="numeric" oninput="this.value=this.value.replace(/\D/g,'')">
</div>
<small class="form-text">11 cifre numeriche senza prefisso IT</small>
</div>
<div id="s1-result" class="d-none" role="status"></div>
<button class="btn btn-primary w-100" id="s1-fetch-btn" onclick="step1FetchCompany()">
<svg class="ico me-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-search"></use></svg>
Cerca Azienda
</button>
</div>
</div>
<div class="wizard-loading" id="s1-loading" aria-live="polite">
<div class="progress-spinner progress-spinner-active size-sm mb-2"><span class="visually-hidden">Caricamento...</span></div>
<div class="wizard-loading-text">Ricerca in corso...</div>
<div class="wizard-loading-subtext">Interrogazione registro imprese</div>
</div>
<div style="text-align:center; margin-top:1.5rem;">
<button class="btn btn-link" onclick="step1ManualEntry()">
Non trovi l'azienda? Inserisci manualmente
</button>
</div>
<div class="wizard-actions" style="margin-top:2rem;">
<div></div>
<div class="wizard-actions-right">
<button class="btn btn-primary" id="s1-continue-btn" onclick="goToStep(2)" style="display:none;">
Continua
<svg class="ico ms-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-right"></use></svg>
</button>
</div>
</div>
</div>
<!-- ═══ STEP 2: Dati Aziendali ═══ -->
<div class="wizard-step" id="step-2">
<h2 class="wizard-step-title">Verifica e Completa i Dati Aziendali</h2>
<p class="wizard-step-subtitle">Controlla i dati qui sotto e completa le informazioni mancanti.</p>
<div class="data-source-banner alert alert-success" id="data-source-banner" role="status">
<span id="data-source-text"></span>
</div>
<div class="card card-bg has-bkg-grey shadow">
<div class="card-body">
<div class="form-grid-2">
<div class="form-group form-group-full">
<label class="active" for="company-name">Ragione Sociale <span aria-hidden="true">*</span></label>
<input type="text" id="company-name" class="form-control" placeholder="Es. Acme S.r.l." required>
</div>
<div class="form-group">
<label class="active" for="vat-number">Partita IVA</label>
<input type="text" id="vat-number" class="form-control" placeholder="IT12345678901" maxlength="16">
</div>
<div class="form-group">
<label class="active" for="fiscal-code">Codice Fiscale</label>
<input type="text" id="fiscal-code" class="form-control" placeholder="Es. 12345678901" maxlength="16">
</div>
<div class="form-group form-group-full">
<label class="active" for="address">Indirizzo</label>
<input type="text" id="address" class="form-control" placeholder="Via Roma, 1">
</div>
<div class="form-group">
<label class="active" for="city">Citta'</label>
<input type="text" id="city" class="form-control" placeholder="Es. Milano">
</div>
<div class="form-group">
<label class="active" for="website">Sito Web</label>
<input type="text" id="website" class="form-control" placeholder="https://www.esempio.it">
</div>
<div class="form-group">
<label class="active" for="company-email">Email Aziendale</label>
<input type="email" id="company-email" class="form-control" placeholder="info@azienda.it">
</div>
<div class="form-group">
<label class="active" for="company-phone">Telefono Aziendale</label>
<input type="text" id="company-phone" class="form-control" placeholder="+39 02 1234567">
</div>
<div class="form-group form-group-full">
<label class="active" for="sector">Settore NIS2 <span aria-hidden="true">*</span></label>
<select id="sector" class="form-select select-pdnd" required>
<option value="">-- Seleziona settore --</option>
<optgroup label="Settori ad Alta Criticita' (Allegato I)">
<option value="energy_electricity">Energia - Elettricita'</option>
<option value="energy_district_heating">Energia - Teleriscaldamento</option>
<option value="energy_oil">Energia - Petrolio</option>
<option value="energy_gas">Energia - Gas</option>
<option value="energy_hydrogen">Energia - Idrogeno</option>
<option value="transport_air">Trasporti - Aereo</option>
<option value="transport_rail">Trasporti - Ferroviario</option>
<option value="transport_water">Trasporti - Marittimo/Fluviale</option>
<option value="transport_road">Trasporti - Stradale</option>
<option value="banking">Banche</option>
<option value="financial_markets">Infrastrutture Mercati Finanziari</option>
<option value="health">Sanita'</option>
<option value="drinking_water">Acqua Potabile</option>
<option value="waste_water">Acque Reflue</option>
<option value="digital_infrastructure">Infrastruttura Digitale</option>
<option value="ict_service_management">Gestione Servizi ICT (B2B)</option>
<option value="public_administration">Pubblica Amministrazione</option>
<option value="space">Spazio</option>
</optgroup>
<optgroup label="Altri Settori Critici (Allegato II)">
<option value="postal_courier">Servizi Postali e Corrieri</option>
<option value="waste_management">Gestione Rifiuti</option>
<option value="chemicals">Fabbricazione Prodotti Chimici</option>
<option value="food">Produzione e Distribuzione Alimentare</option>
<option value="manufacturing_medical">Fabbricazione - Dispositivi Medici</option>
<option value="manufacturing_computers">Fabbricazione - Computer/Elettronica</option>
<option value="manufacturing_electrical">Fabbricazione - Apparecchiature Elettriche</option>
<option value="manufacturing_machinery">Fabbricazione - Macchinari</option>
<option value="manufacturing_vehicles">Fabbricazione - Autoveicoli</option>
<option value="manufacturing_transport">Fabbricazione - Altri Mezzi di Trasporto</option>
<option value="digital_providers">Fornitori Servizi Digitali</option>
<option value="research">Organizzazioni di Ricerca</option>
</optgroup>
<optgroup label="Altro">
<option value="other">Altro Settore</option>
</optgroup>
</select>
</div>
<div class="form-group">
<label class="active" for="employee-count">Numero Dipendenti <span aria-hidden="true">*</span></label>
<input type="number" id="employee-count" class="form-control" placeholder="Es. 150" min="1" required>
</div>
<div class="form-group">
<label class="active" for="annual-turnover">Fatturato Annuo EUR <span aria-hidden="true">*</span></label>
<input type="number" id="annual-turnover" class="form-control" placeholder="Es. 15000000" min="0" required>
</div>
</div>
</div>
</div>
<div class="wizard-actions">
<button class="btn btn-outline-primary" onclick="goToStep(1)">
<svg class="ico me-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-left"></use></svg>
Indietro
</button>
<div class="wizard-actions-right">
<button class="btn btn-primary" onclick="submitStep3()">
Continua
<svg class="ico ms-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-right"></use></svg>
</button>
</div>
</div>
</div>
<!-- ═══ STEP 3: Il Tuo Profilo ═══ -->
<div class="wizard-step" id="step-3">
<h2 class="wizard-step-title">Completa il tuo profilo</h2>
<p class="wizard-step-subtitle">Inserisci le informazioni sul tuo ruolo in azienda.</p>
<div class="profile-form-wrapper">
<div class="card card-bg has-bkg-grey shadow">
<div class="card-body">
<div class="form-group">
<label class="active" for="full-name">Nome Completo</label>
<input type="text" id="full-name" class="form-control" placeholder="Mario Rossi">
</div>
<div class="form-group">
<label class="active" for="role">Ruolo in Azienda <span aria-hidden="true">*</span></label>
<select id="role" class="form-select select-pdnd" required>
<option value="">-- Seleziona ruolo --</option>
<option value="ceo">CEO/Amministratore Delegato</option>
<option value="cto">CTO/Direttore Tecnico</option>
<option value="ciso">CISO/Responsabile Sicurezza</option>
<option value="dpo">DPO/Data Protection Officer</option>
<option value="compliance_manager">Compliance Manager</option>
<option value="it_manager">IT Manager</option>
<option value="legal">Responsabile Legale</option>
<option value="board_member">Membro CDA</option>
<option value="consultant">Consulente Esterno</option>
<option value="other">Altro</option>
</select>
</div>
<div class="form-group mb-0">
<label class="active" for="user-phone">Telefono</label>
<input type="text" id="user-phone" class="form-control" placeholder="+39 333 1234567">
</div>
</div>
</div>
</div>
<div class="wizard-actions">
<button class="btn btn-outline-primary" onclick="goToStep(2)">
<svg class="ico me-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-left"></use></svg>
Indietro
</button>
<div class="wizard-actions-right">
<button class="btn btn-primary" onclick="submitStep4()">
Continua
<svg class="ico ms-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-right"></use></svg>
</button>
</div>
</div>
</div>
<!-- ═══ STEP 4: Classificazione NIS2 e Riepilogo ═══ -->
<div class="wizard-step" id="step-4">
<h2 class="wizard-step-title">Classificazione NIS2</h2>
<p class="wizard-step-subtitle">In base ai dati forniti, ecco la classificazione della tua organizzazione secondo la Direttiva NIS2.</p>
<div class="classification-result" id="classification-result">
<div class="classification-result-icon" id="classification-icon">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/></svg>
</div>
<div class="classification-result-label" id="classification-label"></div>
<div class="classification-result-desc" id="classification-desc"></div>
</div>
<div class="voluntary-section" id="voluntary-section">
<div class="voluntary-card" id="voluntary-card">
<input type="checkbox" id="voluntary-checkbox" onchange="onVoluntaryChange(this.checked)" aria-label="Adesione volontaria alla NIS2">
<div>
<h4>Adesione Volontaria alla NIS2</h4>
<p>
Anche se la tua organizzazione non rientra nell'ambito di applicazione obbligatorio della
Direttiva NIS2, puoi scegliere di aderire volontariamente adottando le misure di sicurezza
previste dalla normativa. Questo consente di migliorare la postura di cybersicurezza e di
prepararsi a possibili future designazioni.
</p>
</div>
</div>
</div>
<div class="card card-bg has-bkg-grey shadow mb-4">
<div class="card-header bg-white"><h3 class="h6 mb-0">Riepilogo Dati Aziendali</h3></div>
<div class="card-body">
<div class="summary-grid">
<div class="summary-item"><div class="summary-item-label">Ragione Sociale</div><div class="summary-item-value" id="sum-company">-</div></div>
<div class="summary-item"><div class="summary-item-label">Partita IVA</div><div class="summary-item-value" id="sum-vat">-</div></div>
<div class="summary-item"><div class="summary-item-label">Settore</div><div class="summary-item-value" id="sum-sector">-</div></div>
<div class="summary-item"><div class="summary-item-label">Tipologia Soggetto</div><div class="summary-item-value" id="sum-entity-type">-</div></div>
<div class="summary-item"><div class="summary-item-label">Dipendenti</div><div class="summary-item-value" id="sum-employees">-</div></div>
<div class="summary-item"><div class="summary-item-label">Fatturato Annuo</div><div class="summary-item-value" id="sum-turnover">-</div></div>
</div>
</div>
</div>
<div class="wizard-actions">
<button class="btn btn-outline-primary" onclick="goToStep(3)">
<svg class="ico me-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-arrow-left"></use></svg>
Indietro
</button>
<div class="wizard-actions-right">
<button class="btn btn-primary" id="complete-btn" onclick="completeOnboarding()">
<svg class="ico me-1" aria-hidden="true"><use href="vendor/bootstrap-italia/dist/svg/sprites.svg#it-check-circle"></use></svg>
Completa e Vai alla Dashboard
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<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>
<!-- Stessa logica della onboarding.html: ZERO modifiche backend -->
<script src="js/api.js"></script>
<script src="js/common.js"></script>
<script>
// ── Auth check ──────────────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
// ── Extend API with onboarding methods ──────────────────────────
api.uploadVisura = async function(file) {
const formData = new FormData();
formData.append('visura', file);
const response = await fetch(this.baseUrl + '/onboarding/upload-visura', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + this.token,
'X-Organization-Id': this.orgId || ''
},
body: formData
});
return response.json();
};
api.fetchCompany = function(vatNumber) {
return this.post('/onboarding/fetch-company', { vat_number: vatNumber });
};
api.completeOnboarding = function(data) {
return this.post('/onboarding/complete', data);
};
// ── Wizard State ────────────────────────────────────────────────
const wizardState = {
currentStep: 1,
method: null,
dataSource: null,
visuraFile: null,
company: {
name: '', vat_number: '', fiscal_code: '', address: '', city: '',
website: '', email: '', phone: '', sector: '', employee_count: '', annual_turnover: ''
},
profile: { full_name: '', role: '', phone: '' },
classification: null,
autoFilledFields: new Set(),
voluntaryCompliance: false
};
function parseEmployeesRange(range) {
if (!range) return '';
if (typeof range === 'number') return range;
const str = String(range).trim();
if (str.endsWith('+')) return parseInt(str);
const parts = str.split('-');
if (parts.length === 2) return parseInt(parts[0]);
return parseInt(str) || '';
}
// ── Step 1: P.IVA Fetch ─────────────────────────────────────────
async function step1FetchCompany() {
const vat = document.getElementById('s1-vat').value.trim();
if (!vat || vat.length !== 11) {
showNotification('Inserisci una Partita IVA valida (11 cifre numeriche).', 'warning');
return;
}
const btn = document.getElementById('s1-fetch-btn');
const loading = document.getElementById('s1-loading');
const resultDiv = document.getElementById('s1-result');
btn.disabled = true;
loading.classList.add('active');
resultDiv.classList.add('d-none');
try {
const result = await api.fetchCompany(vat);
if (result.success && result.data) {
const d = result.data;
wizardState.company.name = d.company_name || d.ragione_sociale || d.name || '';
wizardState.company.vat_number = vat;
wizardState.company.fiscal_code = d.fiscal_code || d.codice_fiscale || '';
wizardState.company.address = d.address || d.indirizzo || '';
wizardState.company.city = d.city || d.citta || '';
wizardState.company.website = d.website || d.sito_web || '';
wizardState.company.email = d.email || d.pec || '';
wizardState.company.phone = d.phone || d.telefono || '';
wizardState.company.sector = d.suggested_sector || d.sector || '';
wizardState.company.employee_count = parseEmployeesRange(d.employees_range) || d.employee_count || d.employees || '';
wizardState.company.annual_turnover = '';
wizardState.dataSource = 'Dati precompilati da CertiSource';
wizardState.autoFilledFields.clear();
if (wizardState.company.name) wizardState.autoFilledFields.add('company-name');
if (wizardState.company.vat_number) wizardState.autoFilledFields.add('vat-number');
if (wizardState.company.fiscal_code) wizardState.autoFilledFields.add('fiscal-code');
if (wizardState.company.address) wizardState.autoFilledFields.add('address');
if (wizardState.company.city) wizardState.autoFilledFields.add('city');
if (wizardState.company.email) wizardState.autoFilledFields.add('company-email');
if (wizardState.company.phone) wizardState.autoFilledFields.add('company-phone');
if (wizardState.company.sector) wizardState.autoFilledFields.add('sector');
if (wizardState.company.employee_count) wizardState.autoFilledFields.add('employee-count');
resultDiv.className = 'alert alert-success';
resultDiv.innerHTML = '<strong>✓ ' + (wizardState.company.name || 'Azienda trovata') + '</strong><br><small>' + (d.address || d.city || '') + '</small>';
document.getElementById('s1-continue-btn').style.display = 'inline-flex';
showNotification('Azienda trovata: ' + (wizardState.company.name || vat), 'success');
} else {
resultDiv.className = 'alert alert-warning';
resultDiv.textContent = 'Azienda non trovata. Puoi inserire i dati manualmente.';
step1ManualEntry(vat);
}
} catch (err) {
showNotification('Errore di connessione al server.', 'error');
} finally {
btn.disabled = false;
loading.classList.remove('active');
}
}
function step1ManualEntry(vat) {
wizardState.company.vat_number = vat || document.getElementById('s1-vat').value.trim();
wizardState.dataSource = null;
wizardState.autoFilledFields.clear();
document.getElementById('s1-continue-btn').style.display = 'inline-flex';
showNotification('Inserisci i dati aziendali manualmente.', 'info');
}
// ── Step navigation ─────────────────────────────────────────────
function goToStep(step) {
wizardState.currentStep = step;
document.querySelectorAll('.wizard-step').forEach(s => s.classList.remove('active'));
const targetStep = document.getElementById('step-' + step);
if (targetStep) targetStep.classList.add('active');
if (step === 2) populateCompanyForm();
if (step === 3) loadUserProfile();
if (step === 4) runClassificationAndSummary();
updateStepper(step);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function updateStepper(currentStep) {
document.querySelectorAll('.stepper-step').forEach(el => {
const s = parseInt(el.dataset.step);
el.classList.remove('active', 'completed');
const numEl = el.querySelector('.stepper-number');
if (s < currentStep) {
el.classList.add('completed');
numEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>';
} else if (s === currentStep) {
el.classList.add('active');
numEl.textContent = s;
} else {
numEl.textContent = s;
}
});
document.querySelectorAll('.stepper-connector').forEach(el => {
const afterStep = parseInt(el.dataset.after);
el.classList.toggle('completed', afterStep < currentStep);
});
}
// ── Visura (legacy, kept for API completeness) ──
function handleVisuraFile(file) {
if (file.type !== 'application/pdf') { showNotification('Il file deve essere in formato PDF.', 'warning'); return; }
if (file.size > 10 * 1024 * 1024) { showNotification('Il file non deve superare i 10 MB.', 'warning'); return; }
wizardState.visuraFile = file;
}
async function analyzeVisura() {
if (!wizardState.visuraFile) return;
try {
const result = await api.uploadVisura(wizardState.visuraFile);
if (result.success && result.data) {
const d = result.data;
wizardState.company.name = d.company_name || d.ragione_sociale || d.name || '';
wizardState.company.vat_number = d.vat_number || d.partita_iva || '';
wizardState.company.fiscal_code = d.fiscal_code || d.codice_fiscale || '';
wizardState.company.address = d.address || d.indirizzo || '';
wizardState.company.city = d.city || d.citta || '';
wizardState.company.website = d.website || d.sito_web || '';
wizardState.company.email = d.email || d.pec || '';
wizardState.company.phone = d.phone || d.telefono || '';
wizardState.company.sector = d.suggested_sector || d.sector || '';
wizardState.company.employee_count = parseEmployeesRange(d.employees_range) || d.employee_count || d.employees || '';
wizardState.company.annual_turnover = '';
wizardState.dataSource = 'Dati precompilati dalla visura camerale';
showNotification('Visura analizzata con successo!', 'success');
setTimeout(() => goToStep(2), 600);
} else {
showNotification(result.message || 'Errore nell\'analisi della visura.', 'error');
}
} catch (err) {
showNotification('Errore di connessione al server.', 'error');
}
}
async function fetchCompany() {
const vatInput = document.getElementById('cs-vat');
if (!vatInput) return;
const vat = vatInput.value.trim();
if (!vat) { showNotification('Inserisci la Partita IVA.', 'warning'); return; }
try {
const result = await api.fetchCompany(vat);
if (result.success && result.data) {
const d = result.data;
wizardState.company.name = d.company_name || d.ragione_sociale || d.name || '';
wizardState.company.vat_number = d.vat_number || d.partita_iva || vat;
wizardState.company.sector = d.suggested_sector || d.sector || '';
wizardState.dataSource = 'Dati recuperati da CertiSource';
showNotification('Dati aziendali recuperati con successo!', 'success');
setTimeout(() => goToStep(2), 600);
} else {
showNotification(result.message || 'Impossibile recuperare i dati. Verifica la Partita IVA.', 'error');
}
} catch (err) {
showNotification('Errore di connessione al server.', 'error');
}
}
// ── Step 2: Company Form ────────────────────────────────────────
function populateCompanyForm() {
const c = wizardState.company;
document.getElementById('company-name').value = c.name || '';
document.getElementById('vat-number').value = c.vat_number || '';
document.getElementById('fiscal-code').value = c.fiscal_code || '';
document.getElementById('address').value = c.address || '';
document.getElementById('city').value = c.city || '';
document.getElementById('website').value = c.website || '';
document.getElementById('company-email').value = c.email || '';
document.getElementById('company-phone').value = c.phone || '';
document.getElementById('employee-count').value = c.employee_count || '';
document.getElementById('annual-turnover').value = c.annual_turnover || '';
if (c.sector) {
const sectorSelect = document.getElementById('sector');
if (sectorSelect.querySelector('option[value="' + c.sector + '"]')) sectorSelect.value = c.sector;
}
const banner = document.getElementById('data-source-banner');
const bannerText = document.getElementById('data-source-text');
if (wizardState.dataSource) {
bannerText.textContent = wizardState.dataSource;
banner.classList.add('visible');
} else {
banner.classList.remove('visible');
}
const autoFields = wizardState.autoFilledFields;
document.querySelectorAll('#step-2 .form-control, #step-2 .form-select').forEach(el => {
const wrapper = el.closest('.form-group');
if (!wrapper) return;
wrapper.classList.remove('auto-filled');
const existing = wrapper.querySelector('.auto-fill-badge');
if (existing) existing.remove();
if (autoFields.has(el.id)) {
wrapper.classList.add('auto-filled');
const badge = document.createElement('span');
badge.className = 'auto-fill-badge';
badge.textContent = wizardState.dataSource ? 'auto-compilato' : 'da visura';
const label = wrapper.querySelector('label');
if (label) label.appendChild(badge);
}
});
}
function submitStep3() {
const name = document.getElementById('company-name').value.trim();
const sector = document.getElementById('sector').value;
const employees = document.getElementById('employee-count').value;
const turnover = document.getElementById('annual-turnover').value;
if (!name) { showNotification('Inserisci la ragione sociale.', 'warning'); document.getElementById('company-name').focus(); return; }
if (!sector) { showNotification('Seleziona un settore NIS2.', 'warning'); document.getElementById('sector').focus(); return; }
if (!employees || parseInt(employees) <= 0) { showNotification('Inserisci il numero di dipendenti.', 'warning'); document.getElementById('employee-count').focus(); return; }
if (!turnover || parseInt(turnover) < 0) { showNotification('Inserisci il fatturato annuo.', 'warning'); document.getElementById('annual-turnover').focus(); return; }
wizardState.company.name = name;
wizardState.company.vat_number = document.getElementById('vat-number').value.trim();
wizardState.company.fiscal_code = document.getElementById('fiscal-code').value.trim();
wizardState.company.address = document.getElementById('address').value.trim();
wizardState.company.city = document.getElementById('city').value.trim();
wizardState.company.website = document.getElementById('website').value.trim();
wizardState.company.email = document.getElementById('company-email').value.trim();
wizardState.company.phone = document.getElementById('company-phone').value.trim();
wizardState.company.sector = sector;
wizardState.company.employee_count = parseInt(employees);
wizardState.company.annual_turnover = parseInt(turnover);
goToStep(3);
}
// ── Step 3: User Profile ────────────────────────────────────────
async function loadUserProfile() {
try {
const result = await api.getMe();
if (result.success && result.data) {
const nameInput = document.getElementById('full-name');
if (!nameInput.value && result.data.full_name) {
nameInput.value = result.data.full_name;
wizardState.profile.full_name = result.data.full_name;
}
}
} catch (e) { /* ignore */ }
}
function submitStep4() {
const role = document.getElementById('role').value;
if (!role) { showNotification('Seleziona il tuo ruolo in azienda.', 'warning'); document.getElementById('role').focus(); return; }
wizardState.profile.full_name = document.getElementById('full-name').value.trim();
wizardState.profile.role = role;
wizardState.profile.phone = document.getElementById('user-phone').value.trim();
goToStep(4);
}
// ── Step 4: Classification & Summary ────────────────────────────
function classifyLocally(sector, employees, turnover) {
const annexI = [
'energy_electricity', 'energy_district_heating', 'energy_oil', 'energy_gas',
'energy_hydrogen', 'transport_air', 'transport_rail', 'transport_water',
'transport_road', 'banking', 'financial_markets', 'health', 'drinking_water',
'waste_water', 'digital_infrastructure', 'ict_service_management',
'public_administration', 'space'
];
const annexII = [
'postal_courier', 'waste_management', 'chemicals', 'food',
'manufacturing_medical', 'manufacturing_computers', 'manufacturing_electrical',
'manufacturing_machinery', 'manufacturing_vehicles', 'manufacturing_transport',
'digital_providers', 'research'
];
const isAnnexI = annexI.includes(sector);
const isAnnexII = annexII.includes(sector);
const isLarge = employees >= 250 || turnover >= 50000000;
const isMedium = employees >= 50 || turnover >= 10000000;
if (isAnnexI && isLarge) {
return { classification: 'essential', label: 'Soggetto Essenziale',
description: 'La vostra organizzazione rientra tra i soggetti essenziali ai sensi della Direttiva NIS2, in quanto opera in un settore ad alta criticita\' (Allegato I) e supera le soglie dimensionali per la classificazione come grande impresa. Siete tenuti al rispetto completo degli obblighi di sicurezza con vigilanza ex ante.' };
} else if ((isAnnexI || isAnnexII) && isMedium) {
return { classification: 'important', label: 'Soggetto Importante',
description: 'La vostra organizzazione rientra tra i soggetti importanti ai sensi della Direttiva NIS2. Siete tenuti a rispettare gli obblighi di sicurezza e di notifica degli incidenti, con un regime di vigilanza ex post.' };
} else if (sector === 'other' || (!isAnnexI && !isAnnexII)) {
return { classification: 'not_applicable', label: 'Non Applicabile',
description: 'In base ai dati forniti, la vostra organizzazione non sembra rientrare nell\'ambito di applicazione della Direttiva NIS2. Consigliamo comunque di adottare misure di cybersicurezza adeguate e di verificare con le autorita\' competenti.' };
} else {
return { classification: 'not_applicable', label: 'Sotto le Soglie',
description: 'La vostra organizzazione opera in un settore coperto dalla NIS2 ma non raggiunge le soglie dimensionali minime (50 dipendenti o 10M EUR di fatturato). Potreste comunque essere designati dalle autorita\' nazionali.' };
}
}
function runClassificationAndSummary() {
const c = wizardState.company;
const sector = c.sector;
const employees = parseInt(c.employee_count) || 0;
const turnover = parseInt(c.annual_turnover) || 0;
const result = classifyLocally(sector, employees, turnover);
wizardState.classification = result;
wizardState.voluntaryCompliance = false;
const voluntaryCheckbox = document.getElementById('voluntary-checkbox');
if (voluntaryCheckbox) voluntaryCheckbox.checked = false;
const voluntaryCard = document.getElementById('voluntary-card');
if (voluntaryCard) voluntaryCard.classList.remove('checked');
const voluntarySection = document.getElementById('voluntary-section');
if (result.classification === 'not_applicable') voluntarySection.classList.add('visible');
else voluntarySection.classList.remove('visible');
const container = document.getElementById('classification-result');
const labelEl = document.getElementById('classification-label');
const descEl = document.getElementById('classification-desc');
const iconEl = document.getElementById('classification-icon');
container.className = 'classification-result';
if (result.classification === 'essential') {
container.classList.add('essential');
iconEl.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/><path d="M10 12.5l-2-2-1.41 1.41L10 15.32l5.41-5.41L14 8.5l-4 4z"/></svg>';
} else if (result.classification === 'important') {
container.classList.add('important');
iconEl.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/></svg>';
} else {
container.classList.add('not-applicable');
iconEl.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>';
}
labelEl.textContent = result.label;
descEl.textContent = result.description;
const sectorSelect = document.getElementById('sector');
const sectorOption = sectorSelect.querySelector('option[value="' + c.sector + '"]');
const sectorText = sectorOption ? sectorOption.textContent : c.sector;
document.getElementById('sum-company').textContent = c.name || '-';
document.getElementById('sum-vat').textContent = c.vat_number || '-';
document.getElementById('sum-sector').textContent = sectorText || '-';
document.getElementById('sum-entity-type').textContent = result.label || '-';
document.getElementById('sum-employees').textContent = employees ? employees.toLocaleString('it-IT') : '-';
document.getElementById('sum-turnover').textContent = turnover ? '€ ' + turnover.toLocaleString('it-IT') : '-';
}
function onVoluntaryChange(checked) {
wizardState.voluntaryCompliance = checked;
const card = document.getElementById('voluntary-card');
const container = document.getElementById('classification-result');
const labelEl = document.getElementById('classification-label');
const sumEntityType = document.getElementById('sum-entity-type');
card.classList.toggle('checked', checked);
if (checked) {
container.className = 'classification-result voluntary';
container.querySelector('.classification-result-icon').innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 2.18l7 3.12v4.7c0 4.83-3.23 9.36-7 10.57-3.77-1.21-7-5.74-7-10.57V6.3l7-3.12z"/><path d="M10 12.5l-2-2-1.41 1.41L10 15.32l5.41-5.41L14 8.5l-4 4z"/></svg>';
labelEl.textContent = 'Adesione Volontaria';
document.getElementById('classification-desc').textContent = 'La tua organizzazione aderisce volontariamente alla Direttiva NIS2, adottando le misure di sicurezza previste dalla normativa per migliorare la propria postura di cybersicurezza.';
sumEntityType.textContent = 'Adesione Volontaria';
} else {
container.className = 'classification-result not-applicable';
container.querySelector('.classification-result-icon').innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>';
labelEl.textContent = wizardState.classification.label;
document.getElementById('classification-desc').textContent = wizardState.classification.description;
sumEntityType.textContent = wizardState.classification.label;
}
}
// ── Complete Onboarding ─────────────────────────────────────────
async function completeOnboarding() {
const btn = document.getElementById('complete-btn');
btn.disabled = true;
btn.textContent = 'Completamento in corso...';
const c = wizardState.company;
const p = wizardState.profile;
const payload = {
name: c.name, vat_number: c.vat_number, fiscal_code: c.fiscal_code,
address: c.address, city: c.city, website: c.website,
contact_email: c.email, contact_phone: c.phone, sector: c.sector,
employee_count: parseInt(c.employee_count) || 0,
annual_turnover_eur: parseInt(c.annual_turnover) || 0,
voluntary_compliance: wizardState.voluntaryCompliance ? 1 : 0,
full_name: p.full_name, phone: p.phone, country: 'IT',
};
try {
const result = await api.completeOnboarding(payload);
if (result.success && result.data) {
const orgId = result.data.id || result.data.organization_id;
if (orgId) api.setOrganization(orgId);
showNotification('Configurazione completata con successo!', 'success');
setTimeout(() => {
const dest = api.isConsultant() ? 'companies.html' : 'dashboard.html';
window.location.href = dest;
}, 1000);
} else {
showNotification(result.message || 'Errore nel completamento.', 'error');
btn.disabled = false;
btn.textContent = 'Completa e Vai alla Dashboard';
}
} catch (err) {
showNotification('Errore di connessione al server.', 'error');
btn.disabled = false;
btn.textContent = 'Completa e Vai alla Dashboard';
}
}
// ── Allow P.IVA input to submit with Enter ───────────────────────
document.getElementById('s1-vat').addEventListener('keypress', (e) => {
if (e.key === 'Enter') { e.preventDefault(); step1FetchCompany(); }
});
</script>
</body>
</html>