[UX] Standardizzazione login/register/onboarding + Test Runner v2

login.html: eye toggle, forgot password, auth-terms footer
register.html: wizard 3-step, 5 ruoli NIS2, invite_token URL, P.IVA lookup
onboarding.html: Font Awesome, brand color cyan (#06B6D4)
test-runner.php: L1-L5 test levels, SIM-06 B2B, tab Coverage/Stats,
  DB row counts, run history (localStorage), 5 tabs totali

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile 2026-03-07 17:11:25 +01:00
parent 47a7a25d35
commit e4e7d94043
4 changed files with 1182 additions and 314 deletions

View File

@ -5,6 +5,28 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accedi - NIS2 Agile</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
.pw-wrap { position: relative; }
.pw-wrap .form-input { padding-right: 42px; }
.pw-toggle {
position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
background: none; border: none; cursor: pointer;
color: #9CA3AF; font-size: 15px; padding: 0; transition: color .2s;
}
.pw-toggle:hover { color: var(--color-primary, #2563eb); }
.forgot-link {
display: block; text-align: center; margin-top: 10px;
font-size: .78rem; color: #6B7280; text-decoration: none; transition: color .2s;
}
.forgot-link:hover { color: var(--color-primary, #2563eb); }
.auth-terms {
margin-top: 14px; padding-top: 14px;
border-top: 1px solid var(--border-color, #e5e7eb);
text-align: center; font-size: .72rem; color: #9CA3AF; line-height: 1.8;
}
.auth-terms a { color: #6B7280; text-decoration: underline; }
</style>
</head>
<body>
<div class="auth-page">
@ -34,18 +56,32 @@
<div class="form-group">
<label class="form-label" for="password">Password</label>
<input type="password" id="password" name="password" class="form-input"
placeholder="La tua password" autocomplete="current-password" required>
<div class="pw-wrap">
<input type="password" id="password" name="password" class="form-input"
placeholder="La tua password" autocomplete="current-password" required>
<button type="button" class="pw-toggle" id="pw-toggle" tabindex="-1"
onclick="(function(){var i=document.getElementById('password'),b=document.getElementById('pw-toggle');if(i.type==='password'){i.type='text';b.innerHTML='<i class=\'fas fa-eye-slash\'></i>';}else{i.type='password';b.innerHTML='<i class=\'fas fa-eye\'></i>';}})()" aria-label="Mostra/nascondi password">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<button type="submit" class="btn btn-primary btn-lg w-full" id="login-btn">
Accedi
</button>
<a href="#" class="forgot-link" onclick="alert('Contatta presidenza@agile.software per il reset password.');return false;">
Password dimenticata?
</a>
</form>
</div>
<div class="auth-footer">
<p>Non hai un account? <a href="register.html">Registrati</a></p>
<div class="auth-terms">
Accedendo accetti i nostri
<a href="https://agentai.agile.software/terms" target="_blank">Termini di Servizio</a>
e la <a href="https://agentai.agile.software/privacy" target="_blank">Privacy Policy</a>
</div>
</div>
</div>
</div>

View File

@ -5,7 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Onboarding - NIS2 Agile</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
/* NIS2 brand color override for wizard */
:root { --nis2-cyan: #06B6D4; --nis2-cyan-light: #ecfeff; --nis2-cyan-ring: rgba(6,182,212,.15); }
/* ── Wizard Layout ──────────────────────────────────────────── */
.wizard-page {
min-height: 100vh;
@ -100,23 +103,23 @@
}
.stepper-step.active .stepper-number {
background: var(--primary);
background: var(--nis2-cyan);
color: #fff;
box-shadow: 0 0 0 4px rgba(26, 115, 232, 0.15);
box-shadow: 0 0 0 4px var(--nis2-cyan-ring);
}
.stepper-step.active .stepper-label {
color: var(--primary);
color: var(--nis2-cyan);
font-weight: 600;
}
.stepper-step.completed .stepper-number {
background: var(--secondary);
background: var(--nis2-cyan);
color: #fff;
}
.stepper-step.completed .stepper-label {
color: var(--secondary);
color: var(--nis2-cyan);
}
.stepper-connector {
@ -129,7 +132,7 @@
}
.stepper-connector.completed {
background: var(--secondary);
background: var(--nis2-cyan);
}
/* ── Wizard Body ────────────────────────────────────────────── */
@ -201,9 +204,9 @@
}
.option-card.selected {
border-color: var(--primary);
background: var(--primary-bg);
box-shadow: 0 0 0 4px rgba(26, 115, 232, 0.12);
border-color: var(--nis2-cyan);
background: var(--nis2-cyan-light);
box-shadow: 0 0 0 4px var(--nis2-cyan-ring);
}
.option-card-icon {
@ -220,7 +223,7 @@
}
.option-card.selected .option-card-icon {
background: var(--primary);
background: var(--nis2-cyan);
color: #fff;
}

View File

@ -5,27 +5,136 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Registrazione - NIS2 Agile</title>
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
.profile-choice { display: flex; gap: 1rem; margin-bottom: 1.5rem; }
.profile-card {
flex: 1; border: 2px solid var(--border-color, #e5e7eb); border-radius: 12px;
padding: 1.25rem 1rem; text-align: center; cursor: pointer;
transition: all .2s; background: var(--bg-card, #fff);
/* Step indicator */
.step-indicator {
display: flex; align-items: center; justify-content: center;
gap: 0; margin-bottom: 1.75rem;
}
.profile-card:hover { border-color: var(--color-primary, #2563eb); background: #eff6ff; }
.profile-card.selected {
border-color: var(--color-primary, #2563eb); background: #eff6ff;
box-shadow: 0 0 0 3px rgba(37,99,235,.15);
.step-item {
display: flex; flex-direction: column; align-items: center;
position: relative;
}
.profile-card-icon { font-size: 2rem; margin-bottom: .5rem; }
.profile-card-title { font-weight: 700; font-size: .95rem; color: var(--text-primary, #111); margin-bottom: .25rem; }
.profile-card-desc { font-size: .75rem; color: var(--text-secondary, #6b7280); line-height: 1.4; }
.step-indicator { display: flex; align-items: center; justify-content: center; gap: .5rem; margin-bottom: 1.5rem; }
.step-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--border-color, #e5e7eb); transition: background .2s; }
.step-dot.active { background: var(--color-primary, #2563eb); }
.step-dot.done { background: var(--color-success, #10b981); }
.step-circle {
width: 32px; height: 32px; border-radius: 50%;
border: 2px solid var(--border-color, #e5e7eb);
display: flex; align-items: center; justify-content: center;
font-size: .8rem; font-weight: 700;
color: var(--text-secondary, #6b7280);
background: var(--bg-card, #fff);
transition: all .25s;
}
.step-item.active .step-circle {
border-color: var(--color-primary, #2563eb);
color: var(--color-primary, #2563eb);
background: #eff6ff;
}
.step-item.done .step-circle {
border-color: var(--color-success, #10b981);
background: var(--color-success, #10b981);
color: #fff;
}
.step-label {
font-size: .68rem; color: var(--text-secondary, #6b7280);
margin-top: 4px; white-space: nowrap;
}
.step-item.active .step-label { color: var(--color-primary, #2563eb); font-weight: 600; }
.step-connector {
width: 48px; height: 2px;
background: var(--border-color, #e5e7eb);
margin-bottom: 18px; transition: background .25s;
}
.step-connector.done { background: var(--color-success, #10b981); }
/* Role cards */
.role-grid {
display: grid; grid-template-columns: 1fr 1fr;
gap: .75rem; margin-bottom: 1.25rem;
}
.role-card {
border: 2px solid var(--border-color, #e5e7eb);
border-radius: 12px; padding: 1rem .875rem;
cursor: pointer; transition: all .2s;
background: var(--bg-card, #fff);
text-align: left;
}
.role-card:hover { border-color: #06B6D4; background: #ecfeff; }
.role-card.selected {
border-color: #06B6D4; background: #ecfeff;
box-shadow: 0 0 0 3px rgba(6,182,212,.15);
}
.role-card-icon {
width: 36px; height: 36px; border-radius: 8px;
background: rgba(6,182,212,.12); color: #06B6D4;
display: flex; align-items: center; justify-content: center;
font-size: 1rem; margin-bottom: .6rem;
}
.role-card.selected .role-card-icon { background: rgba(6,182,212,.2); }
.role-card-title { font-weight: 700; font-size: .88rem; color: var(--text-primary, #111); margin-bottom: .2rem; }
.role-card-desc { font-size: .72rem; color: var(--text-secondary, #6b7280); line-height: 1.45; }
/* Invite code section */
.invite-section {
border: 1px dashed var(--border-color, #e5e7eb);
border-radius: 10px; padding: .875rem 1rem; margin-bottom: 1rem;
}
.invite-section summary {
cursor: pointer; font-size: .82rem; font-weight: 600;
color: var(--text-secondary, #6b7280); list-style: none;
display: flex; align-items: center; gap: .5rem;
}
.invite-section summary::-webkit-details-marker { display: none; }
.invite-section[open] summary { color: var(--color-primary, #2563eb); }
.invite-section .form-group { margin-top: .75rem; margin-bottom: 0; }
.invite-badge {
display: inline-flex; align-items: center; gap: .35rem;
font-size: .75rem; padding: .25rem .6rem; border-radius: 20px;
background: #dcfce7; color: #15803d; font-weight: 600;
margin-top: .5rem;
}
/* Password eye toggle */
.pw-wrap { position: relative; }
.pw-wrap .form-input { padding-right: 42px; }
.pw-toggle {
position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
background: none; border: none; cursor: pointer;
color: #9CA3AF; font-size: 15px; padding: 0; transition: color .2s;
}
.pw-toggle:hover { color: #06B6D4; }
/* Lookup status */
.lookup-status {
font-size: .75rem; margin-top: .35rem; min-height: 1.1rem;
display: flex; align-items: center; gap: .35rem;
}
.lookup-status.ok { color: #15803d; }
.lookup-status.err { color: var(--color-danger, #ef4444); }
.lookup-status.loading { color: #6b7280; }
/* Success step */
.success-body { text-align: center; padding: .5rem 0 1.5rem; }
.success-icon {
width: 64px; height: 64px; border-radius: 50%;
background: linear-gradient(135deg, #06B6D4, #0891b2);
display: flex; align-items: center; justify-content: center;
margin: 0 auto 1.25rem; color: #fff; font-size: 1.75rem;
}
.success-title { font-size: 1.3rem; font-weight: 800; color: var(--text-primary, #111); margin-bottom: .5rem; }
.success-desc { font-size: .9rem; color: var(--text-secondary, #6b7280); line-height: 1.6; margin-bottom: 1.5rem; }
/* Auth terms */
.auth-terms {
margin-top: 14px; padding-top: 14px;
border-top: 1px solid var(--border-color, #e5e7eb);
text-align: center; font-size: .72rem; color: #9CA3AF; line-height: 1.8;
}
.auth-terms a { color: #6B7280; text-decoration: underline; }
/* Steps display */
#step-0, #step-1, #step-2 { display: none; }
#step-0 { display: block; }
#step-1 { display: none; }
</style>
</head>
<body>
@ -41,43 +150,90 @@
</div>
<span class="auth-logo-text">NIS2 <span>Agile</span></span>
</div>
<p class="auth-subtitle" id="auth-subtitle">Scegli il tuo profilo</p>
<p class="auth-subtitle" id="auth-subtitle">Crea il tuo account</p>
</div>
<div class="auth-body">
<div class="step-indicator">
<div class="step-dot active" id="dot-0"></div>
<div class="step-dot" id="dot-1"></div>
<!-- Step indicator -->
<div class="step-indicator" id="step-indicator">
<div class="step-item active" id="si-0">
<div class="step-circle">1</div>
<div class="step-label">Ruolo</div>
</div>
<div class="step-connector" id="conn-0"></div>
<div class="step-item" id="si-1">
<div class="step-circle">2</div>
<div class="step-label">Dati</div>
</div>
<div class="step-connector" id="conn-1"></div>
<div class="step-item" id="si-2">
<div class="step-circle">3</div>
<div class="step-label">Fatto</div>
</div>
</div>
<div class="auth-error" id="register-error"></div>
<!-- STEP 0: Scelta profilo -->
<!-- STEP 0: Ruolo + Codice Invito -->
<div id="step-0">
<div class="profile-choice">
<div class="profile-card" id="card-azienda" onclick="selectProfile('azienda')">
<div class="profile-card-icon">🏢</div>
<div class="profile-card-title">Azienda</div>
<div class="profile-card-desc">Porto la mia organizzazione in compliance NIS2</div>
<div class="role-grid">
<div class="role-card" id="card-compliance_manager" onclick="selectRole('compliance_manager')">
<div class="role-card-icon"><i class="fas fa-shield-alt"></i></div>
<div class="role-card-title">CISO / Compliance Manager</div>
<div class="role-card-desc">Responsabile sicurezza e conformità NIS2 dell'organizzazione</div>
</div>
<div class="profile-card" id="card-consultant" onclick="selectProfile('consultant')">
<div class="profile-card-icon">👤</div>
<div class="profile-card-title">Consulente / CISO</div>
<div class="profile-card-desc">Gestisco la compliance di più aziende clienti</div>
<div class="role-card" id="card-org_admin" onclick="selectRole('org_admin')">
<div class="role-card-icon"><i class="fas fa-building"></i></div>
<div class="role-card-title">Legale Rappresentante / Admin</div>
<div class="role-card-desc">Amministratore e legale rappresentante dell'organizzazione</div>
</div>
<div class="role-card" id="card-consultant" onclick="selectRole('consultant')">
<div class="role-card-icon"><i class="fas fa-user-tie"></i></div>
<div class="role-card-title">Consulente Cybersecurity</div>
<div class="role-card-desc">Gestisco la compliance NIS2 per più aziende clienti</div>
</div>
<div class="role-card" id="card-auditor" onclick="selectRole('auditor')">
<div class="role-card-icon"><i class="fas fa-search"></i></div>
<div class="role-card-title">Auditor / Revisore</div>
<div class="role-card-desc">Verifico e valuto l'implementazione dei controlli di sicurezza</div>
</div>
<div class="role-card" id="card-board_member" onclick="selectRole('board_member')" style="grid-column: span 2;">
<div class="role-card-icon"><i class="fas fa-chess-king"></i></div>
<div class="role-card-title">Board Member / DPO</div>
<div class="role-card-desc">Membro del consiglio o Responsabile della Protezione dei Dati con visibilità strategica</div>
</div>
</div>
<button class="btn btn-primary btn-lg w-full" id="btn-next" onclick="goToStep1()" disabled>
Continua →
<details class="invite-section" id="invite-details">
<summary><i class="fas fa-ticket-alt"></i> Ho un codice di invito B2B</summary>
<div class="form-group">
<label class="form-label" for="invite-code">Codice Invito</label>
<input type="text" id="invite-code" class="form-input"
placeholder="es. NIS2-XXXX-YYYY" autocomplete="off"
oninput="validateInvite(this.value)">
<div class="lookup-status" id="invite-status"></div>
</div>
</details>
<button class="btn btn-primary btn-lg w-full" id="btn-next-0" onclick="goToStep1()" disabled>
Continua <i class="fas fa-arrow-right" style="margin-left:.4rem;"></i>
</button>
</div>
<!-- STEP 1: Dati account -->
<div id="step-1">
<form id="register-form" novalidate>
<div class="form-group">
<label class="form-label" for="fullname">Nome Completo <span class="required">*</span></label>
<input type="text" id="fullname" name="fullname" class="form-input"
placeholder="Mario Rossi" autocomplete="name" required>
<div style="display:flex; gap:.75rem;">
<div class="form-group" style="flex:1;">
<label class="form-label" for="firstname">Nome <span class="required">*</span></label>
<input type="text" id="firstname" name="firstname" class="form-input"
placeholder="Mario" autocomplete="given-name" required>
</div>
<div class="form-group" style="flex:1;">
<label class="form-label" for="lastname">Cognome <span class="required">*</span></label>
<input type="text" id="lastname" name="lastname" class="form-input"
placeholder="Rossi" autocomplete="family-name" required>
</div>
</div>
<div class="form-group">
@ -86,10 +242,24 @@
placeholder="nome@azienda.it" autocomplete="email" required>
</div>
<div class="form-group" id="piva-group">
<label class="form-label" for="piva">Partita IVA Azienda <span class="required">*</span></label>
<input type="text" id="piva" name="piva" class="form-input"
placeholder="12345678901" maxlength="11"
oninput="lookupPiva(this.value)">
<div class="lookup-status" id="piva-status"></div>
</div>
<div class="form-group">
<label class="form-label" for="password">Password <span class="required">*</span></label>
<input type="password" id="password" name="password" class="form-input"
placeholder="Minimo 8 caratteri" autocomplete="new-password" required>
<div class="pw-wrap">
<input type="password" id="password" name="password" class="form-input"
placeholder="Minimo 8 caratteri" autocomplete="new-password" required>
<button type="button" class="pw-toggle" tabindex="-1"
onclick="togglePw('password', this)" aria-label="Mostra/nascondi password">
<i class="fas fa-eye"></i>
</button>
</div>
<div class="password-strength" id="password-strength">
<div class="password-strength-bar">
<div class="password-strength-segment" id="ps-1"></div>
@ -103,13 +273,19 @@
<div class="form-group">
<label class="form-label" for="password-confirm">Conferma Password <span class="required">*</span></label>
<input type="password" id="password-confirm" name="password-confirm" class="form-input"
placeholder="Ripeti la password" autocomplete="new-password" required>
<div class="pw-wrap">
<input type="password" id="password-confirm" name="password-confirm" class="form-input"
placeholder="Ripeti la password" autocomplete="new-password" required>
<button type="button" class="pw-toggle" tabindex="-1"
onclick="togglePw('password-confirm', this)" aria-label="Mostra/nascondi password">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div style="display:flex; gap:.75rem;">
<button type="button" class="btn btn-secondary" onclick="goToStep0()" style="flex:0 0 auto;">
← Indietro
<i class="fas fa-arrow-left"></i>
</button>
<button type="submit" class="btn btn-primary btn-lg" id="register-btn" style="flex:1;">
Crea Account
@ -117,10 +293,29 @@
</div>
</form>
</div>
<!-- STEP 2: Successo -->
<div id="step-2">
<div class="success-body">
<div class="success-icon"><i class="fas fa-check"></i></div>
<div class="success-title" id="success-title">Account creato!</div>
<div class="success-desc" id="success-desc">
Il tuo account NIS2 Agile è pronto. Stai per essere reindirizzato...
</div>
<button class="btn btn-primary btn-lg w-full" id="success-btn" onclick="goToDashboard()">
Entra nella piattaforma <i class="fas fa-arrow-right" style="margin-left:.4rem;"></i>
</button>
</div>
</div>
</div>
<div class="auth-footer">
<p>Hai gia' un account? <a href="login.html">Accedi</a></p>
<p>Hai già un account? <a href="login.html">Accedi</a></p>
<div class="auth-terms">
Registrandoti accetti i nostri
<a href="https://agentai.agile.software/terms" target="_blank">Termini di Servizio</a>
e la <a href="https://agentai.agile.software/privacy" target="_blank">Privacy Policy</a>
</div>
</div>
</div>
</div>
@ -132,37 +327,208 @@
window.location.href = 'dashboard.html';
}
let selectedUserType = null;
// --- State ---
let selectedRole = null;
let inviteValid = false;
let inviteToken = null;
let pivaData = null;
let pivaLookupTimer = null;
let successRedirect = 'onboarding.html';
function selectProfile(type) {
selectedUserType = type;
document.querySelectorAll('.profile-card').forEach(c => c.classList.remove('selected'));
document.getElementById('card-' + type).classList.add('selected');
document.getElementById('btn-next').disabled = false;
// --- URL Params pre-fill ---
const params = new URLSearchParams(window.location.search);
if (params.get('role')) {
// Will apply after DOM is ready (in DOMContentLoaded equivalent below)
document.addEventListener('DOMContentLoaded', () => {
const r = params.get('role');
if (document.getElementById('card-' + r)) selectRole(r);
});
}
if (params.get('invite_token')) {
inviteToken = params.get('invite_token');
document.addEventListener('DOMContentLoaded', () => {
const details = document.getElementById('invite-details');
details.open = true;
const inp = document.getElementById('invite-code');
inp.value = inviteToken;
validateInvite(inviteToken);
});
}
// --- Role Selection ---
function selectRole(role) {
selectedRole = role;
document.querySelectorAll('.role-card').forEach(c => c.classList.remove('selected'));
const card = document.getElementById('card-' + role);
if (card) card.classList.add('selected');
document.getElementById('btn-next-0').disabled = false;
// Hide P.IVA for consultant role
const pivaGroup = document.getElementById('piva-group');
pivaGroup.style.display = (role === 'consultant') ? 'none' : 'block';
}
// Pre-fill from URL after DOM ready
(function() {
const r = params.get('role');
if (r && document.getElementById('card-' + r)) selectRole(r);
const tok = params.get('invite_token');
if (tok) {
inviteToken = tok;
document.getElementById('invite-details').open = true;
document.getElementById('invite-code').value = tok;
validateInvite(tok);
}
// Pre-fill name/email
if (params.get('nome')) document.getElementById('firstname').value = params.get('nome');
if (params.get('cognome')) document.getElementById('lastname').value = params.get('cognome');
if (params.get('email')) document.getElementById('email').value = params.get('email');
})();
// --- Invite Code Validation ---
let inviteTimer = null;
function validateInvite(val) {
const statusEl = document.getElementById('invite-status');
clearTimeout(inviteTimer);
val = val.trim();
if (!val) {
inviteValid = false;
inviteToken = null;
statusEl.innerHTML = '';
return;
}
statusEl.className = 'lookup-status loading';
statusEl.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Verifica codice...';
inviteTimer = setTimeout(async () => {
try {
const res = await fetch(api.baseUrl + '/auth/validate-invite', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ invite_token: val })
});
const data = await res.json();
if (data.success) {
inviteValid = true;
inviteToken = val;
statusEl.className = 'lookup-status ok';
statusEl.innerHTML = '<i class="fas fa-check-circle"></i> Codice valido — accesso B2B confermato';
// Pre-fill role if provided
if (data.role && document.getElementById('card-' + data.role)) {
selectRole(data.role);
}
// Pre-fill name/email from invite
if (data.nome) document.getElementById('firstname').value = data.nome;
if (data.cognome) document.getElementById('lastname').value = data.cognome;
if (data.email) document.getElementById('email').value = data.email;
} else {
inviteValid = false;
inviteToken = null;
statusEl.className = 'lookup-status err';
statusEl.innerHTML = '<i class="fas fa-times-circle"></i> Codice non valido';
}
} catch {
// Network error: accept token optimistically, validate on submit
inviteValid = false;
statusEl.className = 'lookup-status err';
statusEl.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Impossibile verificare';
}
}, 600);
}
// --- P.IVA Lookup ---
function lookupPiva(val) {
clearTimeout(pivaLookupTimer);
val = val.replace(/\s/g, '');
const statusEl = document.getElementById('piva-status');
if (val.length !== 11) {
pivaData = null;
statusEl.innerHTML = '';
return;
}
statusEl.className = 'lookup-status loading';
statusEl.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Ricerca azienda...';
pivaLookupTimer = setTimeout(async () => {
try {
const res = await fetch(api.baseUrl + '/onboarding/fetch-company', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + (api.getToken ? api.getToken() : '')
},
body: JSON.stringify({ vat_number: val })
});
const data = await res.json();
if (data.success && data.data && data.data.company_name) {
pivaData = data.data;
statusEl.className = 'lookup-status ok';
statusEl.innerHTML = '<i class="fas fa-building"></i> ' + data.data.company_name;
} else {
pivaData = null;
statusEl.className = 'lookup-status';
statusEl.innerHTML = '<i class="fas fa-info-circle" style="color:#6b7280;"></i> Azienda non trovata, potrai inserire i dati manualmente';
}
} catch {
pivaData = null;
statusEl.innerHTML = '';
}
}, 800);
}
// --- Step Navigation ---
function setStep(n) {
[0, 1, 2].forEach(i => {
document.getElementById('step-' + i).style.display = (i === n) ? 'block' : 'none';
const si = document.getElementById('si-' + i);
si.className = 'step-item' + (i < n ? ' done' : i === n ? ' active' : '');
if (i < n) si.querySelector('.step-circle').innerHTML = '<i class="fas fa-check" style="font-size:.75rem;"></i>';
else si.querySelector('.step-circle').textContent = i + 1;
});
document.getElementById('conn-0').className = 'step-connector' + (n > 0 ? ' done' : '');
document.getElementById('conn-1').className = 'step-connector' + (n > 1 ? ' done' : '');
if (n === 2) document.getElementById('step-indicator').style.display = 'none';
else document.getElementById('step-indicator').style.display = 'flex';
}
function goToStep1() {
if (!selectedUserType) return;
document.getElementById('step-0').style.display = 'none';
document.getElementById('step-1').style.display = 'block';
document.getElementById('dot-0').classList.replace('active', 'done');
document.getElementById('dot-1').classList.add('active');
const labels = { azienda: 'Crea il tuo account aziendale', consultant: 'Crea il tuo account da Consulente' };
document.getElementById('auth-subtitle').textContent = labels[selectedUserType];
if (!selectedRole) return;
document.getElementById('register-error').classList.remove('visible');
const labels = {
compliance_manager: 'Dati account — CISO / Compliance Manager',
org_admin: 'Dati account — Legale Rappresentante',
consultant: 'Dati account — Consulente Cybersecurity',
auditor: 'Dati account — Auditor / Revisore',
board_member: 'Dati account — Board Member / DPO'
};
document.getElementById('auth-subtitle').textContent = labels[selectedRole] || 'Dati account';
setStep(1);
document.getElementById('firstname').focus();
}
function goToStep0() {
document.getElementById('step-1').style.display = 'none';
document.getElementById('step-0').style.display = 'block';
document.getElementById('dot-1').classList.remove('active');
document.getElementById('dot-0').className = 'step-dot active';
document.getElementById('auth-subtitle').textContent = 'Scegli il tuo profilo';
document.getElementById('register-error').classList.remove('visible');
document.getElementById('auth-subtitle').textContent = 'Crea il tuo account';
setStep(0);
}
// Password Strength
const passwordInput = document.getElementById('password');
passwordInput.addEventListener('input', () => {
updateStrengthUI(calcPasswordStrength(passwordInput.value));
function goToDashboard() {
window.location.href = successRedirect;
}
// --- Password toggle ---
function togglePw(id, btn) {
const inp = document.getElementById(id);
if (inp.type === 'password') {
inp.type = 'text';
btn.innerHTML = '<i class="fas fa-eye-slash"></i>';
} else {
inp.type = 'password';
btn.innerHTML = '<i class="fas fa-eye"></i>';
}
}
// --- Password strength ---
document.getElementById('password').addEventListener('input', function() {
updateStrengthUI(calcPasswordStrength(this.value), this.value);
});
function calcPasswordStrength(pw) {
@ -175,30 +541,38 @@
return Math.min(4, Math.max(1, s <= 1 ? 1 : s === 2 ? 2 : s === 3 ? 3 : 4));
}
function updateStrengthUI(level) {
function updateStrengthUI(level, pw) {
const labels = { 1: 'Debole', 2: 'Sufficiente', 3: 'Buona', 4: 'Forte' };
const classes = { 1: 'weak', 2: 'fair', 3: 'good', 4: 'strong' };
for (let i = 1; i <= 4; i++) {
const seg = document.getElementById('ps-' + i);
seg.className = 'password-strength-segment';
if (i <= level && passwordInput.value.length > 0) seg.classList.add('active', classes[level]);
if (i <= level && pw.length > 0) seg.classList.add('active', classes[level]);
}
document.getElementById('ps-text').textContent = passwordInput.value.length > 0 ? labels[level] : '';
document.getElementById('ps-text').textContent = pw.length > 0 ? labels[level] : '';
}
// Form Submit
// --- Form Submit ---
document.getElementById('register-form').addEventListener('submit', async (e) => {
e.preventDefault();
const errorEl = document.getElementById('register-error');
errorEl.classList.remove('visible');
const fullname = document.getElementById('fullname').value.trim();
const firstname = document.getElementById('firstname').value.trim();
const lastname = document.getElementById('lastname').value.trim();
const email = document.getElementById('email').value.trim();
const piva = document.getElementById('piva').value.trim();
const password = document.getElementById('password').value;
const passwordConfirm = document.getElementById('password-confirm').value;
const fullname = firstname + ' ' + lastname;
if (!fullname || !email || !password || !passwordConfirm) {
errorEl.textContent = 'Tutti i campi sono obbligatori.';
if (!firstname || !lastname || !email || !password) {
errorEl.textContent = 'Nome, cognome, email e password sono obbligatori.';
errorEl.classList.add('visible');
return;
}
if (selectedRole !== 'consultant' && !piva) {
errorEl.textContent = 'Inserisci la Partita IVA della tua azienda.';
errorEl.classList.add('visible');
return;
}
@ -218,10 +592,49 @@
btn.textContent = 'Registrazione in corso...';
try {
const result = await api.register(email, password, fullname, selectedUserType);
let result;
if (inviteToken) {
// B2B provision flow
result = await fetch(api.baseUrl + '/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email, password, full_name: fullname,
role: selectedRole,
invite_token: inviteToken,
vat_number: piva || undefined
})
}).then(r => r.json());
} else {
result = await api.register(email, password, fullname, selectedRole);
}
if (result.success) {
showNotification('Account creato con successo!', 'success');
setTimeout(() => { window.location.href = 'onboarding.html'; }, 500);
// Determine redirect
if (inviteToken) {
successRedirect = 'dashboard.html';
} else if (selectedRole === 'consultant') {
successRedirect = 'companies.html';
} else {
successRedirect = 'onboarding.html';
}
// Success step
const titles = {
consultant: 'Account Consulente creato!',
board_member: 'Account Board Member creato!',
};
const descs = {
consultant: 'Puoi ora gestire i tuoi clienti dalla sezione Aziende.',
org_admin: pivaData ? 'Abbiamo trovato ' + pivaData.company_name + '. Completa l\'onboarding per configurare la tua organizzazione.' : 'Completa l\'onboarding per configurare la tua organizzazione NIS2.',
};
document.getElementById('success-title').textContent = titles[selectedRole] || 'Account creato!';
document.getElementById('success-desc').textContent = descs[selectedRole] || 'Il tuo account NIS2 Agile è pronto. Stai per essere reindirizzato...';
document.getElementById('auth-subtitle').textContent = 'Registrazione completata';
setStep(2);
setTimeout(goToDashboard, 2500);
} else {
errorEl.textContent = result.message || 'Errore durante la registrazione.';
errorEl.classList.add('visible');

File diff suppressed because it is too large Load Diff