Secondo modulo del modello relazionale A4 (docs/DESIGN_A4_RELATIONAL.md). Backend: - Migration 042 (docs/sql/042_competences.sql) + runner aggiornato (scripts/migrate-a4.php). 4 tabelle additive/idempotenti: skills (org-owned o globali), role_skills (competenze richieste dal ruolo, UNIQUE role+skill), user_skills (possedute, UNIQUE org+user+skill), skill_course_map. APPLICATA su prod (nis2-db v8.0.45, TLSv1.3). - CompetenceController (route 'competences'): catalog, skills CRUD, roleSkills, userSkills, skillCourses (map/unmap), gapGrid (richiesto−posseduto per ruolo con titolare), openAction. Multi-tenancy + anti-IDOR + livelli 1-5; competenze globali in sola lettura per gli utenti org. - alert→azione: openAction RIUSA il workflow NCR/CAPA reale (la tabella azioni è capa_actions figlia di non_conformities, NON 'corrective_actions' che non esiste): crea NCR (source='management_review', source_entity_type='competence_gap', source_entity_id=role_skills.id) + capa_actions figlia. Anti-duplicato sul gap. Zero ALTER alle tabelle esistenti. Ancoraggio PR.AT-01/02, GV.RR-04, art.24 D.Lgs. Frontend: - competenze.html (4 schede: Catalogo, Requisiti per ruolo, Competenze persone, Gap & azioni) + js/competenze.js. Bootstrap Italia V2. CTA "Assegna corso" (riuso /training/assign) e "Apri non conformità". - Voce sidebar "Competenze" (common.js + common-bi.js + BI_PAGES) + nav.competences i18n IT/EN. api.js: metodi comp* + assignTraining. Help/KB/PWA: - help.js: guida contestuale 'competences' (schede, calcolo gap, PR.AT-01/02, GV.RR-04, art.24 D.Lgs., disclaimer no-parere-legale) + mappa pagina. - sw.js: nome cache nis2-shell-v1.17.0 (allineamento PWA). - Design doc corretto (rettifica capa_actions vs corrective_actions) + avanzamento 4.1/4.2. Cache-buster: ?v=20260618 dei 5 JS condivisi su 32 HTML. version.json 1.16.0 -> 1.17.0. Smoke E2E su prod (fpm reale): catalogo, requisito, competenza, gap=2, openAction (NCR+CAPA), anti-dup 409, mappatura corso — tutti verdi; dati di test ripuliti. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1007 lines
68 KiB
HTML
1007 lines
68 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<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>
|
|
<!-- 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>
|
|
<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 class="form-group">
|
|
<label class="active" for="total-balance">Totale di bilancio EUR</label>
|
|
<input type="number" id="total-balance" class="form-control" placeholder="Es. 12000000 (facoltativo)" min="0">
|
|
<small class="form-text text-muted">Terzo criterio dimensionale (Racc. 2003/361/CE): grande impresa se > 43M€ di totale di bilancio.</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="active" for="is-autonomous">Struttura dell'impresa</label>
|
|
<select id="is-autonomous" class="form-select select-pdnd">
|
|
<option value="1">Impresa autonoma</option>
|
|
<option value="0">Parte di un gruppo (imprese collegate/associate)</option>
|
|
</select>
|
|
<small class="form-text text-muted">Se parte di un gruppo, le soglie vanno calcolate in modo consolidato (art. 6 Racc. 2003/361/CE).</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="active" for="size-independent-category">Categoria speciale NIS2 (a prescindere dalla dimensione)</label>
|
|
<select id="size-independent-category" class="form-select select-pdnd">
|
|
<option value="none">Nessuna delle seguenti</option>
|
|
<option value="dns">Fornitore di servizi DNS</option>
|
|
<option value="tld">Gestore di registri TLD</option>
|
|
<option value="ixp">Fornitore IXP (Internet Exchange Point)</option>
|
|
<option value="trust_qualified">Prestatore di servizi fiduciari qualificati</option>
|
|
<option value="trust_unqualified">Prestatore di servizi fiduciari non qualificati</option>
|
|
<option value="ecomm_public_network">Fornitore di reti pubbliche di comunicazione elettronica</option>
|
|
<option value="ecomm_public_service">Fornitore di servizi di comunicazione elettronica accessibili al pubblico</option>
|
|
<option value="domain_registrar">Fornitore di servizi di registrazione nomi di dominio</option>
|
|
<option value="pa_central">Pubblica Amministrazione centrale</option>
|
|
<option value="pa_regional_local">Pubblica Amministrazione regionale/locale</option>
|
|
</select>
|
|
<small class="form-text text-muted">Questi soggetti rientrano nell'ambito a prescindere dalle soglie (art. 3, comma 5, D.Lgs. 138/2024).</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="active" for="mission">Mission / contesto organizzativo</label>
|
|
<textarea id="mission" class="form-control" rows="2" placeholder="Obiettivi, servizi critici e dipendenze principali (facoltativo)"></textarea>
|
|
<small class="form-text text-muted">Alimenta il contesto organizzativo e il registro dei sistemi rilevanti (misura GV.OC).</small>
|
|
</div>
|
|
<div class="form-group mb-0">
|
|
<div class="form-check">
|
|
<input type="checkbox" id="has-code-of-ethics" class="form-check-input">
|
|
<label class="form-check-label" for="has-code-of-ethics">L'organizzazione ha adottato un codice etico</label>
|
|
</div>
|
|
<small class="form-text text-muted">Facoltativo — buona prassi di governance, non richiesto dalla NIS2.</small>
|
|
</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>
|
|
|
|
<ul class="mb-4" id="classification-obligations" style="display:none"></ul>
|
|
|
|
<div class="card card-bg has-bkg-grey shadow mb-4" id="subthreshold-block">
|
|
<div class="card-header bg-white"><h3 class="h6 mb-0">Sei sotto le soglie dimensionali? — criteri qualitativi (art. 3, comma 9, D.Lgs. 138/2024)</h3></div>
|
|
<div class="card-body">
|
|
<p class="small text-muted mb-3">Compila solo se la tua organizzazione <strong>non supera</strong> le soglie dimensionali: anche un soggetto sotto-soglia può essere individuato dall'Autorità (ACN) se ricorre uno di questi criteri.</p>
|
|
<div class="form-check"><input type="checkbox" id="crit-sole" class="form-check-input"><label class="form-check-label" for="crit-sole">Siamo l'<strong>unico fornitore nazionale</strong> di un servizio essenziale per attività sociali o economiche fondamentali <em>(lett. b)</em></label></div>
|
|
<div class="form-check"><input type="checkbox" id="crit-impact" class="form-check-input"><label class="form-check-label" for="crit-impact">Un'interruzione del nostro servizio avrebbe <strong>impatto significativo su sicurezza, incolumità o salute pubblica</strong> <em>(lett. c)</em></label></div>
|
|
<div class="form-check"><input type="checkbox" id="crit-systemic" class="form-check-input"><label class="form-check-label" for="crit-systemic">Un'interruzione comporterebbe un <strong>rischio sistemico significativo</strong>, anche transfrontaliero <em>(lett. d)</em></label></div>
|
|
<button class="btn btn-sm btn-outline-primary mt-3" type="button" onclick="runClassificationAndSummary()">Ricalcola classificazione</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-info" id="classification-disclaimer" role="alert" style="font-size:.85rem;display:none"></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?v=20260618"></script>
|
|
<script src="js/common.js?v=20260618"></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: '',
|
|
total_balance: '', is_autonomous: 1, size_independent_category: 'none',
|
|
crit_sole_supplier: 0, crit_public_impact: 0, crit_systemic_risk: 0,
|
|
mission: '', has_code_of_ethics: 0
|
|
},
|
|
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);
|
|
wizardState.company.total_balance = parseInt(document.getElementById('total-balance').value) || 0;
|
|
wizardState.company.is_autonomous = parseInt(document.getElementById('is-autonomous').value);
|
|
wizardState.company.size_independent_category = document.getElementById('size-independent-category').value;
|
|
wizardState.company.mission = document.getElementById('mission').value.trim();
|
|
wizardState.company.has_code_of_ethics = document.getElementById('has-code-of-ethics').checked ? 1 : 0;
|
|
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.' };
|
|
}
|
|
}
|
|
|
|
async function runClassificationAndSummary() {
|
|
const c = wizardState.company;
|
|
const employees = parseInt(c.employee_count) || 0;
|
|
const turnover = parseInt(c.annual_turnover) || 0;
|
|
// criteri qualitativi sotto-soglia (art. 3 c. 9) letti dai checkbox dello step 4
|
|
c.crit_sole_supplier = (document.getElementById('crit-sole') || {}).checked ? 1 : 0;
|
|
c.crit_public_impact = (document.getElementById('crit-impact') || {}).checked ? 1 : 0;
|
|
c.crit_systemic_risk = (document.getElementById('crit-systemic') || {}).checked ? 1 : 0;
|
|
|
|
// Classificazione v2 lato server (single source of truth: Nis2ClassificationService)
|
|
let result = null;
|
|
try {
|
|
const resp = await api.post('/organizations/classify', {
|
|
sector: c.sector, employee_count: employees, annual_turnover_eur: turnover,
|
|
total_balance_eur: parseInt(c.total_balance) || 0,
|
|
is_autonomous: c.is_autonomous, size_independent_category: c.size_independent_category,
|
|
crit_sole_supplier: c.crit_sole_supplier, crit_public_impact: c.crit_public_impact, crit_systemic_risk: c.crit_systemic_risk
|
|
});
|
|
if (resp && resp.success && resp.data) {
|
|
const d = resp.data;
|
|
result = { classification: d.entity_type, label: d.label, description: d.explanation,
|
|
designation_basis: d.designation_basis, sub_threshold_candidate: !!d.sub_threshold_candidate,
|
|
disclaimer: d.disclaimer || '', obligations: d.obligations || [] };
|
|
}
|
|
} catch (e) { /* fallback locale sotto */ }
|
|
if (!result) {
|
|
const r = classifyLocally(c.sector, employees, turnover);
|
|
result = { classification: r.classification, label: r.label, description: r.description, obligations: [], disclaimer: '', sub_threshold_candidate: false };
|
|
}
|
|
wizardState.classification = result;
|
|
|
|
// Adesione volontaria: visibile solo per soggetti non in ambito
|
|
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>';
|
|
}
|
|
|
|
// Etichetta + badge "autovalutazione preliminare"
|
|
labelEl.textContent = (result.label || '') + ' — autovalutazione preliminare';
|
|
descEl.textContent = result.description || '';
|
|
|
|
// Obblighi (dal server, citano il D.Lgs. 138/2024)
|
|
const obEl = document.getElementById('classification-obligations');
|
|
if (obEl) {
|
|
if (result.obligations && result.obligations.length) {
|
|
obEl.innerHTML = result.obligations.map(o => '<li>' + String(o).replace(/[<>]/g, '') + '</li>').join('');
|
|
obEl.style.display = '';
|
|
} else { obEl.style.display = 'none'; obEl.innerHTML = ''; }
|
|
}
|
|
// Disclaimer ACN (non vincolante)
|
|
const disEl = document.getElementById('classification-disclaimer');
|
|
if (disEl) {
|
|
if (result.disclaimer) { disEl.textContent = result.disclaimer; disEl.style.display = ''; }
|
|
else { disEl.style.display = 'none'; }
|
|
}
|
|
|
|
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,
|
|
total_balance_eur: parseInt(c.total_balance) || 0,
|
|
is_autonomous: c.is_autonomous,
|
|
size_independent_category: c.size_independent_category,
|
|
crit_sole_supplier: c.crit_sole_supplier,
|
|
crit_public_impact: c.crit_public_impact,
|
|
crit_systemic_risk: c.crit_systemic_risk,
|
|
mission: c.mission,
|
|
has_code_of_ethics: c.has_code_of_ethics,
|
|
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>
|